Completed
Push — master ( 229d02...ab22bb )
by José
18s queued 10s
created

SqlServerAdapter::getColumnCommentSqlDefinition()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.7
c 0
b 0
f 0
ccs 0
cts 11
cp 0
cc 3
nc 4
nop 2
crap 12
1
<?php
2
/**
3
 * Phinx
4
 *
5
 * (The MIT license)
6
 * Copyright (c) 2015 Rob Morgan
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated * documentation files (the "Software"), to
10
 * deal in the Software without restriction, including without limitation the
11
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
12
 * sell copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24
 * IN THE SOFTWARE.
25
 *
26
 * @package    Phinx
27
 * @subpackage Phinx\Db\Adapter
28
 */
29
namespace Phinx\Db\Adapter;
30
31
use Cake\Database\Connection;
32
use Cake\Database\Driver\Sqlserver as SqlServerDriver;
33
use Phinx\Db\Table\Column;
34
use Phinx\Db\Table\ForeignKey;
35
use Phinx\Db\Table\Index;
36
use Phinx\Db\Table\Table;
37
use Phinx\Db\Util\AlterInstructions;
38
use Phinx\Util\Literal;
39
40
/**
41
 * Phinx SqlServer Adapter.
42
 *
43
 * @author Rob Morgan <[email protected]>
44
 */
45
class SqlServerAdapter extends PdoAdapter implements AdapterInterface
46
{
47
    protected $schema = 'dbo';
48
49
    protected $signedColumnTypes = ['integer' => true, 'biginteger' => true, 'float' => true, 'decimal' => true];
50
51
    /**
52
     * {@inheritdoc}
53
     */
54
    public function connect()
55
    {
56
        if ($this->connection === null) {
57
            if (!class_exists('PDO') || !in_array('sqlsrv', \PDO::getAvailableDrivers(), true)) {
58
                // try our connection via freetds (Mac/Linux)
59
                $this->connectDblib();
60
61
                return;
62
            }
63
64
            $db = null;
65
            $options = $this->getOptions();
66
67
            // if port is specified use it, otherwise use the SqlServer default
68 View Code Duplication
            if (empty($options['port'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
69
                $dsn = 'sqlsrv:server=' . $options['host'] . ';database=' . $options['name'];
70
            } else {
71
                $dsn = 'sqlsrv:server=' . $options['host'] . ',' . $options['port'] . ';database=' . $options['name'];
72
            }
73
            $dsn .= ';MultipleActiveResultSets=false';
74
75
            $driverOptions = [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION];
76
77
            // charset support
78
            if (isset($options['charset'])) {
79
                $driverOptions[\PDO::SQLSRV_ATTR_ENCODING] = $options['charset'];
80
            }
81
82
            // support arbitrary \PDO::SQLSRV_ATTR_* driver options and pass them to PDO
83
            // http://php.net/manual/en/ref.pdo-sqlsrv.php#pdo-sqlsrv.constants
84 View Code Duplication
            foreach ($options as $key => $option) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
85
                if (strpos($key, 'sqlsrv_attr_') === 0) {
86
                    $driverOptions[constant('\PDO::' . strtoupper($key))] = $option;
87
                }
88
            }
89
90
            try {
91
                $db = new \PDO($dsn, $options['user'], $options['pass'], $driverOptions);
92
            } catch (\PDOException $exception) {
93
                throw new \InvalidArgumentException(sprintf(
94
                    'There was a problem connecting to the database: %s',
95
                    $exception->getMessage()
96
                ));
97
            }
98
99
            $this->setConnection($db);
100
        }
101
    }
102
103
    /**
104
     * Connect to MSSQL using dblib/freetds.
105
     *
106
     * The "sqlsrv" driver is not available on Unix machines.
107
     *
108
     * @throws \InvalidArgumentException
109
     */
110
    protected function connectDblib()
111
    {
112
        if (!class_exists('PDO') || !in_array('dblib', \PDO::getAvailableDrivers(), true)) {
113
            // @codeCoverageIgnoreStart
114
            throw new \RuntimeException('You need to enable the PDO_Dblib extension for Phinx to run properly.');
115
            // @codeCoverageIgnoreEnd
116
        }
117
118
        $options = $this->getOptions();
119
120
        // if port is specified use it, otherwise use the SqlServer default
121 View Code Duplication
        if (empty($options['port'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
122
            $dsn = 'dblib:host=' . $options['host'] . ';dbname=' . $options['name'];
123
        } else {
124
            $dsn = 'dblib:host=' . $options['host'] . ':' . $options['port'] . ';dbname=' . $options['name'];
125
        }
126
127
        $driverOptions = [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION];
128
129
        try {
130
            $db = new \PDO($dsn, $options['user'], $options['pass'], $driverOptions);
131
        } catch (\PDOException $exception) {
132
            throw new \InvalidArgumentException(sprintf(
133
                'There was a problem connecting to the database: %s',
134
                $exception->getMessage()
135
            ));
136
        }
137
138
        $this->setConnection($db);
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function disconnect()
145
    {
146
        $this->connection = null;
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152
    public function hasTransactions()
153
    {
154
        return true;
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160
    public function beginTransaction()
161
    {
162
        $this->execute('BEGIN TRANSACTION');
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168
    public function commitTransaction()
169
    {
170
        $this->execute('COMMIT TRANSACTION');
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176
    public function rollbackTransaction()
177
    {
178
        $this->execute('ROLLBACK TRANSACTION');
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184
    public function quoteTableName($tableName)
185
    {
186
        return str_replace('.', '].[', $this->quoteColumnName($tableName));
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192
    public function quoteColumnName($columnName)
193
    {
194
        return '[' . str_replace(']', '\]', $columnName) . ']';
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200
    public function hasTable($tableName)
201
    {
202
        $result = $this->fetchRow(sprintf('SELECT count(*) as [count] FROM information_schema.tables WHERE table_name = \'%s\';', $tableName));
203
204
        return $result['count'] > 0;
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    public function createTable(Table $table, array $columns = [], array $indexes = [])
211
    {
212
        $options = $table->getOptions();
213
214
        // Add the default primary key
215 View Code Duplication
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
216
            $options['id'] = 'id';
217
        }
218
219 View Code Duplication
        if (isset($options['id']) && is_string($options['id'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
220
            // Handle id => "field_name" to support AUTO_INCREMENT
221
            $column = new Column();
222
            $column->setName($options['id'])
223
                   ->setType('integer')
224
                   ->setIdentity(true);
225
226
            array_unshift($columns, $column);
227
            $options['primary_key'] = $options['id'];
228
        }
229
230
        $sql = 'CREATE TABLE ';
231
        $sql .= $this->quoteTableName($table->getName()) . ' (';
232
        $sqlBuffer = [];
233
        $columnsWithComments = [];
234 View Code Duplication
        foreach ($columns as $column) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
235
            $sqlBuffer[] = $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column);
236
237
            // set column comments, if needed
238
            if ($column->getComment()) {
239
                $columnsWithComments[] = $column;
240
            }
241
        }
242
243
        // set the primary key(s)
244
        if (isset($options['primary_key'])) {
245
            $pkSql = sprintf('CONSTRAINT PK_%s PRIMARY KEY (', $table->getName());
246
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
247
                $pkSql .= $this->quoteColumnName($options['primary_key']);
248
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
249
                $pkSql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
250
            }
251
            $pkSql .= ')';
252
            $sqlBuffer[] = $pkSql;
253
        }
254
255
        $sql .= implode(', ', $sqlBuffer);
256
        $sql .= ');';
257
258
        // process column comments
259
        foreach ($columnsWithComments as $column) {
260
            $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName());
261
        }
262
263
        // set the indexes
264
        foreach ($indexes as $index) {
265
            $sql .= $this->getIndexSqlDefinition($index, $table->getName());
266
        }
267
268
        // execute the sql
269
        $this->execute($sql);
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275 View Code Duplication
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
276
    {
277
        $instructions = new AlterInstructions();
278
279
        // Drop the existing primary key
280
        $primaryKey = $this->getPrimaryKey($table->getName());
281
        if (!empty($primaryKey['constraint'])) {
282
            $sql = sprintf(
283
                'DROP CONSTRAINT %s',
284
                $this->quoteColumnName($primaryKey['constraint'])
285
            );
286
            $instructions->addAlter($sql);
287
        }
288
289
        // Add the primary key(s)
290
        if (!empty($newColumns)) {
291
            $sql = sprintf(
292
                'ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (',
293
                $this->quoteTableName($table->getName()),
294
                $this->quoteColumnName('PK_' . $table->getName())
295
            );
296
            if (is_string($newColumns)) { // handle primary_key => 'id'
297
                $sql .= $this->quoteColumnName($newColumns);
298
            } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id')
299
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns));
300
            } else {
301
                throw new \InvalidArgumentException(sprintf(
302
                    "Invalid value for primary key: %s",
303
                    json_encode($newColumns)
304
                ));
305
            }
306
            $sql .= ')';
307
            $instructions->addPostStep($sql);
308
        }
309
310
        return $instructions;
311
    }
312
313
    /**
314
     * {@inheritdoc}
315
     */
316
    protected function getChangeCommentInstructions(Table $table, $newComment)
317
    {
318
        throw new \BadMethodCallException('SQLite does not have table comments');
319
    }
320
321
    /**
322
     * Gets the SqlServer Column Comment Defininition for a column object.
323
     *
324
     * @param \Phinx\Db\Table\Column $column    Column
325
     * @param string $tableName Table name
326
     *
327
     * @return string
328
     */
329
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
330
    {
331
        // passing 'null' is to remove column comment
332
        $currentComment = $this->getColumnComment($tableName, $column->getName());
333
334
        $comment = (strcasecmp($column->getComment(), 'NULL') !== 0) ? $this->getConnection()->quote($column->getComment()) : '\'\'';
335
        $command = $currentComment === false ? 'sp_addextendedproperty' : 'sp_updateextendedproperty';
336
337
        return sprintf(
338
            "EXECUTE %s N'MS_Description', N%s, N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s';",
339
            $command,
340
            $comment,
341
            $this->schema,
342
            $tableName,
343
            $column->getName()
344
        );
345
    }
346
347
    /**
348
     * {@inheritdoc}
349
     */
350
    protected function getRenameTableInstructions($tableName, $newTableName)
351
    {
352
        $sql = sprintf(
353
            'EXEC sp_rename \'%s\', \'%s\'',
354
            $tableName,
355
            $newTableName
356
        );
357
358
        return new AlterInstructions([], [$sql]);
359
    }
360
361
    /**
362
     * {@inheritdoc}
363
     */
364
    protected function getDropTableInstructions($tableName)
365
    {
366
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
367
368
        return new AlterInstructions([], [$sql]);
369
    }
370
371
    /**
372
     * {@inheritdoc}
373
     */
374
    public function truncateTable($tableName)
375
    {
376
        $sql = sprintf(
377
            'TRUNCATE TABLE %s',
378
            $this->quoteTableName($tableName)
379
        );
380
381
        $this->execute($sql);
382
    }
383
384
    public function getColumnComment($tableName, $columnName)
385
    {
386
        $sql = sprintf("SELECT cast(extended_properties.[value] as nvarchar(4000)) comment
387
  FROM sys.schemas
388
 INNER JOIN sys.tables
389
    ON schemas.schema_id = tables.schema_id
390
 INNER JOIN sys.columns
391
    ON tables.object_id = columns.object_id
392
 INNER JOIN sys.extended_properties
393
    ON tables.object_id = extended_properties.major_id
394
   AND columns.column_id = extended_properties.minor_id
395
   AND extended_properties.name = 'MS_Description'
396
   WHERE schemas.[name] = '%s' AND tables.[name] = '%s' AND columns.[name] = '%s'", $this->schema, $tableName, $columnName);
397
        $row = $this->fetchRow($sql);
398
399
        if ($row) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $row of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
400
            return trim($row['comment']);
401
        }
402
403
        return false;
404
    }
405
406
    /**
407
     * {@inheritdoc}
408
     */
409
    public function getColumns($tableName)
410
    {
411
        $columns = [];
412
        $sql = sprintf(
413
            "SELECT DISTINCT TABLE_SCHEMA AS [schema], TABLE_NAME as [table_name], COLUMN_NAME AS [name], DATA_TYPE AS [type],
414
            IS_NULLABLE AS [null], COLUMN_DEFAULT AS [default],
415
            CHARACTER_MAXIMUM_LENGTH AS [char_length],
416
            NUMERIC_PRECISION AS [precision],
417
            NUMERIC_SCALE AS [scale], ORDINAL_POSITION AS [ordinal_position],
418
            COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as [identity]
419
        FROM INFORMATION_SCHEMA.COLUMNS
420
        WHERE TABLE_NAME = '%s'
421
        ORDER BY ordinal_position",
422
            $tableName
423
        );
424
        $rows = $this->fetchAll($sql);
425
        foreach ($rows as $columnInfo) {
426
            try {
427
                $type = $this->getPhinxType($columnInfo['type']);
428
            } catch (UnsupportedColumnTypeException $e) {
429
                $type = Literal::from($columnInfo['type']);
430
            }
431
432
            $column = new Column();
433
            $column->setName($columnInfo['name'])
434
                   ->setType($type)
435
                   ->setNull($columnInfo['null'] !== 'NO')
436
                   ->setDefault($this->parseDefault($columnInfo['default']))
437
                   ->setIdentity($columnInfo['identity'] === '1')
438
                   ->setComment($this->getColumnComment($columnInfo['table_name'], $columnInfo['name']));
0 ignored issues
show
Security Bug introduced by
It seems like $this->getColumnComment(...], $columnInfo['name']) targeting Phinx\Db\Adapter\SqlServ...ter::getColumnComment() can also be of type false; however, Phinx\Db\Table\Column::setComment() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
439
440
            if (!empty($columnInfo['char_length'])) {
441
                $column->setLimit($columnInfo['char_length']);
442
            }
443
444
            $columns[$columnInfo['name']] = $column;
445
        }
446
447
        return $columns;
448
    }
449
450
    protected function parseDefault($default)
451
    {
452
        $default = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default);
453
454
        if (strtoupper($default) === 'NULL') {
455
            $default = null;
456
        } elseif (is_numeric($default)) {
457
            $default = (int)$default;
458
        }
459
460
        return $default;
461
    }
462
463
    /**
464
     * {@inheritdoc}
465
     */
466
    public function hasColumn($tableName, $columnName)
467
    {
468
        $sql = sprintf(
469
            "SELECT count(*) as [count]
470
             FROM information_schema.columns
471
             WHERE table_name = '%s' AND column_name = '%s'",
472
            $tableName,
473
            $columnName
474
        );
475
        $result = $this->fetchRow($sql);
476
477
        return $result['count'] > 0;
478
    }
479
480
    /**
481
     * {@inheritdoc}
482
     */
483 View Code Duplication
    protected function getAddColumnInstructions(Table $table, Column $column)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
484
    {
485
        $alter = sprintf(
486
            'ALTER TABLE %s ADD %s %s',
487
            $table->getName(),
488
            $this->quoteColumnName($column->getName()),
489
            $this->getColumnSqlDefinition($column)
490
        );
491
492
        return new AlterInstructions([], [$alter]);
493
    }
494
495
    /**
496
     * {@inheritdoc}
497
     */
498
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
499
    {
500
        if (!$this->hasColumn($tableName, $columnName)) {
501
            throw new \InvalidArgumentException("The specified column does not exist: $columnName");
502
        }
503
504
        $instructions = new AlterInstructions();
505
506
        $oldConstraintName = "DF_{$tableName}_{$columnName}";
507
        $newConstraintName = "DF_{$tableName}_{$newColumnName}";
508
        $sql = <<<SQL
509
IF (OBJECT_ID('$oldConstraintName', 'D') IS NOT NULL)
510
BEGIN
511
     EXECUTE sp_rename N'%s', N'%s', N'OBJECT'
512
END
513
SQL;
514
        $instructions->addPostStep(sprintf(
515
            $sql,
516
            $oldConstraintName,
517
            $newConstraintName
518
        ));
519
520
        $instructions->addPostStep(sprintf(
521
            "EXECUTE sp_rename N'%s.%s', N'%s', 'COLUMN' ",
522
            $tableName,
523
            $columnName,
524
            $newColumnName
525
        ));
526
527
        return $instructions;
528
    }
529
530
    /**
531
     * Returns the instructions to change a column default value
532
     *
533
     * @param string $tableName The table where the column is
534
     * @param Column $newColumn The column to alter
535
     * @return AlterInstructions
536
     */
537
    protected function getChangeDefault($tableName, Column $newColumn)
538
    {
539
        $constraintName = "DF_{$tableName}_{$newColumn->getName()}";
540
        $default = $newColumn->getDefault();
541
        $instructions = new AlterInstructions();
542
543
        if ($default === null) {
544
            $default = 'DEFAULT NULL';
545
        } else {
546
            $default = ltrim($this->getDefaultValueDefinition($default));
547
        }
548
549
        if (empty($default)) {
550
            return $instructions;
551
        }
552
553
        $instructions->addPostStep(sprintf(
554
            'ALTER TABLE %s ADD CONSTRAINT %s %s FOR %s',
555
            $this->quoteTableName($tableName),
556
            $constraintName,
557
            $default,
558
            $this->quoteColumnName($newColumn->getName())
559
        ));
560
561
        return $instructions;
562
    }
563
564
    /**
565
     * {@inheritdoc}
566
     */
567
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
568
    {
569
        $columns = $this->getColumns($tableName);
570
        $changeDefault =
571
            $newColumn->getDefault() !== $columns[$columnName]->getDefault() ||
572
            $newColumn->getType() !== $columns[$columnName]->getType();
573
574
        $instructions = new AlterInstructions();
575
576
        if ($columnName !== $newColumn->getName()) {
577
            $instructions->merge(
578
                $this->getRenameColumnInstructions($tableName, $columnName, $newColumn->getName())
579
            );
580
        }
581
582
        if ($changeDefault) {
583
            $instructions->merge($this->getDropDefaultConstraint($tableName, $newColumn->getName()));
584
        }
585
586
        $instructions->addPostStep(sprintf(
587
            'ALTER TABLE %s ALTER COLUMN %s %s',
588
            $this->quoteTableName($tableName),
589
            $this->quoteColumnName($newColumn->getName()),
590
            $this->getColumnSqlDefinition($newColumn, false)
591
        ));
592
        // change column comment if needed
593
        if ($newColumn->getComment()) {
594
            $instructions->addPostStep($this->getColumnCommentSqlDefinition($newColumn, $tableName));
595
        }
596
597
        if ($changeDefault) {
598
            $instructions->merge($this->getChangeDefault($tableName, $newColumn));
599
        }
600
601
        return $instructions;
602
    }
603
604
    /**
605
     * {@inheritdoc}
606
     */
607
    protected function getDropColumnInstructions($tableName, $columnName)
608
    {
609
        $instructions = $this->getDropDefaultConstraint($tableName, $columnName);
610
611
        $instructions->addPostStep(sprintf(
612
            'ALTER TABLE %s DROP COLUMN %s',
613
            $this->quoteTableName($tableName),
614
            $this->quoteColumnName($columnName)
615
        ));
616
617
        return $instructions;
618
    }
619
620
    /**
621
     * {@inheritdoc}
622
     */
623
    protected function getDropDefaultConstraint($tableName, $columnName)
624
    {
625
        $defaultConstraint = $this->getDefaultConstraint($tableName, $columnName);
626
627
        if (!$defaultConstraint) {
628
            return new AlterInstructions();
629
        }
630
631
        return $this->getDropForeignKeyInstructions($tableName, $defaultConstraint);
632
    }
633
634
    protected function getDefaultConstraint($tableName, $columnName)
635
    {
636
        $sql = "SELECT
637
    default_constraints.name
638
FROM
639
    sys.all_columns
640
641
        INNER JOIN
642
    sys.tables
643
        ON all_columns.object_id = tables.object_id
644
645
        INNER JOIN
646
    sys.schemas
647
        ON tables.schema_id = schemas.schema_id
648
649
        INNER JOIN
650
    sys.default_constraints
651
        ON all_columns.default_object_id = default_constraints.object_id
652
653
WHERE
654
        schemas.name = 'dbo'
655
    AND tables.name = '{$tableName}'
656
    AND all_columns.name = '{$columnName}'";
657
658
        $rows = $this->fetchAll($sql);
659
660
        return empty($rows) ? false : $rows[0]['name'];
661
    }
662
663
    protected function getIndexColums($tableId, $indexId)
664
    {
665
        $sql = "SELECT AC.[name] AS [column_name]
666
FROM sys.[index_columns] IC
667
  INNER JOIN sys.[all_columns] AC ON IC.[column_id] = AC.[column_id]
668
WHERE AC.[object_id] = {$tableId} AND IC.[index_id] = {$indexId}  AND IC.[object_id] = {$tableId}
669
ORDER BY IC.[key_ordinal];";
670
671
        $rows = $this->fetchAll($sql);
672
        $columns = [];
673
        foreach ($rows as $row) {
674
            $columns[] = strtolower($row['column_name']);
675
        }
676
677
        return $columns;
678
    }
679
680
    /**
681
     * Get an array of indexes from a particular table.
682
     *
683
     * @param string $tableName Table Name
684
     * @return array
685
     */
686
    public function getIndexes($tableName)
687
    {
688
        $indexes = [];
689
        $sql = "SELECT I.[name] AS [index_name], I.[index_id] as [index_id], T.[object_id] as [table_id]
690
FROM sys.[tables] AS T
691
  INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
692
WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP'  AND T.[name] = '{$tableName}'
693
ORDER BY T.[name], I.[index_id];";
694
695
        $rows = $this->fetchAll($sql);
696
        foreach ($rows as $row) {
697
            $columns = $this->getIndexColums($row['table_id'], $row['index_id']);
698
            $indexes[$row['index_name']] = ['columns' => $columns];
699
        }
700
701
        return $indexes;
702
    }
703
704
    /**
705
     * {@inheritdoc}
706
     */
707 View Code Duplication
    public function hasIndex($tableName, $columns)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
708
    {
709
        if (is_string($columns)) {
710
            $columns = [$columns]; // str to array
711
        }
712
713
        $columns = array_map('strtolower', $columns);
714
        $indexes = $this->getIndexes($tableName);
715
716
        foreach ($indexes as $index) {
717
            $a = array_diff($columns, $index['columns']);
718
719
            if (empty($a)) {
720
                return true;
721
            }
722
        }
723
724
        return false;
725
    }
726
727
    /**
728
     * {@inheritdoc}
729
     */
730 View Code Duplication
    public function hasIndexByName($tableName, $indexName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
731
    {
732
        $indexes = $this->getIndexes($tableName);
733
734
        foreach ($indexes as $name => $index) {
735
            if ($name === $indexName) {
736
                 return true;
737
            }
738
        }
739
740
        return false;
741
    }
742
743
    /**
744
     * {@inheritdoc}
745
     */
746
    protected function getAddIndexInstructions(Table $table, Index $index)
747
    {
748
        $sql = $this->getIndexSqlDefinition($index, $table->getName());
749
750
        return new AlterInstructions([], [$sql]);
751
    }
752
753
    /**
754
     * {@inheritdoc}
755
     */
756
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
757
    {
758
        if (is_string($columns)) {
759
            $columns = [$columns]; // str to array
760
        }
761
762
        $indexes = $this->getIndexes($tableName);
763
        $columns = array_map('strtolower', $columns);
764
        $instructions = new AlterInstructions();
765
766
        foreach ($indexes as $indexName => $index) {
767
            $a = array_diff($columns, $index['columns']);
768 View Code Duplication
            if (empty($a)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
769
                $instructions->addPostStep(sprintf(
770
                    'DROP INDEX %s ON %s',
771
                    $this->quoteColumnName($indexName),
772
                    $this->quoteTableName($tableName)
773
                ));
774
775
                return $instructions;
776
            }
777
        }
778
779
        throw new \InvalidArgumentException(sprintf(
780
            "The specified index on columns '%s' does not exist",
781
            implode(',', $columns)
782
        ));
783
    }
784
785
    /**
786
     * {@inheritdoc}
787
     */
788
    protected function getDropIndexByNameInstructions($tableName, $indexName)
789
    {
790
        $indexes = $this->getIndexes($tableName);
791
        $instructions = new AlterInstructions();
792
793
        foreach ($indexes as $name => $index) {
794 View Code Duplication
            if ($name === $indexName) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
795
                $instructions->addPostStep(sprintf(
796
                    'DROP INDEX %s ON %s',
797
                    $this->quoteColumnName($indexName),
798
                    $this->quoteTableName($tableName)
799
                ));
800
801
                return $instructions;
802
            }
803
        }
804
805
        throw new \InvalidArgumentException(sprintf(
806
            "The specified index name '%s' does not exist",
807
            $indexName
808
        ));
809
    }
810
811
    /**
812
     * {@inheritdoc}
813
     */
814 View Code Duplication
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
815
    {
816
        $primaryKey = $this->getPrimaryKey($tableName);
817
818
        if (empty($primaryKey)) {
819
            return false;
820
        }
821
822
        if ($constraint) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $constraint of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
823
            return ($primaryKey['constraint'] === $constraint);
824
        } else {
825
            if (is_string($columns)) {
826
                $columns = [$columns]; // str to array
827
            }
828
            $missingColumns = array_diff($columns, $primaryKey['columns']);
829
830
            return empty($missingColumns);
831
        }
832
    }
833
834
    /**
835
     * Get the primary key from a particular table.
836
     *
837
     * @param string $tableName Table Name
838
     * @return array
839
     */
840 View Code Duplication
    public function getPrimaryKey($tableName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
841
    {
842
        $rows = $this->fetchAll(sprintf(
843
            "SELECT
844
                    tc.constraint_name,
845
                    kcu.column_name
846
                FROM information_schema.table_constraints AS tc
847
                JOIN information_schema.key_column_usage AS kcu
848
                    ON tc.constraint_name = kcu.constraint_name
849
                WHERE constraint_type = 'PRIMARY KEY'
850
                    AND tc.table_name = '%s'
851
                ORDER BY kcu.ordinal_position",
852
            $tableName
853
        ));
854
855
        $primaryKey = [
856
            'columns' => [],
857
        ];
858
        foreach ($rows as $row) {
859
            $primaryKey['constraint'] = $row['constraint_name'];
860
            $primaryKey['columns'][] = $row['column_name'];
861
        }
862
863
        return $primaryKey;
864
    }
865
866
    /**
867
     * {@inheritdoc}
868
     */
869 View Code Duplication
    public function hasForeignKey($tableName, $columns, $constraint = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
870
    {
871
        if (is_string($columns)) {
872
            $columns = [$columns]; // str to array
873
        }
874
        $foreignKeys = $this->getForeignKeys($tableName);
875
        if ($constraint) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $constraint of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
876
            if (isset($foreignKeys[$constraint])) {
877
                return !empty($foreignKeys[$constraint]);
878
            }
879
880
            return false;
881
        } else {
882
            foreach ($foreignKeys as $key) {
883
                $a = array_diff($columns, $key['columns']);
884
                if (empty($a)) {
885
                    return true;
886
                }
887
            }
888
889
            return false;
890
        }
891
    }
892
893
    /**
894
     * Get an array of foreign keys from a particular table.
895
     *
896
     * @param string $tableName Table Name
897
     * @return array
898
     */
899 View Code Duplication
    protected function getForeignKeys($tableName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
900
    {
901
        $foreignKeys = [];
902
        $rows = $this->fetchAll(sprintf(
903
            "SELECT
904
                    tc.constraint_name,
905
                    tc.table_name, kcu.column_name,
906
                    ccu.table_name AS referenced_table_name,
907
                    ccu.column_name AS referenced_column_name
908
                FROM
909
                    information_schema.table_constraints AS tc
910
                    JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
911
                    JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
912
                WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s'
913
                ORDER BY kcu.ordinal_position",
914
            $tableName
915
        ));
916
        foreach ($rows as $row) {
917
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
918
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
919
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
920
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
921
        }
922
923
        return $foreignKeys;
924
    }
925
926
    /**
927
     * {@inheritdoc}
928
     */
929
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
930
    {
931
        $instructions = new AlterInstructions();
932
        $instructions->addPostStep(sprintf(
933
            'ALTER TABLE %s ADD %s',
934
            $this->quoteTableName($table->getName()),
935
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
936
        ));
937
938
        return $instructions;
939
    }
940
941
    /**
942
     * {@inheritdoc}
943
     */
944
    protected function getDropForeignKeyInstructions($tableName, $constraint)
945
    {
946
        $instructions = new AlterInstructions();
947
        $instructions->addPostStep(sprintf(
948
            'ALTER TABLE %s DROP CONSTRAINT %s',
949
            $this->quoteTableName($tableName),
950
            $constraint
951
        ));
952
953
        return $instructions;
954
    }
955
956
    /**
957
     * {@inheritdoc}
958
     */
959
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
960
    {
961
        $instructions = new AlterInstructions();
962
963 View Code Duplication
        foreach ($columns as $column) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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' and ccu.column_name='%s'
975
            ORDER BY kcu.ordinal_position",
976
                $tableName,
977
                $column
978
            ));
979
            foreach ($rows as $row) {
980
                $instructions->merge(
981
                    $this->getDropForeignKeyInstructions($tableName, $row['constraint_name'])
982
                );
983
            }
984
        }
985
986
        return $instructions;
987
    }
988
989
    /**
990
     * {@inheritdoc}
991
     */
992
    public function getSqlType($type, $limit = null)
993
    {
994
        switch ($type) {
995
            case static::PHINX_TYPE_FLOAT:
996
            case static::PHINX_TYPE_DECIMAL:
997
            case static::PHINX_TYPE_DATETIME:
998
            case static::PHINX_TYPE_TIME:
999
            case static::PHINX_TYPE_DATE:
1000
                return ['name' => $type];
1001
            case static::PHINX_TYPE_STRING:
1002
                return ['name' => 'nvarchar', 'limit' => 255];
1003
            case static::PHINX_TYPE_CHAR:
1004
                return ['name' => 'nchar', 'limit' => 255];
1005
            case static::PHINX_TYPE_TEXT:
1006
                return ['name' => 'ntext'];
1007
            case static::PHINX_TYPE_INTEGER:
1008
                return ['name' => 'int'];
1009
            case static::PHINX_TYPE_SMALL_INTEGER:
1010
                return ['name' => 'smallint'];
1011
            case static::PHINX_TYPE_BIG_INTEGER:
1012
                return ['name' => 'bigint'];
1013
            case static::PHINX_TYPE_TIMESTAMP:
1014
                return ['name' => 'datetime'];
1015
            case static::PHINX_TYPE_BLOB:
1016
            case static::PHINX_TYPE_BINARY:
1017
                return ['name' => 'varbinary'];
1018
            case static::PHINX_TYPE_BOOLEAN:
1019
                return ['name' => 'bit'];
1020
            case static::PHINX_TYPE_UUID:
1021
                return ['name' => 'uniqueidentifier'];
1022
            case static::PHINX_TYPE_FILESTREAM:
1023
                return ['name' => 'varbinary', 'limit' => 'max'];
1024
            // Geospatial database types
1025
            case static::PHINX_TYPE_GEOMETRY:
1026
            case static::PHINX_TYPE_POINT:
1027
            case static::PHINX_TYPE_LINESTRING:
1028
            case static::PHINX_TYPE_POLYGON:
1029
                // SQL Server stores all spatial data using a single data type.
1030
                // Specific types (point, polygon, etc) are set at insert time.
1031
                return ['name' => 'geography'];
1032
            default:
1033
                throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SqlServer.');
1034
        }
1035
    }
1036
1037
    /**
1038
     * Returns Phinx type by SQL type
1039
     *
1040
     * @param string $sqlType SQL Type definition
1041
     * @throws UnsupportedColumnTypeException
1042
     * @internal param string $sqlType SQL type
1043
     * @return string Phinx type
1044
     */
1045
    public function getPhinxType($sqlType)
1046
    {
1047
        switch ($sqlType) {
1048
            case 'nvarchar':
1049
            case 'varchar':
1050
                return static::PHINX_TYPE_STRING;
1051
            case 'char':
1052
            case 'nchar':
1053
                return static::PHINX_TYPE_CHAR;
1054
            case 'text':
1055
            case 'ntext':
1056
                return static::PHINX_TYPE_TEXT;
1057
            case 'int':
1058
            case 'integer':
1059
                return static::PHINX_TYPE_INTEGER;
1060
            case 'decimal':
1061
            case 'numeric':
1062
            case 'money':
1063
                return static::PHINX_TYPE_DECIMAL;
1064
            case 'smallint':
1065
                return static::PHINX_TYPE_SMALL_INTEGER;
1066
            case 'bigint':
1067
                return static::PHINX_TYPE_BIG_INTEGER;
1068
            case 'real':
1069
            case 'float':
1070
                return static::PHINX_TYPE_FLOAT;
1071
            case 'binary':
1072
            case 'image':
1073
            case 'varbinary':
1074
                return static::PHINX_TYPE_BINARY;
1075
            case 'time':
1076
                return static::PHINX_TYPE_TIME;
1077
            case 'date':
1078
                return static::PHINX_TYPE_DATE;
1079
            case 'datetime':
1080
            case 'timestamp':
1081
                return static::PHINX_TYPE_DATETIME;
1082
            case 'bit':
1083
                return static::PHINX_TYPE_BOOLEAN;
1084
            case 'uniqueidentifier':
1085
                return static::PHINX_TYPE_UUID;
1086
            case 'filestream':
1087
                return static::PHINX_TYPE_FILESTREAM;
1088
            default:
1089
                throw new UnsupportedColumnTypeException('Column type "' . $sqlType . '" is not supported by SqlServer.');
1090
        }
1091
    }
1092
1093
    /**
1094
     * {@inheritdoc}
1095
     */
1096
    public function createDatabase($name, $options = [])
1097
    {
1098 View Code Duplication
        if (isset($options['collation'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1099
            $this->execute(sprintf('CREATE DATABASE [%s] COLLATE [%s]', $name, $options['collation']));
1100
        } else {
1101
            $this->execute(sprintf('CREATE DATABASE [%s]', $name));
1102
        }
1103
        $this->execute(sprintf('USE [%s]', $name));
1104
    }
1105
1106
    /**
1107
     * {@inheritdoc}
1108
     */
1109
    public function hasDatabase($name)
1110
    {
1111
        $result = $this->fetchRow(
1112
            sprintf(
1113
                'SELECT count(*) as [count] FROM master.dbo.sysdatabases WHERE [name] = \'%s\'',
1114
                $name
1115
            )
1116
        );
1117
1118
        return $result['count'] > 0;
1119
    }
1120
1121
    /**
1122
     * {@inheritdoc}
1123
     */
1124
    public function dropDatabase($name)
1125
    {
1126
        $sql = <<<SQL
1127
USE master;
1128
IF EXISTS(select * from sys.databases where name=N'$name')
1129
ALTER DATABASE [$name] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
1130
DROP DATABASE [$name];
1131
SQL;
1132
        $this->execute($sql);
1133
    }
1134
1135
    /**
1136
     * Gets the SqlServer Column Definition for a Column object.
1137
     *
1138
     * @param \Phinx\Db\Table\Column $column Column
1139
     * @return string
1140
     */
1141
    protected function getColumnSqlDefinition(Column $column, $create = true)
1142
    {
1143
        $buffer = [];
1144
        if ($column->getType() instanceof Literal) {
1145
            $buffer[] = (string)$column->getType();
1146
        } else {
1147
            $sqlType = $this->getSqlType($column->getType());
1148
            $buffer[] = strtoupper($sqlType['name']);
1149
            // integers cant have limits in SQlServer
1150
            $noLimits = [
1151
                'bigint',
1152
                'int',
1153
                'tinyint'
1154
            ];
1155
            if (static::PHINX_TYPE_DECIMAL === $sqlType['name'] && $column->getPrecision() && $column->getScale()) {
1156
                $buffer[] = sprintf(
1157
                    '(%s, %s)',
1158
                    $column->getPrecision() ?: $sqlType['precision'],
1159
                    $column->getScale() ?: $sqlType['scale']
1160
                );
1161
            } elseif (!in_array($sqlType['name'], $noLimits) && ($column->getLimit() || isset($sqlType['limit']))) {
1162
                $buffer[] = sprintf('(%s)', $column->getLimit() ? $column->getLimit() : $sqlType['limit']);
1163
            }
1164
        }
1165
1166
        $properties = $column->getProperties();
1167
        $buffer[] = $column->getType() === 'filestream' ? 'FILESTREAM' : '';
1168
        $buffer[] = isset($properties['rowguidcol']) ? 'ROWGUIDCOL' : '';
1169
1170
        $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1171
1172
        if ($create === true) {
1173
            if ($column->getDefault() === null && $column->isNull()) {
1174
                $buffer[] = ' DEFAULT NULL';
1175
            } else {
1176
                $buffer[] = $this->getDefaultValueDefinition($column->getDefault());
1177
            }
1178
        }
1179
1180
        if ($column->isIdentity()) {
1181
            $buffer[] = 'IDENTITY(1, 1)';
1182
        }
1183
1184
        return implode(' ', $buffer);
1185
    }
1186
1187
    /**
1188
     * Gets the SqlServer Index Definition for an Index object.
1189
     *
1190
     * @param \Phinx\Db\Table\Index $index Index
1191
     * @return string
1192
     */
1193
    protected function getIndexSqlDefinition(Index $index, $tableName)
1194
    {
1195 View Code Duplication
        if (is_string($index->getName())) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1196
            $indexName = $index->getName();
1197
        } else {
1198
            $columnNames = $index->getColumns();
1199
            $indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames));
1200
        }
1201
        $def = sprintf(
1202
            "CREATE %s INDEX %s ON %s (%s);",
1203
            ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1204
            $indexName,
1205
            $this->quoteTableName($tableName),
1206
            '[' . implode('],[', $index->getColumns()) . ']'
1207
        );
1208
1209
        return $def;
1210
    }
1211
1212
    /**
1213
     * Gets the SqlServer Foreign Key Definition for an ForeignKey object.
1214
     *
1215
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1216
     * @return string
1217
     */
1218
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1219
    {
1220
        $constraintName = $foreignKey->getConstraint() ?: $tableName . '_' . implode('_', $foreignKey->getColumns());
1221
        $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
0 ignored issues
show
Bug introduced by
It seems like $constraintName defined by $foreignKey->getConstrai...reignKey->getColumns()) on line 1220 can also be of type boolean; however, Phinx\Db\Adapter\SqlServ...pter::quoteColumnName() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1222
        $def .= ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")';
1223
        $def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")';
1224
        if ($foreignKey->getOnDelete()) {
1225
            $def .= " ON DELETE {$foreignKey->getOnDelete()}";
1226
        }
1227
        if ($foreignKey->getOnUpdate()) {
1228
            $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1229
        }
1230
1231
        return $def;
1232
    }
1233
1234
    /**
1235
     * {@inheritdoc}
1236
     */
1237
    public function getColumnTypes()
1238
    {
1239
        return array_merge(parent::getColumnTypes(), ['filestream']);
1240
    }
1241
1242
    /**
1243
     * Records a migration being run.
1244
     *
1245
     * @param \Phinx\Migration\MigrationInterface $migration Migration
1246
     * @param string $direction Direction
1247
     * @param int $startTime Start Time
1248
     * @param int $endTime End Time
1249
     * @return \Phinx\Db\Adapter\AdapterInterface
1250
     */
1251
    public function migrated(\Phinx\Migration\MigrationInterface $migration, $direction, $startTime, $endTime)
1252
    {
1253
        $startTime = str_replace(' ', 'T', $startTime);
1254
        $endTime = str_replace(' ', 'T', $endTime);
1255
1256
        return parent::migrated($migration, $direction, $startTime, $endTime);
1257
    }
1258
1259
    /**
1260
     * {@inheritDoc}
1261
     *
1262
     */
1263 View Code Duplication
    public function getDecoratedConnection()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1264
    {
1265
        $options = $this->getOptions();
1266
        $options = [
1267
            'username' => $options['user'],
1268
            'password' => $options['pass'],
1269
            'database' => $options['name'],
1270
            'quoteIdentifiers' => true,
1271
        ] + $options;
1272
1273
        $driver = new SqlServerDriver($options);
1274
1275
        if (method_exists($driver, 'setConnection')) {
1276
            $driver->setConnection($this->connection);
0 ignored issues
show
Bug introduced by
It seems like $this->connection can be null; however, setConnection() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1277
        } else {
1278
            $driver->connection($this->connection);
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Database\Driver::connection() has been deprecated with message: 3.6.0 Use getConnection()/setConnection() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1279
        }
1280
1281
        return new Connection(['driver' => $driver] + $options);
1282
    }
1283
}
1284