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

SqlServerAdapter::getPhinxType()   D

Complexity

Conditions 27
Paths 27

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 756

Importance

Changes 0
Metric Value
dl 0
loc 49
ccs 0
cts 15
cp 0
rs 4.1666
c 0
b 0
f 0
cc 27
nc 27
nop 1
crap 756

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
    /**
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
677
sys.all_columns
678
679
INNER JOIN
680
sys.tables
681
ON all_columns.object_id = tables.object_id
682
683
INNER JOIN
684
sys.schemas
685
ON tables.schema_id = schemas.schema_id
686
687
INNER JOIN
688
sys.default_constraints
689
ON all_columns.default_object_id = default_constraints.object_id
690
691
WHERE
692
schemas.name = 'dbo'
693
AND tables.name = '{$tableName}'
694
AND all_columns.name = '{$columnName}'";
695
696
        $rows = $this->fetchAll($sql);
697
698
        return empty($rows) ? false : $rows[0]['name'];
699
    }
700
701
    /**
702
     * @param int $tableId Table ID
703
     * @param int $indexId Index ID
704
     * @return array
705
     */
706
    protected function getIndexColums($tableId, $indexId)
707
    {
708
        $sql = "SELECT AC.[name] AS [column_name]
709
			FROM sys.[index_columns] IC
710
			INNER JOIN sys.[all_columns] AC ON IC.[column_id] = AC.[column_id]
711
			WHERE AC.[object_id] = {$tableId} AND IC.[index_id] = {$indexId}  AND IC.[object_id] = {$tableId}
712
			ORDER BY IC.[key_ordinal];";
713
714
        $rows = $this->fetchAll($sql);
715
        $columns = [];
716
        foreach ($rows as $row) {
717
            $columns[] = strtolower($row['column_name']);
718
        }
719
720
        return $columns;
721
    }
722
723
    /**
724
     * Get an array of indexes from a particular table.
725
     *
726
     * @param string $tableName Table name
727
     * @return array
728
     */
729
    public function getIndexes($tableName)
730
    {
731
        $indexes = [];
732
        $sql = "SELECT I.[name] AS [index_name], I.[index_id] as [index_id], T.[object_id] as [table_id]
733
			FROM sys.[tables] AS T
734
			INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
735
			WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP'  AND T.[name] = '{$tableName}'
736
			ORDER BY T.[name], I.[index_id];";
737
738
        $rows = $this->fetchAll($sql);
739
        foreach ($rows as $row) {
740
            $columns = $this->getIndexColums($row['table_id'], $row['index_id']);
741
            $indexes[$row['index_name']] = ['columns' => $columns];
742
        }
743
744
        return $indexes;
745
    }
746
747
    /**
748
     * @inheritDoc
749
     */
750
    public function hasIndex($tableName, $columns)
751
    {
752
        if (is_string($columns)) {
753
            $columns = [$columns]; // str to array
754
        }
755
756
        $columns = array_map('strtolower', $columns);
757
        $indexes = $this->getIndexes($tableName);
758
759
        foreach ($indexes as $index) {
760
            $a = array_diff($columns, $index['columns']);
761
762
            if (empty($a)) {
763
                return true;
764
            }
765
        }
766
767
        return false;
768
    }
769
770
    /**
771
     * @inheritDoc
772
     */
773
    public function hasIndexByName($tableName, $indexName)
774
    {
775
        $indexes = $this->getIndexes($tableName);
776
777
        foreach ($indexes as $name => $index) {
778
            if ($name === $indexName) {
779
                return true;
780
            }
781
        }
782
783
        return false;
784
    }
785
786
    /**
787
     * @inheritDoc
788
     */
789
    protected function getAddIndexInstructions(Table $table, Index $index)
790
    {
791
        $sql = $this->getIndexSqlDefinition($index, $table->getName());
792
793
        return new AlterInstructions([], [$sql]);
794
    }
795
796
    /**
797
     * {@inheritDoc}
798
     *
799
     * @throws \InvalidArgumentException
800
     */
801
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
802
    {
803
        if (is_string($columns)) {
804
            $columns = [$columns]; // str to array
805
        }
806
807
        $indexes = $this->getIndexes($tableName);
808
        $columns = array_map('strtolower', $columns);
809
        $instructions = new AlterInstructions();
810
811
        foreach ($indexes as $indexName => $index) {
812
            $a = array_diff($columns, $index['columns']);
813
            if (empty($a)) {
814
                $instructions->addPostStep(sprintf(
815
                    'DROP INDEX %s ON %s',
816
                    $this->quoteColumnName($indexName),
817
                    $this->quoteTableName($tableName)
818
                ));
819
820
                return $instructions;
821
            }
822
        }
823
824
        throw new InvalidArgumentException(sprintf(
825
            "The specified index on columns '%s' does not exist",
826
            implode(',', $columns)
827
        ));
828
    }
829
830
    /**
831
     * {@inheritDoc}
832
     *
833
     * @throws \InvalidArgumentException
834
     */
835
    protected function getDropIndexByNameInstructions($tableName, $indexName)
836
    {
837
        $indexes = $this->getIndexes($tableName);
838
        $instructions = new AlterInstructions();
839
840
        foreach ($indexes as $name => $index) {
841
            if ($name === $indexName) {
842
                $instructions->addPostStep(sprintf(
843
                    'DROP INDEX %s ON %s',
844
                    $this->quoteColumnName($indexName),
845
                    $this->quoteTableName($tableName)
846
                ));
847
848
                return $instructions;
849
            }
850
        }
851
852
        throw new InvalidArgumentException(sprintf(
853
            "The specified index name '%s' does not exist",
854
            $indexName
855
        ));
856
    }
857
858
    /**
859
     * @inheritDoc
860
     */
861
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
862
    {
863
        $primaryKey = $this->getPrimaryKey($tableName);
864
865
        if (empty($primaryKey)) {
866
            return false;
867
        }
868
869
        if ($constraint) {
870
            return $primaryKey['constraint'] === $constraint;
871
        }
872
873
        if (is_string($columns)) {
874
            $columns = [$columns]; // str to array
875
        }
876
        $missingColumns = array_diff($columns, $primaryKey['columns']);
877
878
        return empty($missingColumns);
879
    }
880
881
    /**
882
     * Get the primary key from a particular table.
883
     *
884
     * @param string $tableName Table name
885
     * @return array
886
     */
887
    public function getPrimaryKey($tableName)
888
    {
889
        $rows = $this->fetchAll(sprintf(
890
            "SELECT
891
			tc.constraint_name,
892
			kcu.column_name
893
			FROM information_schema.table_constraints AS tc
894
			JOIN information_schema.key_column_usage AS kcu
895
			ON tc.constraint_name = kcu.constraint_name
896
			WHERE constraint_type = 'PRIMARY KEY'
897
			AND tc.table_name = '%s'
898
			ORDER BY kcu.ordinal_position",
899
            $tableName
900
        ));
901
902
        $primaryKey = [
903
            'columns' => [],
904
        ];
905
        foreach ($rows as $row) {
906
            $primaryKey['constraint'] = $row['constraint_name'];
907
            $primaryKey['columns'][] = $row['column_name'];
908
        }
909
910
        return $primaryKey;
911
    }
912
913
    /**
914
     * @inheritDoc
915
     */
916
    public function hasForeignKey($tableName, $columns, $constraint = null)
917
    {
918
        if (is_string($columns)) {
919
            $columns = [$columns]; // str to array
920
        }
921
        $foreignKeys = $this->getForeignKeys($tableName);
922
        if ($constraint) {
923
            if (isset($foreignKeys[$constraint])) {
924
                return !empty($foreignKeys[$constraint]);
925
            }
926
927
            return false;
928
        }
929
930
        foreach ($foreignKeys as $key) {
931
            $a = array_diff($columns, $key['columns']);
932
            if (empty($a)) {
933
                return true;
934
            }
935
        }
936
937
        return false;
938
    }
939
940
    /**
941
     * Get an array of foreign keys from a particular table.
942
     *
943
     * @param string $tableName Table name
944
     * @return array
945
     */
946
    protected function getForeignKeys($tableName)
947
    {
948
        $foreignKeys = [];
949
        $rows = $this->fetchAll(sprintf(
950
            "SELECT
951
			tc.constraint_name,
952
			tc.table_name, kcu.column_name,
953
			ccu.table_name AS referenced_table_name,
954
			ccu.column_name AS referenced_column_name
955
			FROM
956
			information_schema.table_constraints AS tc
957
			JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
958
			JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
959
			WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s'
960
			ORDER BY kcu.ordinal_position",
961
            $tableName
962
        ));
963
        foreach ($rows as $row) {
964
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
965
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
966
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
967
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
968
        }
969
970
        return $foreignKeys;
971
    }
972
973
    /**
974
     * @inheritDoc
975
     */
976
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
977
    {
978
        $instructions = new AlterInstructions();
979
        $instructions->addPostStep(sprintf(
980
            'ALTER TABLE %s ADD %s',
981
            $this->quoteTableName($table->getName()),
982
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
983
        ));
984
985
        return $instructions;
986
    }
987
988
    /**
989
     * @inheritDoc
990
     */
991
    protected function getDropForeignKeyInstructions($tableName, $constraint)
992
    {
993
        $instructions = new AlterInstructions();
994
        $instructions->addPostStep(sprintf(
995
            'ALTER TABLE %s DROP CONSTRAINT %s',
996
            $this->quoteTableName($tableName),
997
            $constraint
998
        ));
999
1000
        return $instructions;
1001
    }
1002
1003
    /**
1004
     * @inheritDoc
1005
     */
1006
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1007
    {
1008
        $instructions = new AlterInstructions();
1009
1010
        foreach ($columns as $column) {
1011
            $rows = $this->fetchAll(sprintf(
1012
                "SELECT
1013
				tc.constraint_name,
1014
				tc.table_name, kcu.column_name,
1015
				ccu.table_name AS referenced_table_name,
1016
				ccu.column_name AS referenced_column_name
1017
				FROM
1018
				information_schema.table_constraints AS tc
1019
				JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
1020
				JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
1021
				WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' and ccu.column_name='%s'
1022
				ORDER BY kcu.ordinal_position",
1023
                $tableName,
1024
                $column
1025
            ));
1026
            foreach ($rows as $row) {
1027
                $instructions->merge(
1028
                    $this->getDropForeignKeyInstructions($tableName, $row['constraint_name'])
1029
                );
1030
            }
1031
        }
1032
1033
        return $instructions;
1034
    }
1035
1036
    /**
1037
     * {@inheritDoc}
1038
     *
1039
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1040
     */
1041
    public function getSqlType($type, $limit = null)
1042
    {
1043
        switch ($type) {
1044
            case static::PHINX_TYPE_FLOAT:
1045
            case static::PHINX_TYPE_DECIMAL:
1046
            case static::PHINX_TYPE_DATETIME:
1047
            case static::PHINX_TYPE_TIME:
1048
            case static::PHINX_TYPE_DATE:
1049
                return ['name' => $type];
1050
            case static::PHINX_TYPE_STRING:
1051
                return ['name' => 'nvarchar', 'limit' => 255];
1052
            case static::PHINX_TYPE_CHAR:
1053
                return ['name' => 'nchar', 'limit' => 255];
1054
            case static::PHINX_TYPE_TEXT:
1055
                return ['name' => 'ntext'];
1056
            case static::PHINX_TYPE_INTEGER:
1057
                return ['name' => 'int'];
1058
            case static::PHINX_TYPE_SMALL_INTEGER:
1059
                return ['name' => 'smallint'];
1060
            case static::PHINX_TYPE_BIG_INTEGER:
1061
                return ['name' => 'bigint'];
1062
            case static::PHINX_TYPE_TIMESTAMP:
1063
                return ['name' => 'datetime'];
1064
            case static::PHINX_TYPE_BLOB:
1065
            case static::PHINX_TYPE_BINARY:
1066
                return ['name' => 'varbinary'];
1067
            case static::PHINX_TYPE_BOOLEAN:
1068
                return ['name' => 'bit'];
1069
            case static::PHINX_TYPE_BINARYUUID:
1070
            case static::PHINX_TYPE_UUID:
1071
                return ['name' => 'uniqueidentifier'];
1072
            case static::PHINX_TYPE_FILESTREAM:
1073
                return ['name' => 'varbinary', 'limit' => 'max'];
1074
            // Geospatial database types
1075
            case static::PHINX_TYPE_GEOMETRY:
1076
            case static::PHINX_TYPE_POINT:
1077
            case static::PHINX_TYPE_LINESTRING:
1078
            case static::PHINX_TYPE_POLYGON:
1079
                // SQL Server stores all spatial data using a single data type.
1080
                // Specific types (point, polygon, etc) are set at insert time.
1081
                return ['name' => 'geography'];
1082
            default:
1083
                throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SqlServer.');
1084
        }
1085
    }
1086
1087
    /**
1088
     * Returns Phinx type by SQL type
1089
     *
1090
     * @internal param string $sqlType SQL type
1091
     * @param string $sqlType SQL Type definition
1092
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1093
     * @return string Phinx type
1094
     */
1095
    public function getPhinxType($sqlType)
1096
    {
1097
        switch ($sqlType) {
1098
            case 'nvarchar':
1099
            case 'varchar':
1100
                return static::PHINX_TYPE_STRING;
1101
            case 'char':
1102
            case 'nchar':
1103
                return static::PHINX_TYPE_CHAR;
1104
            case 'text':
1105
            case 'ntext':
1106
                return static::PHINX_TYPE_TEXT;
1107
            case 'int':
1108
            case 'integer':
1109
                return static::PHINX_TYPE_INTEGER;
1110
            case 'decimal':
1111
            case 'numeric':
1112
            case 'money':
1113
                return static::PHINX_TYPE_DECIMAL;
1114
            case 'tinyint':
1115
                return static::PHINX_TYPE_TINY_INTEGER;
1116
            case 'smallint':
1117
                return static::PHINX_TYPE_SMALL_INTEGER;
1118
            case 'bigint':
1119
                return static::PHINX_TYPE_BIG_INTEGER;
1120
            case 'real':
1121
            case 'float':
1122
                return static::PHINX_TYPE_FLOAT;
1123
            case 'binary':
1124
            case 'image':
1125
            case 'varbinary':
1126
                return static::PHINX_TYPE_BINARY;
1127
            case 'time':
1128
                return static::PHINX_TYPE_TIME;
1129
            case 'date':
1130
                return static::PHINX_TYPE_DATE;
1131
            case 'datetime':
1132
            case 'timestamp':
1133
                return static::PHINX_TYPE_DATETIME;
1134
            case 'bit':
1135
                return static::PHINX_TYPE_BOOLEAN;
1136
            case 'uniqueidentifier':
1137
                return static::PHINX_TYPE_UUID;
1138
            case 'filestream':
1139
                return static::PHINX_TYPE_FILESTREAM;
1140
            default:
1141
                throw new UnsupportedColumnTypeException('Column type "' . $sqlType . '" is not supported by SqlServer.');
1142
        }
1143
    }
1144
1145
    /**
1146
     * @inheritDoc
1147
     */
1148
    public function createDatabase($name, $options = [])
1149
    {
1150
        if (isset($options['collation'])) {
1151
            $this->execute(sprintf('CREATE DATABASE [%s] COLLATE [%s]', $name, $options['collation']));
1152
        } else {
1153
            $this->execute(sprintf('CREATE DATABASE [%s]', $name));
1154
        }
1155
        $this->execute(sprintf('USE [%s]', $name));
1156
    }
1157
1158
    /**
1159
     * @inheritDoc
1160
     */
1161
    public function hasDatabase($name)
1162
    {
1163
        $result = $this->fetchRow(
1164
            sprintf(
1165
                "SELECT count(*) as [count] FROM master.dbo.sysdatabases WHERE [name] = '%s'",
1166
                $name
1167
            )
1168
        );
1169
1170
        return $result['count'] > 0;
1171
    }
1172
1173
    /**
1174
     * @inheritDoc
1175
     */
1176
    public function dropDatabase($name)
1177
    {
1178
        $sql = <<<SQL
1179
USE master;
1180
IF EXISTS(select * from sys.databases where name=N'$name')
1181
ALTER DATABASE [$name] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
1182
DROP DATABASE [$name];
1183
SQL;
1184
		$this->execute($sql);
1185
		$this->createdTables = [];
1186
	}
1187
1188
	/**
1189
	 * Gets the SqlServer Column Definition for a Column object.
1190
	 *
1191
	 * @param \Phinx\Db\Table\Column $column Column
1192
	 * @param bool $create Create column flag
1193
	 * @return string
1194
	 */
1195
	protected function getColumnSqlDefinition(Column $column, $create = true)
1196
	{
1197
		$buffer = [];
1198
		if ($column->getType() instanceof Literal) {
1199
			$buffer[] = (string)$column->getType();
1200
		} else {
1201
			$sqlType = $this->getSqlType($column->getType());
1202
			$buffer[] = strtoupper($sqlType['name']);
1203
			// integers cant have limits in SQlServer
1204
			$noLimits = [
1205
				'bigint',
1206
				'int',
1207
				'tinyint',
1208
			];
1209
			if ($sqlType['name'] === static::PHINX_TYPE_DECIMAL && $column->getPrecision() && $column->getScale()) {
1210
				$buffer[] = sprintf(
1211
					'(%s, %s)',
1212
					$column->getPrecision() ?: $sqlType['precision'],
1213
					$column->getScale() ?: $sqlType['scale']
1214
				);
1215
			} elseif (!in_array($sqlType['name'], $noLimits) && ($column->getLimit() || isset($sqlType['limit']))) {
1216
				$buffer[] = sprintf('(%s)', $column->getLimit() ?: $sqlType['limit']);
1217
			}
1218
		}
1219
1220
		$properties = $column->getProperties();
1221
		$buffer[] = $column->getType() === 'filestream' ? 'FILESTREAM' : '';
1222
		$buffer[] = isset($properties['rowguidcol']) ? 'ROWGUIDCOL' : '';
1223
1224
		$buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1225
1226
		if ($create === true) {
1227
			if ($column->getDefault() === null && $column->isNull()) {
1228
				$buffer[] = ' DEFAULT NULL';
1229
			} else {
1230
				$buffer[] = $this->getDefaultValueDefinition($column->getDefault());
1231
			}
1232
		}
1233
1234
		if ($column->isIdentity()) {
1235
			$seed = $column->getSeed() ?: 1;
1236
			$increment = $column->getIncrement() ?: 1;
1237
			$buffer[] = sprintf('IDENTITY(%d,%d)', $seed, $increment);
1238
		}
1239
1240
		return implode(' ', $buffer);
1241
	}
1242
1243
	/**
1244
	 * Gets the SqlServer Index Definition for an Index object.
1245
	 *
1246
	 * @param \Phinx\Db\Table\Index $index Index
1247
	 * @param string $tableName Table name
1248
	 * @return string
1249
	 */
1250
	protected function getIndexSqlDefinition(Index $index, $tableName)
1251
	{
1252
		$columnNames = $index->getColumns();
1253
		if (is_string($index->getName())) {
1254
			$indexName = $index->getName();
1255
		} else {
1256
			$indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames));
1257
		}
1258
		$order = $index->getOrder()
1259
		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...
1260
			foreach ($order as $key => $value) {
1261
				$loc = array_search($key, $columnNames);
1262
				$columnNames[$loc] = sprintf('[%s] %s', $key, $value);
1263
			}
1264
		}
1265
		return sprintf(
1266
			'CREATE %s INDEX %s ON %s (%s);',
1267
			($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1268
			$indexName,
1269
			$this->quoteTableName($tableName),
1270
			implode(',', $index->getColumns())
1271
		);
1272
	}
1273
1274
	/**
1275
	 * Gets the SqlServer Foreign Key Definition for an ForeignKey object.
1276
	 *
1277
	 * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1278
	 * @param string $tableName Table name
1279
	 * @return string
1280
	 */
1281
	protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1282
	{
1283
		$constraintName = $foreignKey->getConstraint() ?: $tableName . '_' . implode('_', $foreignKey->getColumns());
1284
		$def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
1285
		$def .= ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")';
1286
		$def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")';
1287
		if ($foreignKey->getOnDelete()) {
1288
			$def .= " ON DELETE {$foreignKey->getOnDelete()}";
1289
		}
1290
		if ($foreignKey->getOnUpdate()) {
1291
			$def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1292
		}
1293
1294
		return $def;
1295
	}
1296
1297
	/**
1298
	 * @inheritDoc
1299
	 */
1300
	public function getColumnTypes()
1301
	{
1302
		return array_merge(parent::getColumnTypes(), static::$specificColumnTypes);
1303
	}
1304
1305
	/**
1306
	 * Records a migration being run.
1307
	 *
1308
	 * @param \Phinx\Migration\MigrationInterface $migration Migration
1309
	 * @param string $direction Direction
1310
	 * @param string $startTime Start Time
1311
	 * @param string $endTime End Time
1312
	 * @return \Phinx\Db\Adapter\AdapterInterface
1313
	 */
1314
	public function migrated(MigrationInterface $migration, $direction, $startTime, $endTime)
1315
	{
1316
		$startTime = str_replace(' ', 'T', $startTime);
1317
		$endTime = str_replace(' ', 'T', $endTime);
1318
1319
		return parent::migrated($migration, $direction, $startTime, $endTime);
1320
	}
1321
1322
	/**
1323
	 * @inheritDoc
1324
	 */
1325
	public function getDecoratedConnection()
1326
	{
1327
		$options = $this->getOptions();
1328
		$options = [
1329
			'username' => $options['user'],
1330
			'password' => $options['pass'],
1331
			'database' => $options['name'],
1332
			'quoteIdentifiers' => true,
1333
		] + $options;
1334
1335
		$driver = new SqlServerDriver($options);
1336
		$driver->setConnection($this->connection);
1337
1338
		return new Connection(['driver' => $driver] + $options);
1339
	}
1340
1341