Completed
Pull Request — master (#1884)
by
unknown
01:27
created

SqlServerAdapter::getColumnSqlDefinition()   F

Complexity

Conditions 20
Paths 480

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 420

Importance

Changes 0
Metric Value
dl 0
loc 47
ccs 0
cts 0
cp 0
rs 0.7221
c 0
b 0
f 0
cc 20
nc 480
nop 2
crap 420

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * MIT License
5
 * For full license information, please view the LICENSE file that was distributed with this source code.
6
 */
7
8
namespace Phinx\Db\Adapter;
9
10
use BadMethodCallException;
11
use Cake\Database\Connection;
12
use Cake\Database\Driver\Sqlserver as SqlServerDriver;
13
use InvalidArgumentException;
14
use PDO;
15
use PDOException;
16
use Phinx\Db\Table\Column;
17
use Phinx\Db\Table\ForeignKey;
18
use Phinx\Db\Table\Index;
19
use Phinx\Db\Table\Table;
20
use Phinx\Db\Util\AlterInstructions;
21
use Phinx\Migration\MigrationInterface;
22
use Phinx\Util\Literal;
23
use RuntimeException;
24
25
/**
26
 * Phinx SqlServer Adapter.
27
 *
28
 * @author Rob Morgan <[email protected]>
29
 */
30
class SqlServerAdapter extends PdoAdapter
31
{
32
<<<<<<< HEAD
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_SL, expecting T_FUNCTION or T_CONST
Loading history...
33
	/**
34
	 * @var string[]
35
	 */
36
	protected static $specificColumnTypes = [
37
		self::PHINX_TYPE_FILESTREAM,
38
		self::PHINX_TYPE_BINARYUUID,
39
	];
40
41
	/**
42
	 * @var string
43
	 */
44
	protected $schema = 'dbo';
45
46
	/**
47
	 * @var bool[]
48
	 */
49
	protected $signedColumnTypes = [
50
		self::PHINX_TYPE_INTEGER => true,
51
		self::PHINX_TYPE_BIG_INTEGER => true,
52
		self::PHINX_TYPE_FLOAT => true,
53
		self::PHINX_TYPE_DECIMAL => true,
54
	];
55
56
	/**
57
	 * {@inheritDoc}
58
	 *
59
	 * @throws \InvalidArgumentException
60
	 * @return void
61
	 */
62
	public function connect()
63
	{
64
		if ($this->connection === null) {
65
			if (!class_exists('PDO') || !in_array('sqlsrv', PDO::getAvailableDrivers(), true)) {
66
				// try our connection via freetds (Mac/Linux)
67
				$this->connectDblib();
68
69
				return;
70
			}
71
72
			$options = $this->getOptions();
73
74
			$dsn = 'sqlsrv:server=' . $options['host'];
75
			// if port is specified use it, otherwise use the SqlServer default
76
			if (!empty($options['port'])) {
77
				$dsn .= ',' . $options['port'];
78
			}
79
			$dsn .= ';database=' . $options['name'] . ';MultipleActiveResultSets=false';
80
81
			$driverOptions = [];
82
83
			// charset support
84
			if (isset($options['charset'])) {
85
				$driverOptions[PDO::SQLSRV_ATTR_ENCODING] = $options['charset'];
86
			}
87
88
			// use custom data fetch mode
89
			if (!empty($options['fetch_mode'])) {
90
				$driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
91
			}
92
93
			// support arbitrary \PDO::SQLSRV_ATTR_* driver options and pass them to PDO
94
			// http://php.net/manual/en/ref.pdo-sqlsrv.php#pdo-sqlsrv.constants
95
			foreach ($options as $key => $option) {
96
				if (strpos($key, 'sqlsrv_attr_') === 0) {
97
					$driverOptions[constant('\PDO::' . strtoupper($key))] = $option;
98
				}
99
			}
100
			$db = $this->createPdoConnection($dsn, $options['user'], $options['pass'], $driverOptions);
101
102
			$this->setConnection($db);
103
		}
104
	}
105
106
	/**
107
	 * Connect to MSSQL using dblib/freetds.
108
	 *
109
	 * The "sqlsrv" driver is not available on Unix machines.
110
	 *
111
	 * @throws \InvalidArgumentException
112
	 * @throws \RuntimeException
113
	 * @return void
114
	 */
115
	protected function connectDblib()
116
	{
117
		if (!class_exists('PDO') || !in_array('dblib', PDO::getAvailableDrivers(), true)) {
118
			// @codeCoverageIgnoreStart
119
			throw new RuntimeException('You need to enable the PDO_Dblib extension for Phinx to run properly.');
120
			// @codeCoverageIgnoreEnd
121
		}
122
123
		$options = $this->getOptions();
124
125
		// if port is specified use it, otherwise use the SqlServer default
126
		if (empty($options['port'])) {
127
			$dsn = 'dblib:host=' . $options['host'] . ';dbname=' . $options['name'];
128
		} else {
129
			$dsn = 'dblib:host=' . $options['host'] . ':' . $options['port'] . ';dbname=' . $options['name'];
130
		}
131
132
		$driverOptions = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
133
134
		try {
135
			$db = new PDO($dsn, $options['user'], $options['pass'], $driverOptions);
136
		} catch (PDOException $exception) {
137
			throw new InvalidArgumentException(sprintf(
138
				'There was a problem connecting to the database: %s',
139
				$exception->getMessage()
140
			));
141
		}
142
143
		$this->setConnection($db);
144
	}
145
146
	/**
147
	 * @inheritDoc
148
	 */
149
	public function disconnect()
150
	{
151
		$this->connection = null;
152
	}
153
154
	/**
155
	 * @inheritDoc
156
	 */
157
	public function hasTransactions()
158
	{
159
		return true;
160
	}
161
162
	/**
163
	 * @inheritDoc
164
	 */
165
	public function beginTransaction()
166
	{
167
		$this->execute('BEGIN TRANSACTION');
168
	}
169
170
	/**
171
	 * @inheritDoc
172
	 */
173
	public function commitTransaction()
174
	{
175
		$this->execute('COMMIT TRANSACTION');
176
	}
177
178
	/**
179
	 * @inheritDoc
180
	 */
181
	public function rollbackTransaction()
182
	{
183
		$this->execute('ROLLBACK TRANSACTION');
184
	}
185
186
	/**
187
	 * @inheritDoc
188
	 */
189
	public function quoteTableName($tableName)
190
	{
191
		return str_replace('.', '].[', $this->quoteColumnName($tableName));
192
	}
193
194
	/**
195
	 * @inheritDoc
196
	 */
197
	public function quoteColumnName($columnName)
198
	{
199
		return '[' . str_replace(']', '\]', $columnName) . ']';
200
	}
201
202
	/**
203
	 * @inheritDoc
204
	 */
205
	public function hasTable($tableName)
206
	{
207
		if ($this->hasCreatedTable($tableName)) {
208
			return true;
209
		}
210
211
		$result = $this->fetchRow(sprintf("SELECT count(*) as [count] FROM information_schema.tables WHERE table_name = '%s';", $tableName));
212
213
		return $result['count'] > 0;
214
	}
215
216
	/**
217
	 * @inheritDoc
218
	 */
219
	public function createTable(Table $table, array $columns = [], array $indexes = [])
220
	{
221
		$options = $table->getOptions();
222
223
		// Add the default primary key
224
		if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
225
			$options['id'] = 'id';
226
		}
227
228
		if (isset($options['id']) && is_string($options['id'])) {
229
			// Handle id => "field_name" to support AUTO_INCREMENT
230
			$column = new Column();
231
			$column->setName($options['id'])
232
	  ->setType('integer')
233
	  ->setIdentity(true);
234
235
			array_unshift($columns, $column);
236
			if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) {
237
				throw new InvalidArgumentException('You cannot enable an auto incrementing ID field and a primary key');
238
			}
239
			$options['primary_key'] = $options['id'];
240
		}
241
242
		$sql = 'CREATE TABLE ';
243
		$sql .= $this->quoteTableName($table->getName()) . ' (';
244
		$sqlBuffer = [];
245
		$columnsWithComments = [];
246
		foreach ($columns as $column) {
247
			$sqlBuffer[] = $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column);
248
249
			// set column comments, if needed
250
			if ($column->getComment()) {
251
				$columnsWithComments[] = $column;
252
			}
253
		}
254
255
		// set the primary key(s)
256
		if (isset($options['primary_key'])) {
257
			$pkSql = sprintf('CONSTRAINT PK_%s PRIMARY KEY (', $table->getName());
258
			if (is_string($options['primary_key'])) { // handle primary_key => 'id'
259
				$pkSql .= $this->quoteColumnName($options['primary_key']);
260
			} elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
261
				$pkSql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
262
			}
263
			$pkSql .= ')';
264
			$sqlBuffer[] = $pkSql;
265
		}
266
267
		$sql .= implode(', ', $sqlBuffer);
268
		$sql .= ');';
269
270
		// process column comments
271
		foreach ($columnsWithComments as $column) {
272
			$sql .= $this->getColumnCommentSqlDefinition($column, $table->getName());
273
		}
274
275
		// set the indexes
276
		foreach ($indexes as $index) {
277
			$sql .= $this->getIndexSqlDefinition($index, $table->getName());
278
		}
279
		// execute the sql
280
		$this->execute($sql);
281
282
		$this->addCreatedTable($table->getName());
283
	}
284
285
	/**
286
	 * {@inheritDoc}
287
	 *
288
	 * @throws \InvalidArgumentException
289
	 */
290
	protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
291
	{
292
		$instructions = new AlterInstructions();
293
294
		// Drop the existing primary key
295
		$primaryKey = $this->getPrimaryKey($table->getName());
296
		if (!empty($primaryKey['constraint'])) {
297
			$sql = sprintf(
298
				'DROP CONSTRAINT %s',
299
				$this->quoteColumnName($primaryKey['constraint'])
300
			);
301
			$instructions->addAlter($sql);
302
		}
303
304
		// Add the primary key(s)
305
		if (!empty($newColumns)) {
306
			$sql = sprintf(
307
				'ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (',
308
				$this->quoteTableName($table->getName()),
309
				$this->quoteColumnName('PK_' . $table->getName())
310
			);
311
			if (is_string($newColumns)) { // handle primary_key => 'id'
312
				$sql .= $this->quoteColumnName($newColumns);
313
			} elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id')
314
				$sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns));
315
			} else {
316
				throw new InvalidArgumentException(sprintf(
317
					'Invalid value for primary key: %s',
318
					json_encode($newColumns)
319
				));
320
			}
321
			$sql .= ')';
322
			$instructions->addPostStep($sql);
323
		}
324
325
		return $instructions;
326
	}
327
328
	/**
329
	 * @inheritDoc
330
	 *
331
	 * SqlServer does not implement this functionality, and so will always throw an exception if used.
332
	 * @throws \BadMethodCallException
333
	 */
334
	protected function getChangeCommentInstructions(Table $table, $newComment)
335
	{
336
		throw new BadMethodCallException('SqlServer does not have table comments');
337
	}
338
339
	/**
340
	 * Gets the SqlServer Column Comment Defininition for a column object.
341
	 *
342
	 * @param \Phinx\Db\Table\Column $column Column
343
	 * @param string $tableName Table name
344
	 * @return string
345
	 */
346
	protected function getColumnCommentSqlDefinition(Column $column, $tableName)
347
	{
348
		// passing 'null' is to remove column comment
349
		$currentComment = $this->getColumnComment($tableName, $column->getName());
350
351
		$comment = strcasecmp($column->getComment(), 'NULL') !== 0 ? $this->getConnection()->quote($column->getComment()) : '\'\'';
352
		$command = $currentComment === false ? 'sp_addextendedproperty' : 'sp_updateextendedproperty';
353
354
		return sprintf(
355
			"EXECUTE %s N'MS_Description', N%s, N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s';",
356
			$command,
357
			$comment,
358
			$this->schema,
359
			$tableName,
360
			$column->getName()
361
		);
362
	}
363
364
	/**
365
	 * @inheritDoc
366
	 */
367
	protected function getRenameTableInstructions($tableName, $newTableName)
368
	{
369
		$this->updateCreatedTableName($tableName, $newTableName);
370
		$sql = sprintf(
371
			"EXEC sp_rename '%s', '%s'",
372
			$tableName,
373
			$newTableName
374
		);
375
376
		return new AlterInstructions([], [$sql]);
377
	}
378
379
	/**
380
	 * @inheritDoc
381
	 */
382
	protected function getDropTableInstructions($tableName)
383
	{
384
		$this->removeCreatedTable($tableName);
385
		$sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
386
387
		return new AlterInstructions([], [$sql]);
388
	}
389
390
	/**
391
	 * @inheritDoc
392
	 */
393
	public function truncateTable($tableName)
394
	{
395
		$sql = sprintf(
396
			'TRUNCATE TABLE %s',
397
			$this->quoteTableName($tableName)
398
		);
399
400
		$this->execute($sql);
401
	}
402
403
	/**
404
	 * @param string $tableName Table name
405
	 * @param string $columnName Column name
406
	 * @return string|false
407
	 */
408
	public function getColumnComment($tableName, $columnName)
409
	{
410
		$sql = sprintf("SELECT cast(extended_properties.[value] as nvarchar(4000)) comment
411
=======
412
    /**
413
     * @var string[]
414
     */
415
    protected static $specificColumnTypes = [
416
        self::PHINX_TYPE_FILESTREAM,
417
        self::PHINX_TYPE_BINARYUUID,
418
    ];
419
420
    /**
421
     * @var string
422
     */
423
    protected $schema = 'dbo';
424
425
    /**
426
     * @var bool[]
427
     */
428
    protected $signedColumnTypes = [
429
        self::PHINX_TYPE_INTEGER => true,
430
        self::PHINX_TYPE_BIG_INTEGER => true,
431
        self::PHINX_TYPE_FLOAT => true,
432
        self::PHINX_TYPE_DECIMAL => true,
433
    ];
434
435
    /**
436
     * {@inheritDoc}
437
     *
438
     * @throws \InvalidArgumentException
439
     * @return void
440
     */
441
    public function connect()
442
    {
443
        if ($this->connection === null) {
444
            if (!class_exists('PDO') || !in_array('sqlsrv', PDO::getAvailableDrivers(), true)) {
445
                // try our connection via freetds (Mac/Linux)
446
                $this->connectDblib();
447
448
                return;
449
            }
450
451
            $options = $this->getOptions();
452
453
            $dsn = 'sqlsrv:server=' . $options['host'];
454
            // if port is specified use it, otherwise use the SqlServer default
455
            if (!empty($options['port'])) {
456
                $dsn .= ',' . $options['port'];
457
            }
458
            $dsn .= ';database=' . $options['name'] . ';MultipleActiveResultSets=false';
459
460
            $driverOptions = [];
461
462
            // charset support
463
            if (isset($options['charset'])) {
464
                $driverOptions[PDO::SQLSRV_ATTR_ENCODING] = $options['charset'];
465
            }
466
467
            // use custom data fetch mode
468
            if (!empty($options['fetch_mode'])) {
469
                $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
470
            }
471
472
            // support arbitrary \PDO::SQLSRV_ATTR_* driver options and pass them to PDO
473
            // http://php.net/manual/en/ref.pdo-sqlsrv.php#pdo-sqlsrv.constants
474
            foreach ($options as $key => $option) {
475
                if (strpos($key, 'sqlsrv_attr_') === 0) {
476
                    $driverOptions[constant('\PDO::' . strtoupper($key))] = $option;
477
                }
478
            }
479
480
            $db = $this->createPdoConnection($dsn, $options['user'], $options['pass'], $driverOptions);
481
482
            $this->setConnection($db);
483
        }
484
    }
485
486
    /**
487
     * Connect to MSSQL using dblib/freetds.
488
     *
489
     * The "sqlsrv" driver is not available on Unix machines.
490
     *
491
     * @throws \InvalidArgumentException
492
     * @throws \RuntimeException
493
     * @return void
494
     */
495
    protected function connectDblib()
496
    {
497
        if (!class_exists('PDO') || !in_array('dblib', PDO::getAvailableDrivers(), true)) {
498
            // @codeCoverageIgnoreStart
499
            throw new RuntimeException('You need to enable the PDO_Dblib extension for Phinx to run properly.');
500
            // @codeCoverageIgnoreEnd
501
        }
502
503
        $options = $this->getOptions();
504
505
        // if port is specified use it, otherwise use the SqlServer default
506
        if (empty($options['port'])) {
507
            $dsn = 'dblib:host=' . $options['host'] . ';dbname=' . $options['name'];
508
        } else {
509
            $dsn = 'dblib:host=' . $options['host'] . ':' . $options['port'] . ';dbname=' . $options['name'];
510
        }
511
512
        $driverOptions = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
513
514
        try {
515
            $db = new PDO($dsn, $options['user'], $options['pass'], $driverOptions);
516
        } catch (PDOException $exception) {
517
            throw new InvalidArgumentException(sprintf(
518
                'There was a problem connecting to the database: %s',
519
                $exception->getMessage()
520
            ));
521
        }
522
523
        $this->setConnection($db);
524
    }
525
526
    /**
527
     * @inheritDoc
528
     */
529
    public function disconnect()
530
    {
531
        $this->connection = null;
532
    }
533
534
    /**
535
     * @inheritDoc
536
     */
537
    public function hasTransactions()
538
    {
539
        return true;
540
    }
541
542
    /**
543
     * @inheritDoc
544
     */
545
    public function beginTransaction()
546
    {
547
        $this->execute('BEGIN TRANSACTION');
548
    }
549
550
    /**
551
     * @inheritDoc
552
     */
553
    public function commitTransaction()
554
    {
555
        $this->execute('COMMIT TRANSACTION');
556
    }
557
558
    /**
559
     * @inheritDoc
560
     */
561
    public function rollbackTransaction()
562
    {
563
        $this->execute('ROLLBACK TRANSACTION');
564
    }
565
566
    /**
567
     * @inheritDoc
568
     */
569
    public function quoteTableName($tableName)
570
    {
571
        return str_replace('.', '].[', $this->quoteColumnName($tableName));
572
    }
573
574
    /**
575
     * @inheritDoc
576
     */
577
    public function quoteColumnName($columnName)
578
    {
579
        return '[' . str_replace(']', '\]', $columnName) . ']';
580
    }
581
582
    /**
583
     * @inheritDoc
584
     */
585
    public function hasTable($tableName)
586
    {
587
        if ($this->hasCreatedTable($tableName)) {
588
            return true;
589
        }
590
591
        $result = $this->fetchRow(sprintf("SELECT count(*) as [count] FROM information_schema.tables WHERE table_name = '%s';", $tableName));
592
593
        return $result['count'] > 0;
594
    }
595
596
    /**
597
     * @inheritDoc
598
     */
599
    public function createTable(Table $table, array $columns = [], array $indexes = [])
600
    {
601
        $options = $table->getOptions();
602
603
        // Add the default primary key
604
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
605
            $options['id'] = 'id';
606
        }
607
608
        if (isset($options['id']) && is_string($options['id'])) {
609
            // Handle id => "field_name" to support AUTO_INCREMENT
610
            $column = new Column();
611
            $column->setName($options['id'])
612
            ->setType('integer')
613
            ->setIdentity(true);
614
615
            array_unshift($columns, $column);
616
            if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) {
617
                throw new InvalidArgumentException('You cannot enable an auto incrementing ID field and a primary key');
618
            }
619
            $options['primary_key'] = $options['id'];
620
        }
621
622
        $sql = 'CREATE TABLE ';
623
        $sql .= $this->quoteTableName($table->getName()) . ' (';
624
        $sqlBuffer = [];
625
        $columnsWithComments = [];
626
        foreach ($columns as $column) {
627
            $sqlBuffer[] = $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column);
628
629
            // set column comments, if needed
630
            if ($column->getComment()) {
631
                $columnsWithComments[] = $column;
632
            }
633
        }
634
635
        // set the primary key(s)
636
        if (isset($options['primary_key'])) {
637
            $pkSql = sprintf('CONSTRAINT PK_%s PRIMARY KEY (', $table->getName());
638
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
639
                $pkSql .= $this->quoteColumnName($options['primary_key']);
640
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
641
                $pkSql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
642
            }
643
            $pkSql .= ')';
644
            $sqlBuffer[] = $pkSql;
645
        }
646
647
        $sql .= implode(', ', $sqlBuffer);
648
        $sql .= ');';
649
650
        // process column comments
651
        foreach ($columnsWithComments as $column) {
652
            $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName());
653
        }
654
655
        // set the indexes
656
        foreach ($indexes as $index) {
657
            $sql .= $this->getIndexSqlDefinition($index, $table->getName());
658
        }
659
660
        // execute the sql
661
        $this->execute($sql);
662
663
        $this->addCreatedTable($table->getName());
664
    }
665
666
    /**
667
     * {@inheritDoc}
668
     *
669
     * @throws \InvalidArgumentException
670
     */
671
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
672
    {
673
        $instructions = new AlterInstructions();
674
675
        // Drop the existing primary key
676
        $primaryKey = $this->getPrimaryKey($table->getName());
677
        if (!empty($primaryKey['constraint'])) {
678
            $sql = sprintf(
679
                'DROP CONSTRAINT %s',
680
                $this->quoteColumnName($primaryKey['constraint'])
681
            );
682
            $instructions->addAlter($sql);
683
        }
684
685
        // Add the primary key(s)
686
        if (!empty($newColumns)) {
687
            $sql = sprintf(
688
                'ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (',
689
                $this->quoteTableName($table->getName()),
690
                $this->quoteColumnName('PK_' . $table->getName())
691
            );
692
            if (is_string($newColumns)) { // handle primary_key => 'id'
693
                $sql .= $this->quoteColumnName($newColumns);
694
            } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id')
695
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns));
696
            } else {
697
                throw new InvalidArgumentException(sprintf(
698
                    'Invalid value for primary key: %s',
699
                    json_encode($newColumns)
700
                ));
701
            }
702
            $sql .= ')';
703
            $instructions->addPostStep($sql);
704
        }
705
706
        return $instructions;
707
    }
708
709
    /**
710
     * @inheritDoc
711
     *
712
     * SqlServer does not implement this functionality, and so will always throw an exception if used.
713
     * @throws \BadMethodCallException
714
     */
715
    protected function getChangeCommentInstructions(Table $table, $newComment)
716
    {
717
        throw new BadMethodCallException('SqlServer does not have table comments');
718
    }
719
720
    /**
721
     * Gets the SqlServer Column Comment Defininition for a column object.
722
     *
723
     * @param \Phinx\Db\Table\Column $column Column
724
     * @param string $tableName Table name
725
     * @return string
726
     */
727
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
728
    {
729
        // passing 'null' is to remove column comment
730
        $currentComment = $this->getColumnComment($tableName, $column->getName());
731
732
        $comment = strcasecmp($column->getComment(), 'NULL') !== 0 ? $this->getConnection()->quote($column->getComment()) : '\'\'';
733
        $command = $currentComment === false ? 'sp_addextendedproperty' : 'sp_updateextendedproperty';
734
735
        return sprintf(
736
            "EXECUTE %s N'MS_Description', N%s, N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s';",
737
            $command,
738
            $comment,
739
            $this->schema,
740
            $tableName,
741
            $column->getName()
742
        );
743
    }
744
745
    /**
746
     * @inheritDoc
747
     */
748
    protected function getRenameTableInstructions($tableName, $newTableName)
749
    {
750
        $this->updateCreatedTableName($tableName, $newTableName);
751
        $sql = sprintf(
752
            "EXEC sp_rename '%s', '%s'",
753
            $tableName,
754
            $newTableName
755
        );
756
757
        return new AlterInstructions([], [$sql]);
758
    }
759
760
    /**
761
     * @inheritDoc
762
     */
763
    protected function getDropTableInstructions($tableName)
764
    {
765
        $this->removeCreatedTable($tableName);
766
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
767
768
        return new AlterInstructions([], [$sql]);
769
    }
770
771
    /**
772
     * @inheritDoc
773
     */
774
    public function truncateTable($tableName)
775
    {
776
        $sql = sprintf(
777
            'TRUNCATE TABLE %s',
778
            $this->quoteTableName($tableName)
779
        );
780
781
        $this->execute($sql);
782
    }
783
784
    /**
785
     * @param string $tableName Table name
786
     * @param string $columnName Column name
787
     * @return string|false
788
     */
789
    public function getColumnComment($tableName, $columnName)
790
    {
791
        $sql = sprintf("SELECT cast(extended_properties.[value] as nvarchar(4000)) comment
792
>>>>>>> 4886c8a2e8321f50e08b13b0077293120a953585
793
			FROM sys.schemas
794
			INNER JOIN sys.tables
795
			ON schemas.schema_id = tables.schema_id
796
			INNER JOIN sys.columns
797
			ON tables.object_id = columns.object_id
798
			INNER JOIN sys.extended_properties
799
			ON tables.object_id = extended_properties.major_id
800
			AND columns.column_id = extended_properties.minor_id
801
			AND extended_properties.name = 'MS_Description'
802
			WHERE schemas.[name] = '%s' AND tables.[name] = '%s' AND columns.[name] = '%s'", $this->schema, $tableName, $columnName);
803
        $row = $this->fetchRow($sql);
804
805
        if ($row) {
806
            return trim($row['comment']);
807
        }
808
809
        return false;
810
    }
811
812
    /**
813
     * @inheritDoc
814
     */
815
    public function getColumns($tableName)
816
    {
817
        $columns = [];
818
        $sql = sprintf(
819
            "SELECT DISTINCT TABLE_SCHEMA AS [schema], TABLE_NAME as [table_name], COLUMN_NAME AS [name], DATA_TYPE AS [type],
820
			IS_NULLABLE AS [null], COLUMN_DEFAULT AS [default],
821
			CHARACTER_MAXIMUM_LENGTH AS [char_length],
822
			NUMERIC_PRECISION AS [precision],
823
			NUMERIC_SCALE AS [scale], ORDINAL_POSITION AS [ordinal_position],
824
			COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as [identity]
825
			FROM INFORMATION_SCHEMA.COLUMNS
826
			WHERE TABLE_NAME = '%s'
827
			ORDER BY ordinal_position",
828
            $tableName
829
        );
830
        $rows = $this->fetchAll($sql);
831
        foreach ($rows as $columnInfo) {
832
            try {
833
                $type = $this->getPhinxType($columnInfo['type']);
834
            } catch (UnsupportedColumnTypeException $e) {
835
                $type = Literal::from($columnInfo['type']);
836
            }
837
838
            $column = new Column();
839
            $column->setName($columnInfo['name'])
840
            ->setType($type)
841
            ->setNull($columnInfo['null'] !== 'NO')
842
            ->setDefault($this->parseDefault($columnInfo['default']))
843
            ->setIdentity($columnInfo['identity'] === '1')
844
            ->setComment($this->getColumnComment($columnInfo['table_name'], $columnInfo['name']));
845
846
            if (!empty($columnInfo['char_length'])) {
847
                $column->setLimit($columnInfo['char_length']);
848
            }
849
850
            $columns[$columnInfo['name']] = $column;
851
        }
852
853
        return $columns;
854
    }
855
856
    /**
857
     * @param string $default Default
858
     * @return int|string|null
859
     */
860
    protected function parseDefault($default)
861
    {
862
        $result = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default);
863
864
        if (strtoupper($result) === 'NULL') {
865
            $result = null;
866
        } elseif (is_numeric($result)) {
867
            $result = (int)$result;
868
        }
869
870
        return $result;
871
    }
872
873
    /**
874
     * @inheritDoc
875
     */
876
    public function hasColumn($tableName, $columnName)
877
    {
878
        $sql = sprintf(
879
            "SELECT count(*) as [count]
880
			FROM information_schema.columns
881
			WHERE table_name = '%s' AND column_name = '%s'",
882
            $tableName,
883
            $columnName
884
        );
885
        $result = $this->fetchRow($sql);
886
887
        return $result['count'] > 0;
888
    }
889
890
    /**
891
     * @inheritDoc
892
     */
893
    protected function getAddColumnInstructions(Table $table, Column $column)
894
    {
895
        $alter = sprintf(
896
            'ALTER TABLE %s ADD %s %s',
897
            $table->getName(),
898
            $this->quoteColumnName($column->getName()),
899
            $this->getColumnSqlDefinition($column)
900
        );
901
902
        return new AlterInstructions([], [$alter]);
903
    }
904
905
    /**
906
     * {@inheritDoc}
907
     *
908
     * @throws \InvalidArgumentException
909
     */
910
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
911
    {
912
        if (!$this->hasColumn($tableName, $columnName)) {
913
            throw new InvalidArgumentException("The specified column does not exist: $columnName");
914
        }
915
916
        $instructions = new AlterInstructions();
917
918
        $oldConstraintName = "DF_{$tableName}_{$columnName}";
919
        $newConstraintName = "DF_{$tableName}_{$newColumnName}";
920
        $sql = <<<SQL
921
		       IF (OBJECT_ID('$oldConstraintName', 'D') IS NOT NULL)
922
		       BEGIN
923
		       EXECUTE sp_rename N'%s', N'%s', N'OBJECT'
924
		       END
925
SQL;
926
        $instructions->addPostStep(sprintf(
927
            $sql,
928
            $oldConstraintName,
929
            $newConstraintName
930
        ));
931
932
        $instructions->addPostStep(sprintf(
933
            "EXECUTE sp_rename N'%s.%s', N'%s', 'COLUMN' ",
934
            $tableName,
935
            $columnName,
936
            $newColumnName
937
        ));
938
939
        return $instructions;
940
    }
941
942
    /**
943
     * Returns the instructions to change a column default value
944
     *
945
     * @param string $tableName The table where the column is
946
     * @param \Phinx\Db\Table\Column $newColumn The column to alter
947
     * @return \Phinx\Db\Util\AlterInstructions
948
     */
949
    protected function getChangeDefault($tableName, Column $newColumn)
950
    {
951
        $constraintName = "DF_{$tableName}_{$newColumn->getName()}";
952
        $default = $newColumn->getDefault();
953
        $instructions = new AlterInstructions();
954
955
        if ($default === null) {
956
            $default = 'DEFAULT NULL';
957
        } else {
958
            $default = ltrim($this->getDefaultValueDefinition($default));
959
        }
960
961
        if (empty($default)) {
962
            return $instructions;
963
        }
964
965
        $instructions->addPostStep(sprintf(
966
            'ALTER TABLE %s ADD CONSTRAINT %s %s FOR %s',
967
            $this->quoteTableName($tableName),
968
            $constraintName,
969
            $default,
970
            $this->quoteColumnName($newColumn->getName())
971
        ));
972
973
        return $instructions;
974
    }
975
976
    /**
977
     * @inheritDoc
978
     */
979
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
980
    {
981
        $columns = $this->getColumns($tableName);
982
        $changeDefault =
983
            $newColumn->getDefault() !== $columns[$columnName]->getDefault() ||
984
            $newColumn->getType() !== $columns[$columnName]->getType();
985
986
        $instructions = new AlterInstructions();
987
988
        if ($columnName !== $newColumn->getName()) {
989
            $instructions->merge(
990
                $this->getRenameColumnInstructions($tableName, $columnName, $newColumn->getName())
991
            );
992
        }
993
994
        if ($changeDefault) {
995
            $instructions->merge($this->getDropDefaultConstraint($tableName, $newColumn->getName()));
996
        }
997
998
        $instructions->addPostStep(sprintf(
999
            'ALTER TABLE %s ALTER COLUMN %s %s',
1000
            $this->quoteTableName($tableName),
1001
            $this->quoteColumnName($newColumn->getName()),
1002
            $this->getColumnSqlDefinition($newColumn, false)
1003
        ));
1004
        // change column comment if needed
1005
        if ($newColumn->getComment()) {
1006
            $instructions->addPostStep($this->getColumnCommentSqlDefinition($newColumn, $tableName));
1007
        }
1008
1009
        if ($changeDefault) {
1010
            $instructions->merge($this->getChangeDefault($tableName, $newColumn));
1011
        }
1012
1013
        return $instructions;
1014
    }
1015
1016
    /**
1017
     * @inheritDoc
1018
     */
1019
    protected function getDropColumnInstructions($tableName, $columnName)
1020
    {
1021
        $instructions = $this->getDropDefaultConstraint($tableName, $columnName);
1022
1023
        $instructions->addPostStep(sprintf(
1024
            'ALTER TABLE %s DROP COLUMN %s',
1025
            $this->quoteTableName($tableName),
1026
            $this->quoteColumnName($columnName)
1027
        ));
1028
1029
        return $instructions;
1030
    }
1031
1032
    /**
1033
     * @param string $tableName Table name
1034
     * @param string|null $columnName Column name
1035
     * @return \Phinx\Db\Util\AlterInstructions
1036
     */
1037
    protected function getDropDefaultConstraint($tableName, $columnName)
1038
    {
1039
        $defaultConstraint = $this->getDefaultConstraint($tableName, $columnName);
1040
1041
        if (!$defaultConstraint) {
1042
            return new AlterInstructions();
1043
        }
1044
1045
        return $this->getDropForeignKeyInstructions($tableName, $defaultConstraint);
1046
    }
1047
1048
    /**
1049
     * @param string $tableName Table name
1050
     * @param string $columnName Column name
1051
     * @return string|false
1052
     */
1053
    protected function getDefaultConstraint($tableName, $columnName)
1054
    {
1055
        $sql = "SELECT
1056
			default_constraints.name 
1057
			FROM sys.all_columns
1058
			INNER JOIN sys.tables ON all_columns.object_id = tables.object_id
1059
			INNER JOIN sys.schemas ON tables.schema_id = schemas.schema_id
1060
			INNER JOIN sys.default_constraints
1061
			ON all_columns.default_object_id = default_constraints.object_id
1062
			WHERE schemas.name = 'dbo'
1063
			AND tables.name = '{$tableName}'
1064
			AND all_columns.name = '{$columnName}'";
1065
1066
        $rows = $this->fetchAll($sql);
1067
1068
        return empty($rows) ? false : $rows[0]['name'];
1069
    }
1070
1071
    /**
1072
     * @param int $tableId Table ID
1073
     * @param int $indexId Index ID
1074
     * @return array
1075
     */
1076
    protected function getIndexColums($tableId, $indexId)
1077
    {
1078
        $sql = "SELECT AC.[name] AS [column_name]
1079
			FROM sys.[index_columns] IC
1080
			INNER JOIN sys.[all_columns] AC ON IC.[column_id] = AC.[column_id]
1081
			WHERE AC.[object_id] = {$tableId} AND IC.[index_id] = {$indexId}  AND IC.[object_id] = {$tableId}
1082
			ORDER BY IC.[key_ordinal];";
1083
1084
        $rows = $this->fetchAll($sql);
1085
        $columns = [];
1086
        foreach ($rows as $row) {
1087
            $columns[] = strtolower($row['column_name']);
1088
        }
1089
1090
        return $columns;
1091
    }
1092
1093
    /**
1094
     * Get an array of indexes from a particular table.
1095
     *
1096
     * @param string $tableName Table name
1097
     * @return array
1098
     */
1099
    public function getIndexes($tableName)
1100
    {
1101
        $indexes = [];
1102
        $sql = "SELECT I.[name] AS [index_name], I.[index_id] as [index_id], T.[object_id] as [table_id]
1103
			FROM sys.[tables] AS T
1104
			INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
1105
			WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP'  AND T.[name] = '{$tableName}'
1106
			ORDER BY T.[name], I.[index_id];";
1107
1108
        $rows = $this->fetchAll($sql);
1109
        foreach ($rows as $row) {
1110
            $columns = $this->getIndexColums($row['table_id'], $row['index_id']);
1111
            $indexes[$row['index_name']] = ['columns' => $columns];
1112
        }
1113
1114
        return $indexes;
1115
    }
1116
1117
    /**
1118
     * @inheritDoc
1119
     */
1120
    public function hasIndex($tableName, $columns)
1121
    {
1122
        if (is_string($columns)) {
1123
            $columns = [$columns]; // str to array
1124
        }
1125
1126
        $columns = array_map('strtolower', $columns);
1127
        $indexes = $this->getIndexes($tableName);
1128
1129
        foreach ($indexes as $index) {
1130
            $a = array_diff($columns, $index['columns']);
1131
1132
            if (empty($a)) {
1133
                return true;
1134
            }
1135
        }
1136
1137
        return false;
1138
    }
1139
1140
    /**
1141
     * @inheritDoc
1142
     */
1143
    public function hasIndexByName($tableName, $indexName)
1144
    {
1145
        $indexes = $this->getIndexes($tableName);
1146
1147
        foreach ($indexes as $name => $index) {
1148
            if ($name === $indexName) {
1149
                return true;
1150
            }
1151
        }
1152
1153
        return false;
1154
    }
1155
1156
    /**
1157
     * @inheritDoc
1158
     */
1159
    protected function getAddIndexInstructions(Table $table, Index $index)
1160
    {
1161
        $sql = $this->getIndexSqlDefinition($index, $table->getName());
1162
1163
        return new AlterInstructions([], [$sql]);
1164
    }
1165
1166
    /**
1167
     * {@inheritDoc}
1168
     *
1169
     * @throws \InvalidArgumentException
1170
     */
1171
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
1172
    {
1173
        if (is_string($columns)) {
1174
            $columns = [$columns]; // str to array
1175
        }
1176
1177
        $indexes = $this->getIndexes($tableName);
1178
        $columns = array_map('strtolower', $columns);
1179
        $instructions = new AlterInstructions();
1180
1181
        foreach ($indexes as $indexName => $index) {
1182
            $a = array_diff($columns, $index['columns']);
1183
            if (empty($a)) {
1184
                $instructions->addPostStep(sprintf(
1185
                    'DROP INDEX %s ON %s',
1186
                    $this->quoteColumnName($indexName),
1187
                    $this->quoteTableName($tableName)
1188
                ));
1189
1190
                return $instructions;
1191
            }
1192
        }
1193
1194
        throw new InvalidArgumentException(sprintf(
1195
            "The specified index on columns '%s' does not exist",
1196
            implode(',', $columns)
1197
        ));
1198
    }
1199
1200
    /**
1201
     * {@inheritDoc}
1202
     *
1203
     * @throws \InvalidArgumentException
1204
     */
1205
    protected function getDropIndexByNameInstructions($tableName, $indexName)
1206
    {
1207
        $indexes = $this->getIndexes($tableName);
1208
        $instructions = new AlterInstructions();
1209
1210
        foreach ($indexes as $name => $index) {
1211
            if ($name === $indexName) {
1212
                $instructions->addPostStep(sprintf(
1213
                    'DROP INDEX %s ON %s',
1214
                    $this->quoteColumnName($indexName),
1215
                    $this->quoteTableName($tableName)
1216
                ));
1217
1218
                return $instructions;
1219
            }
1220
        }
1221
1222
        throw new InvalidArgumentException(sprintf(
1223
            "The specified index name '%s' does not exist",
1224
            $indexName
1225
        ));
1226
    }
1227
1228
    /**
1229
     * @inheritDoc
1230
     */
1231
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
1232
    {
1233
        $primaryKey = $this->getPrimaryKey($tableName);
1234
1235
        if (empty($primaryKey)) {
1236
            return false;
1237
        }
1238
1239
        if ($constraint) {
1240
            return $primaryKey['constraint'] === $constraint;
1241
        }
1242
1243
        if (is_string($columns)) {
1244
            $columns = [$columns]; // str to array
1245
        }
1246
        $missingColumns = array_diff($columns, $primaryKey['columns']);
1247
1248
        return empty($missingColumns);
1249
    }
1250
1251
    /**
1252
     * Get the primary key from a particular table.
1253
     *
1254
     * @param string $tableName Table name
1255
     * @return array
1256
     */
1257
    public function getPrimaryKey($tableName)
1258
    {
1259
        $rows = $this->fetchAll(sprintf(
1260
            "SELECT
1261
			tc.constraint_name,
1262
			kcu.column_name
1263
			FROM information_schema.table_constraints AS tc
1264
			JOIN information_schema.key_column_usage AS kcu
1265
			ON tc.constraint_name = kcu.constraint_name
1266
			WHERE constraint_type = 'PRIMARY KEY'
1267
			AND tc.table_name = '%s'
1268
			ORDER BY kcu.ordinal_position",
1269
            $tableName
1270
        ));
1271
1272
        $primaryKey = [
1273
            'columns' => [],
1274
        ];
1275
        foreach ($rows as $row) {
1276
            $primaryKey['constraint'] = $row['constraint_name'];
1277
            $primaryKey['columns'][] = $row['column_name'];
1278
        }
1279
1280
        return $primaryKey;
1281
    }
1282
1283
    /**
1284
     * @inheritDoc
1285
     */
1286
    public function hasForeignKey($tableName, $columns, $constraint = null)
1287
    {
1288
        if (is_string($columns)) {
1289
            $columns = [$columns]; // str to array
1290
        }
1291
        $foreignKeys = $this->getForeignKeys($tableName);
1292
        if ($constraint) {
1293
            if (isset($foreignKeys[$constraint])) {
1294
                return !empty($foreignKeys[$constraint]);
1295
            }
1296
1297
            return false;
1298
        }
1299
1300
        foreach ($foreignKeys as $key) {
1301
            $a = array_diff($columns, $key['columns']);
1302
            if (empty($a)) {
1303
                return true;
1304
            }
1305
        }
1306
1307
        return false;
1308
    }
1309
1310
    /**
1311
     * Get an array of foreign keys from a particular table.
1312
     *
1313
     * @param string $tableName Table name
1314
     * @return array
1315
     */
1316
    protected function getForeignKeys($tableName)
1317
    {
1318
        $foreignKeys = [];
1319
        $rows = $this->fetchAll(sprintf(
1320
            "SELECT
1321
			tc.constraint_name,
1322
			tc.table_name, kcu.column_name,
1323
			ccu.table_name AS referenced_table_name,
1324
			ccu.column_name AS referenced_column_name
1325
			FROM
1326
			information_schema.table_constraints AS tc
1327
			JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
1328
			JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
1329
			WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s'
1330
			ORDER BY kcu.ordinal_position",
1331
            $tableName
1332
        ));
1333
        foreach ($rows as $row) {
1334
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
1335
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
1336
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
1337
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
1338
        }
1339
1340
        return $foreignKeys;
1341
    }
1342
1343
    /**
1344
     * @inheritDoc
1345
     */
1346
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
1347
    {
1348
        $instructions = new AlterInstructions();
1349
        $instructions->addPostStep(sprintf(
1350
            'ALTER TABLE %s ADD %s',
1351
            $this->quoteTableName($table->getName()),
1352
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
1353
        ));
1354
1355
        return $instructions;
1356
    }
1357
1358
    /**
1359
     * @inheritDoc
1360
     */
1361
    protected function getDropForeignKeyInstructions($tableName, $constraint)
1362
    {
1363
        $instructions = new AlterInstructions();
1364
        $instructions->addPostStep(sprintf(
1365
            'ALTER TABLE %s DROP CONSTRAINT %s',
1366
            $this->quoteTableName($tableName),
1367
            $constraint
1368
        ));
1369
1370
        return $instructions;
1371
    }
1372
1373
    /**
1374
     * @inheritDoc
1375
     */
1376
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1377
    {
1378
        $instructions = new AlterInstructions();
1379
1380
        foreach ($columns as $column) {
1381
            $rows = $this->fetchAll(sprintf(
1382
                "SELECT
1383
				tc.constraint_name,
1384
				tc.table_name, kcu.column_name,
1385
				ccu.table_name AS referenced_table_name,
1386
				ccu.column_name AS referenced_column_name
1387
				FROM
1388
				information_schema.table_constraints AS tc
1389
				JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
1390
				JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
1391
				WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' and ccu.column_name='%s'
1392
				ORDER BY kcu.ordinal_position",
1393
                $tableName,
1394
                $column
1395
            ));
1396
            foreach ($rows as $row) {
1397
                $instructions->merge(
1398
                    $this->getDropForeignKeyInstructions($tableName, $row['constraint_name'])
1399
                );
1400
            }
1401
        }
1402
1403
        return $instructions;
1404
    }
1405
1406
    /**
1407
     * {@inheritDoc}
1408
     *
1409
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1410
     */
1411
    public function getSqlType($type, $limit = null)
1412
    {
1413
        switch ($type) {
1414
            case static::PHINX_TYPE_FLOAT:
1415
            case static::PHINX_TYPE_DECIMAL:
1416
            case static::PHINX_TYPE_DATETIME:
1417
            case static::PHINX_TYPE_TIME:
1418
            case static::PHINX_TYPE_DATE:
1419
                return ['name' => $type];
1420
            case static::PHINX_TYPE_STRING:
1421
                return ['name' => 'nvarchar', 'limit' => 255];
1422
            case static::PHINX_TYPE_CHAR:
1423
                return ['name' => 'nchar', 'limit' => 255];
1424
            case static::PHINX_TYPE_TEXT:
1425
                return ['name' => 'ntext'];
1426
            case static::PHINX_TYPE_INTEGER:
1427
                return ['name' => 'int'];
1428
            case static::PHINX_TYPE_SMALL_INTEGER:
1429
                return ['name' => 'smallint'];
1430
            case static::PHINX_TYPE_BIG_INTEGER:
1431
                return ['name' => 'bigint'];
1432
            case static::PHINX_TYPE_TIMESTAMP:
1433
                return ['name' => 'datetime'];
1434
            case static::PHINX_TYPE_BLOB:
1435
            case static::PHINX_TYPE_BINARY:
1436
                return ['name' => 'varbinary'];
1437
            case static::PHINX_TYPE_BOOLEAN:
1438
                return ['name' => 'bit'];
1439
            case static::PHINX_TYPE_BINARYUUID:
1440
            case static::PHINX_TYPE_UUID:
1441
                return ['name' => 'uniqueidentifier'];
1442
            case static::PHINX_TYPE_FILESTREAM:
1443
                return ['name' => 'varbinary', 'limit' => 'max'];
1444
            // Geospatial database types
1445
            case static::PHINX_TYPE_GEOMETRY:
1446
            case static::PHINX_TYPE_POINT:
1447
            case static::PHINX_TYPE_LINESTRING:
1448
            case static::PHINX_TYPE_POLYGON:
1449
                // SQL Server stores all spatial data using a single data type.
1450
                // Specific types (point, polygon, etc) are set at insert time.
1451
                return ['name' => 'geography'];
1452
            default:
1453
                throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SqlServer.');
1454
        }
1455
    }
1456
1457
    /**
1458
     * Returns Phinx type by SQL type
1459
     *
1460
     * @internal param string $sqlType SQL type
1461
     * @param string $sqlType SQL Type definition
1462
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1463
     * @return string Phinx type
1464
     */
1465
    public function getPhinxType($sqlType)
1466
    {
1467
        switch ($sqlType) {
1468
            case 'nvarchar':
1469
            case 'varchar':
1470
                return static::PHINX_TYPE_STRING;
1471
            case 'char':
1472
            case 'nchar':
1473
                return static::PHINX_TYPE_CHAR;
1474
            case 'text':
1475
            case 'ntext':
1476
                return static::PHINX_TYPE_TEXT;
1477
            case 'int':
1478
            case 'integer':
1479
                return static::PHINX_TYPE_INTEGER;
1480
            case 'decimal':
1481
            case 'numeric':
1482
            case 'money':
1483
                return static::PHINX_TYPE_DECIMAL;
1484
            case 'tinyint':
1485
                return static::PHINX_TYPE_TINY_INTEGER;
1486
            case 'smallint':
1487
                return static::PHINX_TYPE_SMALL_INTEGER;
1488
            case 'bigint':
1489
                return static::PHINX_TYPE_BIG_INTEGER;
1490
            case 'real':
1491
            case 'float':
1492
                return static::PHINX_TYPE_FLOAT;
1493
            case 'binary':
1494
            case 'image':
1495
            case 'varbinary':
1496
                return static::PHINX_TYPE_BINARY;
1497
            case 'time':
1498
                return static::PHINX_TYPE_TIME;
1499
            case 'date':
1500
                return static::PHINX_TYPE_DATE;
1501
            case 'datetime':
1502
            case 'timestamp':
1503
                return static::PHINX_TYPE_DATETIME;
1504
            case 'bit':
1505
                return static::PHINX_TYPE_BOOLEAN;
1506
            case 'uniqueidentifier':
1507
                return static::PHINX_TYPE_UUID;
1508
            case 'filestream':
1509
                return static::PHINX_TYPE_FILESTREAM;
1510
            default:
1511
                throw new UnsupportedColumnTypeException('Column type "' . $sqlType . '" is not supported by SqlServer.');
1512
        }
1513
    }
1514
1515
    /**
1516
     * @inheritDoc
1517
     */
1518
    public function createDatabase($name, $options = [])
1519
    {
1520
        if (isset($options['collation'])) {
1521
            $this->execute(sprintf('CREATE DATABASE [%s] COLLATE [%s]', $name, $options['collation']));
1522
        } else {
1523
            $this->execute(sprintf('CREATE DATABASE [%s]', $name));
1524
        }
1525
        $this->execute(sprintf('USE [%s]', $name));
1526
    }
1527
1528
    /**
1529
     * @inheritDoc
1530
     */
1531
    public function hasDatabase($name)
1532
    {
1533
        $result = $this->fetchRow(
1534
            sprintf(
1535
                "SELECT count(*) as [count] FROM master.dbo.sysdatabases WHERE [name] = '%s'",
1536
                $name
1537
            )
1538
        );
1539
1540
        return $result['count'] > 0;
1541
    }
1542
1543
    /**
1544
     * @inheritDoc
1545
     */
1546
    public function dropDatabase($name)
1547
    {
1548
        $sql = <<<SQL
1549
USE master;
1550
IF EXISTS(select * from sys.databases where name=N'$name')
1551
ALTER DATABASE [$name] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
1552
DROP DATABASE [$name];
1553
SQL;
1554
		$this->execute($sql);
1555
		$this->createdTables = [];
1556
	}
1557
1558
	/**
1559
	 * Gets the SqlServer Column Definition for a Column object.
1560
	 *
1561
	 * @param \Phinx\Db\Table\Column $column Column
1562
	 * @param bool $create Create column flag
1563
	 * @return string
1564
	 */
1565
	protected function getColumnSqlDefinition(Column $column, $create = true)
1566
	{
1567
		$buffer = [];
1568
		if ($column->getType() instanceof Literal) {
1569
			$buffer[] = (string)$column->getType();
1570
		} else {
1571
			$sqlType = $this->getSqlType($column->getType());
1572
			$buffer[] = strtoupper($sqlType['name']);
1573
			// integers cant have limits in SQlServer
1574
			$noLimits = [
1575
				'bigint',
1576
				'int',
1577
				'tinyint',
1578
			];
1579
			if ($sqlType['name'] === static::PHINX_TYPE_DECIMAL && $column->getPrecision() && $column->getScale()) {
1580
				$buffer[] = sprintf(
1581
					'(%s, %s)',
1582
					$column->getPrecision() ?: $sqlType['precision'],
1583
					$column->getScale() ?: $sqlType['scale']
1584
				);
1585
			} elseif (!in_array($sqlType['name'], $noLimits) && ($column->getLimit() || isset($sqlType['limit']))) {
1586
				$buffer[] = sprintf('(%s)', $column->getLimit() ?: $sqlType['limit']);
1587
			}
1588
		}
1589
1590
		$properties = $column->getProperties();
1591
		$buffer[] = $column->getType() === 'filestream' ? 'FILESTREAM' : '';
1592
		$buffer[] = isset($properties['rowguidcol']) ? 'ROWGUIDCOL' : '';
1593
1594
		$buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1595
1596
		if ($create === true) {
1597
			if ($column->getDefault() === null && $column->isNull()) {
1598
				$buffer[] = ' DEFAULT NULL';
1599
			} else {
1600
				$buffer[] = $this->getDefaultValueDefinition($column->getDefault());
1601
			}
1602
		}
1603
1604
		if ($column->isIdentity()) {
1605
			$seed = $column->getSeed() ?: 1;
1606
			$increment = $column->getIncrement() ?: 1;
1607
			$buffer[] = sprintf('IDENTITY(%d,%d)', $seed, $increment);
1608
		}
1609
1610
		return implode(' ', $buffer);
1611
	}
1612
1613
	/**
1614
	 * Gets the SqlServer Index Definition for an Index object.
1615
	 *
1616
	 * @param \Phinx\Db\Table\Index $index Index
1617
	 * @param string $tableName Table name
1618
	 * @return string
1619
	 */
1620
	protected function getIndexSqlDefinition(Index $index, $tableName)
1621
	{
1622
		$columnNames = $index->getColumns();
1623
		if (is_string($index->getName())) {
1624
			$indexName = $index->getName();
1625
		} else {
1626
			$indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames));
1627
		}
1628
		$order = $index->getOrder();
1629
		if(!empty($order)){
1630
			foreach ($order as $key => $value) {
1631
				$loc = array_search($key, $columnNames);
1632
				$columnNames[$loc] = sprintf('[%s] %s', $key, $value);
1633
			}
1634
		}
1635
		return sprintf(
1636
			'CREATE %s INDEX %s ON %s (%s);',
1637
			($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1638
			$indexName,
1639
			$this->quoteTableName($tableName),
1640
			implode(',', $columnNames)
1641
		);
1642
	}
1643
1644
	/**
1645
	 * Gets the SqlServer Foreign Key Definition for an ForeignKey object.
1646
	 *
1647
	 * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1648
	 * @param string $tableName Table name
1649
	 * @return string
1650
	 */
1651
	protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1652
	{
1653
		$constraintName = $foreignKey->getConstraint() ?: $tableName . '_' . implode('_', $foreignKey->getColumns());
1654
		$def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
1655
		$def .= ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")';
1656
		$def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")';
1657
		if ($foreignKey->getOnDelete()) {
1658
			$def .= " ON DELETE {$foreignKey->getOnDelete()}";
1659
		}
1660
		if ($foreignKey->getOnUpdate()) {
1661
			$def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1662
		}
1663
1664
		return $def;
1665
	}
1666
1667
	/**
1668
	 * @inheritDoc
1669
	 */
1670
	public function getColumnTypes()
1671
	{
1672
		return array_merge(parent::getColumnTypes(), static::$specificColumnTypes);
1673
	}
1674
1675
	/**
1676
	 * Records a migration being run.
1677
	 *
1678
	 * @param \Phinx\Migration\MigrationInterface $migration Migration
1679
	 * @param string $direction Direction
1680
	 * @param string $startTime Start Time
1681
	 * @param string $endTime End Time
1682
	 * @return \Phinx\Db\Adapter\AdapterInterface
1683
	 */
1684
	public function migrated(MigrationInterface $migration, $direction, $startTime, $endTime)
1685
	{
1686
		$startTime = str_replace(' ', 'T', $startTime);
1687
		$endTime = str_replace(' ', 'T', $endTime);
1688
1689
		return parent::migrated($migration, $direction, $startTime, $endTime);
1690
	}
1691
1692
	/**
1693
	 * @inheritDoc
1694
	 */
1695
	public function getDecoratedConnection()
1696
	{
1697
		$options = $this->getOptions();
1698
		$options = [
1699
			'username' => $options['user'],
1700
			'password' => $options['pass'],
1701
			'database' => $options['name'],
1702
			'quoteIdentifiers' => true,
1703
		] + $options;
1704
1705
		$driver = new SqlServerDriver($options);
1706
		$driver->setConnection($this->connection);
1707
1708
		return new Connection(['driver' => $driver] + $options);
1709
	}
1710
}
1711