Passed
Pull Request — master (#1946)
by Oliver
04:50 queued 01:14
created

SqlServerAdapter::getRenameTableInstructions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 10
ccs 0
cts 10
cp 0
rs 10
cc 1
nc 1
nop 2
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
            if (isset($options['limit'])) {
242
                $column->setLimit($options['limit']);
243
            }
244
245
            array_unshift($columns, $column);
246
            if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) {
247
                throw new InvalidArgumentException('You cannot enable an auto incrementing ID field and a primary key');
248
            }
249
            $options['primary_key'] = $options['id'];
250
        }
251
252
        $sql = 'CREATE TABLE ';
253
        $sql .= $this->quoteTableName($table->getName()) . ' (';
254
        $sqlBuffer = [];
255
        $columnsWithComments = [];
256
        foreach ($columns as $column) {
257
            $sqlBuffer[] = $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column);
258
259
            // set column comments, if needed
260
            if ($column->getComment()) {
261
                $columnsWithComments[] = $column;
262
            }
263
        }
264
265
        // set the primary key(s)
266
        if (isset($options['primary_key'])) {
267
            $pkSql = sprintf('CONSTRAINT PK_%s PRIMARY KEY (', $table->getName());
268
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
269
                $pkSql .= $this->quoteColumnName($options['primary_key']);
270
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
271
                $pkSql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
272
            }
273
            $pkSql .= ')';
274
            $sqlBuffer[] = $pkSql;
275
        }
276
277
        $sql .= implode(', ', $sqlBuffer);
278
        $sql .= ');';
279
280
        // process column comments
281
        foreach ($columnsWithComments as $column) {
282
            $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName());
283
        }
284
285
        // set the indexes
286
        foreach ($indexes as $index) {
287
            $sql .= $this->getIndexSqlDefinition($index, $table->getName());
288
        }
289
290
        // execute the sql
291
        $this->execute($sql);
292
293
        $this->addCreatedTable($table->getName());
294
    }
295
296
    /**
297
     * {@inheritDoc}
298
     *
299
     * @throws \InvalidArgumentException
300
     */
301
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
302
    {
303
        $instructions = new AlterInstructions();
304
305
        // Drop the existing primary key
306
        $primaryKey = $this->getPrimaryKey($table->getName());
307
        if (!empty($primaryKey['constraint'])) {
308
            $sql = sprintf(
309
                'DROP CONSTRAINT %s',
310
                $this->quoteColumnName($primaryKey['constraint'])
311
            );
312
            $instructions->addAlter($sql);
313
        }
314
315
        // Add the primary key(s)
316
        if (!empty($newColumns)) {
317
            $sql = sprintf(
318
                'ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (',
319
                $this->quoteTableName($table->getName()),
320
                $this->quoteColumnName('PK_' . $table->getName())
321
            );
322
            if (is_string($newColumns)) { // handle primary_key => 'id'
323
                $sql .= $this->quoteColumnName($newColumns);
324
            } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id')
325
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns));
326
            } else {
327
                throw new InvalidArgumentException(sprintf(
328
                    'Invalid value for primary key: %s',
329
                    json_encode($newColumns)
330
                ));
331
            }
332
            $sql .= ')';
333
            $instructions->addPostStep($sql);
334
        }
335
336
        return $instructions;
337
    }
338
339
    /**
340
     * @inheritDoc
341
     *
342
     * SqlServer does not implement this functionality, and so will always throw an exception if used.
343
     *
344
     * @throws \BadMethodCallException
345
     */
346
    protected function getChangeCommentInstructions(Table $table, $newComment)
347
    {
348
        throw new BadMethodCallException('SqlServer does not have table comments');
349
    }
350
351
    /**
352
     * Gets the SqlServer Column Comment Defininition for a column object.
353
     *
354
     * @param \Phinx\Db\Table\Column $column Column
355
     * @param string $tableName Table name
356
     *
357
     * @return string
358
     */
359
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
360
    {
361
        // passing 'null' is to remove column comment
362
        $currentComment = $this->getColumnComment($tableName, $column->getName());
363
364
        $comment = (strcasecmp($column->getComment(), 'NULL') !== 0) ? $this->getConnection()->quote($column->getComment()) : '\'\'';
365
        $command = $currentComment === false ? 'sp_addextendedproperty' : 'sp_updateextendedproperty';
366
367
        return sprintf(
368
            "EXECUTE %s N'MS_Description', N%s, N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s';",
369
            $command,
370
            $comment,
371
            $this->schema,
372
            $tableName,
373
            $column->getName()
374
        );
375
    }
376
377
    /**
378
     * @inheritDoc
379
     */
380
    protected function getRenameTableInstructions($tableName, $newTableName)
381
    {
382
        $this->updateCreatedTableName($tableName, $newTableName);
383
        $sql = sprintf(
384
            "EXEC sp_rename '%s', '%s'",
385
            $tableName,
386
            $newTableName
387
        );
388
389
        return new AlterInstructions([], [$sql]);
390
    }
391
392
    /**
393
     * @inheritDoc
394
     */
395
    protected function getDropTableInstructions($tableName)
396
    {
397
        $this->removeCreatedTable($tableName);
398
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
399
400
        return new AlterInstructions([], [$sql]);
401
    }
402
403
    /**
404
     * @inheritDoc
405
     */
406
    public function truncateTable($tableName)
407
    {
408
        $sql = sprintf(
409
            'TRUNCATE TABLE %s',
410
            $this->quoteTableName($tableName)
411
        );
412
413
        $this->execute($sql);
414
    }
415
416
    /**
417
     * @param string $tableName Table name
418
     * @param string $columnName Column name
419
     *
420
     * @return string|false
421
     */
422
    public function getColumnComment($tableName, $columnName)
423
    {
424
        $sql = sprintf("SELECT cast(extended_properties.[value] as nvarchar(4000)) comment
425
  FROM sys.schemas
426
 INNER JOIN sys.tables
427
    ON schemas.schema_id = tables.schema_id
428
 INNER JOIN sys.columns
429
    ON tables.object_id = columns.object_id
430
 INNER JOIN sys.extended_properties
431
    ON tables.object_id = extended_properties.major_id
432
   AND columns.column_id = extended_properties.minor_id
433
   AND extended_properties.name = 'MS_Description'
434
   WHERE schemas.[name] = '%s' AND tables.[name] = '%s' AND columns.[name] = '%s'", $this->schema, $tableName, $columnName);
435
        $row = $this->fetchRow($sql);
436
437
        if ($row) {
438
            return trim($row['comment']);
439
        }
440
441
        return false;
442
    }
443
444
    /**
445
     * @inheritDoc
446
     */
447
    public function getColumns($tableName)
448
    {
449
        $columns = [];
450
        $sql = sprintf(
451
            "SELECT DISTINCT TABLE_SCHEMA AS [schema], TABLE_NAME as [table_name], COLUMN_NAME AS [name], DATA_TYPE AS [type],
452
            IS_NULLABLE AS [null], COLUMN_DEFAULT AS [default],
453
            CHARACTER_MAXIMUM_LENGTH AS [char_length],
454
            NUMERIC_PRECISION AS [precision],
455
            NUMERIC_SCALE AS [scale], ORDINAL_POSITION AS [ordinal_position],
456
            COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as [identity]
457
        FROM INFORMATION_SCHEMA.COLUMNS
458
        WHERE TABLE_NAME = '%s'
459
        ORDER BY ordinal_position",
460
            $tableName
461
        );
462
        $rows = $this->fetchAll($sql);
463
        foreach ($rows as $columnInfo) {
464
            try {
465
                $type = $this->getPhinxType($columnInfo['type']);
466
            } catch (UnsupportedColumnTypeException $e) {
467
                $type = Literal::from($columnInfo['type']);
468
            }
469
470
            $column = new Column();
471
            $column->setName($columnInfo['name'])
472
                   ->setType($type)
473
                   ->setNull($columnInfo['null'] !== 'NO')
474
                   ->setDefault($this->parseDefault($columnInfo['default']))
475
                   ->setIdentity($columnInfo['identity'] === '1')
476
                   ->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

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