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

SqlServerAdapter   F

Complexity

Total Complexity 199

Size/Duplication

Total Lines 1325
Duplicated Lines 4.68 %

Coupling/Cohesion

Components 3
Dependencies 10

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 199
lcom 3
cbo 10
dl 62
loc 1325
ccs 0
cts 785
cp 0
rs 0.8
c 0
b 0
f 0

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SqlServerAdapter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SqlServerAdapter, and based on these observations, apply Extract Interface, too.

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
	/**
33
	 * @var string[]
34
	 */
35
	protected static $specificColumnTypes = [
36
		self::PHINX_TYPE_FILESTREAM,
37
		self::PHINX_TYPE_BINARYUUID,
38
	];
39
40
	/**
41
	 * @var string
42
	 */
43
	protected $schema = 'dbo';
44
45
	/**
46
	 * @var bool[]
47
	 */
48
	protected $signedColumnTypes = [
49
		self::PHINX_TYPE_INTEGER => true,
50
		self::PHINX_TYPE_BIG_INTEGER => true,
51
		self::PHINX_TYPE_FLOAT => true,
52
		self::PHINX_TYPE_DECIMAL => true,
53
	];
54
55
	/**
56
	 * {@inheritDoc}
57
	 *
58
	 * @throws \InvalidArgumentException
59
	 * @return void
60
	 */
61
	public function connect()
62
	{
63
		if ($this->connection === null) {
64
			if (!class_exists('PDO') || !in_array('sqlsrv', PDO::getAvailableDrivers(), true)) {
65
				// try our connection via freetds (Mac/Linux)
66
				$this->connectDblib();
67
68
				return;
69
			}
70
71
			$options = $this->getOptions();
72
73
			$dsn = 'sqlsrv:server=' . $options['host'];
74
			// if port is specified use it, otherwise use the SqlServer default
75
			if (!empty($options['port'])) {
76
				$dsn .= ',' . $options['port'];
77
			}
78
			$dsn .= ';database=' . $options['name'] . ';MultipleActiveResultSets=false';
79
80
			$driverOptions = [];
81
82
			// charset support
83
			if (isset($options['charset'])) {
84
				$driverOptions[PDO::SQLSRV_ATTR_ENCODING] = $options['charset'];
85
			}
86
87
			// use custom data fetch mode
88
			if (!empty($options['fetch_mode'])) {
89
				$driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
90
			}
91
92
			// support arbitrary \PDO::SQLSRV_ATTR_* driver options and pass them to PDO
93
			// http://php.net/manual/en/ref.pdo-sqlsrv.php#pdo-sqlsrv.constants
94
			foreach ($options as $key => $option) {
95
				if (strpos($key, 'sqlsrv_attr_') === 0) {
96
					$driverOptions[constant('\PDO::' . strtoupper($key))] = $option;
97
				}
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
280
		// execute the sql
281
		$this->execute($sql);
282
283
		$this->addCreatedTable($table->getName());
284
	}
285
286
	/**
287
	 * {@inheritDoc}
288
	 *
289
	 * @throws \InvalidArgumentException
290
	 */
291
	protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
292
	{
293
		$instructions = new AlterInstructions();
294
295
		// Drop the existing primary key
296
		$primaryKey = $this->getPrimaryKey($table->getName());
297
		if (!empty($primaryKey['constraint'])) {
298
			$sql = sprintf(
299
				'DROP CONSTRAINT %s',
300
				$this->quoteColumnName($primaryKey['constraint'])
301
			);
302
			$instructions->addAlter($sql);
303
		}
304
305
		// Add the primary key(s)
306
		if (!empty($newColumns)) {
307
			$sql = sprintf(
308
				'ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (',
309
				$this->quoteTableName($table->getName()),
310
				$this->quoteColumnName('PK_' . $table->getName())
311
			);
312
			if (is_string($newColumns)) { // handle primary_key => 'id'
313
				$sql .= $this->quoteColumnName($newColumns);
314
			} elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id')
315
				$sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns));
316
			} else {
317
				throw new InvalidArgumentException(sprintf(
318
					'Invalid value for primary key: %s',
319
					json_encode($newColumns)
320
				));
321
			}
322
			$sql .= ')';
323
			$instructions->addPostStep($sql);
324
		}
325
326
		return $instructions;
327
	}
328
329
	/**
330
	 * @inheritDoc
331
	 *
332
	 * SqlServer does not implement this functionality, and so will always throw an exception if used.
333
	 * @throws \BadMethodCallException
334
	 */
335
	protected function getChangeCommentInstructions(Table $table, $newComment)
336
	{
337
		throw new BadMethodCallException('SqlServer does not have table comments');
338
	}
339
340
	/**
341
	 * Gets the SqlServer Column Comment Defininition for a column object.
342
	 *
343
	 * @param \Phinx\Db\Table\Column $column Column
344
	 * @param string $tableName Table name
345
	 * @return string
346
	 */
347
	protected function getColumnCommentSqlDefinition(Column $column, $tableName)
348
	{
349
		// passing 'null' is to remove column comment
350
		$currentComment = $this->getColumnComment($tableName, $column->getName());
351
352
		$comment = strcasecmp($column->getComment(), 'NULL') !== 0 ? $this->getConnection()->quote($column->getComment()) : '\'\'';
353
		$command = $currentComment === false ? 'sp_addextendedproperty' : 'sp_updateextendedproperty';
354
355
		return sprintf(
356
			"EXECUTE %s N'MS_Description', N%s, N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s';",
357
			$command,
358
			$comment,
359
			$this->schema,
360
			$tableName,
361
			$column->getName()
362
		);
363
	}
364
365
	/**
366
	 * @inheritDoc
367
	 */
368
	protected function getRenameTableInstructions($tableName, $newTableName)
369
	{
370
		$this->updateCreatedTableName($tableName, $newTableName);
371
		$sql = sprintf(
372
			"EXEC sp_rename '%s', '%s'",
373
			$tableName,
374
			$newTableName
375
		);
376
377
		return new AlterInstructions([], [$sql]);
378
	}
379
380
	/**
381
	 * @inheritDoc
382
	 */
383
	protected function getDropTableInstructions($tableName)
384
	{
385
		$this->removeCreatedTable($tableName);
386
		$sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
387
388
		return new AlterInstructions([], [$sql]);
389
	}
390
391
	/**
392
	 * @inheritDoc
393
	 */
394
	public function truncateTable($tableName)
395
	{
396
		$sql = sprintf(
397
			'TRUNCATE TABLE %s',
398
			$this->quoteTableName($tableName)
399
		);
400
401
		$this->execute($sql);
402
	}
403
404
	/**
405
	 * @param string $tableName Table name
406
	 * @param string $columnName Column name
407
	 * @return string|false
408
	 */
409
	public function getColumnComment($tableName, $columnName)
410
	{
411
		$sql = sprintf("SELECT cast(extended_properties.[value] as nvarchar(4000)) comment
412
			FROM sys.schemas
413
			INNER JOIN sys.tables
414
			ON schemas.schema_id = tables.schema_id
415
			INNER JOIN sys.columns
416
			ON tables.object_id = columns.object_id
417
			INNER JOIN sys.extended_properties
418
			ON tables.object_id = extended_properties.major_id
419
			AND columns.column_id = extended_properties.minor_id
420
			AND extended_properties.name = 'MS_Description'
421
			WHERE schemas.[name] = '%s' AND tables.[name] = '%s' AND columns.[name] = '%s'", $this->schema, $tableName, $columnName);
422
		$row = $this->fetchRow($sql);
423
424
		if ($row) {
425
			return trim($row['comment']);
426
		}
427
428
		return false;
429
	}
430
431
	/**
432
	 * @inheritDoc
433
	 */
434
	public function getColumns($tableName)
435
	{
436
		$columns = [];
437
		$sql = sprintf(
438
			"SELECT DISTINCT TABLE_SCHEMA AS [schema], TABLE_NAME as [table_name], COLUMN_NAME AS [name], DATA_TYPE AS [type],
439
			IS_NULLABLE AS [null], COLUMN_DEFAULT AS [default],
440
			CHARACTER_MAXIMUM_LENGTH AS [char_length],
441
			NUMERIC_PRECISION AS [precision],
442
			NUMERIC_SCALE AS [scale], ORDINAL_POSITION AS [ordinal_position],
443
			COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as [identity]
444
			FROM INFORMATION_SCHEMA.COLUMNS
445
			WHERE TABLE_NAME = '%s'
446
			ORDER BY ordinal_position",
447
			$tableName
448
		);
449
		$rows = $this->fetchAll($sql);
450
		foreach ($rows as $columnInfo) {
451
			try {
452
				$type = $this->getPhinxType($columnInfo['type']);
453
			} catch (UnsupportedColumnTypeException $e) {
454
				$type = Literal::from($columnInfo['type']);
455
			}
456
457
			$column = new Column();
458
			$column->setName($columnInfo['name'])
459
	  ->setType($type)
460
	  ->setNull($columnInfo['null'] !== 'NO')
461
	  ->setDefault($this->parseDefault($columnInfo['default']))
462
	  ->setIdentity($columnInfo['identity'] === '1')
463
	  ->setComment($this->getColumnComment($columnInfo['table_name'], $columnInfo['name']));
464
465
			if (!empty($columnInfo['char_length'])) {
466
				$column->setLimit($columnInfo['char_length']);
467
			}
468
469
			$columns[$columnInfo['name']] = $column;
470
		}
471
472
		return $columns;
473
	}
474
475
	/**
476
	 * @param string $default Default
477
	 * @return int|string|null
478
	 */
479
	protected function parseDefault($default)
480
	{
481
		$result = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default);
482
483
		if (strtoupper($result) === 'NULL') {
484
			$result = null;
485
		} elseif (is_numeric($result)) {
486
			$result = (int)$result;
487
		}
488
489
		return $result;
490
	}
491
492
	/**
493
	 * @inheritDoc
494
	 */
495
	public function hasColumn($tableName, $columnName)
496
	{
497
		$sql = sprintf(
498
			"SELECT count(*) as [count]
499
			FROM information_schema.columns
500
			WHERE table_name = '%s' AND column_name = '%s'",
501
			$tableName,
502
			$columnName
503
		);
504
		$result = $this->fetchRow($sql);
505
506
		return $result['count'] > 0;
507
	}
508
509
	/**
510
	 * @inheritDoc
511
	 */
512
	protected function getAddColumnInstructions(Table $table, Column $column)
513
	{
514
		$alter = sprintf(
515
			'ALTER TABLE %s ADD %s %s',
516
			$table->getName(),
517
			$this->quoteColumnName($column->getName()),
518
			$this->getColumnSqlDefinition($column)
519
		);
520
521
		return new AlterInstructions([], [$alter]);
522
	}
523
524
	/**
525
	 * {@inheritDoc}
526
	 *
527
	 * @throws \InvalidArgumentException
528
	 */
529
	protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
530
	{
531
		if (!$this->hasColumn($tableName, $columnName)) {
532
			throw new InvalidArgumentException("The specified column does not exist: $columnName");
533
		}
534
535
		$instructions = new AlterInstructions();
536
537
		$oldConstraintName = "DF_{$tableName}_{$columnName}";
538
		$newConstraintName = "DF_{$tableName}_{$newColumnName}";
539
		$sql = <<<SQL
540
		       IF (OBJECT_ID('$oldConstraintName', 'D') IS NOT NULL)
541
		       BEGIN
542
		       EXECUTE sp_rename N'%s', N'%s', N'OBJECT'
543
		       END
544
SQL;
545
		$instructions->addPostStep(sprintf(
546
			$sql,
547
			$oldConstraintName,
548
			$newConstraintName
549
		));
550
551
		$instructions->addPostStep(sprintf(
552
			"EXECUTE sp_rename N'%s.%s', N'%s', 'COLUMN' ",
553
			$tableName,
554
			$columnName,
555
			$newColumnName
556
		));
557
558
		return $instructions;
559
	}
560
561
	/**
562
	 * Returns the instructions to change a column default value
563
	 *
564
	 * @param string $tableName The table where the column is
565
	 * @param \Phinx\Db\Table\Column $newColumn The column to alter
566
	 * @return \Phinx\Db\Util\AlterInstructions
567
	 */
568
	protected function getChangeDefault($tableName, Column $newColumn)
569
	{
570
		$constraintName = "DF_{$tableName}_{$newColumn->getName()}";
571
		$default = $newColumn->getDefault();
572
		$instructions = new AlterInstructions();
573
574
		if ($default === null) {
575
			$default = 'DEFAULT NULL';
576
		} else {
577
			$default = ltrim($this->getDefaultValueDefinition($default));
578
		}
579
580
		if (empty($default)) {
581
			return $instructions;
582
		}
583
584
		$instructions->addPostStep(sprintf(
585
			'ALTER TABLE %s ADD CONSTRAINT %s %s FOR %s',
586
			$this->quoteTableName($tableName),
587
			$constraintName,
588
			$default,
589
			$this->quoteColumnName($newColumn->getName())
590
		));
591
592
		return $instructions;
593
	}
594
595
	/**
596
	 * @inheritDoc
597
	 */
598
	protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
599
	{
600
		$columns = $this->getColumns($tableName);
601
		$changeDefault =
602
			$newColumn->getDefault() !== $columns[$columnName]->getDefault() ||
603
			$newColumn->getType() !== $columns[$columnName]->getType();
604
605
		$instructions = new AlterInstructions();
606
607
		if ($columnName !== $newColumn->getName()) {
608
			$instructions->merge(
609
				$this->getRenameColumnInstructions($tableName, $columnName, $newColumn->getName())
610
			);
611
		}
612
613
		if ($changeDefault) {
614
			$instructions->merge($this->getDropDefaultConstraint($tableName, $newColumn->getName()));
615
		}
616
617
		$instructions->addPostStep(sprintf(
618
			'ALTER TABLE %s ALTER COLUMN %s %s',
619
			$this->quoteTableName($tableName),
620
			$this->quoteColumnName($newColumn->getName()),
621
			$this->getColumnSqlDefinition($newColumn, false)
622
		));
623
		// change column comment if needed
624
		if ($newColumn->getComment()) {
625
			$instructions->addPostStep($this->getColumnCommentSqlDefinition($newColumn, $tableName));
626
		}
627
628
		if ($changeDefault) {
629
			$instructions->merge($this->getChangeDefault($tableName, $newColumn));
630
		}
631
632
		return $instructions;
633
	}
634
635
	/**
636
	 * @inheritDoc
637
	 */
638
	protected function getDropColumnInstructions($tableName, $columnName)
639
	{
640
		$instructions = $this->getDropDefaultConstraint($tableName, $columnName);
641
642
		$instructions->addPostStep(sprintf(
643
			'ALTER TABLE %s DROP COLUMN %s',
644
			$this->quoteTableName($tableName),
645
			$this->quoteColumnName($columnName)
646
		));
647
648
		return $instructions;
649
	}
650
651
	/**
652
	 * @param string $tableName Table name
653
	 * @param string|null $columnName Column name
654
	 * @return \Phinx\Db\Util\AlterInstructions
655
	 */
656
	protected function getDropDefaultConstraint($tableName, $columnName)
657
	{
658
		$defaultConstraint = $this->getDefaultConstraint($tableName, $columnName);
659
660
		if (!$defaultConstraint) {
661
			return new AlterInstructions();
662
		}
663
664
		return $this->getDropForeignKeyInstructions($tableName, $defaultConstraint);
665
	}
666
667
	/**
668
	 * @param string $tableName Table name
669
	 * @param string $columnName Column name
670
	 * @return string|false
671
	 */
672
	protected function getDefaultConstraint($tableName, $columnName)
673
	{
674
		$sql = "SELECT
675
			default_constraints.name 
676
			FROM sys.all_columns
677
			INNER JOIN sys.tables ON all_columns.object_id = tables.object_id
678
			INNER JOIN sys.schemas ON tables.schema_id = schemas.schema_id
679
			INNER JOIN sys.default_constraints
680
			ON all_columns.default_object_id = default_constraints.object_id
681
			WHERE schemas.name = 'dbo'
682
			AND tables.name = '{$tableName}'
683
			AND all_columns.name = '{$columnName}'";
684
685
		$rows = $this->fetchAll($sql);
686
687
		return empty($rows) ? false : $rows[0]['name'];
688
	}
689
690
	/**
691
	 * @param int $tableId Table ID
692
	 * @param int $indexId Index ID
693
	 * @return array
694
	 */
695
	protected function getIndexColums($tableId, $indexId)
696
	{
697
		$sql = "SELECT AC.[name] AS [column_name]
698
			FROM sys.[index_columns] IC
699
			INNER JOIN sys.[all_columns] AC ON IC.[column_id] = AC.[column_id]
700
			WHERE AC.[object_id] = {$tableId} AND IC.[index_id] = {$indexId}  AND IC.[object_id] = {$tableId}
701
			ORDER BY IC.[key_ordinal];";
702
703
		$rows = $this->fetchAll($sql);
704
		$columns = [];
705
		foreach ($rows as $row) {
706
			$columns[] = strtolower($row['column_name']);
707
		}
708
709
		return $columns;
710
	}
711
712
	/**
713
	 * Get an array of indexes from a particular table.
714
	 *
715
	 * @param string $tableName Table name
716
	 * @return array
717
	 */
718
	public function getIndexes($tableName)
719
	{
720
		$indexes = [];
721
		$sql = "SELECT I.[name] AS [index_name], I.[index_id] as [index_id], T.[object_id] as [table_id]
722
			FROM sys.[tables] AS T
723
			INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
724
			WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP'  AND T.[name] = '{$tableName}'
725
			ORDER BY T.[name], I.[index_id];";
726
727
		$rows = $this->fetchAll($sql);
728
		foreach ($rows as $row) {
729
			$columns = $this->getIndexColums($row['table_id'], $row['index_id']);
730
			$indexes[$row['index_name']] = ['columns' => $columns];
731
		}
732
733
		return $indexes;
734
	}
735
736
	/**
737
	 * @inheritDoc
738
	 */
739
	public function hasIndex($tableName, $columns)
740
	{
741
		if (is_string($columns)) {
742
			$columns = [$columns]; // str to array
743
		}
744
745
		$columns = array_map('strtolower', $columns);
746
		$indexes = $this->getIndexes($tableName);
747
748
		foreach ($indexes as $index) {
749
			$a = array_diff($columns, $index['columns']);
750
751
			if (empty($a)) {
752
				return true;
753
			}
754
		}
755
756
		return false;
757
	}
758
759
	/**
760
	 * @inheritDoc
761
	 */
762
	public function hasIndexByName($tableName, $indexName)
763
	{
764
		$indexes = $this->getIndexes($tableName);
765
766
		foreach ($indexes as $name => $index) {
767
			if ($name === $indexName) {
768
				return true;
769
			}
770
		}
771
772
		return false;
773
	}
774
775
	/**
776
	 * @inheritDoc
777
	 */
778
	protected function getAddIndexInstructions(Table $table, Index $index)
779
	{
780
		$sql = $this->getIndexSqlDefinition($index, $table->getName());
781
782
		return new AlterInstructions([], [$sql]);
783
	}
784
785
	/**
786
	 * {@inheritDoc}
787
	 *
788
	 * @throws \InvalidArgumentException
789
	 */
790
	protected function getDropIndexByColumnsInstructions($tableName, $columns)
791
	{
792
		if (is_string($columns)) {
793
			$columns = [$columns]; // str to array
794
		}
795
796
		$indexes = $this->getIndexes($tableName);
797
		$columns = array_map('strtolower', $columns);
798
		$instructions = new AlterInstructions();
799
800
		foreach ($indexes as $indexName => $index) {
801
			$a = array_diff($columns, $index['columns']);
802
			if (empty($a)) {
803
				$instructions->addPostStep(sprintf(
804
					'DROP INDEX %s ON %s',
805
					$this->quoteColumnName($indexName),
806
					$this->quoteTableName($tableName)
807
				));
808
809
				return $instructions;
810
			}
811
		}
812
813
		throw new InvalidArgumentException(sprintf(
814
			"The specified index on columns '%s' does not exist",
815
			implode(',', $columns)
816
		));
817
	}
818
819
	/**
820
	 * {@inheritDoc}
821
	 *
822
	 * @throws \InvalidArgumentException
823
	 */
824
	protected function getDropIndexByNameInstructions($tableName, $indexName)
825
	{
826
		$indexes = $this->getIndexes($tableName);
827
		$instructions = new AlterInstructions();
828
829
		foreach ($indexes as $name => $index) {
830
			if ($name === $indexName) {
831
				$instructions->addPostStep(sprintf(
832
					'DROP INDEX %s ON %s',
833
					$this->quoteColumnName($indexName),
834
					$this->quoteTableName($tableName)
835
				));
836
837
				return $instructions;
838
			}
839
		}
840
841
		throw new InvalidArgumentException(sprintf(
842
			"The specified index name '%s' does not exist",
843
			$indexName
844
		));
845
	}
846
847
	/**
848
	 * @inheritDoc
849
	 */
850
	public function hasPrimaryKey($tableName, $columns, $constraint = null)
851
	{
852
		$primaryKey = $this->getPrimaryKey($tableName);
853
854
		if (empty($primaryKey)) {
855
			return false;
856
		}
857
858
		if ($constraint) {
859
			return $primaryKey['constraint'] === $constraint;
860
		}
861
862
		if (is_string($columns)) {
863
			$columns = [$columns]; // str to array
864
		}
865
		$missingColumns = array_diff($columns, $primaryKey['columns']);
866
867
		return empty($missingColumns);
868
	}
869
870
	/**
871
	 * Get the primary key from a particular table.
872
	 *
873
	 * @param string $tableName Table name
874
	 * @return array
875
	 */
876
	public function getPrimaryKey($tableName)
877
	{
878
		$rows = $this->fetchAll(sprintf(
879
			"SELECT
880
			tc.constraint_name,
881
			kcu.column_name
882
			FROM information_schema.table_constraints AS tc
883
			JOIN information_schema.key_column_usage AS kcu
884
			ON tc.constraint_name = kcu.constraint_name
885
			WHERE constraint_type = 'PRIMARY KEY'
886
			AND tc.table_name = '%s'
887
			ORDER BY kcu.ordinal_position",
888
			$tableName
889
		));
890
891
		$primaryKey = [
892
			'columns' => [],
893
		];
894
		foreach ($rows as $row) {
895
			$primaryKey['constraint'] = $row['constraint_name'];
896
			$primaryKey['columns'][] = $row['column_name'];
897
		}
898
899
		return $primaryKey;
900
	}
901
902
	/**
903
	 * @inheritDoc
904
	 */
905
	public function hasForeignKey($tableName, $columns, $constraint = null)
906
	{
907
		if (is_string($columns)) {
908
			$columns = [$columns]; // str to array
909
		}
910
		$foreignKeys = $this->getForeignKeys($tableName);
911
		if ($constraint) {
912
			if (isset($foreignKeys[$constraint])) {
913
				return !empty($foreignKeys[$constraint]);
914
			}
915
916
			return false;
917
		}
918
919
		foreach ($foreignKeys as $key) {
920
			$a = array_diff($columns, $key['columns']);
921
			if (empty($a)) {
922
				return true;
923
			}
924
		}
925
926
		return false;
927
	}
928
929
	/**
930
	 * Get an array of foreign keys from a particular table.
931
	 *
932
	 * @param string $tableName Table name
933
	 * @return array
934
	 */
935
	protected function getForeignKeys($tableName)
936
	{
937
		$foreignKeys = [];
938
		$rows = $this->fetchAll(sprintf(
939
			"SELECT
940
			tc.constraint_name,
941
			tc.table_name, kcu.column_name,
942
			ccu.table_name AS referenced_table_name,
943
			ccu.column_name AS referenced_column_name
944
			FROM
945
			information_schema.table_constraints AS tc
946
			JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
947
			JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
948
			WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s'
949
			ORDER BY kcu.ordinal_position",
950
			$tableName
951
		));
952
		foreach ($rows as $row) {
953
			$foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
954
			$foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
955
			$foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
956
			$foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
957
		}
958
959
		return $foreignKeys;
960
	}
961
962
	/**
963
	 * @inheritDoc
964
	 */
965
	protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
966
	{
967
		$instructions = new AlterInstructions();
968
		$instructions->addPostStep(sprintf(
969
			'ALTER TABLE %s ADD %s',
970
			$this->quoteTableName($table->getName()),
971
			$this->getForeignKeySqlDefinition($foreignKey, $table->getName())
972
		));
973
974
		return $instructions;
975
	}
976
977
	/**
978
	 * @inheritDoc
979
	 */
980
	protected function getDropForeignKeyInstructions($tableName, $constraint)
981
	{
982
		$instructions = new AlterInstructions();
983
		$instructions->addPostStep(sprintf(
984
			'ALTER TABLE %s DROP CONSTRAINT %s',
985
			$this->quoteTableName($tableName),
986
			$constraint
987
		));
988
989
		return $instructions;
990
	}
991
992
	/**
993
	 * @inheritDoc
994
	 */
995
	protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
996
	{
997
		$instructions = new AlterInstructions();
998
999
		foreach ($columns as $column) {
1000
			$rows = $this->fetchAll(sprintf(
1001
				"SELECT
1002
				tc.constraint_name,
1003
				tc.table_name, kcu.column_name,
1004
				ccu.table_name AS referenced_table_name,
1005
				ccu.column_name AS referenced_column_name
1006
				FROM
1007
				information_schema.table_constraints AS tc
1008
				JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
1009
				JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
1010
				WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' and ccu.column_name='%s'
1011
				ORDER BY kcu.ordinal_position",
1012
			$tableName,
1013
			$column
1014
		));
1015
		foreach ($rows as $row) {
1016
			$instructions->merge(
1017
				$this->getDropForeignKeyInstructions($tableName, $row['constraint_name'])
1018
			);
1019
		}
1020
		}
1021
1022
		return $instructions;
1023
	}
1024
1025
	/**
1026
	 * {@inheritDoc}
1027
	 *
1028
	 * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1029
	 */
1030
	public function getSqlType($type, $limit = null)
1031
	{
1032
		switch ($type) {
1033
		case static::PHINX_TYPE_FLOAT:
1034
		case static::PHINX_TYPE_DECIMAL:
1035
		case static::PHINX_TYPE_DATETIME:
1036
		case static::PHINX_TYPE_TIME:
1037
		case static::PHINX_TYPE_DATE:
1038
			return ['name' => $type];
1039
		case static::PHINX_TYPE_STRING:
1040
			return ['name' => 'nvarchar', 'limit' => 255];
1041
		case static::PHINX_TYPE_CHAR:
1042
			return ['name' => 'nchar', 'limit' => 255];
1043
		case static::PHINX_TYPE_TEXT:
1044
			return ['name' => 'ntext'];
1045
		case static::PHINX_TYPE_INTEGER:
1046
			return ['name' => 'int'];
1047
		case static::PHINX_TYPE_SMALL_INTEGER:
1048
			return ['name' => 'smallint'];
1049
		case static::PHINX_TYPE_BIG_INTEGER:
1050
			return ['name' => 'bigint'];
1051
		case static::PHINX_TYPE_TIMESTAMP:
1052
			return ['name' => 'datetime'];
1053
		case static::PHINX_TYPE_BLOB:
1054
		case static::PHINX_TYPE_BINARY:
1055
			return ['name' => 'varbinary'];
1056
		case static::PHINX_TYPE_BOOLEAN:
1057
			return ['name' => 'bit'];
1058
		case static::PHINX_TYPE_BINARYUUID:
1059
		case static::PHINX_TYPE_UUID:
1060
			return ['name' => 'uniqueidentifier'];
1061
		case static::PHINX_TYPE_FILESTREAM:
1062
			return ['name' => 'varbinary', 'limit' => 'max'];
1063
			// Geospatial database types
1064
		case static::PHINX_TYPE_GEOMETRY:
1065
		case static::PHINX_TYPE_POINT:
1066
		case static::PHINX_TYPE_LINESTRING:
1067
		case static::PHINX_TYPE_POLYGON:
1068
			// SQL Server stores all spatial data using a single data type.
1069
			// Specific types (point, polygon, etc) are set at insert time.
1070
			return ['name' => 'geography'];
1071
		default:
1072
			throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SqlServer.');
1073
		}
1074
	}
1075
1076
	/**
1077
	 * Returns Phinx type by SQL type
1078
	 *
1079
	 * @internal param string $sqlType SQL type
1080
	 * @param string $sqlType SQL Type definition
1081
	 * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1082
	 * @return string Phinx type
1083
	 */
1084
	public function getPhinxType($sqlType)
1085
	{
1086
		switch ($sqlType) {
1087
		case 'nvarchar':
1088
		case 'varchar':
1089
			return static::PHINX_TYPE_STRING;
1090
		case 'char':
1091
		case 'nchar':
1092
			return static::PHINX_TYPE_CHAR;
1093
		case 'text':
1094
		case 'ntext':
1095
			return static::PHINX_TYPE_TEXT;
1096
		case 'int':
1097
		case 'integer':
1098
			return static::PHINX_TYPE_INTEGER;
1099
		case 'decimal':
1100
		case 'numeric':
1101
		case 'money':
1102
			return static::PHINX_TYPE_DECIMAL;
1103
		case 'tinyint':
1104
			return static::PHINX_TYPE_TINY_INTEGER;
1105
		case 'smallint':
1106
			return static::PHINX_TYPE_SMALL_INTEGER;
1107
		case 'bigint':
1108
			return static::PHINX_TYPE_BIG_INTEGER;
1109
		case 'real':
1110
		case 'float':
1111
			return static::PHINX_TYPE_FLOAT;
1112
		case 'binary':
1113
		case 'image':
1114
		case 'varbinary':
1115
			return static::PHINX_TYPE_BINARY;
1116
		case 'time':
1117
			return static::PHINX_TYPE_TIME;
1118
		case 'date':
1119
			return static::PHINX_TYPE_DATE;
1120
		case 'datetime':
1121
		case 'timestamp':
1122
			return static::PHINX_TYPE_DATETIME;
1123
		case 'bit':
1124
			return static::PHINX_TYPE_BOOLEAN;
1125
		case 'uniqueidentifier':
1126
			return static::PHINX_TYPE_UUID;
1127
		case 'filestream':
1128
			return static::PHINX_TYPE_FILESTREAM;
1129
		default:
1130
			throw new UnsupportedColumnTypeException('Column type "' . $sqlType . '" is not supported by SqlServer.');
1131
		}
1132
	}
1133
1134
	/**
1135
	 * @inheritDoc
1136
	 */
1137
	public function createDatabase($name, $options = [])
1138
	{
1139
		if (isset($options['collation'])) {
1140
			$this->execute(sprintf('CREATE DATABASE [%s] COLLATE [%s]', $name, $options['collation']));
1141
		} else {
1142
			$this->execute(sprintf('CREATE DATABASE [%s]', $name));
1143
		}
1144
		$this->execute(sprintf('USE [%s]', $name));
1145
	}
1146
1147
	/**
1148
	 * @inheritDoc
1149
	 */
1150
	public function hasDatabase($name)
1151
	{
1152
		$result = $this->fetchRow(
1153
			sprintf(
1154
				"SELECT count(*) as [count] FROM master.dbo.sysdatabases WHERE [name] = '%s'",
1155
				$name
1156
			)
1157
		);
1158
1159
		return $result['count'] > 0;
1160
	}
1161
1162
	/**
1163
	 * @inheritDoc
1164
	 */
1165
	public function dropDatabase($name)
1166
	{
1167
		$sql = <<<SQL
1168
USE master;
1169
IF EXISTS(select * from sys.databases where name=N'$name')
1170
ALTER DATABASE [$name] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
1171
DROP DATABASE [$name];
1172
SQL;
1173
		$this->execute($sql);
1174
		$this->createdTables = [];
1175
	}
1176
1177
	/**
1178
	 * Gets the SqlServer Column Definition for a Column object.
1179
	 *
1180
	 * @param \Phinx\Db\Table\Column $column Column
1181
	 * @param bool $create Create column flag
1182
	 * @return string
1183
	 */
1184
	protected function getColumnSqlDefinition(Column $column, $create = true)
1185
	{
1186
		$buffer = [];
1187
		if ($column->getType() instanceof Literal) {
1188
			$buffer[] = (string)$column->getType();
1189
		} else {
1190
			$sqlType = $this->getSqlType($column->getType());
1191
			$buffer[] = strtoupper($sqlType['name']);
1192
			// integers cant have limits in SQlServer
1193
			$noLimits = [
1194
				'bigint',
1195
				'int',
1196
				'tinyint',
1197
			];
1198
			if ($sqlType['name'] === static::PHINX_TYPE_DECIMAL && $column->getPrecision() && $column->getScale()) {
1199
				$buffer[] = sprintf(
1200
					'(%s, %s)',
1201
					$column->getPrecision() ?: $sqlType['precision'],
1202
					$column->getScale() ?: $sqlType['scale']
1203
				);
1204
			} elseif (!in_array($sqlType['name'], $noLimits) && ($column->getLimit() || isset($sqlType['limit']))) {
1205
				$buffer[] = sprintf('(%s)', $column->getLimit() ?: $sqlType['limit']);
1206
			}
1207
		}
1208
1209
		$properties = $column->getProperties();
1210
		$buffer[] = $column->getType() === 'filestream' ? 'FILESTREAM' : '';
1211
		$buffer[] = isset($properties['rowguidcol']) ? 'ROWGUIDCOL' : '';
1212
1213
		$buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1214
1215
		if ($create === true) {
1216
			if ($column->getDefault() === null && $column->isNull()) {
1217
				$buffer[] = ' DEFAULT NULL';
1218
			} else {
1219
				$buffer[] = $this->getDefaultValueDefinition($column->getDefault());
1220
			}
1221
		}
1222
1223
		if ($column->isIdentity()) {
1224
			$seed = $column->getSeed() ?: 1;
1225
			$increment = $column->getIncrement() ?: 1;
1226
			$buffer[] = sprintf('IDENTITY(%d,%d)', $seed, $increment);
1227
		}
1228
1229
		return implode(' ', $buffer);
1230
	}
1231
1232
	/**
1233
	 * Gets the SqlServer Index Definition for an Index object.
1234
	 *
1235
	 * @param \Phinx\Db\Table\Index $index Index
1236
	 * @param string $tableName Table name
1237
	 * @return string
1238
	 */
1239
	protected function getIndexSqlDefinition(Index $index, $tableName)
1240
	{
1241
		$columnNames = $index->getColumns();
1242
		if (is_string($index->getName())) {
1243
			$indexName = $index->getName();
1244
		} else {
1245
			$indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames));
1246
		}
1247
		$order = $index->getOrder()
1248
			if(!empty($order)){
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_IF
Loading history...
1249
				foreach ($order as $key => $value) {
1250
					$loc = array_search($key, $columnNames);
1251
					$columnNames[$loc] = sprintf('[%s] %s', $key, $value);
1252
				}
1253
			}
1254
		return sprintf(
1255
			'CREATE %s INDEX %s ON %s (%s);',
1256
			($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1257
			$indexName,
1258
			$this->quoteTableName($tableName),
1259
			implode(',', $index->getColumns())
1260
		);
1261
	}
1262
1263
	/**
1264
	 * Gets the SqlServer Foreign Key Definition for an ForeignKey object.
1265
	 *
1266
	 * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1267
	 * @param string $tableName Table name
1268
	 * @return string
1269
	 */
1270
	protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1271
	{
1272
		$constraintName = $foreignKey->getConstraint() ?: $tableName . '_' . implode('_', $foreignKey->getColumns());
1273
		$def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
1274
		$def .= ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")';
1275
		$def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")';
1276
		if ($foreignKey->getOnDelete()) {
1277
			$def .= " ON DELETE {$foreignKey->getOnDelete()}";
1278
		}
1279
		if ($foreignKey->getOnUpdate()) {
1280
			$def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1281
		}
1282
1283
		return $def;
1284
	}
1285
1286
	/**
1287
	 * @inheritDoc
1288
	 */
1289
	public function getColumnTypes()
1290
	{
1291
		return array_merge(parent::getColumnTypes(), static::$specificColumnTypes);
1292
	}
1293
1294
	/**
1295
	 * Records a migration being run.
1296
	 *
1297
	 * @param \Phinx\Migration\MigrationInterface $migration Migration
1298
	 * @param string $direction Direction
1299
	 * @param string $startTime Start Time
1300
	 * @param string $endTime End Time
1301
	 * @return \Phinx\Db\Adapter\AdapterInterface
1302
	 */
1303
	public function migrated(MigrationInterface $migration, $direction, $startTime, $endTime)
1304
	{
1305
		$startTime = str_replace(' ', 'T', $startTime);
1306
		$endTime = str_replace(' ', 'T', $endTime);
1307
1308
		return parent::migrated($migration, $direction, $startTime, $endTime);
1309
	}
1310
1311
	/**
1312
	 * @inheritDoc
1313
	 */
1314
	public function getDecoratedConnection()
1315
	{
1316
		$options = $this->getOptions();
1317
		$options = [
1318
			'username' => $options['user'],
1319
			'password' => $options['pass'],
1320
			'database' => $options['name'],
1321
			'quoteIdentifiers' => true,
1322
		] + $options;
1323
1324
		$driver = new SqlServerDriver($options);
1325
		$driver->setConnection($this->connection);
1326
1327
		return new Connection(['driver' => $driver] + $options);
1328
	}
1329
}
1330