SqlServerAdapter::getDecoratedConnection()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

478
                   ->setComment(/** @scrutinizer ignore-type */ $this->getColumnComment($columnInfo['table_name'], $columnInfo['name']));
Loading history...
479
480
            if (!empty($columnInfo['char_length'])) {
481
                $column->setLimit($columnInfo['char_length']);
482
            }
483
484
            $columns[$columnInfo['name']] = $column;
485
        }
486
487
        return $columns;
488
    }
489
490
    /**
491
     * @param string $default Default
492
     * @return int|string|null
493
     */
494
    protected function parseDefault($default)
495
    {
496
        $result = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default);
497
498
        if (strtoupper($result) === 'NULL') {
499
            $result = null;
500
        } elseif (is_numeric($result)) {
501
            $result = (int)$result;
502
        }
503
504
        return $result;
505
    }
506
507
    /**
508
     * @inheritDoc
509
     */
510
    public function hasColumn($tableName, $columnName)
511
    {
512
        $sql = sprintf(
513
            "SELECT count(*) as [count]
514
             FROM information_schema.columns
515
             WHERE table_name = '%s' AND column_name = '%s'",
516
            $tableName,
517
            $columnName
518
        );
519
        $result = $this->fetchRow($sql);
520
521
        return $result['count'] > 0;
522
    }
523
524
    /**
525
     * @inheritDoc
526
     */
527
    protected function getAddColumnInstructions(Table $table, Column $column)
528
    {
529
        $alter = sprintf(
530
            'ALTER TABLE %s ADD %s %s',
531
            $table->getName(),
532
            $this->quoteColumnName($column->getName()),
533
            $this->getColumnSqlDefinition($column)
534
        );
535
536
        return new AlterInstructions([], [$alter]);
537
    }
538
539
    /**
540
     * {@inheritDoc}
541
     *
542
     * @throws \InvalidArgumentException
543
     */
544
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
545
    {
546
        if (!$this->hasColumn($tableName, $columnName)) {
547
            throw new InvalidArgumentException("The specified column does not exist: $columnName");
548
        }
549
550
        $instructions = new AlterInstructions();
551
552
        $oldConstraintName = "DF_{$tableName}_{$columnName}";
553
        $newConstraintName = "DF_{$tableName}_{$newColumnName}";
554
        $sql = <<<SQL
555
IF (OBJECT_ID('$oldConstraintName', 'D') IS NOT NULL)
556
BEGIN
557
     EXECUTE sp_rename N'%s', N'%s', N'OBJECT'
558
END
559
SQL;
560
        $instructions->addPostStep(sprintf(
561
            $sql,
562
            $oldConstraintName,
563
            $newConstraintName
564
        ));
565
566
        $instructions->addPostStep(sprintf(
567
            "EXECUTE sp_rename N'%s.%s', N'%s', 'COLUMN' ",
568
            $tableName,
569
            $columnName,
570
            $newColumnName
571
        ));
572
573
        return $instructions;
574
    }
575
576
    /**
577
     * Returns the instructions to change a column default value
578
     *
579
     * @param string $tableName The table where the column is
580
     * @param \Phinx\Db\Table\Column $newColumn The column to alter
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
     * @return \Phinx\Db\Util\AlterInstructions
670
     */
671
    protected function getDropDefaultConstraint($tableName, $columnName)
672
    {
673
        $defaultConstraint = $this->getDefaultConstraint($tableName, $columnName);
674
675
        if (!$defaultConstraint) {
676
            return new AlterInstructions();
677
        }
678
679
        return $this->getDropForeignKeyInstructions($tableName, $defaultConstraint);
680
    }
681
682
    /**
683
     * @param string $tableName Table name
684
     * @param string $columnName Column name
685
     * @return string|false
686
     */
687
    protected function getDefaultConstraint($tableName, $columnName)
688
    {
689
        $sql = "SELECT
690
    default_constraints.name
691
FROM
692
    sys.all_columns
693
694
        INNER JOIN
695
    sys.tables
696
        ON all_columns.object_id = tables.object_id
697
698
        INNER JOIN
699
    sys.schemas
700
        ON tables.schema_id = schemas.schema_id
701
702
        INNER JOIN
703
    sys.default_constraints
704
        ON all_columns.default_object_id = default_constraints.object_id
705
706
WHERE
707
        schemas.name = 'dbo'
708
    AND tables.name = '{$tableName}'
709
    AND all_columns.name = '{$columnName}'";
710
711
        $rows = $this->fetchAll($sql);
712
713
        return empty($rows) ? false : $rows[0]['name'];
714
    }
715
716
    /**
717
     * @param int $tableId Table ID
718
     * @param int $indexId Index ID
719
     * @return array
720
     */
721
    protected function getIndexColums($tableId, $indexId)
722
    {
723
        $sql = "SELECT AC.[name] AS [column_name]
724
FROM sys.[index_columns] IC
725
  INNER JOIN sys.[all_columns] AC ON IC.[column_id] = AC.[column_id]
726
WHERE AC.[object_id] = {$tableId} AND IC.[index_id] = {$indexId}  AND IC.[object_id] = {$tableId}
727
ORDER BY IC.[key_ordinal];";
728
729
        $rows = $this->fetchAll($sql);
730
        $columns = [];
731
        foreach ($rows as $row) {
732
            $columns[] = strtolower($row['column_name']);
733
        }
734
735
        return $columns;
736
    }
737
738
    /**
739
     * Get an array of indexes from a particular table.
740
     *
741
     * @param string $tableName Table name
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
     * @return array
901
     */
902
    public function getPrimaryKey($tableName)
903
    {
904
        $rows = $this->fetchAll(sprintf(
905
            "SELECT
906
                    tc.constraint_name,
907
                    kcu.column_name
908
                FROM information_schema.table_constraints AS tc
909
                JOIN information_schema.key_column_usage AS kcu
910
                    ON tc.constraint_name = kcu.constraint_name
911
                WHERE constraint_type = 'PRIMARY KEY'
912
                    AND tc.table_name = '%s'
913
                ORDER BY kcu.ordinal_position",
914
            $tableName
915
        ));
916
917
        $primaryKey = [
918
            'columns' => [],
919
        ];
920
        foreach ($rows as $row) {
921
            $primaryKey['constraint'] = $row['constraint_name'];
922
            $primaryKey['columns'][] = $row['column_name'];
923
        }
924
925
        return $primaryKey;
926
    }
927
928
    /**
929
     * @inheritDoc
930
     */
931
    public function hasForeignKey($tableName, $columns, $constraint = null)
932
    {
933
        if (is_string($columns)) {
934
            $columns = [$columns]; // str to array
935
        }
936
        $foreignKeys = $this->getForeignKeys($tableName);
937
        if ($constraint) {
938
            if (isset($foreignKeys[$constraint])) {
939
                return !empty($foreignKeys[$constraint]);
940
            }
941
942
            return false;
943
        }
944
945
        foreach ($foreignKeys as $key) {
946
            $a = array_diff($columns, $key['columns']);
947
            if (empty($a)) {
948
                return true;
949
            }
950
        }
951
952
        return false;
953
    }
954
955
    /**
956
     * Get an array of foreign keys from a particular table.
957
     *
958
     * @param string $tableName Table name
959
     * @return array
960
     */
961
    protected function getForeignKeys($tableName)
962
    {
963
        $foreignKeys = [];
964
        $rows = $this->fetchAll(sprintf(
965
            "SELECT
966
                    tc.constraint_name,
967
                    tc.table_name, kcu.column_name,
968
                    ccu.table_name AS referenced_table_name,
969
                    ccu.column_name AS referenced_column_name
970
                FROM
971
                    information_schema.table_constraints AS tc
972
                    JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
973
                    JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
974
                WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s'
975
                ORDER BY kcu.ordinal_position",
976
            $tableName
977
        ));
978
        foreach ($rows as $row) {
979
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
980
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
981
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
982
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
983
        }
984
985
        return $foreignKeys;
986
    }
987
988
    /**
989
     * @inheritDoc
990
     */
991
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
992
    {
993
        $instructions = new AlterInstructions();
994
        $instructions->addPostStep(sprintf(
995
            'ALTER TABLE %s ADD %s',
996
            $this->quoteTableName($table->getName()),
997
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
998
        ));
999
1000
        return $instructions;
1001
    }
1002
1003
    /**
1004
     * @inheritDoc
1005
     */
1006
    protected function getDropForeignKeyInstructions($tableName, $constraint)
1007
    {
1008
        $instructions = new AlterInstructions();
1009
        $instructions->addPostStep(sprintf(
1010
            'ALTER TABLE %s DROP CONSTRAINT %s',
1011
            $this->quoteTableName($tableName),
1012
            $constraint
1013
        ));
1014
1015
        return $instructions;
1016
    }
1017
1018
    /**
1019
     * @inheritDoc
1020
     */
1021
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1022
    {
1023
        $instructions = new AlterInstructions();
1024
1025
        foreach ($columns as $column) {
1026
            $rows = $this->fetchAll(sprintf(
1027
                "SELECT
1028
                tc.constraint_name,
1029
                tc.table_name, kcu.column_name,
1030
                ccu.table_name AS referenced_table_name,
1031
                ccu.column_name AS referenced_column_name
1032
            FROM
1033
                information_schema.table_constraints AS tc
1034
                JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
1035
                JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
1036
            WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' and ccu.column_name='%s'
1037
            ORDER BY kcu.ordinal_position",
1038
                $tableName,
1039
                $column
1040
            ));
1041
            foreach ($rows as $row) {
1042
                $instructions->merge(
1043
                    $this->getDropForeignKeyInstructions($tableName, $row['constraint_name'])
1044
                );
1045
            }
1046
        }
1047
1048
        return $instructions;
1049
    }
1050
1051
    /**
1052
     * {@inheritDoc}
1053
     *
1054
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1055
     */
1056
    public function getSqlType($type, $limit = null)
1057
    {
1058
        switch ($type) {
1059
            case static::PHINX_TYPE_FLOAT:
1060
            case static::PHINX_TYPE_DECIMAL:
1061
            case static::PHINX_TYPE_DATETIME:
1062
            case static::PHINX_TYPE_TIME:
1063
            case static::PHINX_TYPE_DATE:
1064
                return ['name' => $type];
1065
            case static::PHINX_TYPE_STRING:
1066
                return ['name' => 'nvarchar', 'limit' => 255];
1067
            case static::PHINX_TYPE_CHAR:
1068
                return ['name' => 'nchar', 'limit' => 255];
1069
            case static::PHINX_TYPE_TEXT:
1070
                return ['name' => 'ntext'];
1071
            case static::PHINX_TYPE_INTEGER:
1072
                return ['name' => 'int'];
1073
            case static::PHINX_TYPE_TINY_INTEGER:
1074
                return ['name' => 'tinyint'];
1075
            case static::PHINX_TYPE_SMALL_INTEGER:
1076
                return ['name' => 'smallint'];
1077
            case static::PHINX_TYPE_BIG_INTEGER:
1078
                return ['name' => 'bigint'];
1079
            case static::PHINX_TYPE_TIMESTAMP:
1080
                return ['name' => 'datetime'];
1081
            case static::PHINX_TYPE_BLOB:
1082
            case static::PHINX_TYPE_BINARY:
1083
                return ['name' => 'varbinary'];
1084
            case static::PHINX_TYPE_BOOLEAN:
1085
                return ['name' => 'bit'];
1086
            case static::PHINX_TYPE_BINARYUUID:
1087
            case static::PHINX_TYPE_UUID:
1088
                return ['name' => 'uniqueidentifier'];
1089
            case static::PHINX_TYPE_FILESTREAM:
1090
                return ['name' => 'varbinary', 'limit' => 'max'];
1091
            // Geospatial database types
1092
            case static::PHINX_TYPE_GEOMETRY:
1093
            case static::PHINX_TYPE_POINT:
1094
            case static::PHINX_TYPE_LINESTRING:
1095
            case static::PHINX_TYPE_POLYGON:
1096
                // SQL Server stores all spatial data using a single data type.
1097
                // Specific types (point, polygon, etc) are set at insert time.
1098
                return ['name' => 'geography'];
1099
            default:
1100
                throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SqlServer.');
1101
        }
1102
    }
1103
1104
    /**
1105
     * Returns Phinx type by SQL type
1106
     *
1107
     * @internal param string $sqlType SQL type
1108
     * @param string $sqlType SQL Type definition
1109
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1110
     * @return string Phinx type
1111
     */
1112
    public function getPhinxType($sqlType)
1113
    {
1114
        switch ($sqlType) {
1115
            case 'nvarchar':
1116
            case 'varchar':
1117
                return static::PHINX_TYPE_STRING;
1118
            case 'char':
1119
            case 'nchar':
1120
                return static::PHINX_TYPE_CHAR;
1121
            case 'text':
1122
            case 'ntext':
1123
                return static::PHINX_TYPE_TEXT;
1124
            case 'int':
1125
            case 'integer':
1126
                return static::PHINX_TYPE_INTEGER;
1127
            case 'decimal':
1128
            case 'numeric':
1129
            case 'money':
1130
                return static::PHINX_TYPE_DECIMAL;
1131
            case 'tinyint':
1132
                return static::PHINX_TYPE_TINY_INTEGER;
1133
            case 'smallint':
1134
                return static::PHINX_TYPE_SMALL_INTEGER;
1135
            case 'bigint':
1136
                return static::PHINX_TYPE_BIG_INTEGER;
1137
            case 'real':
1138
            case 'float':
1139
                return static::PHINX_TYPE_FLOAT;
1140
            case 'binary':
1141
            case 'image':
1142
            case 'varbinary':
1143
                return static::PHINX_TYPE_BINARY;
1144
            case 'time':
1145
                return static::PHINX_TYPE_TIME;
1146
            case 'date':
1147
                return static::PHINX_TYPE_DATE;
1148
            case 'datetime':
1149
            case 'timestamp':
1150
                return static::PHINX_TYPE_DATETIME;
1151
            case 'bit':
1152
                return static::PHINX_TYPE_BOOLEAN;
1153
            case 'uniqueidentifier':
1154
                return static::PHINX_TYPE_UUID;
1155
            case 'filestream':
1156
                return static::PHINX_TYPE_FILESTREAM;
1157
            default:
1158
                throw new UnsupportedColumnTypeException('Column type "' . $sqlType . '" is not supported by SqlServer.');
1159
        }
1160
    }
1161
1162
    /**
1163
     * @inheritDoc
1164
     */
1165
    public function createDatabase($name, $options = [])
1166
    {
1167
        if (isset($options['collation'])) {
1168
            $this->execute(sprintf('CREATE DATABASE [%s] COLLATE [%s]', $name, $options['collation']));
1169
        } else {
1170
            $this->execute(sprintf('CREATE DATABASE [%s]', $name));
1171
        }
1172
        $this->execute(sprintf('USE [%s]', $name));
1173
    }
1174
1175
    /**
1176
     * @inheritDoc
1177
     */
1178
    public function hasDatabase($name)
1179
    {
1180
        $result = $this->fetchRow(
1181
            sprintf(
1182
                "SELECT count(*) as [count] FROM master.dbo.sysdatabases WHERE [name] = '%s'",
1183
                $name
1184
            )
1185
        );
1186
1187
        return $result['count'] > 0;
1188
    }
1189
1190
    /**
1191
     * @inheritDoc
1192
     */
1193
    public function dropDatabase($name)
1194
    {
1195
        $sql = <<<SQL
1196
USE master;
1197
IF EXISTS(select * from sys.databases where name=N'$name')
1198
ALTER DATABASE [$name] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
1199
DROP DATABASE [$name];
1200
SQL;
1201
        $this->execute($sql);
1202
        $this->createdTables = [];
1203
    }
1204
1205
    /**
1206
     * Gets the SqlServer Column Definition for a Column object.
1207
     *
1208
     * @param \Phinx\Db\Table\Column $column Column
1209
     * @param bool $create Create column flag
1210
     * @return string
1211
     */
1212
    protected function getColumnSqlDefinition(Column $column, $create = true)
1213
    {
1214
        $buffer = [];
1215
        if ($column->getType() instanceof Literal) {
1216
            $buffer[] = (string)$column->getType();
1217
        } else {
1218
            $sqlType = $this->getSqlType($column->getType());
1219
            $buffer[] = strtoupper($sqlType['name']);
1220
            // integers cant have limits in SQlServer
1221
            $noLimits = [
1222
                'bigint',
1223
                'int',
1224
                'tinyint',
1225
                'smallint',
1226
            ];
1227
            if ($sqlType['name'] === static::PHINX_TYPE_DECIMAL && $column->getPrecision() && $column->getScale()) {
1228
                $buffer[] = sprintf(
1229
                    '(%s, %s)',
1230
                    $column->getPrecision() ?: $sqlType['precision'],
1231
                    $column->getScale() ?: $sqlType['scale']
1232
                );
1233
            } elseif (!in_array($sqlType['name'], $noLimits) && ($column->getLimit() || isset($sqlType['limit']))) {
1234
                $buffer[] = sprintf('(%s)', $column->getLimit() ?: $sqlType['limit']);
1235
            }
1236
        }
1237
1238
        $properties = $column->getProperties();
1239
        $buffer[] = $column->getType() === 'filestream' ? 'FILESTREAM' : '';
1240
        $buffer[] = isset($properties['rowguidcol']) ? 'ROWGUIDCOL' : '';
1241
1242
        $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1243
1244
        if ($create === true) {
1245
            if ($column->getDefault() === null && $column->isNull()) {
1246
                $buffer[] = ' DEFAULT NULL';
1247
            } else {
1248
                $buffer[] = $this->getDefaultValueDefinition($column->getDefault());
1249
            }
1250
        }
1251
1252
        if ($column->isIdentity()) {
1253
            $seed = $column->getSeed() ?: 1;
1254
            $increment = $column->getIncrement() ?: 1;
1255
            $buffer[] = sprintf('IDENTITY(%d,%d)', $seed, $increment);
1256
        }
1257
1258
        return implode(' ', $buffer);
1259
    }
1260
1261
    /**
1262
     * Gets the SqlServer Index Definition for an Index object.
1263
     *
1264
     * @param \Phinx\Db\Table\Index $index Index
1265
     * @param string $tableName Table name
1266
     * @return string
1267
     */
1268
    protected function getIndexSqlDefinition(Index $index, $tableName)
1269
    {
1270
        $columnNames = $index->getColumns();
1271
        if (is_string($index->getName())) {
1272
            $indexName = $index->getName();
1273
        } else {
1274
            $indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames));
1275
        }
1276
        $order = $index->getOrder() ?? [];
1277
        $columnNames = array_map(function ($columnName) use ($order) {
1278
            $ret = '[' . $columnName . ']';
1279
            if (isset($order[$columnName])) {
1280
                $ret .= ' ' . $order[$columnName];
1281
            }
1282
1283
            return $ret;
1284
        }, $columnNames);
1285
1286
        $includedColumns = $index->getInclude() ? sprintf('INCLUDE ([%s])', implode('],[', $index->getInclude())) : '';
1287
1288
        return sprintf(
1289
            'CREATE %s INDEX %s ON %s (%s) %s;',
1290
            ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1291
            $indexName,
1292
            $this->quoteTableName($tableName),
1293
            implode(',', $columnNames),
1294
            $includedColumns
1295
        );
1296
    }
1297
1298
    /**
1299
     * Gets the SqlServer Foreign Key Definition for an ForeignKey object.
1300
     *
1301
     * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1302
     * @param string $tableName Table name
1303
     * @return string
1304
     */
1305
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1306
    {
1307
        $constraintName = $foreignKey->getConstraint() ?: $tableName . '_' . implode('_', $foreignKey->getColumns());
1308
        $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
1309
        $def .= ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")';
1310
        $def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")';
1311
        if ($foreignKey->getOnDelete()) {
1312
            $def .= " ON DELETE {$foreignKey->getOnDelete()}";
1313
        }
1314
        if ($foreignKey->getOnUpdate()) {
1315
            $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1316
        }
1317
1318
        return $def;
1319
    }
1320
1321
    /**
1322
     * @inheritDoc
1323
     */
1324
    public function getColumnTypes()
1325
    {
1326
        return array_merge(parent::getColumnTypes(), static::$specificColumnTypes);
1327
    }
1328
1329
    /**
1330
     * Records a migration being run.
1331
     *
1332
     * @param \Phinx\Migration\MigrationInterface $migration Migration
1333
     * @param string $direction Direction
1334
     * @param string $startTime Start Time
1335
     * @param string $endTime End Time
1336
     * @return \Phinx\Db\Adapter\AdapterInterface
1337
     */
1338
    public function migrated(MigrationInterface $migration, $direction, $startTime, $endTime)
1339
    {
1340
        $startTime = str_replace(' ', 'T', $startTime);
1341
        $endTime = str_replace(' ', 'T', $endTime);
1342
1343
        return parent::migrated($migration, $direction, $startTime, $endTime);
1344
    }
1345
1346
    /**
1347
     * @inheritDoc
1348
     */
1349
    public function getDecoratedConnection()
1350
    {
1351
        $options = $this->getOptions();
1352
        $options = [
1353
            'username' => $options['user'] ?? null,
1354
            'password' => $options['pass'] ?? null,
1355
            'database' => $options['name'],
1356
            'quoteIdentifiers' => true,
1357
        ] + $options;
1358
1359
        $driver = new SqlServerDriver($options);
1360
        $driver->setConnection($this->connection);
1361
1362
        return new Connection(['driver' => $driver] + $options);
1363
    }
1364
}
1365