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

SqlServerAdapter::getAddColumnInstructions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 11
Ratio 100 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 11
loc 11
ccs 0
cts 7
cp 0
rs 9.9
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2
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
        {
288
            $sql = sprintf(
289
                'ALTER TABLE %s DROP CONSTRAINT %s',
290
                $this->quoteTableName($table->getName()),
291
                $this->quoteColumnName($primaryKey['constraint'])
292
            );
293
            $this->execute($sql);
294
        }
295
296
        // Set the default primary key and add associated column
297 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...
298
            if ($newOptions['id'] === true) {
299
                $newOptions['primary_key'] = 'id';
300
            } else if (is_string($newOptions['id'])) {
301
                // Handle id => "field_name" to support AUTO_INCREMENT
302
                $newOptions['primary_key'] = $newOptions['id'];
303
            } else {
304
                throw new \InvalidArgumentException(sprintf(
305
                    "Invalid value for option 'id': %s",
306
                    json_encode($newOptions['id'])
307
                ));
308
            }
309
310
            if ($this->hasColumn($table->getName(), $newOptions['primary_key'])) {
311
                throw new \RuntimeException(sprintf(
312
                    "Tried to create primary key column %s for table %s, but that column already exists",
313
                    $this->quoteColumnName($newOptions['primary_key']),
314
                    $this->quoteTableName($table->getName())
315
                ));
316
            }
317
318
            $column = new Column();
319
            $column
320
                ->setName($newOptions['primary_key'])
321
                ->setType('integer')
322
                ->setIdentity(true);
323
            $this->addColumn($table, $column);
324
        }
325
326
        // Add the primary key(s)
327 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...
328
            $sql = sprintf(
329
                'ADD CONSTRAINT %s PRIMARY KEY (',
330
                $this->quoteColumnName('PK_' . $table->getName())
331
            );
332
            if (is_string($newOptions['primary_key'])) { // handle primary_key => 'id'
333
                $sql .= $this->quoteColumnName($newOptions['primary_key']);
334
            } else if (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...
335
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newOptions['primary_key']));
336
            } else {
337
                throw new \InvalidArgumentException(sprintf(
338
                    "Invalid value for option 'primary_key': %s",
339
                    json_encode($newOptions['primary_key'])
340
                ));
341
            }
342
            $sql .= ')';
343
            $instructions->addAlter($sql);
344
        }
345
346
        return $instructions;
347
    }
348
349
    /**
350
     * Gets the SqlServer Column Comment Defininition for a column object.
351
     *
352
     * @param \Phinx\Db\Table\Column $column    Column
353
     * @param string $tableName Table name
354
     *
355
     * @return string
356
     */
357
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
358
    {
359
        // passing 'null' is to remove column comment
360
        $currentComment = $this->getColumnComment($tableName, $column->getName());
361
362
        $comment = (strcasecmp($column->getComment(), 'NULL') !== 0) ? $this->getConnection()->quote($column->getComment()) : '\'\'';
363
        $command = $currentComment === false ? 'sp_addextendedproperty' : 'sp_updateextendedproperty';
364
365
        return sprintf(
366
            "EXECUTE %s N'MS_Description', N%s, N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s';",
367
            $command,
368
            $comment,
369
            $this->schema,
370
            $tableName,
371
            $column->getName()
372
        );
373
    }
374
375
    /**
376
     * {@inheritdoc}
377
     */
378
    protected function getRenameTableInstructions($tableName, $newTableName)
379
    {
380
        $sql = sprintf(
381
            'EXEC sp_rename \'%s\', \'%s\'',
382
            $tableName,
383
            $newTableName
384
        );
385
386
        return new AlterInstructions([], [$sql]);
387
    }
388
389
    /**
390
     * {@inheritdoc}
391
     */
392
    protected function getDropTableInstructions($tableName)
393
    {
394
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
395
396
        return new AlterInstructions([], [$sql]);
397
    }
398
399
    /**
400
     * {@inheritdoc}
401
     */
402
    public function truncateTable($tableName)
403
    {
404
        $sql = sprintf(
405
            'TRUNCATE TABLE %s',
406
            $this->quoteTableName($tableName)
407
        );
408
409
        $this->execute($sql);
410
    }
411
412
    public function getColumnComment($tableName, $columnName)
413
    {
414
        $sql = sprintf("SELECT cast(extended_properties.[value] as nvarchar(4000)) comment
415
  FROM sys.schemas
416
 INNER JOIN sys.tables
417
    ON schemas.schema_id = tables.schema_id
418
 INNER JOIN sys.columns
419
    ON tables.object_id = columns.object_id
420
 INNER JOIN sys.extended_properties
421
    ON tables.object_id = extended_properties.major_id
422
   AND columns.column_id = extended_properties.minor_id
423
   AND extended_properties.name = 'MS_Description'
424
   WHERE schemas.[name] = '%s' AND tables.[name] = '%s' AND columns.[name] = '%s'", $this->schema, $tableName, $columnName);
425
        $row = $this->fetchRow($sql);
426
427
        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...
428
            return $row['comment'];
429
        }
430
431
        return false;
432
    }
433
434
    /**
435
     * {@inheritdoc}
436
     */
437
    public function getColumns($tableName)
438
    {
439
        $columns = [];
440
        $sql = sprintf(
441
            "SELECT DISTINCT TABLE_SCHEMA AS [schema], TABLE_NAME as [table_name], COLUMN_NAME AS [name], DATA_TYPE AS [type],
442
            IS_NULLABLE AS [null], COLUMN_DEFAULT AS [default],
443
            CHARACTER_MAXIMUM_LENGTH AS [char_length],
444
            NUMERIC_PRECISION AS [precision],
445
            NUMERIC_SCALE AS [scale], ORDINAL_POSITION AS [ordinal_position],
446
            COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as [identity]
447
        FROM INFORMATION_SCHEMA.COLUMNS
448
        WHERE TABLE_NAME = '%s'
449
        ORDER BY ordinal_position",
450
            $tableName
451
        );
452
        $rows = $this->fetchAll($sql);
453
        foreach ($rows as $columnInfo) {
454
            $column = new Column();
455
            $column->setName($columnInfo['name'])
456
                   ->setType($this->getPhinxType($columnInfo['type']))
457
                   ->setNull($columnInfo['null'] !== 'NO')
458
                   ->setDefault($this->parseDefault($columnInfo['default']))
459
                   ->setIdentity($columnInfo['identity'] === '1')
460
                   ->setComment($this->getColumnComment($columnInfo['table_name'], $columnInfo['name']));
461
462
            if (!empty($columnInfo['char_length'])) {
463
                $column->setLimit($columnInfo['char_length']);
464
            }
465
466
            $columns[$columnInfo['name']] = $column;
467
        }
468
469
        return $columns;
470
    }
471
472
    protected function parseDefault($default)
473
    {
474
        $default = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default);
475
476
        if (strtoupper($default) === 'NULL') {
477
            $default = null;
478
        } elseif (is_numeric($default)) {
479
            $default = (int)$default;
480
        }
481
482
        return $default;
483
    }
484
485
    /**
486
     * {@inheritdoc}
487
     */
488
    public function hasColumn($tableName, $columnName)
489
    {
490
        $sql = sprintf(
491
            "SELECT count(*) as [count]
492
             FROM information_schema.columns
493
             WHERE table_name = '%s' AND column_name = '%s'",
494
            $tableName,
495
            $columnName
496
        );
497
        $result = $this->fetchRow($sql);
498
499
        return $result['count'] > 0;
500
    }
501
502
    /**
503
     * {@inheritdoc}
504
     */
505 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...
506
    {
507
        $alter = sprintf(
508
            'ALTER TABLE %s ADD %s %s',
509
            $table->getName(),
510
            $this->quoteColumnName($column->getName()),
511
            $this->getColumnSqlDefinition($column)
512
        );
513
514
        return new AlterInstructions([], [$alter]);
515
    }
516
517
    /**
518
     * {@inheritdoc}
519
     */
520
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
521
    {
522
        if (!$this->hasColumn($tableName, $columnName)) {
523
            throw new \InvalidArgumentException("The specified column does not exist: $columnName");
524
        }
525
526
        $instructions = new AlterInstructions();
527
528
        $oldConstraintName = "DF_{$tableName}_{$columnName}";
529
        $newConstraintName = "DF_{$tableName}_{$newColumnName}";
530
        $sql = <<<SQL
531
IF (OBJECT_ID('$oldConstraintName', 'D') IS NOT NULL)
532
BEGIN
533
     EXECUTE sp_rename N'%s', N'%s', N'OBJECT'
534
END
535
SQL;
536
        $instructions->addPostStep(sprintf(
537
            $sql,
538
            $oldConstraintName,
539
            $newConstraintName
540
        ));
541
542
        $instructions->addPostStep(sprintf(
543
            "EXECUTE sp_rename N'%s.%s', N'%s', 'COLUMN' ",
544
            $tableName,
545
            $columnName,
546
            $newColumnName
547
        ));
548
549
        return $instructions;
550
    }
551
552
    /**
553
     * Returns the instructions to change a column default value
554
     *
555
     * @param string $tableName The table where the column is
556
     * @param Column $newColumn The column to alter
557
     * @return AlterInstructions
558
     */
559
    protected function getChangeDefault($tableName, Column $newColumn)
560
    {
561
        $constraintName = "DF_{$tableName}_{$newColumn->getName()}";
562
        $default = $newColumn->getDefault();
563
        $instructions = new AlterInstructions();
564
565
        if ($default === null) {
566
            $default = 'DEFAULT NULL';
567
        } else {
568
            $default = ltrim($this->getDefaultValueDefinition($default));
569
        }
570
571
        if (empty($default)) {
572
            return $instructions;
573
        }
574
575
        $instructions->addPostStep(sprintf(
576
            'ALTER TABLE %s ADD CONSTRAINT %s %s FOR %s',
577
            $this->quoteTableName($tableName),
578
            $constraintName,
579
            $default,
580
            $this->quoteColumnName($newColumn->getName())
581
        ));
582
583
        return $instructions;
584
    }
585
586
    /**
587
     * {@inheritdoc}
588
     */
589
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
590
    {
591
        $columns = $this->getColumns($tableName);
592
        $changeDefault =
593
            $newColumn->getDefault() !== $columns[$columnName]->getDefault() ||
594
            $newColumn->getType() !== $columns[$columnName]->getType();
595
596
        $instructions = new AlterInstructions();
597
598
        if ($columnName !== $newColumn->getName()) {
599
            $instructions->merge(
600
                $this->getRenameColumnInstructions($tableName, $columnName, $newColumn->getName())
601
            );
602
        }
603
604
        if ($changeDefault) {
605
            $instructions->merge($this->getDropDefaultConstraint($tableName, $newColumn->getName()));
606
        }
607
608
        $instructions->addPostStep(sprintf(
609
            'ALTER TABLE %s ALTER COLUMN %s %s',
610
            $this->quoteTableName($tableName),
611
            $this->quoteColumnName($newColumn->getName()),
612
            $this->getColumnSqlDefinition($newColumn, false)
613
        ));
614
        // change column comment if needed
615
        if ($newColumn->getComment()) {
616
            $instructions->merge($this->getColumnCommentSqlDefinition($newColumn, $tableName));
0 ignored issues
show
Documentation introduced by
$this->getColumnCommentS...$newColumn, $tableName) is of type string, but the function expects a object<Phinx\Db\Util\AlterInstructions>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
617
        }
618
619
        if ($changeDefault) {
620
            $instructions->merge($this->getChangeDefault($tableName, $newColumn));
621
        }
622
623
        return $instructions;
624
    }
625
626
    /**
627
     * {@inheritdoc}
628
     */
629
    protected function getDropColumnInstructions($tableName, $columnName)
630
    {
631
        $instructions = $this->getDropDefaultConstraint($tableName, $columnName);
632
633
        $instructions->addPostStep(sprintf(
634
            'ALTER TABLE %s DROP COLUMN %s',
635
            $this->quoteTableName($tableName),
636
            $this->quoteColumnName($columnName)
637
        ));
638
639
        return $instructions;
640
    }
641
642
    /**
643
     * {@inheritdoc}
644
     */
645
    protected function getDropDefaultConstraint($tableName, $columnName)
646
    {
647
        $defaultConstraint = $this->getDefaultConstraint($tableName, $columnName);
648
649
        if (!$defaultConstraint) {
650
            return new AlterInstructions();
651
        }
652
653
        return $this->getDropForeignKeyInstructions($tableName, $defaultConstraint);
654
    }
655
656
    protected function getDefaultConstraint($tableName, $columnName)
657
    {
658
        $sql = "SELECT
659
    default_constraints.name
660
FROM
661
    sys.all_columns
662
663
        INNER JOIN
664
    sys.tables
665
        ON all_columns.object_id = tables.object_id
666
667
        INNER JOIN
668
    sys.schemas
669
        ON tables.schema_id = schemas.schema_id
670
671
        INNER JOIN
672
    sys.default_constraints
673
        ON all_columns.default_object_id = default_constraints.object_id
674
675
WHERE
676
        schemas.name = 'dbo'
677
    AND tables.name = '{$tableName}'
678
    AND all_columns.name = '{$columnName}'";
679
680
        $rows = $this->fetchAll($sql);
681
682
        return empty($rows) ? false : $rows[0]['name'];
683
    }
684
685
    protected function getIndexColums($tableId, $indexId)
686
    {
687
        $sql = "SELECT AC.[name] AS [column_name]
688
FROM sys.[index_columns] IC
689
  INNER JOIN sys.[all_columns] AC ON IC.[column_id] = AC.[column_id]
690
WHERE AC.[object_id] = {$tableId} AND IC.[index_id] = {$indexId}  AND IC.[object_id] = {$tableId}
691
ORDER BY IC.[key_ordinal];";
692
693
        $rows = $this->fetchAll($sql);
694
        $columns = [];
695
        foreach ($rows as $row) {
696
            $columns[] = strtolower($row['column_name']);
697
        }
698
699
        return $columns;
700
    }
701
702
    /**
703
     * Get an array of indexes from a particular table.
704
     *
705
     * @param string $tableName Table Name
706
     * @return array
707
     */
708
    public function getIndexes($tableName)
709
    {
710
        $indexes = [];
711
        $sql = "SELECT I.[name] AS [index_name], I.[index_id] as [index_id], T.[object_id] as [table_id]
712
FROM sys.[tables] AS T
713
  INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
714
WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP'  AND T.[name] = '{$tableName}'
715
ORDER BY T.[name], I.[index_id];";
716
717
        $rows = $this->fetchAll($sql);
718
        foreach ($rows as $row) {
719
            $columns = $this->getIndexColums($row['table_id'], $row['index_id']);
720
            $indexes[$row['index_name']] = ['columns' => $columns];
721
        }
722
723
        return $indexes;
724
    }
725
726
    /**
727
     * {@inheritdoc}
728
     */
729 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...
730
    {
731
        if (is_string($columns)) {
732
            $columns = [$columns]; // str to array
733
        }
734
735
        $columns = array_map('strtolower', $columns);
736
        $indexes = $this->getIndexes($tableName);
737
738
        foreach ($indexes as $index) {
739
            $a = array_diff($columns, $index['columns']);
740
741
            if (empty($a)) {
742
                return true;
743
            }
744
        }
745
746
        return false;
747
    }
748
749
    /**
750
     * {@inheritdoc}
751
     */
752 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...
753
    {
754
        $indexes = $this->getIndexes($tableName);
755
756
        foreach ($indexes as $name => $index) {
757
            if ($name === $indexName) {
758
                 return true;
759
            }
760
        }
761
762
        return false;
763
    }
764
765
    /**
766
     * {@inheritdoc}
767
     */
768
    protected function getAddIndexInstructions(Table $table, Index $index)
769
    {
770
        $sql = $this->getIndexSqlDefinition($index, $table->getName());
771
772
        return new AlterInstructions([], [$sql]);
773
    }
774
775
    /**
776
     * {@inheritdoc}
777
     */
778
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
779
    {
780
        if (is_string($columns)) {
781
            $columns = [$columns]; // str to array
782
        }
783
784
        $indexes = $this->getIndexes($tableName);
785
        $columns = array_map('strtolower', $columns);
786
        $instructions = new AlterInstructions();
787
788
        foreach ($indexes as $indexName => $index) {
789
            $a = array_diff($columns, $index['columns']);
790 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...
791
                $instructions->addPostStep(sprintf(
792
                    'DROP INDEX %s ON %s',
793
                    $this->quoteColumnName($indexName),
794
                    $this->quoteTableName($tableName)
795
                ));
796
797
                return $instructions;
798
            }
799
        }
800
801
        throw new \InvalidArgumentException(sprintf(
802
            "The specified index on columns '%s' does not exist",
803
            implode(',', $columns)
804
        ));
805
    }
806
807
    /**
808
     * {@inheritdoc}
809
     */
810
    protected function getDropIndexByNameInstructions($tableName, $indexName)
811
    {
812
        $indexes = $this->getIndexes($tableName);
813
        $instructions = new AlterInstructions();
814
815
        foreach ($indexes as $name => $index) {
816 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...
817
                $instructions->addPostStep(sprintf(
818
                    'DROP INDEX %s ON %s',
819
                    $this->quoteColumnName($indexName),
820
                    $this->quoteTableName($tableName)
821
                ));
822
823
                return $instructions;
824
            }
825
        }
826
827
        throw new \InvalidArgumentException(sprintf(
828
            "The specified index name '%s' does not exist",
829
            $indexName
830
        ));
831
    }
832
833
    /**
834
     * {@inheritdoc}
835
     */
836 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...
837
    {
838
        $primaryKey = $this->getPrimaryKey($tableName);
839
840
        if (empty($primaryKey)) {
841
            return false;
842
        }
843
844
        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...
845
            return ($primaryKey['constraint'] === $constraint);
846
        } else {
847
            if (is_string($columns)) {
848
                $columns = [$columns]; // str to array
849
            }
850
            $missingColumns = array_diff($columns, $primaryKey['columns']);
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
        return $primaryKey;
884
    }
885
886
    /**
887
     * {@inheritdoc}
888
     */
889 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...
890
    {
891
        if (is_string($columns)) {
892
            $columns = [$columns]; // str to array
893
        }
894
        $foreignKeys = $this->getForeignKeys($tableName);
895
        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...
896
            if (isset($foreignKeys[$constraint])) {
897
                return !empty($foreignKeys[$constraint]);
898
            }
899
900
            return false;
901
        } else {
902
            foreach ($foreignKeys as $key) {
903
                $a = array_diff($columns, $key['columns']);
904
                if (empty($a)) {
905
                    return true;
906
                }
907
            }
908
909
            return false;
910
        }
911
    }
912
913
    /**
914
     * Get an array of foreign keys from a particular table.
915
     *
916
     * @param string $tableName Table Name
917
     * @return array
918
     */
919 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...
920
    {
921
        $foreignKeys = [];
922
        $rows = $this->fetchAll(sprintf(
923
            "SELECT
924
                    tc.constraint_name,
925
                    tc.table_name, kcu.column_name,
926
                    ccu.table_name AS referenced_table_name,
927
                    ccu.column_name AS referenced_column_name
928
                FROM
929
                    information_schema.table_constraints AS tc
930
                    JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
931
                    JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
932
                WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s'
933
                ORDER BY kcu.ordinal_position",
934
            $tableName
935
        ));
936
        foreach ($rows as $row) {
937
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
938
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
939
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
940
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
941
        }
942
943
        return $foreignKeys;
944
    }
945
946
    /**
947
     * {@inheritdoc}
948
     */
949
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
950
    {
951
        $instructions = new AlterInstructions();
952
        $instructions->addPostStep(sprintf(
953
            'ALTER TABLE %s ADD %s',
954
            $this->quoteTableName($table->getName()),
955
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
956
        ));
957
958
        return $instructions;
959
    }
960
961
    /**
962
     * {@inheritdoc}
963
     */
964
    protected function getDropForeignKeyInstructions($tableName, $constraint)
965
    {
966
        $instructions = new AlterInstructions();
967
        $instructions->addPostStep(sprintf(
968
            'ALTER TABLE %s DROP CONSTRAINT %s',
969
            $this->quoteTableName($tableName),
970
            $constraint
971
        ));
972
973
        return $instructions;
974
    }
975
976
    /**
977
     * {@inheritdoc}
978
     */
979
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
980
    {
981
        $instructions = new AlterInstructions();
982
983 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...
984
            $rows = $this->fetchAll(sprintf(
985
                "SELECT
986
                tc.constraint_name,
987
                tc.table_name, kcu.column_name,
988
                ccu.table_name AS referenced_table_name,
989
                ccu.column_name AS referenced_column_name
990
            FROM
991
                information_schema.table_constraints AS tc
992
                JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
993
                JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
994
            WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' and ccu.column_name='%s'
995
            ORDER BY kcu.ordinal_position",
996
                $tableName,
997
                $column
998
            ));
999
            foreach ($rows as $row) {
1000
                $instructions->merge(
1001
                    $this->getDropForeignKeyInstructions($tableName, $row['constraint_name'])
1002
                );
1003
            }
1004
        }
1005
1006
        return $instructions;
1007
    }
1008
1009
    /**
1010
     * {@inheritdoc}
1011
     */
1012
    public function getSqlType($type, $limit = null)
1013
    {
1014
        switch ($type) {
1015
            case static::PHINX_TYPE_FLOAT:
1016
            case static::PHINX_TYPE_DECIMAL:
1017
            case static::PHINX_TYPE_DATETIME:
1018
            case static::PHINX_TYPE_TIME:
1019
            case static::PHINX_TYPE_DATE:
1020
                return ['name' => $type];
1021
            case static::PHINX_TYPE_STRING:
1022
                return ['name' => 'nvarchar', 'limit' => 255];
1023
            case static::PHINX_TYPE_CHAR:
1024
                return ['name' => 'nchar', 'limit' => 255];
1025
            case static::PHINX_TYPE_TEXT:
1026
                return ['name' => 'ntext'];
1027
            case static::PHINX_TYPE_INTEGER:
1028
                return ['name' => 'int'];
1029
            case static::PHINX_TYPE_BIG_INTEGER:
1030
                return ['name' => 'bigint'];
1031
            case static::PHINX_TYPE_TIMESTAMP:
1032
                return ['name' => 'datetime'];
1033
            case static::PHINX_TYPE_BLOB:
1034
            case static::PHINX_TYPE_BINARY:
1035
                return ['name' => 'varbinary'];
1036
            case static::PHINX_TYPE_BOOLEAN:
1037
                return ['name' => 'bit'];
1038
            case static::PHINX_TYPE_UUID:
1039
                return ['name' => 'uniqueidentifier'];
1040
            case static::PHINX_TYPE_FILESTREAM:
1041
                return ['name' => 'varbinary', 'limit' => 'max'];
1042
            // Geospatial database types
1043
            case static::PHINX_TYPE_GEOMETRY:
1044
            case static::PHINX_TYPE_POINT:
1045
            case static::PHINX_TYPE_LINESTRING:
1046
            case static::PHINX_TYPE_POLYGON:
1047
                // SQL Server stores all spatial data using a single data type.
1048
                // Specific types (point, polygon, etc) are set at insert time.
1049
                return ['name' => 'geography'];
1050
            default:
1051
                throw new \RuntimeException('The type: "' . $type . '" is not supported.');
1052
        }
1053
    }
1054
1055
    /**
1056
     * Returns Phinx type by SQL type
1057
     *
1058
     * @param string $sqlType SQL Type definition
1059
     * @throws \RuntimeException
1060
     * @internal param string $sqlType SQL type
1061
     * @returns string Phinx type
1062
     */
1063
    public function getPhinxType($sqlType)
1064
    {
1065
        switch ($sqlType) {
1066
            case 'nvarchar':
1067
            case 'varchar':
1068
                return static::PHINX_TYPE_STRING;
1069
            case 'char':
1070
            case 'nchar':
1071
                return static::PHINX_TYPE_CHAR;
1072
            case 'text':
1073
            case 'ntext':
1074
                return static::PHINX_TYPE_TEXT;
1075
            case 'int':
1076
            case 'integer':
1077
                return static::PHINX_TYPE_INTEGER;
1078
            case 'decimal':
1079
            case 'numeric':
1080
            case 'money':
1081
                return static::PHINX_TYPE_DECIMAL;
1082
            case 'bigint':
1083
                return static::PHINX_TYPE_BIG_INTEGER;
1084
            case 'real':
1085
            case 'float':
1086
                return static::PHINX_TYPE_FLOAT;
1087
            case 'binary':
1088
            case 'image':
1089
            case 'varbinary':
1090
                return static::PHINX_TYPE_BINARY;
1091
            case 'time':
1092
                return static::PHINX_TYPE_TIME;
1093
            case 'date':
1094
                return static::PHINX_TYPE_DATE;
1095
            case 'datetime':
1096
            case 'timestamp':
1097
                return static::PHINX_TYPE_DATETIME;
1098
            case 'bit':
1099
                return static::PHINX_TYPE_BOOLEAN;
1100
            case 'uniqueidentifier':
1101
                return static::PHINX_TYPE_UUID;
1102
            case 'filestream':
1103
                return static::PHINX_TYPE_FILESTREAM;
1104
            default:
1105
                throw new \RuntimeException('The SqlServer type: "' . $sqlType . '" is not supported');
1106
        }
1107
    }
1108
1109
    /**
1110
     * {@inheritdoc}
1111
     */
1112
    public function createDatabase($name, $options = [])
1113
    {
1114 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...
1115
            $this->execute(sprintf('CREATE DATABASE [%s] COLLATE [%s]', $name, $options['collation']));
1116
        } else {
1117
            $this->execute(sprintf('CREATE DATABASE [%s]', $name));
1118
        }
1119
        $this->execute(sprintf('USE [%s]', $name));
1120
    }
1121
1122
    /**
1123
     * {@inheritdoc}
1124
     */
1125
    public function hasDatabase($name)
1126
    {
1127
        $result = $this->fetchRow(
1128
            sprintf(
1129
                'SELECT count(*) as [count] FROM master.dbo.sysdatabases WHERE [name] = \'%s\'',
1130
                $name
1131
            )
1132
        );
1133
1134
        return $result['count'] > 0;
1135
    }
1136
1137
    /**
1138
     * {@inheritdoc}
1139
     */
1140
    public function dropDatabase($name)
1141
    {
1142
        $sql = <<<SQL
1143
USE master;
1144
IF EXISTS(select * from sys.databases where name=N'$name')
1145
ALTER DATABASE [$name] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
1146
DROP DATABASE [$name];
1147
SQL;
1148
        $this->execute($sql);
1149
    }
1150
1151
    /**
1152
     * Gets the SqlServer Column Definition for a Column object.
1153
     *
1154
     * @param \Phinx\Db\Table\Column $column Column
1155
     * @return string
1156
     */
1157
    protected function getColumnSqlDefinition(Column $column, $create = true)
1158
    {
1159
        $buffer = [];
1160
        if ($column->getType() instanceof Literal) {
1161
            $buffer[] = (string)$column->getType();
1162
        } else {
1163
            $sqlType = $this->getSqlType($column->getType());
1164
            $buffer[] = strtoupper($sqlType['name']);
1165
            // integers cant have limits in SQlServer
1166
            $noLimits = [
1167
                'bigint',
1168
                'int',
1169
                'tinyint'
1170
            ];
1171
            if (!in_array($sqlType['name'], $noLimits) && ($column->getLimit() || isset($sqlType['limit']))) {
1172
                $buffer[] = sprintf('(%s)', $column->getLimit() ? $column->getLimit() : $sqlType['limit']);
1173
            }
1174
        }
1175 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...
1176
            $buffer[] = '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1177
        }
1178
1179
        $properties = $column->getProperties();
1180
        $buffer[] = $column->getType() === 'filestream' ? 'FILESTREAM' : '';
1181
        $buffer[] = isset($properties['rowguidcol']) ? 'ROWGUIDCOL' : '';
1182
1183
        $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1184
1185
        if ($create === true) {
1186
            if ($column->getDefault() === null && $column->isNull()) {
1187
                $buffer[] = ' DEFAULT NULL';
1188
            } else {
1189
                $buffer[] = $this->getDefaultValueDefinition($column->getDefault());
1190
            }
1191
        }
1192
1193
        if ($column->isIdentity()) {
1194
            $buffer[] = 'IDENTITY(1, 1)';
1195
        }
1196
1197
        return implode(' ', $buffer);
1198
    }
1199
1200
    /**
1201
     * Gets the SqlServer Index Definition for an Index object.
1202
     *
1203
     * @param \Phinx\Db\Table\Index $index Index
1204
     * @return string
1205
     */
1206
    protected function getIndexSqlDefinition(Index $index, $tableName)
1207
    {
1208 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...
1209
            $indexName = $index->getName();
1210
        } else {
1211
            $columnNames = $index->getColumns();
1212
            $indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames));
1213
        }
1214
        $def = sprintf(
1215
            "CREATE %s INDEX %s ON %s (%s);",
1216
            ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1217
            $indexName,
1218
            $this->quoteTableName($tableName),
1219
            '[' . implode('],[', $index->getColumns()) . ']'
1220
        );
1221
1222
        return $def;
1223
    }
1224
1225
    /**
1226
     * Gets the SqlServer Foreign Key Definition for an ForeignKey object.
1227
     *
1228
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1229
     * @return string
1230
     */
1231
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1232
    {
1233
        $constraintName = $foreignKey->getConstraint() ?: $tableName . '_' . implode('_', $foreignKey->getColumns());
1234
        $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
0 ignored issues
show
Bug introduced by
It seems like $constraintName defined by $foreignKey->getConstrai...reignKey->getColumns()) on line 1233 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...
1235
        $def .= ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")';
1236
        $def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")';
1237
        if ($foreignKey->getOnDelete()) {
1238
            $def .= " ON DELETE {$foreignKey->getOnDelete()}";
1239
        }
1240
        if ($foreignKey->getOnUpdate()) {
1241
            $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1242
        }
1243
1244
        return $def;
1245
    }
1246
1247
    /**
1248
     * {@inheritdoc}
1249
     */
1250
    public function getColumnTypes()
1251
    {
1252
        return array_merge(parent::getColumnTypes(), ['filestream']);
1253
    }
1254
1255
    /**
1256
     * Records a migration being run.
1257
     *
1258
     * @param \Phinx\Migration\MigrationInterface $migration Migration
1259
     * @param string $direction Direction
1260
     * @param int $startTime Start Time
1261
     * @param int $endTime End Time
1262
     * @return \Phinx\Db\Adapter\AdapterInterface
1263
     */
1264
    public function migrated(\Phinx\Migration\MigrationInterface $migration, $direction, $startTime, $endTime)
1265
    {
1266
        $startTime = str_replace(' ', 'T', $startTime);
1267
        $endTime = str_replace(' ', 'T', $endTime);
1268
1269
        return parent::migrated($migration, $direction, $startTime, $endTime);
1270
    }
1271
1272
    /**
1273
     * {@inheritDoc}
1274
     *
1275
     */
1276 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...
1277
    {
1278
        $options = $this->getOptions();
1279
        $options = [
1280
            'username' => $options['user'],
1281
            'password' => $options['pass'],
1282
            'database' => $options['name'],
1283
            'quoteIdentifiers' => true,
1284
        ] + $options;
1285
1286
        $driver = new SqlServerDriver($options);
1287
1288
        if (method_exists($driver, 'setConnection')) {
1289
            $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...
1290
        } else {
1291
            $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...
1292
        }
1293
1294
        return new Connection(['driver' => $driver] + $options);
1295
    }
1296
}
1297