Completed
Push — master ( 152a5f...57e856 )
by José
11s
created

SqlServerAdapter::createTable()   C

Complexity

Conditions 13
Paths 144

Size

Total Lines 65
Code Lines 39

Duplication

Lines 26
Ratio 40 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 0
Metric Value
dl 26
loc 65
rs 5.4525
c 0
b 0
f 0
ccs 0
cts 50
cp 0
cc 13
eloc 39
nc 144
nop 3
crap 182

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Phinx\Db\Table\Column;
32
use Phinx\Db\Table\ForeignKey;
33
use Phinx\Db\Table\Index;
34
use Phinx\Db\Table\Table;
35
use Phinx\Db\Util\AlterInstructions;
36
use Phinx\Util\Literal;
37
38
/**
39
 * Phinx SqlServer Adapter.
40
 *
41
 * @author Rob Morgan <[email protected]>
42
 */
43
class SqlServerAdapter extends PdoAdapter implements AdapterInterface
44
{
45
    protected $schema = 'dbo';
46
47
    protected $signedColumnTypes = ['integer' => true, 'biginteger' => true, 'float' => true, 'decimal' => true];
48
49
    /**
50
     * {@inheritdoc}
51
     */
52
    public function connect()
53
    {
54
        if ($this->connection === null) {
55
            if (!class_exists('PDO') || !in_array('sqlsrv', \PDO::getAvailableDrivers(), true)) {
56
                // try our connection via freetds (Mac/Linux)
57
                $this->connectDblib();
58
59
                return;
60
            }
61
62
            $db = null;
63
            $options = $this->getOptions();
64
65
            // if port is specified use it, otherwise use the SqlServer default
66 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...
67
                $dsn = 'sqlsrv:server=' . $options['host'] . ';database=' . $options['name'];
68
            } else {
69
                $dsn = 'sqlsrv:server=' . $options['host'] . ',' . $options['port'] . ';database=' . $options['name'];
70
            }
71
            $dsn .= ';MultipleActiveResultSets=false';
72
73
            $driverOptions = [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION];
74
75
            // charset support
76
            if (isset($options['charset'])) {
77
                $driverOptions[\PDO::SQLSRV_ATTR_ENCODING] = $options['charset'];
78
            }
79
80
            // support arbitrary \PDO::SQLSRV_ATTR_* driver options and pass them to PDO
81
            // http://php.net/manual/en/ref.pdo-sqlsrv.php#pdo-sqlsrv.constants
82 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...
83
                if (strpos($key, 'sqlsrv_attr_') === 0) {
84
                    $driverOptions[constant('\PDO::' . strtoupper($key))] = $option;
85
                }
86
            }
87
88
            try {
89
                $db = new \PDO($dsn, $options['user'], $options['pass'], $driverOptions);
90
            } catch (\PDOException $exception) {
91
                throw new \InvalidArgumentException(sprintf(
92
                    'There was a problem connecting to the database: %s',
93
                    $exception->getMessage()
94
                ));
95
            }
96
97
            $this->setConnection($db);
98
        }
99
    }
100
101
    /**
102
     * Connect to MSSQL using dblib/freetds.
103
     *
104
     * The "sqlsrv" driver is not available on Unix machines.
105
     *
106
     * @throws \InvalidArgumentException
107
     */
108
    protected function connectDblib()
109
    {
110
        if (!class_exists('PDO') || !in_array('dblib', \PDO::getAvailableDrivers(), true)) {
111
            // @codeCoverageIgnoreStart
112
            throw new \RuntimeException('You need to enable the PDO_Dblib extension for Phinx to run properly.');
113
            // @codeCoverageIgnoreEnd
114
        }
115
116
        $options = $this->getOptions();
117
118
        // if port is specified use it, otherwise use the SqlServer default
119 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...
120
            $dsn = 'dblib:host=' . $options['host'] . ';dbname=' . $options['name'];
121
        } else {
122
            $dsn = 'dblib:host=' . $options['host'] . ':' . $options['port'] . ';dbname=' . $options['name'];
123
        }
124
125
        $driverOptions = [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION];
126
127
        try {
128
            $db = new \PDO($dsn, $options['user'], $options['pass'], $driverOptions);
129
        } catch (\PDOException $exception) {
130
            throw new \InvalidArgumentException(sprintf(
131
                'There was a problem connecting to the database: %s',
132
                $exception->getMessage()
133
            ));
134
        }
135
136
        $this->setConnection($db);
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142
    public function disconnect()
143
    {
144
        $this->connection = null;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150
    public function hasTransactions()
151
    {
152
        return true;
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function beginTransaction()
159
    {
160
        $this->execute('BEGIN TRANSACTION');
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166
    public function commitTransaction()
167
    {
168
        $this->execute('COMMIT TRANSACTION');
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174
    public function rollbackTransaction()
175
    {
176
        $this->execute('ROLLBACK TRANSACTION');
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182
    public function quoteTableName($tableName)
183
    {
184
        return str_replace('.', '].[', $this->quoteColumnName($tableName));
185
    }
186
187
    /**
188
     * {@inheritdoc}
189
     */
190
    public function quoteColumnName($columnName)
191
    {
192
        return '[' . str_replace(']', '\]', $columnName) . ']';
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198
    public function hasTable($tableName)
199
    {
200
        $result = $this->fetchRow(sprintf('SELECT count(*) as [count] FROM information_schema.tables WHERE table_name = \'%s\';', $tableName));
201
202
        return $result['count'] > 0;
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     */
208
    public function createTable(Table $table, array $columns = [], array $indexes = [])
209
    {
210
        $options = $table->getOptions();
211
212
        // Add the default primary key
213 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...
214
            $column = new Column();
215
            $column->setName('id')
216
                   ->setType('integer')
217
                   ->setIdentity(true);
218
219
            array_unshift($columns, $column);
220
            $options['primary_key'] = 'id';
221
        } elseif (isset($options['id']) && is_string($options['id'])) {
222
            // Handle id => "field_name" to support AUTO_INCREMENT
223
            $column = new Column();
224
            $column->setName($options['id'])
225
                   ->setType('integer')
226
                   ->setIdentity(true);
227
228
            array_unshift($columns, $column);
229
            $options['primary_key'] = $options['id'];
230
        }
231
232
        $sql = 'CREATE TABLE ';
233
        $sql .= $this->quoteTableName($table->getName()) . ' (';
234
        $sqlBuffer = [];
235
        $columnsWithComments = [];
236 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...
237
            $sqlBuffer[] = $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column);
238
239
            // set column comments, if needed
240
            if ($column->getComment()) {
241
                $columnsWithComments[] = $column;
242
            }
243
        }
244
245
        // set the primary key(s)
246
        if (isset($options['primary_key'])) {
247
            $pkSql = sprintf('CONSTRAINT PK_%s PRIMARY KEY (', $table->getName());
248
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
249
                $pkSql .= $this->quoteColumnName($options['primary_key']);
250
            } 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...
251
                $pkSql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
252
            }
253
            $pkSql .= ')';
254
            $sqlBuffer[] = $pkSql;
255
        }
256
257
        $sql .= implode(', ', $sqlBuffer);
258
        $sql .= ');';
259
260
        // process column comments
261
        foreach ($columnsWithComments as $column) {
262
            $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName());
263
        }
264
265
        // set the indexes
266
        foreach ($indexes as $index) {
267
            $sql .= $this->getIndexSqlDefinition($index, $table->getName());
268
        }
269
270
        // execute the sql
271
        $this->execute($sql);
272
    }
273
274
    /**
275
     * Gets the SqlServer Column Comment Defininition for a column object.
276
     *
277
     * @param \Phinx\Db\Table\Column $column    Column
278
     * @param string $tableName Table name
279
     *
280
     * @return string
281
     */
282
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
283
    {
284
        // passing 'null' is to remove column comment
285
        $currentComment = $this->getColumnComment($tableName, $column->getName());
286
287
        $comment = (strcasecmp($column->getComment(), 'NULL') !== 0) ? $this->getConnection()->quote($column->getComment()) : '\'\'';
288
        $command = $currentComment === false ? 'sp_addextendedproperty' : 'sp_updateextendedproperty';
289
290
        return sprintf(
291
            "EXECUTE %s N'MS_Description', N%s, N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s';",
292
            $command,
293
            $comment,
294
            $this->schema,
295
            $tableName,
296
            $column->getName()
297
        );
298
    }
299
300
    /**
301
     * {@inheritdoc}
302
     */
303
    protected function getRenameTableInstructions($tableName, $newTableName)
304
    {
305
        $sql = sprintf(
306
            'EXEC sp_rename \'%s\', \'%s\'',
307
            $tableName,
308
            $newTableName
309
        );
310
311
        return new AlterInstructions([], [$sql]);
312
    }
313
314
    /**
315
     * {@inheritdoc}
316
     */
317
    protected function getDropTableInstructions($tableName)
318
    {
319
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
320
321
        return new AlterInstructions([], [$sql]);
322
    }
323
324
    /**
325
     * {@inheritdoc}
326
     */
327
    public function truncateTable($tableName)
328
    {
329
        $sql = sprintf(
330
            'TRUNCATE TABLE %s',
331
            $this->quoteTableName($tableName)
332
        );
333
334
        $this->execute($sql);
335
    }
336
337
    public function getColumnComment($tableName, $columnName)
338
    {
339
        $sql = sprintf("SELECT cast(extended_properties.[value] as nvarchar(4000)) comment
340
  FROM sys.schemas
341
 INNER JOIN sys.tables
342
    ON schemas.schema_id = tables.schema_id
343
 INNER JOIN sys.columns
344
    ON tables.object_id = columns.object_id
345
 INNER JOIN sys.extended_properties
346
    ON tables.object_id = extended_properties.major_id
347
   AND columns.column_id = extended_properties.minor_id
348
   AND extended_properties.name = 'MS_Description'
349
   WHERE schemas.[name] = '%s' AND tables.[name] = '%s' AND columns.[name] = '%s'", $this->schema, $tableName, $columnName);
350
        $row = $this->fetchRow($sql);
351
352
        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...
353
            return $row['comment'];
354
        }
355
356
        return false;
357
    }
358
359
    /**
360
     * {@inheritdoc}
361
     */
362
    public function getColumns($tableName)
363
    {
364
        $columns = [];
365
        $sql = sprintf(
366
            "SELECT DISTINCT TABLE_SCHEMA AS [schema], TABLE_NAME as [table_name], COLUMN_NAME AS [name], DATA_TYPE AS [type],
367
            IS_NULLABLE AS [null], COLUMN_DEFAULT AS [default],
368
            CHARACTER_MAXIMUM_LENGTH AS [char_length],
369
            NUMERIC_PRECISION AS [precision],
370
            NUMERIC_SCALE AS [scale], ORDINAL_POSITION AS [ordinal_position],
371
            COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as [identity]
372
        FROM INFORMATION_SCHEMA.COLUMNS
373
        WHERE TABLE_NAME = '%s'
374
        ORDER BY ordinal_position",
375
            $tableName
376
        );
377
        $rows = $this->fetchAll($sql);
378
        foreach ($rows as $columnInfo) {
379
            $column = new Column();
380
            $column->setName($columnInfo['name'])
381
                   ->setType($this->getPhinxType($columnInfo['type']))
382
                   ->setNull($columnInfo['null'] !== 'NO')
383
                   ->setDefault($this->parseDefault($columnInfo['default']))
384
                   ->setIdentity($columnInfo['identity'] === '1')
385
                   ->setComment($this->getColumnComment($columnInfo['table_name'], $columnInfo['name']));
386
387
            if (!empty($columnInfo['char_length'])) {
388
                $column->setLimit($columnInfo['char_length']);
389
            }
390
391
            $columns[$columnInfo['name']] = $column;
392
        }
393
394
        return $columns;
395
    }
396
397
    protected function parseDefault($default)
398
    {
399
        $default = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default);
400
401
        if (strtoupper($default) === 'NULL') {
402
            $default = null;
403
        } elseif (is_numeric($default)) {
404
            $default = (int)$default;
405
        }
406
407
        return $default;
408
    }
409
410
    /**
411
     * {@inheritdoc}
412
     */
413 View Code Duplication
    public function hasColumn($tableName, $columnName)
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...
414
    {
415
        $sql = sprintf(
416
            "SELECT count(*) as [count]
417
             FROM information_schema.columns
418
             WHERE table_name = '%s' AND column_name = '%s'",
419
            $tableName,
420
            $columnName
421
        );
422
        $result = $this->fetchRow($sql);
423
424
        return $result['count'] > 0;
425
    }
426
427
    /**
428
     * {@inheritdoc}
429
     */
430 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...
431
    {
432
        $alter = sprintf(
433
            'ALTER TABLE %s ADD %s %s',
434
            $table->getName(),
435
            $this->quoteColumnName($column->getName()),
436
            $this->getColumnSqlDefinition($column)
437
        );
438
439
        return new AlterInstructions([], [$alter]);
440
    }
441
442
    /**
443
     * {@inheritdoc}
444
     */
445
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
446
    {
447
        if (!$this->hasColumn($tableName, $columnName)) {
448
            throw new \InvalidArgumentException("The specified column does not exist: $columnName");
449
        }
450
451
        $instructions = new AlterInstructions();
452
453
        $oldConstraintName = "DF_{$tableName}_{$columnName}";
454
        $newConstraintName = "DF_{$tableName}_{$newColumnName}";
455
        $sql = <<<SQL
456
IF (OBJECT_ID('$oldConstraintName', 'D') IS NOT NULL)
457
BEGIN
458
     EXECUTE sp_rename N'%s', N'%s', N'OBJECT'
459
END
460
SQL;
461
        $instructions->addPostStep(sprintf(
462
            $sql,
463
            $oldConstraintName,
464
            $newConstraintName
465
        ));
466
467
        $instructions->addPostStep(sprintf(
468
            "EXECUTE sp_rename N'%s.%s', N'%s', 'COLUMN' ",
469
            $tableName,
470
            $columnName,
471
            $newColumnName
472
        ));
473
474
        return $instructions;
475
    }
476
477
    /**
478
     * Returns the instructions to change a column default value
479
     *
480
     * @param string $tableName The table where the column is
481
     * @param Column $newColumn The column to alter
482
     * @return AlterInstructions
483
     */
484
    protected function getChangeDefault($tableName, Column $newColumn)
485
    {
486
        $constraintName = "DF_{$tableName}_{$newColumn->getName()}";
487
        $default = $newColumn->getDefault();
488
        $instructions = new AlterInstructions();
489
490
        if ($default === null) {
491
            $default = 'DEFAULT NULL';
492
        } else {
493
            $default = ltrim($this->getDefaultValueDefinition($default));
494
        }
495
496
        if (empty($default)) {
497
            return $instructions;
498
        }
499
500
        $instructions->addPostStep(sprintf(
501
            'ALTER TABLE %s ADD CONSTRAINT %s %s FOR %s',
502
            $this->quoteTableName($tableName),
503
            $constraintName,
504
            $default,
505
            $this->quoteColumnName($newColumn->getName())
506
        ));
507
508
        return $instructions;
509
    }
510
511
    /**
512
     * {@inheritdoc}
513
     */
514
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
515
    {
516
        $columns = $this->getColumns($tableName);
517
        $changeDefault =
518
            $newColumn->getDefault() !== $columns[$columnName]->getDefault() ||
519
            $newColumn->getType() !== $columns[$columnName]->getType();
520
521
        $instructions = new AlterInstructions();
522
523
        if ($columnName !== $newColumn->getName()) {
524
            $instructions->merge(
525
                $this->getRenameColumnInstructions($tableName, $columnName, $newColumn->getName())
526
            );
527
        }
528
529
        if ($changeDefault) {
530
            $instructions->merge($this->getDropDefaultConstraint($tableName, $newColumn->getName()));
531
        }
532
533
        $instructions->addPostStep(sprintf(
534
            'ALTER TABLE %s ALTER COLUMN %s %s',
535
            $this->quoteTableName($tableName),
536
            $this->quoteColumnName($newColumn->getName()),
537
            $this->getColumnSqlDefinition($newColumn, false)
538
        ));
539
        // change column comment if needed
540
        if ($newColumn->getComment()) {
541
            $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...
542
        }
543
544
        if ($changeDefault) {
545
            $instructions->merge($this->getChangeDefault($tableName, $newColumn));
546
        }
547
548
        return $instructions;
549
    }
550
551
    /**
552
     * {@inheritdoc}
553
     */
554
    protected function getDropColumnInstructions($tableName, $columnName)
555
    {
556
        $instructions = $this->getDropDefaultConstraint($tableName, $columnName);
557
558
        $instructions->addPostStep(sprintf(
559
            'ALTER TABLE %s DROP COLUMN %s',
560
            $this->quoteTableName($tableName),
561
            $this->quoteColumnName($columnName)
562
        ));
563
564
        return $instructions;
565
    }
566
567
    /**
568
     * {@inheritdoc}
569
     */
570
    protected function getDropDefaultConstraint($tableName, $columnName)
571
    {
572
        $defaultConstraint = $this->getDefaultConstraint($tableName, $columnName);
573
574
        if (!$defaultConstraint) {
575
            return new AlterInstructions();
576
        }
577
578
        return $this->getDropForeignKeyInstructions($tableName, $defaultConstraint);
579
    }
580
581
    protected function getDefaultConstraint($tableName, $columnName)
582
    {
583
        $sql = "SELECT
584
    default_constraints.name
585
FROM
586
    sys.all_columns
587
588
        INNER JOIN
589
    sys.tables
590
        ON all_columns.object_id = tables.object_id
591
592
        INNER JOIN
593
    sys.schemas
594
        ON tables.schema_id = schemas.schema_id
595
596
        INNER JOIN
597
    sys.default_constraints
598
        ON all_columns.default_object_id = default_constraints.object_id
599
600
WHERE
601
        schemas.name = 'dbo'
602
    AND tables.name = '{$tableName}'
603
    AND all_columns.name = '{$columnName}'";
604
605
        $rows = $this->fetchAll($sql);
606
607
        return empty($rows) ? false : $rows[0]['name'];
608
    }
609
610
    protected function getIndexColums($tableId, $indexId)
611
    {
612
        $sql = "SELECT AC.[name] AS [column_name]
613
FROM sys.[index_columns] IC
614
  INNER JOIN sys.[all_columns] AC ON IC.[column_id] = AC.[column_id]
615
WHERE AC.[object_id] = {$tableId} AND IC.[index_id] = {$indexId}  AND IC.[object_id] = {$tableId}
616
ORDER BY IC.[key_ordinal];";
617
618
        $rows = $this->fetchAll($sql);
619
        $columns = [];
620
        foreach ($rows as $row) {
621
            $columns[] = strtolower($row['column_name']);
622
        }
623
624
        return $columns;
625
    }
626
627
    /**
628
     * Get an array of indexes from a particular table.
629
     *
630
     * @param string $tableName Table Name
631
     * @return array
632
     */
633
    public function getIndexes($tableName)
634
    {
635
        $indexes = [];
636
        $sql = "SELECT I.[name] AS [index_name], I.[index_id] as [index_id], T.[object_id] as [table_id]
637
FROM sys.[tables] AS T
638
  INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
639
WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP'  AND T.[name] = '{$tableName}'
640
ORDER BY T.[name], I.[index_id];";
641
642
        $rows = $this->fetchAll($sql);
643
        foreach ($rows as $row) {
644
            $columns = $this->getIndexColums($row['table_id'], $row['index_id']);
645
            $indexes[$row['index_name']] = ['columns' => $columns];
646
        }
647
648
        return $indexes;
649
    }
650
651
    /**
652
     * {@inheritdoc}
653
     */
654 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...
655
    {
656
        if (is_string($columns)) {
657
            $columns = [$columns]; // str to array
658
        }
659
660
        $columns = array_map('strtolower', $columns);
661
        $indexes = $this->getIndexes($tableName);
662
663
        foreach ($indexes as $index) {
664
            $a = array_diff($columns, $index['columns']);
665
666
            if (empty($a)) {
667
                return true;
668
            }
669
        }
670
671
        return false;
672
    }
673
674
    /**
675
     * {@inheritdoc}
676
     */
677 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...
678
    {
679
        $indexes = $this->getIndexes($tableName);
680
681
        foreach ($indexes as $name => $index) {
682
            if ($name === $indexName) {
683
                 return true;
684
            }
685
        }
686
687
        return false;
688
    }
689
690
    /**
691
     * {@inheritdoc}
692
     */
693
    protected function getAddIndexInstructions(Table $table, Index $index)
694
    {
695
        $sql = $this->getIndexSqlDefinition($index, $table->getName());
696
697
        return new AlterInstructions([], [$sql]);
698
    }
699
700
    /**
701
     * {@inheritdoc}
702
     */
703
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
704
    {
705
        if (is_string($columns)) {
706
            $columns = [$columns]; // str to array
707
        }
708
709
        $indexes = $this->getIndexes($tableName);
710
        $columns = array_map('strtolower', $columns);
711
        $instructions = new AlterInstructions();
712
713
        foreach ($indexes as $indexName => $index) {
714
            $a = array_diff($columns, $index['columns']);
715 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...
716
                $instructions->addPostStep(sprintf(
717
                    'DROP INDEX %s ON %s',
718
                    $this->quoteColumnName($indexName),
719
                    $this->quoteTableName($tableName)
720
                ));
721
722
                return $instructions;
723
            }
724
        }
725
726
        throw new \InvalidArgumentException(sprintf(
727
            "The specified index on columns '%s' does not exist",
728
            implode(',', $columns)
729
        ));
730
    }
731
732
    /**
733
     * {@inheritdoc}
734
     */
735
    protected function getDropIndexByNameInstructions($tableName, $indexName)
736
    {
737
        $indexes = $this->getIndexes($tableName);
738
        $instructions = new AlterInstructions();
739
740
        foreach ($indexes as $name => $index) {
741 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...
742
                $instructions->addPostStep(sprintf(
743
                    'DROP INDEX %s ON %s',
744
                    $this->quoteColumnName($indexName),
745
                    $this->quoteTableName($tableName)
746
                ));
747
748
                return $instructions;
749
            }
750
        }
751
752
        throw new \InvalidArgumentException(sprintf(
753
            "The specified index name '%s' does not exist",
754
            $indexName
755
        ));
756
    }
757
758
    /**
759
     * {@inheritdoc}
760
     */
761 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...
762
    {
763
        if (is_string($columns)) {
764
            $columns = [$columns]; // str to array
765
        }
766
        $foreignKeys = $this->getForeignKeys($tableName);
767
        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...
768
            if (isset($foreignKeys[$constraint])) {
769
                return !empty($foreignKeys[$constraint]);
770
            }
771
772
            return false;
773
        } else {
774
            foreach ($foreignKeys as $key) {
775
                $a = array_diff($columns, $key['columns']);
776
                if (empty($a)) {
777
                    return true;
778
                }
779
            }
780
781
            return false;
782
        }
783
    }
784
785
    /**
786
     * Get an array of foreign keys from a particular table.
787
     *
788
     * @param string $tableName Table Name
789
     * @return array
790
     */
791 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...
792
    {
793
        $foreignKeys = [];
794
        $rows = $this->fetchAll(sprintf(
795
            "SELECT
796
                    tc.constraint_name,
797
                    tc.table_name, kcu.column_name,
798
                    ccu.table_name AS referenced_table_name,
799
                    ccu.column_name AS referenced_column_name
800
                FROM
801
                    information_schema.table_constraints AS tc
802
                    JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
803
                    JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
804
                WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s'
805
                ORDER BY kcu.ordinal_position",
806
            $tableName
807
        ));
808
        foreach ($rows as $row) {
809
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
810
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
811
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
812
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
813
        }
814
815
        return $foreignKeys;
816
    }
817
818
    /**
819
     * {@inheritdoc}
820
     */
821
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
822
    {
823
        $instructions = new AlterInstructions();
824
        $instructions->addPostStep(sprintf(
825
            'ALTER TABLE %s ADD %s',
826
            $this->quoteTableName($table->getName()),
827
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
828
        ));
829
830
        return $instructions;
831
    }
832
833
    /**
834
     * {@inheritdoc}
835
     */
836
    protected function getDropForeignKeyInstructions($tableName, $constraint)
837
    {
838
        $instructions = new AlterInstructions();
839
        $instructions->addPostStep(sprintf(
840
            'ALTER TABLE %s DROP CONSTRAINT %s',
841
            $this->quoteTableName($tableName),
842
            $constraint
843
        ));
844
845
        return $instructions;
846
    }
847
848
    /**
849
     * {@inheritdoc}
850
     */
851 View Code Duplication
    protected function getDropForeignKeyByColumnsInstructions($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...
852
    {
853
        $instructions = new AlterInstructions();
854
855
        foreach ($columns as $column) {
856
            $rows = $this->fetchAll(sprintf(
857
                "SELECT
858
                tc.constraint_name,
859
                tc.table_name, kcu.column_name,
860
                ccu.table_name AS referenced_table_name,
861
                ccu.column_name AS referenced_column_name
862
            FROM
863
                information_schema.table_constraints AS tc
864
                JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
865
                JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
866
            WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' and ccu.column_name='%s'
867
            ORDER BY kcu.ordinal_position",
868
                $tableName,
869
                $column
870
            ));
871
            foreach ($rows as $row) {
872
                $instructions->merge(
873
                    $this->getDropForeignKeyInstructions($tableName, $row['constraint_name'])
874
                );
875
            }
876
        }
877
878
        return $instructions;
879
    }
880
881
    /**
882
     * {@inheritdoc}
883
     */
884
    public function getSqlType($type, $limit = null)
885
    {
886
        switch ($type) {
887
            case static::PHINX_TYPE_FLOAT:
888
            case static::PHINX_TYPE_DECIMAL:
889
            case static::PHINX_TYPE_DATETIME:
890
            case static::PHINX_TYPE_TIME:
891
            case static::PHINX_TYPE_DATE:
892
                return ['name' => $type];
893
            case static::PHINX_TYPE_STRING:
894
                return ['name' => 'nvarchar', 'limit' => 255];
895
            case static::PHINX_TYPE_CHAR:
896
                return ['name' => 'nchar', 'limit' => 255];
897
            case static::PHINX_TYPE_TEXT:
898
                return ['name' => 'ntext'];
899
            case static::PHINX_TYPE_INTEGER:
900
                return ['name' => 'int'];
901
            case static::PHINX_TYPE_BIG_INTEGER:
902
                return ['name' => 'bigint'];
903
            case static::PHINX_TYPE_TIMESTAMP:
904
                return ['name' => 'datetime'];
905
            case static::PHINX_TYPE_BLOB:
906
            case static::PHINX_TYPE_BINARY:
907
                return ['name' => 'varbinary'];
908
            case static::PHINX_TYPE_BOOLEAN:
909
                return ['name' => 'bit'];
910
            case static::PHINX_TYPE_UUID:
911
                return ['name' => 'uniqueidentifier'];
912
            case static::PHINX_TYPE_FILESTREAM:
913
                return ['name' => 'varbinary', 'limit' => 'max'];
914
            // Geospatial database types
915
            case static::PHINX_TYPE_GEOMETRY:
916
            case static::PHINX_TYPE_POINT:
917
            case static::PHINX_TYPE_LINESTRING:
918
            case static::PHINX_TYPE_POLYGON:
919
                // SQL Server stores all spatial data using a single data type.
920
                // Specific types (point, polygon, etc) are set at insert time.
921
                return ['name' => 'geography'];
922
            default:
923
                throw new \RuntimeException('The type: "' . $type . '" is not supported.');
924
        }
925
    }
926
927
    /**
928
     * Returns Phinx type by SQL type
929
     *
930
     * @param string $sqlType SQL Type definition
931
     * @throws \RuntimeException
932
     * @internal param string $sqlType SQL type
933
     * @returns string Phinx type
934
     */
935
    public function getPhinxType($sqlType)
936
    {
937
        switch ($sqlType) {
938
            case 'nvarchar':
939
            case 'varchar':
940
                return static::PHINX_TYPE_STRING;
941
            case 'char':
942
            case 'nchar':
943
                return static::PHINX_TYPE_CHAR;
944
            case 'text':
945
            case 'ntext':
946
                return static::PHINX_TYPE_TEXT;
947
            case 'int':
948
            case 'integer':
949
                return static::PHINX_TYPE_INTEGER;
950
            case 'decimal':
951
            case 'numeric':
952
            case 'money':
953
                return static::PHINX_TYPE_DECIMAL;
954
            case 'bigint':
955
                return static::PHINX_TYPE_BIG_INTEGER;
956
            case 'real':
957
            case 'float':
958
                return static::PHINX_TYPE_FLOAT;
959
            case 'binary':
960
            case 'image':
961
            case 'varbinary':
962
                return static::PHINX_TYPE_BINARY;
963
            case 'time':
964
                return static::PHINX_TYPE_TIME;
965
            case 'date':
966
                return static::PHINX_TYPE_DATE;
967
            case 'datetime':
968
            case 'timestamp':
969
                return static::PHINX_TYPE_DATETIME;
970
            case 'bit':
971
                return static::PHINX_TYPE_BOOLEAN;
972
            case 'uniqueidentifier':
973
                return static::PHINX_TYPE_UUID;
974
            case 'filestream':
975
                return static::PHINX_TYPE_FILESTREAM;
976
            default:
977
                throw new \RuntimeException('The SqlServer type: "' . $sqlType . '" is not supported');
978
        }
979
    }
980
981
    /**
982
     * {@inheritdoc}
983
     */
984
    public function createDatabase($name, $options = [])
985
    {
986 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...
987
            $this->execute(sprintf('CREATE DATABASE [%s] COLLATE [%s]', $name, $options['collation']));
988
        } else {
989
            $this->execute(sprintf('CREATE DATABASE [%s]', $name));
990
        }
991
        $this->execute(sprintf('USE [%s]', $name));
992
    }
993
994
    /**
995
     * {@inheritdoc}
996
     */
997
    public function hasDatabase($name)
998
    {
999
        $result = $this->fetchRow(
1000
            sprintf(
1001
                'SELECT count(*) as [count] FROM master.dbo.sysdatabases WHERE [name] = \'%s\'',
1002
                $name
1003
            )
1004
        );
1005
1006
        return $result['count'] > 0;
1007
    }
1008
1009
    /**
1010
     * {@inheritdoc}
1011
     */
1012
    public function dropDatabase($name)
1013
    {
1014
        $sql = <<<SQL
1015
USE master;
1016
IF EXISTS(select * from sys.databases where name=N'$name')
1017
ALTER DATABASE [$name] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
1018
DROP DATABASE [$name];
1019
SQL;
1020
        $this->execute($sql);
1021
    }
1022
1023
    /**
1024
     * Gets the SqlServer Column Definition for a Column object.
1025
     *
1026
     * @param \Phinx\Db\Table\Column $column Column
1027
     * @return string
1028
     */
1029
    protected function getColumnSqlDefinition(Column $column, $create = true)
1030
    {
1031
        $buffer = [];
1032
        if ($column->getType() instanceof Literal) {
1033
            $buffer[] = (string)$column->getType();
1034
        } else {
1035
            $sqlType = $this->getSqlType($column->getType());
1036
            $buffer[] = strtoupper($sqlType['name']);
1037
            // integers cant have limits in SQlServer
1038
            $noLimits = [
1039
                'bigint',
1040
                'int',
1041
                'tinyint'
1042
            ];
1043
            if (!in_array($sqlType['name'], $noLimits) && ($column->getLimit() || isset($sqlType['limit']))) {
1044
                $buffer[] = sprintf('(%s)', $column->getLimit() ? $column->getLimit() : $sqlType['limit']);
1045
            }
1046
        }
1047 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...
1048
            $buffer[] = '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1049
        }
1050
1051
        $properties = $column->getProperties();
1052
        $buffer[] = $column->getType() === 'filestream' ? 'FILESTREAM' : '';
1053
        $buffer[] = isset($properties['rowguidcol']) ? 'ROWGUIDCOL' : '';
1054
1055
        $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1056
1057
        if ($create === true) {
1058
            if ($column->getDefault() === null && $column->isNull()) {
1059
                $buffer[] = ' DEFAULT NULL';
1060
            } else {
1061
                $buffer[] = $this->getDefaultValueDefinition($column->getDefault());
1062
            }
1063
        }
1064
1065
        if ($column->isIdentity()) {
1066
            $buffer[] = 'IDENTITY(1, 1)';
1067
        }
1068
1069
        return implode(' ', $buffer);
1070
    }
1071
1072
    /**
1073
     * Gets the SqlServer Index Definition for an Index object.
1074
     *
1075
     * @param \Phinx\Db\Table\Index $index Index
1076
     * @return string
1077
     */
1078
    protected function getIndexSqlDefinition(Index $index, $tableName)
1079
    {
1080
        if (is_string($index->getName())) {
1081
            $indexName = $index->getName();
1082
        } else {
1083
            $columnNames = $index->getColumns();
1084
            $indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames));
1085
        }
1086
        $def = sprintf(
1087
            "CREATE %s INDEX %s ON %s (%s);",
1088
            ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1089
            $indexName,
1090
            $this->quoteTableName($tableName),
1091
            '[' . implode('],[', $index->getColumns()) . ']'
1092
        );
1093
1094
        return $def;
1095
    }
1096
1097
    /**
1098
     * Gets the SqlServer Foreign Key Definition for an ForeignKey object.
1099
     *
1100
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1101
     * @return string
1102
     */
1103 View Code Duplication
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $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...
1104
    {
1105
        $constraintName = $foreignKey->getConstraint() ?: $tableName . '_' . implode('_', $foreignKey->getColumns());
1106
        $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
0 ignored issues
show
Bug introduced by
It seems like $constraintName defined by $foreignKey->getConstrai...reignKey->getColumns()) on line 1105 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...
1107
        $def .= ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")';
1108
        $def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")';
1109
        if ($foreignKey->getOnDelete()) {
1110
            $def .= " ON DELETE {$foreignKey->getOnDelete()}";
1111
        }
1112
        if ($foreignKey->getOnUpdate()) {
1113
            $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1114
        }
1115
1116
        return $def;
1117
    }
1118
1119
    /**
1120
     * {@inheritdoc}
1121
     */
1122
    public function getColumnTypes()
1123
    {
1124
        return array_merge(parent::getColumnTypes(), ['filestream']);
1125
    }
1126
1127
    /**
1128
     * Records a migration being run.
1129
     *
1130
     * @param \Phinx\Migration\MigrationInterface $migration Migration
1131
     * @param string $direction Direction
1132
     * @param int $startTime Start Time
1133
     * @param int $endTime End Time
1134
     * @return \Phinx\Db\Adapter\AdapterInterface
1135
     */
1136
    public function migrated(\Phinx\Migration\MigrationInterface $migration, $direction, $startTime, $endTime)
1137
    {
1138
        $startTime = str_replace(' ', 'T', $startTime);
1139
        $endTime = str_replace(' ', 'T', $endTime);
1140
1141
        return parent::migrated($migration, $direction, $startTime, $endTime);
1142
    }
1143
}
1144