Completed
Pull Request — master (#1393)
by
unknown
03:29
created

SqlServerAdapter::getChangeDefault()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 9.504
c 0
b 0
f 0
ccs 0
cts 20
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
            $column = new Column();
217
            $column->setName('id')
218
                   ->setType('integer')
219
                   ->setIdentity(true);
220
221
            array_unshift($columns, $column);
222
            $options['primary_key'] = 'id';
223
        } elseif (isset($options['id']) && is_string($options['id'])) {
224
            // Handle id => "field_name" to support AUTO_INCREMENT
225
            $column = new Column();
226
            $column->setName($options['id'])
227
                   ->setType('integer')
228
                   ->setIdentity(true);
229
230
            array_unshift($columns, $column);
231
            $options['primary_key'] = $options['id'];
232
        }
233
234
        $sql = 'CREATE TABLE ';
235
        $sql .= $this->quoteTableName($table->getName()) . ' (';
236
        $sqlBuffer = [];
237
        $columnsWithComments = [];
238 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...
239
            $sqlBuffer[] = $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column);
240
241
            // set column comments, if needed
242
            if ($column->getComment()) {
243
                $columnsWithComments[] = $column;
244
            }
245
        }
246
247
        // set the primary key(s)
248
        if (isset($options['primary_key'])) {
249
            $pkSql = sprintf('CONSTRAINT PK_%s PRIMARY KEY (', $table->getName());
250
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
251
                $pkSql .= $this->quoteColumnName($options['primary_key']);
252
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
253
                $pkSql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
254
            }
255
            $pkSql .= ')';
256
            $sqlBuffer[] = $pkSql;
257
        }
258
259
        $sql .= implode(', ', $sqlBuffer);
260
        $sql .= ');';
261
262
        // process column comments
263
        foreach ($columnsWithComments as $column) {
264
            $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName());
265
        }
266
267
        // set the indexes
268
        foreach ($indexes as $index) {
269
            $sql .= $this->getIndexSqlDefinition($index, $table->getName());
270
        }
271
272
        // execute the sql
273
        $this->execute($sql);
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279
    protected function getChangeTableInstructions(Table $table, array $newOptions)
280
    {
281
        $instructions = new AlterInstructions();
282
283
        // Drop the existing primary key
284
        $primaryKey = $this->getPrimaryKey($table->getName());
285
        if ((isset($newOptions['id']) || isset($newOptions['primary_key']))
286
            && !empty($primaryKey['constraint'])) {
287
            $sql = sprintf(
288
                'ALTER TABLE %s DROP CONSTRAINT %s',
289
                $this->quoteTableName($table->getName()),
290
                $this->quoteColumnName($primaryKey['constraint'])
291
            );
292
            $this->execute($sql);
293
        }
294
295
        // Set the default primary key and add associated column
296 View Code Duplication
        if (isset($newOptions['id']) && $newOptions['id'] !== false) {
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...
297
            if ($newOptions['id'] === true) {
298
                $newOptions['primary_key'] = 'id';
299
            } elseif (is_string($newOptions['id'])) {
300
                // Handle id => "field_name" to support AUTO_INCREMENT
301
                $newOptions['primary_key'] = $newOptions['id'];
302
            } else {
303
                throw new \InvalidArgumentException(sprintf(
304
                    "Invalid value for option 'id': %s",
305
                    json_encode($newOptions['id'])
306
                ));
307
            }
308
309
            if ($this->hasColumn($table->getName(), $newOptions['primary_key'])) {
310
                throw new \RuntimeException(sprintf(
311
                    "Tried to create primary key column %s for table %s, but that column already exists",
312
                    $this->quoteColumnName($newOptions['primary_key']),
313
                    $this->quoteTableName($table->getName())
314
                ));
315
            }
316
317
            $column = new Column();
318
            $column
319
                ->setName($newOptions['primary_key'])
320
                ->setType('integer')
321
                ->setIdentity(true);
322
            $this->addColumn($table, $column);
323
        }
324
325
        // Add the primary key(s)
326 View Code Duplication
        if (isset($newOptions['primary_key']) && $newOptions['primary_key'] !== false) {
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...
327
            $sql = sprintf(
328
                'ADD CONSTRAINT %s PRIMARY KEY (',
329
                $this->quoteColumnName('PK_' . $table->getName())
330
            );
331
            if (is_string($newOptions['primary_key'])) { // handle primary_key => 'id'
332
                $sql .= $this->quoteColumnName($newOptions['primary_key']);
333
            } elseif (is_array($newOptions['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
334
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newOptions['primary_key']));
335
            } else {
336
                throw new \InvalidArgumentException(sprintf(
337
                    "Invalid value for option 'primary_key': %s",
338
                    json_encode($newOptions['primary_key'])
339
                ));
340
            }
341
            $sql .= ')';
342
            $instructions->addAlter($sql);
343
        }
344
345
        return $instructions;
346
    }
347
348
    /**
349
     * Gets the SqlServer Column Comment Defininition for a column object.
350
     *
351
     * @param \Phinx\Db\Table\Column $column    Column
352
     * @param string $tableName Table name
353
     *
354
     * @return string
355
     */
356
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
357
    {
358
        // passing 'null' is to remove column comment
359
        $currentComment = $this->getColumnComment($tableName, $column->getName());
360
361
        $comment = (strcasecmp($column->getComment(), 'NULL') !== 0) ? $this->getConnection()->quote($column->getComment()) : '\'\'';
362
        $command = $currentComment === false ? 'sp_addextendedproperty' : 'sp_updateextendedproperty';
363
364
        return sprintf(
365
            "EXECUTE %s N'MS_Description', N%s, N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s';",
366
            $command,
367
            $comment,
368
            $this->schema,
369
            $tableName,
370
            $column->getName()
371
        );
372
    }
373
374
    /**
375
     * {@inheritdoc}
376
     */
377
    protected function getRenameTableInstructions($tableName, $newTableName)
378
    {
379
        $sql = sprintf(
380
            'EXEC sp_rename \'%s\', \'%s\'',
381
            $tableName,
382
            $newTableName
383
        );
384
385
        return new AlterInstructions([], [$sql]);
386
    }
387
388
    /**
389
     * {@inheritdoc}
390
     */
391
    protected function getDropTableInstructions($tableName)
392
    {
393
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
394
395
        return new AlterInstructions([], [$sql]);
396
    }
397
398
    /**
399
     * {@inheritdoc}
400
     */
401
    public function truncateTable($tableName)
402
    {
403
        $sql = sprintf(
404
            'TRUNCATE TABLE %s',
405
            $this->quoteTableName($tableName)
406
        );
407
408
        $this->execute($sql);
409
    }
410
411
    public function getColumnComment($tableName, $columnName)
412
    {
413
        $sql = sprintf("SELECT cast(extended_properties.[value] as nvarchar(4000)) comment
414
  FROM sys.schemas
415
 INNER JOIN sys.tables
416
    ON schemas.schema_id = tables.schema_id
417
 INNER JOIN sys.columns
418
    ON tables.object_id = columns.object_id
419
 INNER JOIN sys.extended_properties
420
    ON tables.object_id = extended_properties.major_id
421
   AND columns.column_id = extended_properties.minor_id
422
   AND extended_properties.name = 'MS_Description'
423
   WHERE schemas.[name] = '%s' AND tables.[name] = '%s' AND columns.[name] = '%s'", $this->schema, $tableName, $columnName);
424
        $row = $this->fetchRow($sql);
425
426
        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...
427
            return trim($row['comment']);
428
        }
429
430
        return false;
431
    }
432
433
    /**
434
     * {@inheritdoc}
435
     */
436
    public function getColumns($tableName)
437
    {
438
        $columns = [];
439
        $sql = sprintf(
440
            "SELECT DISTINCT TABLE_SCHEMA AS [schema], TABLE_NAME as [table_name], COLUMN_NAME AS [name], DATA_TYPE AS [type],
441
            IS_NULLABLE AS [null], COLUMN_DEFAULT AS [default],
442
            CHARACTER_MAXIMUM_LENGTH AS [char_length],
443
            NUMERIC_PRECISION AS [precision],
444
            NUMERIC_SCALE AS [scale], ORDINAL_POSITION AS [ordinal_position],
445
            COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as [identity]
446
        FROM INFORMATION_SCHEMA.COLUMNS
447
        WHERE TABLE_NAME = '%s'
448
        ORDER BY ordinal_position",
449
            $tableName
450
        );
451
        $rows = $this->fetchAll($sql);
452
        foreach ($rows as $columnInfo) {
453
            $column = new Column();
454
            $column->setName($columnInfo['name'])
455
                   ->setType($this->getPhinxType($columnInfo['type']))
456
                   ->setNull($columnInfo['null'] !== 'NO')
457
                   ->setDefault($this->parseDefault($columnInfo['default']))
458
                   ->setIdentity($columnInfo['identity'] === '1')
459
                   ->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...
460
461
            if (!empty($columnInfo['char_length'])) {
462
                $column->setLimit($columnInfo['char_length']);
463
            }
464
465
            $columns[$columnInfo['name']] = $column;
466
        }
467
468
        return $columns;
469
    }
470
471
    protected function parseDefault($default)
472
    {
473
        $default = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default);
474
475
        if (strtoupper($default) === 'NULL') {
476
            $default = null;
477
        } elseif (is_numeric($default)) {
478
            $default = (int)$default;
479
        }
480
481
        return $default;
482
    }
483
484
    /**
485
     * {@inheritdoc}
486
     */
487
    public function hasColumn($tableName, $columnName)
488
    {
489
        $sql = sprintf(
490
            "SELECT count(*) as [count]
491
             FROM information_schema.columns
492
             WHERE table_name = '%s' AND column_name = '%s'",
493
            $tableName,
494
            $columnName
495
        );
496
        $result = $this->fetchRow($sql);
497
498
        return $result['count'] > 0;
499
    }
500
501
    /**
502
     * {@inheritdoc}
503
     */
504 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...
505
    {
506
        $alter = sprintf(
507
            'ALTER TABLE %s ADD %s %s',
508
            $table->getName(),
509
            $this->quoteColumnName($column->getName()),
510
            $this->getColumnSqlDefinition($column)
511
        );
512
513
        return new AlterInstructions([], [$alter]);
514
    }
515
516
    /**
517
     * {@inheritdoc}
518
     */
519
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
520
    {
521
        if (!$this->hasColumn($tableName, $columnName)) {
522
            throw new \InvalidArgumentException("The specified column does not exist: $columnName");
523
        }
524
525
        $instructions = new AlterInstructions();
526
527
        $oldConstraintName = "DF_{$tableName}_{$columnName}";
528
        $newConstraintName = "DF_{$tableName}_{$newColumnName}";
529
        $sql = <<<SQL
530
IF (OBJECT_ID('$oldConstraintName', 'D') IS NOT NULL)
531
BEGIN
532
     EXECUTE sp_rename N'%s', N'%s', N'OBJECT'
533
END
534
SQL;
535
        $instructions->addPostStep(sprintf(
536
            $sql,
537
            $oldConstraintName,
538
            $newConstraintName
539
        ));
540
541
        $instructions->addPostStep(sprintf(
542
            "EXECUTE sp_rename N'%s.%s', N'%s', 'COLUMN' ",
543
            $tableName,
544
            $columnName,
545
            $newColumnName
546
        ));
547
548
        return $instructions;
549
    }
550
551
    /**
552
     * Returns the instructions to change a column default value
553
     *
554
     * @param string $tableName The table where the column is
555
     * @param Column $newColumn The column to alter
556
     * @return AlterInstructions
557
     */
558
    protected function getChangeDefault($tableName, Column $newColumn)
559
    {
560
        $constraintName = "DF_{$tableName}_{$newColumn->getName()}";
561
        $default = $newColumn->getDefault();
562
        $instructions = new AlterInstructions();
563
564
        if ($default === null) {
565
            $default = 'DEFAULT NULL';
566
        } else {
567
            $default = ltrim($this->getDefaultValueDefinition($default));
568
        }
569
570
        if (empty($default)) {
571
            return $instructions;
572
        }
573
574
        $instructions->addPostStep(sprintf(
575
            'ALTER TABLE %s ADD CONSTRAINT %s %s FOR %s',
576
            $this->quoteTableName($tableName),
577
            $constraintName,
578
            $default,
579
            $this->quoteColumnName($newColumn->getName())
580
        ));
581
582
        return $instructions;
583
    }
584
585
    /**
586
     * {@inheritdoc}
587
     */
588
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
589
    {
590
        $columns = $this->getColumns($tableName);
591
        $changeDefault =
592
            $newColumn->getDefault() !== $columns[$columnName]->getDefault() ||
593
            $newColumn->getType() !== $columns[$columnName]->getType();
594
595
        $instructions = new AlterInstructions();
596
597
        if ($columnName !== $newColumn->getName()) {
598
            $instructions->merge(
599
                $this->getRenameColumnInstructions($tableName, $columnName, $newColumn->getName())
600
            );
601
        }
602
603
        if ($changeDefault) {
604
            $instructions->merge($this->getDropDefaultConstraint($tableName, $newColumn->getName()));
605
        }
606
607
        $instructions->addPostStep(sprintf(
608
            'ALTER TABLE %s ALTER COLUMN %s %s',
609
            $this->quoteTableName($tableName),
610
            $this->quoteColumnName($newColumn->getName()),
611
            $this->getColumnSqlDefinition($newColumn, false)
612
        ));
613
        // change column comment if needed
614
        if ($newColumn->getComment()) {
615
            $instructions->addPostStep($this->getColumnCommentSqlDefinition($newColumn, $tableName));
616
        }
617
618
        if ($changeDefault) {
619
            $instructions->merge($this->getChangeDefault($tableName, $newColumn));
620
        }
621
622
        return $instructions;
623
    }
624
625
    /**
626
     * {@inheritdoc}
627
     */
628
    protected function getDropColumnInstructions($tableName, $columnName)
629
    {
630
        $instructions = $this->getDropDefaultConstraint($tableName, $columnName);
631
632
        $instructions->addPostStep(sprintf(
633
            'ALTER TABLE %s DROP COLUMN %s',
634
            $this->quoteTableName($tableName),
635
            $this->quoteColumnName($columnName)
636
        ));
637
638
        return $instructions;
639
    }
640
641
    /**
642
     * {@inheritdoc}
643
     */
644
    protected function getDropDefaultConstraint($tableName, $columnName)
645
    {
646
        $defaultConstraint = $this->getDefaultConstraint($tableName, $columnName);
647
648
        if (!$defaultConstraint) {
649
            return new AlterInstructions();
650
        }
651
652
        return $this->getDropForeignKeyInstructions($tableName, $defaultConstraint);
653
    }
654
655
    protected function getDefaultConstraint($tableName, $columnName)
656
    {
657
        $sql = "SELECT
658
    default_constraints.name
659
FROM
660
    sys.all_columns
661
662
        INNER JOIN
663
    sys.tables
664
        ON all_columns.object_id = tables.object_id
665
666
        INNER JOIN
667
    sys.schemas
668
        ON tables.schema_id = schemas.schema_id
669
670
        INNER JOIN
671
    sys.default_constraints
672
        ON all_columns.default_object_id = default_constraints.object_id
673
674
WHERE
675
        schemas.name = 'dbo'
676
    AND tables.name = '{$tableName}'
677
    AND all_columns.name = '{$columnName}'";
678
679
        $rows = $this->fetchAll($sql);
680
681
        return empty($rows) ? false : $rows[0]['name'];
682
    }
683
684
    protected function getIndexColums($tableId, $indexId)
685
    {
686
        $sql = "SELECT AC.[name] AS [column_name]
687
FROM sys.[index_columns] IC
688
  INNER JOIN sys.[all_columns] AC ON IC.[column_id] = AC.[column_id]
689
WHERE AC.[object_id] = {$tableId} AND IC.[index_id] = {$indexId}  AND IC.[object_id] = {$tableId}
690
ORDER BY IC.[key_ordinal];";
691
692
        $rows = $this->fetchAll($sql);
693
        $columns = [];
694
        foreach ($rows as $row) {
695
            $columns[] = strtolower($row['column_name']);
696
        }
697
698
        return $columns;
699
    }
700
701
    /**
702
     * Get an array of indexes from a particular table.
703
     *
704
     * @param string $tableName Table Name
705
     * @return array
706
     */
707
    public function getIndexes($tableName)
708
    {
709
        $indexes = [];
710
        $sql = "SELECT I.[name] AS [index_name], I.[index_id] as [index_id], T.[object_id] as [table_id]
711
FROM sys.[tables] AS T
712
  INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
713
WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP'  AND T.[name] = '{$tableName}'
714
ORDER BY T.[name], I.[index_id];";
715
716
        $rows = $this->fetchAll($sql);
717
        foreach ($rows as $row) {
718
            $columns = $this->getIndexColums($row['table_id'], $row['index_id']);
719
            $indexes[$row['index_name']] = ['columns' => $columns];
720
        }
721
722
        return $indexes;
723
    }
724
725
    /**
726
     * {@inheritdoc}
727
     */
728 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...
729
    {
730
        if (is_string($columns)) {
731
            $columns = [$columns]; // str to array
732
        }
733
734
        $columns = array_map('strtolower', $columns);
735
        $indexes = $this->getIndexes($tableName);
736
737
        foreach ($indexes as $index) {
738
            $a = array_diff($columns, $index['columns']);
739
740
            if (empty($a)) {
741
                return true;
742
            }
743
        }
744
745
        return false;
746
    }
747
748
    /**
749
     * {@inheritdoc}
750
     */
751 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...
752
    {
753
        $indexes = $this->getIndexes($tableName);
754
755
        foreach ($indexes as $name => $index) {
756
            if ($name === $indexName) {
757
                 return true;
758
            }
759
        }
760
761
        return false;
762
    }
763
764
    /**
765
     * {@inheritdoc}
766
     */
767
    protected function getAddIndexInstructions(Table $table, Index $index)
768
    {
769
        $sql = $this->getIndexSqlDefinition($index, $table->getName());
770
771
        return new AlterInstructions([], [$sql]);
772
    }
773
774
    /**
775
     * {@inheritdoc}
776
     */
777
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
778
    {
779
        if (is_string($columns)) {
780
            $columns = [$columns]; // str to array
781
        }
782
783
        $indexes = $this->getIndexes($tableName);
784
        $columns = array_map('strtolower', $columns);
785
        $instructions = new AlterInstructions();
786
787
        foreach ($indexes as $indexName => $index) {
788
            $a = array_diff($columns, $index['columns']);
789 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...
790
                $instructions->addPostStep(sprintf(
791
                    'DROP INDEX %s ON %s',
792
                    $this->quoteColumnName($indexName),
793
                    $this->quoteTableName($tableName)
794
                ));
795
796
                return $instructions;
797
            }
798
        }
799
800
        throw new \InvalidArgumentException(sprintf(
801
            "The specified index on columns '%s' does not exist",
802
            implode(',', $columns)
803
        ));
804
    }
805
806
    /**
807
     * {@inheritdoc}
808
     */
809
    protected function getDropIndexByNameInstructions($tableName, $indexName)
810
    {
811
        $indexes = $this->getIndexes($tableName);
812
        $instructions = new AlterInstructions();
813
814
        foreach ($indexes as $name => $index) {
815 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...
816
                $instructions->addPostStep(sprintf(
817
                    'DROP INDEX %s ON %s',
818
                    $this->quoteColumnName($indexName),
819
                    $this->quoteTableName($tableName)
820
                ));
821
822
                return $instructions;
823
            }
824
        }
825
826
        throw new \InvalidArgumentException(sprintf(
827
            "The specified index name '%s' does not exist",
828
            $indexName
829
        ));
830
    }
831
832
    /**
833
     * {@inheritdoc}
834
     */
835 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...
836
    {
837
        $primaryKey = $this->getPrimaryKey($tableName);
838
839
        if (empty($primaryKey)) {
840
            return false;
841
        }
842
843
        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...
844
            return ($primaryKey['constraint'] === $constraint);
845
        } else {
846
            if (is_string($columns)) {
847
                $columns = [$columns]; // str to array
848
            }
849
            $missingColumns = array_diff($columns, $primaryKey['columns']);
850
851
            return empty($missingColumns);
852
        }
853
    }
854
855
    /**
856
     * Get the primary key from a particular table.
857
     *
858
     * @param string $tableName Table Name
859
     * @return array
860
     */
861 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...
862
    {
863
        $rows = $this->fetchAll(sprintf(
864
            "SELECT
865
                    tc.constraint_name,
866
                    kcu.column_name
867
                FROM information_schema.table_constraints AS tc
868
                JOIN information_schema.key_column_usage AS kcu
869
                    ON tc.constraint_name = kcu.constraint_name
870
                WHERE constraint_type = 'PRIMARY KEY'
871
                    AND tc.table_name = '%s'
872
                ORDER BY kcu.ordinal_position",
873
            $tableName
874
        ));
875
876
        $primaryKey = [
877
            'columns' => [],
878
        ];
879
        foreach ($rows as $row) {
880
            $primaryKey['constraint'] = $row['constraint_name'];
881
            $primaryKey['columns'][] = $row['column_name'];
882
        }
883
884
        return $primaryKey;
885
    }
886
887
    /**
888
     * {@inheritdoc}
889
     */
890 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...
891
    {
892
        if (is_string($columns)) {
893
            $columns = [$columns]; // str to array
894
        }
895
        $foreignKeys = $this->getForeignKeys($tableName);
896
        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...
897
            if (isset($foreignKeys[$constraint])) {
898
                return !empty($foreignKeys[$constraint]);
899
            }
900
901
            return false;
902
        } else {
903
            foreach ($foreignKeys as $key) {
904
                $a = array_diff($columns, $key['columns']);
905
                if (empty($a)) {
906
                    return true;
907
                }
908
            }
909
910
            return false;
911
        }
912
    }
913
914
    /**
915
     * Get an array of foreign keys from a particular table.
916
     *
917
     * @param string $tableName Table Name
918
     * @return array
919
     */
920 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...
921
    {
922
        $foreignKeys = [];
923
        $rows = $this->fetchAll(sprintf(
924
            "SELECT
925
                    tc.constraint_name,
926
                    tc.table_name, kcu.column_name,
927
                    ccu.table_name AS referenced_table_name,
928
                    ccu.column_name AS referenced_column_name
929
                FROM
930
                    information_schema.table_constraints AS tc
931
                    JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
932
                    JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
933
                WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s'
934
                ORDER BY kcu.ordinal_position",
935
            $tableName
936
        ));
937
        foreach ($rows as $row) {
938
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
939
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
940
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
941
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
942
        }
943
944
        return $foreignKeys;
945
    }
946
947
    /**
948
     * {@inheritdoc}
949
     */
950
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
951
    {
952
        $instructions = new AlterInstructions();
953
        $instructions->addPostStep(sprintf(
954
            'ALTER TABLE %s ADD %s',
955
            $this->quoteTableName($table->getName()),
956
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
957
        ));
958
959
        return $instructions;
960
    }
961
962
    /**
963
     * {@inheritdoc}
964
     */
965
    protected function getDropForeignKeyInstructions($tableName, $constraint)
966
    {
967
        $instructions = new AlterInstructions();
968
        $instructions->addPostStep(sprintf(
969
            'ALTER TABLE %s DROP CONSTRAINT %s',
970
            $this->quoteTableName($tableName),
971
            $constraint
972
        ));
973
974
        return $instructions;
975
    }
976
977
    /**
978
     * {@inheritdoc}
979
     */
980
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
981
    {
982
        $instructions = new AlterInstructions();
983
984 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...
985
            $rows = $this->fetchAll(sprintf(
986
                "SELECT
987
                tc.constraint_name,
988
                tc.table_name, kcu.column_name,
989
                ccu.table_name AS referenced_table_name,
990
                ccu.column_name AS referenced_column_name
991
            FROM
992
                information_schema.table_constraints AS tc
993
                JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
994
                JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
995
            WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' and ccu.column_name='%s'
996
            ORDER BY kcu.ordinal_position",
997
                $tableName,
998
                $column
999
            ));
1000
            foreach ($rows as $row) {
1001
                $instructions->merge(
1002
                    $this->getDropForeignKeyInstructions($tableName, $row['constraint_name'])
1003
                );
1004
            }
1005
        }
1006
1007
        return $instructions;
1008
    }
1009
1010
    /**
1011
     * {@inheritdoc}
1012
     */
1013
    public function getSqlType($type, $limit = null)
1014
    {
1015
        switch ($type) {
1016
            case static::PHINX_TYPE_FLOAT:
1017
            case static::PHINX_TYPE_DECIMAL:
1018
            case static::PHINX_TYPE_DATETIME:
1019
            case static::PHINX_TYPE_TIME:
1020
            case static::PHINX_TYPE_DATE:
1021
                return ['name' => $type];
1022
            case static::PHINX_TYPE_STRING:
1023
                return ['name' => 'nvarchar', 'limit' => 255];
1024
            case static::PHINX_TYPE_CHAR:
1025
                return ['name' => 'nchar', 'limit' => 255];
1026
            case static::PHINX_TYPE_TEXT:
1027
                return ['name' => 'ntext'];
1028
            case static::PHINX_TYPE_INTEGER:
1029
                return ['name' => 'int'];
1030
            case static::PHINX_TYPE_BIG_INTEGER:
1031
                return ['name' => 'bigint'];
1032
            case static::PHINX_TYPE_TIMESTAMP:
1033
                return ['name' => 'datetime'];
1034
            case static::PHINX_TYPE_BLOB:
1035
            case static::PHINX_TYPE_BINARY:
1036
                return ['name' => 'varbinary'];
1037
            case static::PHINX_TYPE_BOOLEAN:
1038
                return ['name' => 'bit'];
1039
            case static::PHINX_TYPE_UUID:
1040
                return ['name' => 'uniqueidentifier'];
1041
            case static::PHINX_TYPE_FILESTREAM:
1042
                return ['name' => 'varbinary', 'limit' => 'max'];
1043
            // Geospatial database types
1044
            case static::PHINX_TYPE_GEOMETRY:
1045
            case static::PHINX_TYPE_POINT:
1046
            case static::PHINX_TYPE_LINESTRING:
1047
            case static::PHINX_TYPE_POLYGON:
1048
                // SQL Server stores all spatial data using a single data type.
1049
                // Specific types (point, polygon, etc) are set at insert time.
1050
                return ['name' => 'geography'];
1051
            default:
1052
                throw new \RuntimeException('The type: "' . $type . '" is not supported.');
1053
        }
1054
    }
1055
1056
    /**
1057
     * Returns Phinx type by SQL type
1058
     *
1059
     * @param string $sqlType SQL Type definition
1060
     * @throws \RuntimeException
1061
     * @internal param string $sqlType SQL type
1062
     * @returns string Phinx type
1063
     */
1064
    public function getPhinxType($sqlType)
1065
    {
1066
        switch ($sqlType) {
1067
            case 'nvarchar':
1068
            case 'varchar':
1069
                return static::PHINX_TYPE_STRING;
1070
            case 'char':
1071
            case 'nchar':
1072
                return static::PHINX_TYPE_CHAR;
1073
            case 'text':
1074
            case 'ntext':
1075
                return static::PHINX_TYPE_TEXT;
1076
            case 'int':
1077
            case 'integer':
1078
                return static::PHINX_TYPE_INTEGER;
1079
            case 'decimal':
1080
            case 'numeric':
1081
            case 'money':
1082
                return static::PHINX_TYPE_DECIMAL;
1083
            case 'bigint':
1084
                return static::PHINX_TYPE_BIG_INTEGER;
1085
            case 'real':
1086
            case 'float':
1087
                return static::PHINX_TYPE_FLOAT;
1088
            case 'binary':
1089
            case 'image':
1090
            case 'varbinary':
1091
                return static::PHINX_TYPE_BINARY;
1092
            case 'time':
1093
                return static::PHINX_TYPE_TIME;
1094
            case 'date':
1095
                return static::PHINX_TYPE_DATE;
1096
            case 'datetime':
1097
            case 'timestamp':
1098
                return static::PHINX_TYPE_DATETIME;
1099
            case 'bit':
1100
                return static::PHINX_TYPE_BOOLEAN;
1101
            case 'uniqueidentifier':
1102
                return static::PHINX_TYPE_UUID;
1103
            case 'filestream':
1104
                return static::PHINX_TYPE_FILESTREAM;
1105
            default:
1106
                throw new \RuntimeException('The SqlServer type: "' . $sqlType . '" is not supported');
1107
        }
1108
    }
1109
1110
    /**
1111
     * {@inheritdoc}
1112
     */
1113
    public function createDatabase($name, $options = [])
1114
    {
1115 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...
1116
            $this->execute(sprintf('CREATE DATABASE [%s] COLLATE [%s]', $name, $options['collation']));
1117
        } else {
1118
            $this->execute(sprintf('CREATE DATABASE [%s]', $name));
1119
        }
1120
        $this->execute(sprintf('USE [%s]', $name));
1121
    }
1122
1123
    /**
1124
     * {@inheritdoc}
1125
     */
1126
    public function hasDatabase($name)
1127
    {
1128
        $result = $this->fetchRow(
1129
            sprintf(
1130
                'SELECT count(*) as [count] FROM master.dbo.sysdatabases WHERE [name] = \'%s\'',
1131
                $name
1132
            )
1133
        );
1134
1135
        return $result['count'] > 0;
1136
    }
1137
1138
    /**
1139
     * {@inheritdoc}
1140
     */
1141
    public function dropDatabase($name)
1142
    {
1143
        $sql = <<<SQL
1144
USE master;
1145
IF EXISTS(select * from sys.databases where name=N'$name')
1146
ALTER DATABASE [$name] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
1147
DROP DATABASE [$name];
1148
SQL;
1149
        $this->execute($sql);
1150
    }
1151
1152
    /**
1153
     * Gets the SqlServer Column Definition for a Column object.
1154
     *
1155
     * @param \Phinx\Db\Table\Column $column Column
1156
     * @return string
1157
     */
1158
    protected function getColumnSqlDefinition(Column $column, $create = true)
1159
    {
1160
        $buffer = [];
1161
        if ($column->getType() instanceof Literal) {
1162
            $buffer[] = (string)$column->getType();
1163
        } else {
1164
            $sqlType = $this->getSqlType($column->getType());
1165
            $buffer[] = strtoupper($sqlType['name']);
1166
            // integers cant have limits in SQlServer
1167
            $noLimits = [
1168
                'bigint',
1169
                'int',
1170
                'tinyint'
1171
            ];
1172
            if (!in_array($sqlType['name'], $noLimits) && ($column->getLimit() || isset($sqlType['limit']))) {
1173
                $buffer[] = sprintf('(%s)', $column->getLimit() ? $column->getLimit() : $sqlType['limit']);
1174
            }
1175
        }
1176 View Code Duplication
        if ($column->getPrecision() && $column->getScale()) {
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...
1177
            $buffer[] = '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1178
        }
1179
1180
        $properties = $column->getProperties();
1181
        $buffer[] = $column->getType() === 'filestream' ? 'FILESTREAM' : '';
1182
        $buffer[] = isset($properties['rowguidcol']) ? 'ROWGUIDCOL' : '';
1183
1184
        $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1185
1186
        if ($create === true) {
1187
            if ($column->getDefault() === null && $column->isNull()) {
1188
                $buffer[] = ' DEFAULT NULL';
1189
            } else {
1190
                $buffer[] = $this->getDefaultValueDefinition($column->getDefault());
1191
            }
1192
        }
1193
1194
        if ($column->isIdentity()) {
1195
            $buffer[] = 'IDENTITY(1, 1)';
1196
        }
1197
1198
        return implode(' ', $buffer);
1199
    }
1200
1201
    /**
1202
     * Gets the SqlServer Index Definition for an Index object.
1203
     *
1204
     * @param \Phinx\Db\Table\Index $index Index
1205
     * @return string
1206
     */
1207
    protected function getIndexSqlDefinition(Index $index, $tableName)
1208
    {
1209 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...
1210
            $indexName = $index->getName();
1211
        } else {
1212
            $columnNames = $index->getColumns();
1213
            $indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames));
1214
        }
1215
        $def = sprintf(
1216
            "CREATE %s INDEX %s ON %s (%s);",
1217
            ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1218
            $indexName,
1219
            $this->quoteTableName($tableName),
1220
            '[' . implode('],[', $index->getColumns()) . ']'
1221
        );
1222
1223
        return $def;
1224
    }
1225
1226
    /**
1227
     * Gets the SqlServer Foreign Key Definition for an ForeignKey object.
1228
     *
1229
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1230
     * @return string
1231
     */
1232
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1233
    {
1234
        $constraintName = $foreignKey->getConstraint() ?: $tableName . '_' . implode('_', $foreignKey->getColumns());
1235
        $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
0 ignored issues
show
Bug introduced by
It seems like $constraintName defined by $foreignKey->getConstrai...reignKey->getColumns()) on line 1234 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...
1236
        $def .= ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")';
1237
        $def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")';
1238
        if ($foreignKey->getOnDelete()) {
1239
            $def .= " ON DELETE {$foreignKey->getOnDelete()}";
1240
        }
1241
        if ($foreignKey->getOnUpdate()) {
1242
            $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1243
        }
1244
1245
        return $def;
1246
    }
1247
1248
    /**
1249
     * {@inheritdoc}
1250
     */
1251
    public function getColumnTypes()
1252
    {
1253
        return array_merge(parent::getColumnTypes(), ['filestream']);
1254
    }
1255
1256
    /**
1257
     * Records a migration being run.
1258
     *
1259
     * @param \Phinx\Migration\MigrationInterface $migration Migration
1260
     * @param string $direction Direction
1261
     * @param int $startTime Start Time
1262
     * @param int $endTime End Time
1263
     * @return \Phinx\Db\Adapter\AdapterInterface
1264
     */
1265
    public function migrated(\Phinx\Migration\MigrationInterface $migration, $direction, $startTime, $endTime)
1266
    {
1267
        $startTime = str_replace(' ', 'T', $startTime);
1268
        $endTime = str_replace(' ', 'T', $endTime);
1269
1270
        return parent::migrated($migration, $direction, $startTime, $endTime);
1271
    }
1272
1273
    /**
1274
     * {@inheritDoc}
1275
     *
1276
     */
1277 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...
1278
    {
1279
        $options = $this->getOptions();
1280
        $options = [
1281
            'username' => $options['user'],
1282
            'password' => $options['pass'],
1283
            'database' => $options['name'],
1284
            'quoteIdentifiers' => true,
1285
        ] + $options;
1286
1287
        $driver = new SqlServerDriver($options);
1288
1289
        if (method_exists($driver, 'setConnection')) {
1290
            $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...
1291
        } else {
1292
            $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...
1293
        }
1294
1295
        return new Connection(['driver' => $driver] + $options);
1296
    }
1297
}
1298