Completed
Push — master ( da6087...8e2986 )
by José
03:06 queued 01:21
created

SqlServerAdapter::addColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 11
Ratio 100 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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