Passed
Pull Request — master (#1928)
by Corey
03:43
created

SqlServerAdapter::dropDatabase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 10
c 0
b 0
f 0
ccs 0
cts 0
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
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
     *
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
                    $pdoConstant = '\PDO::' . strtoupper($key);
98
                    if (!defined($pdoConstant)) {
99
                        throw new \UnexpectedValueException('Invalid PDO attribute: ' . $key . ' (' . $pdoConstant . ')');
100
                    }
101
                    $driverOptions[constant($pdoConstant)] = $option;
102
                }
103
            }
104
105
            $db = $this->createPdoConnection($dsn, $options['user'] ?? null, $options['pass'] ?? null, $driverOptions);
106
107
            $this->setConnection($db);
108
        }
109
    }
110
111
    /**
112
     * Connect to MSSQL using dblib/freetds.
113
     *
114
     * The "sqlsrv" driver is not available on Unix machines.
115
     *
116
     * @throws \InvalidArgumentException
117
     * @throws \RuntimeException
118
     *
119
     * @return void
120
     */
121
    protected function connectDblib()
122
    {
123
        if (!class_exists('PDO') || !in_array('dblib', PDO::getAvailableDrivers(), true)) {
124
            // @codeCoverageIgnoreStart
125
            throw new RuntimeException('You need to enable the PDO_Dblib extension for Phinx to run properly.');
126
            // @codeCoverageIgnoreEnd
127
        }
128
129
        $options = $this->getOptions();
130
131
        // if port is specified use it, otherwise use the SqlServer default
132
        if (empty($options['port'])) {
133
            $dsn = 'dblib:host=' . $options['host'] . ';dbname=' . $options['name'];
134
        } else {
135
            $dsn = 'dblib:host=' . $options['host'] . ':' . $options['port'] . ';dbname=' . $options['name'];
136
        }
137
138
        $driverOptions = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
139
140
        try {
141
            $db = new PDO($dsn, $options['user'], $options['pass'], $driverOptions);
142
        } catch (PDOException $exception) {
143
            throw new InvalidArgumentException(sprintf(
144
                'There was a problem connecting to the database: %s',
145
                $exception->getMessage()
146
            ));
147
        }
148
149
        $this->setConnection($db);
150
    }
151
152
    /**
153
     * @inheritDoc
154
     */
155
    public function disconnect()
156
    {
157
        $this->connection = null;
158
    }
159
160
    /**
161
     * @inheritDoc
162
     */
163
    public function hasTransactions()
164
    {
165
        return true;
166
    }
167
168
    /**
169
     * @inheritDoc
170
     */
171
    public function beginTransaction()
172
    {
173
        $this->execute('BEGIN TRANSACTION');
174
    }
175
176
    /**
177
     * @inheritDoc
178
     */
179
    public function commitTransaction()
180
    {
181
        $this->execute('COMMIT TRANSACTION');
182
    }
183
184
    /**
185
     * @inheritDoc
186
     */
187
    public function rollbackTransaction()
188
    {
189
        $this->execute('ROLLBACK TRANSACTION');
190
    }
191
192
    /**
193
     * @inheritDoc
194
     */
195
    public function quoteTableName($tableName)
196
    {
197
        return str_replace('.', '].[', $this->quoteColumnName($tableName));
198
    }
199
200
    /**
201
     * @inheritDoc
202
     */
203
    public function quoteColumnName($columnName)
204
    {
205
        return '[' . str_replace(']', '\]', $columnName) . ']';
206
    }
207
208
    /**
209
     * @inheritDoc
210
     */
211
    public function hasTable($tableName)
212
    {
213
        if ($this->hasCreatedTable($tableName)) {
214
            return true;
215
        }
216
217
        $result = $this->fetchRow(sprintf("SELECT count(*) as [count] FROM information_schema.tables WHERE table_name = '%s';", $tableName));
218
219
        return $result['count'] > 0;
220
    }
221
222
    /**
223
     * @inheritDoc
224
     */
225
    public function createTable(Table $table, array $columns = [], array $indexes = [])
226
    {
227
        $options = $table->getOptions();
228
229
        // Add the default primary key
230
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
231
            $options['id'] = 'id';
232
        }
233
234
        if (isset($options['id']) && is_string($options['id'])) {
235
            // Handle id => "field_name" to support AUTO_INCREMENT
236
            $column = new Column();
237
            $column->setName($options['id'])
238
                   ->setType('integer')
239
                   ->setIdentity(true);
240
241
            array_unshift($columns, $column);
242
            if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) {
243
                throw new InvalidArgumentException('You cannot enable an auto incrementing ID field and a primary key');
244
            }
245
            $options['primary_key'] = $options['id'];
246
        }
247
248
        $sql = 'CREATE TABLE ';
249
        $sql .= $this->quoteTableName($table->getName()) . ' (';
250
        $sqlBuffer = [];
251
        $columnsWithComments = [];
252
        foreach ($columns as $column) {
253
            $sqlBuffer[] = $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column);
254
255
            // set column comments, if needed
256
            if ($column->getComment()) {
257
                $columnsWithComments[] = $column;
258
            }
259
        }
260
261
        // set the primary key(s)
262
        if (isset($options['primary_key'])) {
263
            $pkSql = sprintf('CONSTRAINT PK_%s PRIMARY KEY (', $table->getName());
264
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
265
                $pkSql .= $this->quoteColumnName($options['primary_key']);
266
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
267
                $pkSql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
268
            }
269
            $pkSql .= ')';
270
            $sqlBuffer[] = $pkSql;
271
        }
272
273
        $sql .= implode(', ', $sqlBuffer);
274
        $sql .= ');';
275
276
        // process column comments
277
        foreach ($columnsWithComments as $column) {
278
            $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName());
279
        }
280
281
        // set the indexes
282
        foreach ($indexes as $index) {
283
            $sql .= $this->getIndexSqlDefinition($index, $table->getName());
284
        }
285
286
        // execute the sql
287
        $this->execute($sql);
288
289
        $this->addCreatedTable($table->getName());
290
    }
291
292
    /**
293
     * {@inheritDoc}
294
     *
295
     * @throws \InvalidArgumentException
296
     */
297
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
298
    {
299
        $instructions = new AlterInstructions();
300
301
        // Drop the existing primary key
302
        $primaryKey = $this->getPrimaryKey($table->getName());
303
        if (!empty($primaryKey['constraint'])) {
304
            $sql = sprintf(
305
                'DROP CONSTRAINT %s',
306
                $this->quoteColumnName($primaryKey['constraint'])
307
            );
308
            $instructions->addAlter($sql);
309
        }
310
311
        // Add the primary key(s)
312
        if (!empty($newColumns)) {
313
            $sql = sprintf(
314
                'ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (',
315
                $this->quoteTableName($table->getName()),
316
                $this->quoteColumnName('PK_' . $table->getName())
317
            );
318
            if (is_string($newColumns)) { // handle primary_key => 'id'
319
                $sql .= $this->quoteColumnName($newColumns);
320
            } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id')
321
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns));
322
            } else {
323
                throw new InvalidArgumentException(sprintf(
324
                    'Invalid value for primary key: %s',
325
                    json_encode($newColumns)
326
                ));
327
            }
328
            $sql .= ')';
329
            $instructions->addPostStep($sql);
330
        }
331
332
        return $instructions;
333
    }
334
335
    /**
336
     * @inheritDoc
337
     *
338
     * SqlServer does not implement this functionality, and so will always throw an exception if used.
339
     *
340
     * @throws \BadMethodCallException
341
     */
342
    protected function getChangeCommentInstructions(Table $table, $newComment)
343
    {
344
        throw new BadMethodCallException('SqlServer does not have table comments');
345
    }
346
347
    /**
348
     * Gets the SqlServer Column Comment Defininition for a column object.
349
     *
350
     * @param \Phinx\Db\Table\Column $column Column
351
     * @param string $tableName Table name
352
     *
353
     * @return string
354
     */
355
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
356
    {
357
        // passing 'null' is to remove column comment
358
        $currentComment = $this->getColumnComment($tableName, $column->getName());
359
360
        $comment = (strcasecmp($column->getComment(), 'NULL') !== 0) ? $this->getConnection()->quote($column->getComment()) : '\'\'';
361
        $command = $currentComment === false ? 'sp_addextendedproperty' : 'sp_updateextendedproperty';
362
363
        return sprintf(
364
            "EXECUTE %s N'MS_Description', N%s, N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s';",
365
            $command,
366
            $comment,
367
            $this->schema,
368
            $tableName,
369
            $column->getName()
370
        );
371
    }
372
373
    /**
374
     * @inheritDoc
375
     */
376
    protected function getRenameTableInstructions($tableName, $newTableName)
377
    {
378
        $this->updateCreatedTableName($tableName, $newTableName);
379
        $sql = sprintf(
380
            "EXEC sp_rename '%s', '%s'",
381
            $tableName,
382
            $newTableName
383
        );
384
385
        return new AlterInstructions([], [$sql]);
386
    }
387
388
    /**
389
     * @inheritDoc
390
     */
391
    protected function getDropTableInstructions($tableName)
392
    {
393
        $this->removeCreatedTable($tableName);
394
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
395
396
        return new AlterInstructions([], [$sql]);
397
    }
398
399
    /**
400
     * @inheritDoc
401
     */
402
    public function truncateTable($tableName)
403
    {
404
        $sql = sprintf(
405
            'TRUNCATE TABLE %s',
406
            $this->quoteTableName($tableName)
407
        );
408
409
        $this->execute($sql);
410
    }
411
412
    /**
413
     * @param string $tableName Table name
414
     * @param string $columnName Column name
415
     *
416
     * @return string|false
417
     */
418
    public function getColumnComment($tableName, $columnName)
419
    {
420
        $sql = sprintf("SELECT cast(extended_properties.[value] as nvarchar(4000)) comment
421
  FROM sys.schemas
422
 INNER JOIN sys.tables
423
    ON schemas.schema_id = tables.schema_id
424
 INNER JOIN sys.columns
425
    ON tables.object_id = columns.object_id
426
 INNER JOIN sys.extended_properties
427
    ON tables.object_id = extended_properties.major_id
428
   AND columns.column_id = extended_properties.minor_id
429
   AND extended_properties.name = 'MS_Description'
430
   WHERE schemas.[name] = '%s' AND tables.[name] = '%s' AND columns.[name] = '%s'", $this->schema, $tableName, $columnName);
431
        $row = $this->fetchRow($sql);
432
433
        if ($row) {
434
            return trim($row['comment']);
435
        }
436
437
        return false;
438
    }
439
440
    /**
441
     * @inheritDoc
442
     */
443
    public function getColumns($tableName)
444
    {
445
        $columns = [];
446
        $sql = sprintf(
447
            "SELECT DISTINCT TABLE_SCHEMA AS [schema], TABLE_NAME as [table_name], COLUMN_NAME AS [name], DATA_TYPE AS [type],
448
            IS_NULLABLE AS [null], COLUMN_DEFAULT AS [default],
449
            CHARACTER_MAXIMUM_LENGTH AS [char_length],
450
            NUMERIC_PRECISION AS [precision],
451
            NUMERIC_SCALE AS [scale], ORDINAL_POSITION AS [ordinal_position],
452
            COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as [identity]
453
        FROM INFORMATION_SCHEMA.COLUMNS
454
        WHERE TABLE_NAME = '%s'
455
        ORDER BY ordinal_position",
456
            $tableName
457
        );
458
        $rows = $this->fetchAll($sql);
459
        foreach ($rows as $columnInfo) {
460
            try {
461
                $type = $this->getPhinxType($columnInfo['type']);
462
            } catch (UnsupportedColumnTypeException $e) {
463
                $type = Literal::from($columnInfo['type']);
464
            }
465
466
            $column = new Column();
467
            $column->setName($columnInfo['name'])
468
                   ->setType($type)
469
                   ->setNull($columnInfo['null'] !== 'NO')
470
                   ->setDefault($this->parseDefault($columnInfo['default']))
471
                   ->setIdentity($columnInfo['identity'] === '1')
472
                   ->setComment($this->getColumnComment($columnInfo['table_name'], $columnInfo['name']));
0 ignored issues
show
Bug introduced by
It seems like $this->getColumnComment(...], $columnInfo['name']) can also be of type false; however, parameter $comment of Phinx\Db\Table\Column::setComment() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

472
                   ->setComment(/** @scrutinizer ignore-type */ $this->getColumnComment($columnInfo['table_name'], $columnInfo['name']));
Loading history...
473
474
            if (!empty($columnInfo['char_length'])) {
475
                $column->setLimit($columnInfo['char_length']);
476
            }
477
478
            $columns[$columnInfo['name']] = $column;
479
        }
480
481
        return $columns;
482
    }
483
484
    /**
485
     * @param string $default Default
486
     *
487
     * @return int|string|null
488
     */
489
    protected function parseDefault($default)
490
    {
491
        $result = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default);
492
493
        if (strtoupper($result) === 'NULL') {
494
            $result = null;
495
        } elseif (is_numeric($result)) {
496
            $result = (int)$result;
497
        }
498
499
        return $result;
500
    }
501
502
    /**
503
     * @inheritDoc
504
     */
505
    public function hasColumn($tableName, $columnName)
506
    {
507
        $sql = sprintf(
508
            "SELECT count(*) as [count]
509
             FROM information_schema.columns
510
             WHERE table_name = '%s' AND column_name = '%s'",
511
            $tableName,
512
            $columnName
513
        );
514
        $result = $this->fetchRow($sql);
515
516
        return $result['count'] > 0;
517
    }
518
519
    /**
520
     * @inheritDoc
521
     */
522
    protected function getAddColumnInstructions(Table $table, Column $column)
523
    {
524
        $alter = sprintf(
525
            'ALTER TABLE %s ADD %s %s',
526
            $table->getName(),
527
            $this->quoteColumnName($column->getName()),
528
            $this->getColumnSqlDefinition($column)
529
        );
530
531
        return new AlterInstructions([], [$alter]);
532
    }
533
534
    /**
535
     * {@inheritDoc}
536
     *
537
     * @throws \InvalidArgumentException
538
     */
539
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
540
    {
541
        if (!$this->hasColumn($tableName, $columnName)) {
542
            throw new InvalidArgumentException("The specified column does not exist: $columnName");
543
        }
544
545
        $instructions = new AlterInstructions();
546
547
        $oldConstraintName = "DF_{$tableName}_{$columnName}";
548
        $newConstraintName = "DF_{$tableName}_{$newColumnName}";
549
        $sql = <<<SQL
550
IF (OBJECT_ID('$oldConstraintName', 'D') IS NOT NULL)
551
BEGIN
552
     EXECUTE sp_rename N'%s', N'%s', N'OBJECT'
553
END
554
SQL;
555
        $instructions->addPostStep(sprintf(
556
            $sql,
557
            $oldConstraintName,
558
            $newConstraintName
559
        ));
560
561
        $instructions->addPostStep(sprintf(
562
            "EXECUTE sp_rename N'%s.%s', N'%s', 'COLUMN' ",
563
            $tableName,
564
            $columnName,
565
            $newColumnName
566
        ));
567
568
        return $instructions;
569
    }
570
571
    /**
572
     * Returns the instructions to change a column default value
573
     *
574
     * @param string $tableName The table where the column is
575
     * @param \Phinx\Db\Table\Column $newColumn The column to alter
576
     *
577
     * @return \Phinx\Db\Util\AlterInstructions
578
     */
579
    protected function getChangeDefault($tableName, Column $newColumn)
580
    {
581
        $constraintName = "DF_{$tableName}_{$newColumn->getName()}";
582
        $default = $newColumn->getDefault();
583
        $instructions = new AlterInstructions();
584
585
        if ($default === null) {
586
            $default = 'DEFAULT NULL';
587
        } else {
588
            $default = ltrim($this->getDefaultValueDefinition($default));
589
        }
590
591
        if (empty($default)) {
592
            return $instructions;
593
        }
594
595
        $instructions->addPostStep(sprintf(
596
            'ALTER TABLE %s ADD CONSTRAINT %s %s FOR %s',
597
            $this->quoteTableName($tableName),
598
            $constraintName,
599
            $default,
600
            $this->quoteColumnName($newColumn->getName())
601
        ));
602
603
        return $instructions;
604
    }
605
606
    /**
607
     * @inheritDoc
608
     */
609
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
610
    {
611
        $columns = $this->getColumns($tableName);
612
        $changeDefault =
613
            $newColumn->getDefault() !== $columns[$columnName]->getDefault() ||
614
            $newColumn->getType() !== $columns[$columnName]->getType();
615
616
        $instructions = new AlterInstructions();
617
618
        if ($columnName !== $newColumn->getName()) {
619
            $instructions->merge(
620
                $this->getRenameColumnInstructions($tableName, $columnName, $newColumn->getName())
621
            );
622
        }
623
624
        if ($changeDefault) {
625
            $instructions->merge($this->getDropDefaultConstraint($tableName, $newColumn->getName()));
626
        }
627
628
        $instructions->addPostStep(sprintf(
629
            'ALTER TABLE %s ALTER COLUMN %s %s',
630
            $this->quoteTableName($tableName),
631
            $this->quoteColumnName($newColumn->getName()),
632
            $this->getColumnSqlDefinition($newColumn, false)
633
        ));
634
        // change column comment if needed
635
        if ($newColumn->getComment()) {
636
            $instructions->addPostStep($this->getColumnCommentSqlDefinition($newColumn, $tableName));
637
        }
638
639
        if ($changeDefault) {
640
            $instructions->merge($this->getChangeDefault($tableName, $newColumn));
641
        }
642
643
        return $instructions;
644
    }
645
646
    /**
647
     * @inheritDoc
648
     */
649
    protected function getDropColumnInstructions($tableName, $columnName)
650
    {
651
        $instructions = $this->getDropDefaultConstraint($tableName, $columnName);
652
653
        $instructions->addPostStep(sprintf(
654
            'ALTER TABLE %s DROP COLUMN %s',
655
            $this->quoteTableName($tableName),
656
            $this->quoteColumnName($columnName)
657
        ));
658
659
        return $instructions;
660
    }
661
662
    /**
663
     * @param string $tableName Table name
664
     * @param string|null $columnName Column name
665
     *
666
     * @return \Phinx\Db\Util\AlterInstructions
667
     */
668
    protected function getDropDefaultConstraint($tableName, $columnName)
669
    {
670
        $defaultConstraint = $this->getDefaultConstraint($tableName, $columnName);
671
672
        if (!$defaultConstraint) {
673
            return new AlterInstructions();
674
        }
675
676
        return $this->getDropForeignKeyInstructions($tableName, $defaultConstraint);
677
    }
678
679
    /**
680
     * @param string $tableName Table name
681
     * @param string $columnName Column name
682
     *
683
     * @return string|false
684
     */
685
    protected function getDefaultConstraint($tableName, $columnName)
686
    {
687
        $sql = "SELECT
688
    default_constraints.name
689
FROM
690
    sys.all_columns
691
692
        INNER JOIN
693
    sys.tables
694
        ON all_columns.object_id = tables.object_id
695
696
        INNER JOIN
697
    sys.schemas
698
        ON tables.schema_id = schemas.schema_id
699
700
        INNER JOIN
701
    sys.default_constraints
702
        ON all_columns.default_object_id = default_constraints.object_id
703
704
WHERE
705
        schemas.name = 'dbo'
706
    AND tables.name = '{$tableName}'
707
    AND all_columns.name = '{$columnName}'";
708
709
        $rows = $this->fetchAll($sql);
710
711
        return empty($rows) ? false : $rows[0]['name'];
712
    }
713
714
    /**
715
     * @param int $tableId Table ID
716
     * @param int $indexId Index ID
717
     *
718
     * @return array
719
     */
720
    protected function getIndexColums($tableId, $indexId)
721
    {
722
        $sql = "SELECT AC.[name] AS [column_name]
723
FROM sys.[index_columns] IC
724
  INNER JOIN sys.[all_columns] AC ON IC.[column_id] = AC.[column_id]
725
WHERE AC.[object_id] = {$tableId} AND IC.[index_id] = {$indexId}  AND IC.[object_id] = {$tableId}
726
ORDER BY IC.[key_ordinal];";
727
728
        $rows = $this->fetchAll($sql);
729
        $columns = [];
730
        foreach ($rows as $row) {
731
            $columns[] = strtolower($row['column_name']);
732
        }
733
734
        return $columns;
735
    }
736
737
    /**
738
     * Get an array of indexes from a particular table.
739
     *
740
     * @param string $tableName Table name
741
     *
742
     * @return array
743
     */
744
    public function getIndexes($tableName)
745
    {
746
        $indexes = [];
747
        $sql = "SELECT I.[name] AS [index_name], I.[index_id] as [index_id], T.[object_id] as [table_id]
748
FROM sys.[tables] AS T
749
  INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
750
WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP'  AND T.[name] = '{$tableName}'
751
ORDER BY T.[name], I.[index_id];";
752
753
        $rows = $this->fetchAll($sql);
754
        foreach ($rows as $row) {
755
            $columns = $this->getIndexColums($row['table_id'], $row['index_id']);
756
            $indexes[$row['index_name']] = ['columns' => $columns];
757
        }
758
759
        return $indexes;
760
    }
761
762
    /**
763
     * @inheritDoc
764
     */
765
    public function hasIndex($tableName, $columns)
766
    {
767
        if (is_string($columns)) {
768
            $columns = [$columns]; // str to array
769
        }
770
771
        $columns = array_map('strtolower', $columns);
772
        $indexes = $this->getIndexes($tableName);
773
774
        foreach ($indexes as $index) {
775
            $a = array_diff($columns, $index['columns']);
776
777
            if (empty($a)) {
778
                return true;
779
            }
780
        }
781
782
        return false;
783
    }
784
785
    /**
786
     * @inheritDoc
787
     */
788
    public function hasIndexByName($tableName, $indexName)
789
    {
790
        $indexes = $this->getIndexes($tableName);
791
792
        foreach ($indexes as $name => $index) {
793
            if ($name === $indexName) {
794
                 return true;
795
            }
796
        }
797
798
        return false;
799
    }
800
801
    /**
802
     * @inheritDoc
803
     */
804
    protected function getAddIndexInstructions(Table $table, Index $index)
805
    {
806
        $sql = $this->getIndexSqlDefinition($index, $table->getName());
807
808
        return new AlterInstructions([], [$sql]);
809
    }
810
811
    /**
812
     * {@inheritDoc}
813
     *
814
     * @throws \InvalidArgumentException
815
     */
816
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
817
    {
818
        if (is_string($columns)) {
819
            $columns = [$columns]; // str to array
820
        }
821
822
        $indexes = $this->getIndexes($tableName);
823
        $columns = array_map('strtolower', $columns);
824
        $instructions = new AlterInstructions();
825
826
        foreach ($indexes as $indexName => $index) {
827
            $a = array_diff($columns, $index['columns']);
828
            if (empty($a)) {
829
                $instructions->addPostStep(sprintf(
830
                    'DROP INDEX %s ON %s',
831
                    $this->quoteColumnName($indexName),
832
                    $this->quoteTableName($tableName)
833
                ));
834
835
                return $instructions;
836
            }
837
        }
838
839
        throw new InvalidArgumentException(sprintf(
840
            "The specified index on columns '%s' does not exist",
841
            implode(',', $columns)
842
        ));
843
    }
844
845
    /**
846
     * {@inheritDoc}
847
     *
848
     * @throws \InvalidArgumentException
849
     */
850
    protected function getDropIndexByNameInstructions($tableName, $indexName)
851
    {
852
        $indexes = $this->getIndexes($tableName);
853
        $instructions = new AlterInstructions();
854
855
        foreach ($indexes as $name => $index) {
856
            if ($name === $indexName) {
857
                $instructions->addPostStep(sprintf(
858
                    'DROP INDEX %s ON %s',
859
                    $this->quoteColumnName($indexName),
860
                    $this->quoteTableName($tableName)
861
                ));
862
863
                return $instructions;
864
            }
865
        }
866
867
        throw new InvalidArgumentException(sprintf(
868
            "The specified index name '%s' does not exist",
869
            $indexName
870
        ));
871
    }
872
873
    /**
874
     * @inheritDoc
875
     */
876
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
877
    {
878
        $primaryKey = $this->getPrimaryKey($tableName);
879
880
        if (empty($primaryKey)) {
881
            return false;
882
        }
883
884
        if ($constraint) {
885
            return ($primaryKey['constraint'] === $constraint);
886
        }
887
888
        if (is_string($columns)) {
889
            $columns = [$columns]; // str to array
890
        }
891
        $missingColumns = array_diff($columns, $primaryKey['columns']);
892
893
        return empty($missingColumns);
894
    }
895
896
    /**
897
     * Get the primary key from a particular table.
898
     *
899
     * @param string $tableName Table name
900
     *
901
     * @return array
902
     */
903
    public function getPrimaryKey($tableName)
904
    {
905
        $rows = $this->fetchAll(sprintf(
906
            "SELECT
907
                    tc.constraint_name,
908
                    kcu.column_name
909
                FROM information_schema.table_constraints AS tc
910
                JOIN information_schema.key_column_usage AS kcu
911
                    ON tc.constraint_name = kcu.constraint_name
912
                WHERE constraint_type = 'PRIMARY KEY'
913
                    AND tc.table_name = '%s'
914
                ORDER BY kcu.ordinal_position",
915
            $tableName
916
        ));
917
918
        $primaryKey = [
919
            'columns' => [],
920
        ];
921
        foreach ($rows as $row) {
922
            $primaryKey['constraint'] = $row['constraint_name'];
923
            $primaryKey['columns'][] = $row['column_name'];
924
        }
925
926
        return $primaryKey;
927
    }
928
929
    /**
930
     * @inheritDoc
931
     */
932
    public function hasForeignKey($tableName, $columns, $constraint = null)
933
    {
934
        if (is_string($columns)) {
935
            $columns = [$columns]; // str to array
936
        }
937
        $foreignKeys = $this->getForeignKeys($tableName);
938
        if ($constraint) {
939
            if (isset($foreignKeys[$constraint])) {
940
                return !empty($foreignKeys[$constraint]);
941
            }
942
943
            return false;
944
        }
945
946
        foreach ($foreignKeys as $key) {
947
            $a = array_diff($columns, $key['columns']);
948
            if (empty($a)) {
949
                return true;
950
            }
951
        }
952
953
        return false;
954
    }
955
956
    /**
957
     * Get an array of foreign keys from a particular table.
958
     *
959
     * @param string $tableName Table name
960
     *
961
     * @return array
962
     */
963
    protected function getForeignKeys($tableName)
964
    {
965
        $foreignKeys = [];
966
        $rows = $this->fetchAll(sprintf(
967
            "SELECT
968
                    tc.constraint_name,
969
                    tc.table_name, kcu.column_name,
970
                    ccu.table_name AS referenced_table_name,
971
                    ccu.column_name AS referenced_column_name
972
                FROM
973
                    information_schema.table_constraints AS tc
974
                    JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
975
                    JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
976
                WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s'
977
                ORDER BY kcu.ordinal_position",
978
            $tableName
979
        ));
980
        foreach ($rows as $row) {
981
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
982
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
983
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
984
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
985
        }
986
987
        return $foreignKeys;
988
    }
989
990
    /**
991
     * @inheritDoc
992
     */
993
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
994
    {
995
        $instructions = new AlterInstructions();
996
        $instructions->addPostStep(sprintf(
997
            'ALTER TABLE %s ADD %s',
998
            $this->quoteTableName($table->getName()),
999
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
1000
        ));
1001
1002
        return $instructions;
1003
    }
1004
1005
    /**
1006
     * @inheritDoc
1007
     */
1008
    protected function getDropForeignKeyInstructions($tableName, $constraint)
1009
    {
1010
        $instructions = new AlterInstructions();
1011
        $instructions->addPostStep(sprintf(
1012
            'ALTER TABLE %s DROP CONSTRAINT %s',
1013
            $this->quoteTableName($tableName),
1014
            $constraint
1015
        ));
1016
1017
        return $instructions;
1018
    }
1019
1020
    /**
1021
     * @inheritDoc
1022
     */
1023
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1024
    {
1025
        $instructions = new AlterInstructions();
1026
1027
        foreach ($columns as $column) {
1028
            $rows = $this->fetchAll(sprintf(
1029
                "SELECT
1030
                tc.constraint_name,
1031
                tc.table_name, kcu.column_name,
1032
                ccu.table_name AS referenced_table_name,
1033
                ccu.column_name AS referenced_column_name
1034
            FROM
1035
                information_schema.table_constraints AS tc
1036
                JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
1037
                JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
1038
            WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' and ccu.column_name='%s'
1039
            ORDER BY kcu.ordinal_position",
1040
                $tableName,
1041
                $column
1042
            ));
1043
            foreach ($rows as $row) {
1044
                $instructions->merge(
1045
                    $this->getDropForeignKeyInstructions($tableName, $row['constraint_name'])
1046
                );
1047
            }
1048
        }
1049
1050
        return $instructions;
1051
    }
1052
1053
    /**
1054
     * {@inheritDoc}
1055
     *
1056
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1057
     */
1058
    public function getSqlType($type, $limit = null)
1059
    {
1060
        switch ($type) {
1061
            case static::PHINX_TYPE_FLOAT:
1062
            case static::PHINX_TYPE_DECIMAL:
1063
            case static::PHINX_TYPE_DATETIME:
1064
            case static::PHINX_TYPE_TIME:
1065
            case static::PHINX_TYPE_DATE:
1066
                return ['name' => $type];
1067
            case static::PHINX_TYPE_STRING:
1068
                return ['name' => 'nvarchar', 'limit' => 255];
1069
            case static::PHINX_TYPE_CHAR:
1070
                return ['name' => 'nchar', 'limit' => 255];
1071
            case static::PHINX_TYPE_TEXT:
1072
                return ['name' => 'ntext'];
1073
            case static::PHINX_TYPE_INTEGER:
1074
                return ['name' => 'int'];
1075
            case static::PHINX_TYPE_TINY_INTEGER:
1076
                return ['name' => 'tinyint'];
1077
            case static::PHINX_TYPE_SMALL_INTEGER:
1078
                return ['name' => 'smallint'];
1079
            case static::PHINX_TYPE_BIG_INTEGER:
1080
                return ['name' => 'bigint'];
1081
            case static::PHINX_TYPE_TIMESTAMP:
1082
                return ['name' => 'datetime'];
1083
            case static::PHINX_TYPE_BLOB:
1084
            case static::PHINX_TYPE_BINARY:
1085
                return ['name' => 'varbinary'];
1086
            case static::PHINX_TYPE_BOOLEAN:
1087
                return ['name' => 'bit'];
1088
            case static::PHINX_TYPE_BINARYUUID:
1089
            case static::PHINX_TYPE_UUID:
1090
                return ['name' => 'uniqueidentifier'];
1091
            case static::PHINX_TYPE_FILESTREAM:
1092
                return ['name' => 'varbinary', 'limit' => 'max'];
1093
            // Geospatial database types
1094
            case static::PHINX_TYPE_GEOMETRY:
1095
            case static::PHINX_TYPE_POINT:
1096
            case static::PHINX_TYPE_LINESTRING:
1097
            case static::PHINX_TYPE_POLYGON:
1098
                // SQL Server stores all spatial data using a single data type.
1099
                // Specific types (point, polygon, etc) are set at insert time.
1100
                return ['name' => 'geography'];
1101
            default:
1102
                throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SqlServer.');
1103
        }
1104
    }
1105
1106
    /**
1107
     * Returns Phinx type by SQL type
1108
     *
1109
     * @internal param string $sqlType SQL type
1110
     *
1111
     * @param string $sqlType SQL Type definition
1112
     *
1113
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1114
     *
1115
     * @return string Phinx type
1116
     */
1117
    public function getPhinxType($sqlType)
1118
    {
1119
        switch ($sqlType) {
1120
            case 'nvarchar':
1121
            case 'varchar':
1122
                return static::PHINX_TYPE_STRING;
1123
            case 'char':
1124
            case 'nchar':
1125
                return static::PHINX_TYPE_CHAR;
1126
            case 'text':
1127
            case 'ntext':
1128
                return static::PHINX_TYPE_TEXT;
1129
            case 'int':
1130
            case 'integer':
1131
                return static::PHINX_TYPE_INTEGER;
1132
            case 'decimal':
1133
            case 'numeric':
1134
            case 'money':
1135
                return static::PHINX_TYPE_DECIMAL;
1136
            case 'tinyint':
1137
                return static::PHINX_TYPE_TINY_INTEGER;
1138
            case 'smallint':
1139
                return static::PHINX_TYPE_SMALL_INTEGER;
1140
            case 'bigint':
1141
                return static::PHINX_TYPE_BIG_INTEGER;
1142
            case 'real':
1143
            case 'float':
1144
                return static::PHINX_TYPE_FLOAT;
1145
            case 'binary':
1146
            case 'image':
1147
            case 'varbinary':
1148
                return static::PHINX_TYPE_BINARY;
1149
            case 'time':
1150
                return static::PHINX_TYPE_TIME;
1151
            case 'date':
1152
                return static::PHINX_TYPE_DATE;
1153
            case 'datetime':
1154
            case 'timestamp':
1155
                return static::PHINX_TYPE_DATETIME;
1156
            case 'bit':
1157
                return static::PHINX_TYPE_BOOLEAN;
1158
            case 'uniqueidentifier':
1159
                return static::PHINX_TYPE_UUID;
1160
            case 'filestream':
1161
                return static::PHINX_TYPE_FILESTREAM;
1162
            default:
1163
                throw new UnsupportedColumnTypeException('Column type "' . $sqlType . '" is not supported by SqlServer.');
1164
        }
1165
    }
1166
1167
    /**
1168
     * @inheritDoc
1169
     */
1170
    public function createDatabase($name, $options = [])
1171
    {
1172
        if (isset($options['collation'])) {
1173
            $this->execute(sprintf('CREATE DATABASE [%s] COLLATE [%s]', $name, $options['collation']));
1174
        } else {
1175
            $this->execute(sprintf('CREATE DATABASE [%s]', $name));
1176
        }
1177
        $this->execute(sprintf('USE [%s]', $name));
1178
    }
1179
1180
    /**
1181
     * @inheritDoc
1182
     */
1183
    public function hasDatabase($name)
1184
    {
1185
        $result = $this->fetchRow(
1186
            sprintf(
1187
                "SELECT count(*) as [count] FROM master.dbo.sysdatabases WHERE [name] = '%s'",
1188
                $name
1189
            )
1190
        );
1191
1192
        return $result['count'] > 0;
1193
    }
1194
1195
    /**
1196
     * @inheritDoc
1197
     */
1198
    public function dropDatabase($name)
1199
    {
1200
        $sql = <<<SQL
1201
USE master;
1202
IF EXISTS(select * from sys.databases where name=N'$name')
1203
ALTER DATABASE [$name] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
1204
DROP DATABASE [$name];
1205
SQL;
1206
        $this->execute($sql);
1207
        $this->createdTables = [];
1208
    }
1209
1210
    /**
1211
     * Gets the SqlServer Column Definition for a Column object.
1212
     *
1213
     * @param \Phinx\Db\Table\Column $column Column
1214
     * @param bool $create Create column flag
1215
     *
1216
     * @return string
1217
     */
1218
    protected function getColumnSqlDefinition(Column $column, $create = true)
1219
    {
1220
        $buffer = [];
1221
        if ($column->getType() instanceof Literal) {
1222
            $buffer[] = (string)$column->getType();
1223
        } else {
1224
            $sqlType = $this->getSqlType($column->getType());
1225
            $buffer[] = strtoupper($sqlType['name']);
1226
            // integers cant have limits in SQlServer
1227
            $noLimits = [
1228
                'bigint',
1229
                'int',
1230
                'tinyint',
1231
                'smallint',
1232
            ];
1233
            if ($sqlType['name'] === static::PHINX_TYPE_DECIMAL && $column->getPrecision() && $column->getScale()) {
1234
                $buffer[] = sprintf(
1235
                    '(%s, %s)',
1236
                    $column->getPrecision() ?: $sqlType['precision'],
1237
                    $column->getScale() ?: $sqlType['scale']
1238
                );
1239
            } elseif (!in_array($sqlType['name'], $noLimits) && ($column->getLimit() || isset($sqlType['limit']))) {
1240
                $buffer[] = sprintf('(%s)', $column->getLimit() ?: $sqlType['limit']);
1241
            }
1242
        }
1243
1244
        $properties = $column->getProperties();
1245
        $buffer[] = $column->getType() === 'filestream' ? 'FILESTREAM' : '';
1246
        $buffer[] = isset($properties['rowguidcol']) ? 'ROWGUIDCOL' : '';
1247
1248
        $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1249
1250
        if ($create === true) {
1251
            if ($column->getDefault() === null && $column->isNull()) {
1252
                $buffer[] = ' DEFAULT NULL';
1253
            } else {
1254
                $buffer[] = $this->getDefaultValueDefinition($column->getDefault());
1255
            }
1256
        }
1257
1258
        if ($column->isIdentity()) {
1259
            $seed = $column->getSeed() ?: 1;
1260
            $increment = $column->getIncrement() ?: 1;
1261
            $buffer[] = sprintf('IDENTITY(%d,%d)', $seed, $increment);
1262
        }
1263
1264
        return implode(' ', $buffer);
1265
    }
1266
1267
    /**
1268
     * Gets the SqlServer Index Definition for an Index object.
1269
     *
1270
     * @param \Phinx\Db\Table\Index $index Index
1271
     * @param string $tableName Table name
1272
     * @return string
1273
     */
1274
    protected function getIndexSqlDefinition(Index $index, $tableName)
1275
    {
1276
        $columnNames = $index->getColumns();
1277
        if (is_string($index->getName())) {
1278
            $indexName = $index->getName();
1279
        } else {
1280
            $indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames));
1281
        }
1282
        $order = $index->getOrder() ?? [];
1283
        $columnNames = array_map(function ($columnName) use ($order) {
1284
            $ret = '[' . $columnName . ']';
1285
            if (isset($order[$columnName])) {
1286
                $ret .= ' ' . $order[$columnName];
1287
            }
1288
1289
            return $ret;
1290
        }, $columnNames);
1291
1292
        $includedColumns = $index->getInclude() ? sprintf('INCLUDE ([%s])', implode('],[', $index->getInclude())) : '';
1293
1294
        return sprintf(
1295
            'CREATE %s INDEX %s ON %s (%s) %s;',
1296
            ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1297
            $indexName,
1298
            $this->quoteTableName($tableName),
1299
            implode(',', $columnNames),
1300
            $includedColumns
1301
        );
1302
    }
1303
1304
    /**
1305
     * Gets the SqlServer Foreign Key Definition for an ForeignKey object.
1306
     *
1307
     * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1308
     * @param string $tableName Table name
1309
     *
1310
     * @return string
1311
     */
1312
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1313
    {
1314
        $constraintName = $foreignKey->getConstraint() ?: $tableName . '_' . implode('_', $foreignKey->getColumns());
1315
        $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
1316
        $def .= ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")';
1317
        $def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")';
1318
        if ($foreignKey->getOnDelete()) {
1319
            $def .= " ON DELETE {$foreignKey->getOnDelete()}";
1320
        }
1321
        if ($foreignKey->getOnUpdate()) {
1322
            $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1323
        }
1324
1325
        return $def;
1326
    }
1327
1328
    /**
1329
     * @inheritDoc
1330
     */
1331
    public function getColumnTypes()
1332
    {
1333
        return array_merge(parent::getColumnTypes(), static::$specificColumnTypes);
1334
    }
1335
1336
    /**
1337
     * Records a migration being run.
1338
     *
1339
     * @param \Phinx\Migration\MigrationInterface $migration Migration
1340
     * @param string $direction Direction
1341
     * @param string $startTime Start Time
1342
     * @param string $endTime End Time
1343
     *
1344
     * @return \Phinx\Db\Adapter\AdapterInterface
1345
     */
1346
    public function migrated(MigrationInterface $migration, $direction, $startTime, $endTime)
1347
    {
1348
        $startTime = str_replace(' ', 'T', $startTime);
1349
        $endTime = str_replace(' ', 'T', $endTime);
1350
1351
        return parent::migrated($migration, $direction, $startTime, $endTime);
1352
    }
1353
1354
    /**
1355
     * @inheritDoc
1356
     */
1357
    public function getDecoratedConnection()
1358
    {
1359
        $options = $this->getOptions();
1360
        $options = [
1361
            'username' => $options['user'],
1362
            'password' => $options['pass'],
1363
            'database' => $options['name'],
1364
            'quoteIdentifiers' => true,
1365
        ] + $options;
1366
1367
        $driver = new SqlServerDriver($options);
1368
        $driver->setConnection($this->connection);
1369
1370
        return new Connection(['driver' => $driver] + $options);
1371
    }
1372
}
1373