Completed
Pull Request — master (#1209)
by
unknown
01:51
created

SqlServerAdapter::commitTransaction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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
36
/**
37
 * Phinx SqlServer Adapter.
38
 *
39
 * @author Rob Morgan <[email protected]>
40
 */
41
class SqlServerAdapter extends PdoAdapter implements AdapterInterface
42
{
43
    protected $schema = 'dbo';
44
45
    protected $signedColumnTypes = ['integer' => true, 'biginteger' => true, 'float' => true, 'decimal' => true];
46
47
    /**
48
     * {@inheritdoc}
49
     */
50
    public function connect()
51
    {
52
        if ($this->connection === null) {
53
            if (!class_exists('PDO') || !in_array('sqlsrv', \PDO::getAvailableDrivers(), true)) {
54
                // try our connection via freetds (Mac/Linux)
55
                $this->connectDblib();
56
57
                return;
58
            }
59
60
            $db = null;
61
            $options = $this->getOptions();
62
63
            // if port is specified use it, otherwise use the SqlServer default
64 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...
65
                $dsn = 'sqlsrv:server=' . $options['host'] . ';database=' . $options['name'];
66
            } else {
67
                $dsn = 'sqlsrv:server=' . $options['host'] . ',' . $options['port'] . ';database=' . $options['name'];
68
            }
69
            $dsn .= ';MultipleActiveResultSets=false';
70
71
            $driverOptions = [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION];
72
73
            // charset support
74
            if (isset($options['charset'])) {
75
                $driverOptions[\PDO::SQLSRV_ATTR_ENCODING] = $options['charset'];
76
            }
77
78
            // support arbitrary \PDO::SQLSRV_ATTR_* driver options and pass them to PDO
79
            // http://php.net/manual/en/ref.pdo-sqlsrv.php#pdo-sqlsrv.constants
80 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...
81
                if (strpos($key, 'sqlsrv_attr_') === 0) {
82
                    $driverOptions[constant('\PDO::' . strtoupper($key))] = $option;
83
                }
84
            }
85
86
            try {
87
                $db = new \PDO($dsn, $options['user'], $options['pass'], $driverOptions);
88
            } catch (\PDOException $exception) {
89
                throw new \InvalidArgumentException(sprintf(
90
                    'There was a problem connecting to the database: %s',
91
                    $exception->getMessage()
92
                ));
93
            }
94
95
            $this->setConnection($db);
96
        }
97
    }
98
99
    /**
100
     * Connect to MSSQL using dblib/freetds.
101
     *
102
     * The "sqlsrv" driver is not available on Unix machines.
103
     *
104
     * @throws \InvalidArgumentException
105
     */
106
    protected function connectDblib()
107
    {
108
        if (!class_exists('PDO') || !in_array('dblib', \PDO::getAvailableDrivers(), true)) {
109
            // @codeCoverageIgnoreStart
110
            throw new \RuntimeException('You need to enable the PDO_Dblib extension for Phinx to run properly.');
111
            // @codeCoverageIgnoreEnd
112
        }
113
114
        $options = $this->getOptions();
115
116
        // if port is specified use it, otherwise use the SqlServer default
117 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...
118
            $dsn = 'dblib:host=' . $options['host'] . ';dbname=' . $options['name'];
119
        } else {
120
            $dsn = 'dblib:host=' . $options['host'] . ':' . $options['port'] . ';dbname=' . $options['name'];
121
        }
122
123
        $driverOptions = [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION];
124
125
        try {
126
            $db = new \PDO($dsn, $options['user'], $options['pass'], $driverOptions);
127
        } catch (\PDOException $exception) {
128
            throw new \InvalidArgumentException(sprintf(
129
                'There was a problem connecting to the database: %s',
130
                $exception->getMessage()
131
            ));
132
        }
133
134
        $this->setConnection($db);
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    public function disconnect()
141
    {
142
        $this->connection = null;
143
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148
    public function hasTransactions()
149
    {
150
        return true;
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156
    public function beginTransaction()
157
    {
158
        $this->execute('BEGIN TRANSACTION');
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164
    public function commitTransaction()
165
    {
166
        $this->execute('COMMIT TRANSACTION');
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function rollbackTransaction()
173
    {
174
        $this->execute('ROLLBACK TRANSACTION');
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180
    public function quoteTableName($tableName)
181
    {
182
        return str_replace('.', '].[', $this->quoteColumnName($tableName));
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188
    public function quoteColumnName($columnName)
189
    {
190
        return '[' . str_replace(']', '\]', $columnName) . ']';
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196
    public function hasTable($tableName)
197
    {
198
        $result = $this->fetchRow(sprintf('SELECT count(*) as [count] FROM information_schema.tables WHERE table_name = \'%s\';', $tableName));
199
200
        return $result['count'] > 0;
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206
    public function createTable(Table $table)
207
    {
208
        $options = $table->getOptions();
209
210
        // Add the default primary key
211
        $columns = $table->getPendingColumns();
212 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...
213
            $column = new Column();
214
            $column->setName('id')
215
                   ->setType('integer')
216
                   ->setIdentity(true);
217
218
            array_unshift($columns, $column);
219
            $options['primary_key'] = 'id';
220
        } elseif (isset($options['id']) && is_string($options['id'])) {
221
            // Handle id => "field_name" to support AUTO_INCREMENT
222
            $column = new Column();
223
            $column->setName($options['id'])
224
                   ->setType('integer')
225
                   ->setIdentity(true);
226
227
            array_unshift($columns, $column);
228
            $options['primary_key'] = $options['id'];
229
        }
230
231
        $sql = 'CREATE TABLE ';
232
        $sql .= $this->quoteTableName($table->getName()) . ' (';
233
        $sqlBuffer = [];
234
        $columnsWithComments = [];
235 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...
236
            $sqlBuffer[] = $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column);
237
238
            // set column comments, if needed
239
            if ($column->getComment()) {
240
                $columnsWithComments[] = $column;
241
            }
242
        }
243
244
        // set the primary key(s)
245
        if (isset($options['primary_key'])) {
246
            $pkSql = sprintf('CONSTRAINT PK_%s PRIMARY KEY (', $table->getName());
247
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
248
                $pkSql .= $this->quoteColumnName($options['primary_key']);
249
            } 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...
250
                // PHP 5.4 will allow access of $this, so we can call quoteColumnName() directly in the anonymous function,
251
                // but for now just hard-code the adapter quotes
252
                $pkSql .= implode(
253
                    ',',
254
                    array_map(
255
                        function ($v) {
256
                            return '[' . $v . ']';
257
                        },
258
                        $options['primary_key']
259
                    )
260
                );
261
            }
262
            $pkSql .= ')';
263
            $sqlBuffer[] = $pkSql;
264
        }
265
266
        // set the foreign keys
267
        $foreignKeys = $table->getForeignKeys();
268
        foreach ($foreignKeys as $foreignKey) {
269
            $sqlBuffer[] = $this->getForeignKeySqlDefinition($foreignKey, $table->getName());
270
        }
271
272
        $sql .= implode(', ', $sqlBuffer);
273
        $sql .= ');';
274
275
        // process column comments
276
        foreach ($columnsWithComments as $column) {
277
            $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName());
278
        }
279
280
        // set the indexes
281
        $indexes = $table->getIndexes();
282
        foreach ($indexes as $index) {
283
            $sql .= $this->getIndexSqlDefinition($index, $table->getName());
284
        }
285
286
        // execute the sql
287
        $this->execute($sql);
288
    }
289
290
    /**
291
     * Gets the SqlServer Column Comment Defininition for a column object.
292
     *
293
     * @param \Phinx\Db\Table\Column $column    Column
294
     * @param string $tableName Table name
295
     *
296
     * @return string
297
     */
298
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
299
    {
300
        // passing 'null' is to remove column comment
301
        $currentComment = $this->getColumnComment($tableName, $column->getName());
302
303
        $comment = (strcasecmp($column->getComment(), 'NULL') !== 0) ? $this->getConnection()->quote($column->getComment()) : '\'\'';
304
        $command = $currentComment === false ? 'sp_addextendedproperty' : 'sp_updateextendedproperty';
305
306
        return sprintf(
307
            "EXECUTE %s N'MS_Description', N%s, N'SCHEMA', N'%s', N'TABLE', N'%s', N'COLUMN', N'%s';",
308
            $command,
309
            $comment,
310
            $this->schema,
311
            $tableName,
312
            $column->getName()
313
        );
314
    }
315
316
    /**
317
     * {@inheritdoc}
318
     */
319
    public function renameTable($tableName, $newTableName)
320
    {
321
        $this->execute(sprintf('EXEC sp_rename \'%s\', \'%s\'', $tableName, $newTableName));
322
    }
323
324
    /**
325
     * {@inheritdoc}
326
     */
327
    public function dropTable($tableName)
328
    {
329
        $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName)));
330
    }
331
332
    /**
333
     * {@inheritdoc}
334
     */
335
    public function truncateTable($tableName)
336
    {
337
        $sql = sprintf(
338
            'TRUNCATE TABLE %s',
339
            $this->quoteTableName($tableName)
340
        );
341
342
        $this->execute($sql);
343
    }
344
345
    public function getColumnComment($tableName, $columnName)
346
    {
347
        $sql = sprintf("SELECT cast(extended_properties.[value] as nvarchar(4000)) comment
348
  FROM sys.schemas
349
 INNER JOIN sys.tables
350
    ON schemas.schema_id = tables.schema_id
351
 INNER JOIN sys.columns
352
    ON tables.object_id = columns.object_id
353
 INNER JOIN sys.extended_properties
354
    ON tables.object_id = extended_properties.major_id
355
   AND columns.column_id = extended_properties.minor_id
356
   AND extended_properties.name = 'MS_Description'
357
   WHERE schemas.[name] = '%s' AND tables.[name] = '%s' AND columns.[name] = '%s'", $this->schema, $tableName, $columnName);
358
        $row = $this->fetchRow($sql);
359
360
        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...
361
            return $row['comment'];
362
        }
363
364
        return false;
365
    }
366
367
    /**
368
     * {@inheritdoc}
369
     */
370
    public function getColumns($tableName)
371
    {
372
        $columns = [];
373
        $sql = sprintf(
374
            "SELECT DISTINCT TABLE_SCHEMA AS [schema], TABLE_NAME as [table_name], COLUMN_NAME AS [name], DATA_TYPE AS [type],
375
            IS_NULLABLE AS [null], COLUMN_DEFAULT AS [default],
376
            CHARACTER_MAXIMUM_LENGTH AS [char_length],
377
            NUMERIC_PRECISION AS [precision],
378
            NUMERIC_SCALE AS [scale], ORDINAL_POSITION AS [ordinal_position],
379
            COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as [identity]
380
        FROM INFORMATION_SCHEMA.COLUMNS
381
        WHERE TABLE_NAME = '%s'
382
        ORDER BY ordinal_position",
383
            $tableName
384
        );
385
        $rows = $this->fetchAll($sql);
386
        foreach ($rows as $columnInfo) {
387
            $column = new Column();
388
            $column->setName($columnInfo['name'])
389
                   ->setType($this->getPhinxType($columnInfo['type']))
390
                   ->setNull($columnInfo['null'] !== 'NO')
391
                   ->setDefault($this->parseDefault($columnInfo['default']))
392
                   ->setIdentity($columnInfo['identity'] === '1')
393
                   ->setComment($this->getColumnComment($columnInfo['table_name'], $columnInfo['name']));
394
395
            if (!empty($columnInfo['char_length'])) {
396
                $column->setLimit($columnInfo['char_length']);
397
            }
398
399
            $columns[$columnInfo['name']] = $column;
400
        }
401
402
        return $columns;
403
    }
404
405
    protected function parseDefault($default)
406
    {
407
        $default = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default);
408
409
        if (strtoupper($default) === 'NULL') {
410
            $default = null;
411
        } elseif (is_numeric($default)) {
412
            $default = (int)$default;
413
        }
414
415
        return $default;
416
    }
417
418
    /**
419
     * {@inheritdoc}
420
     */
421 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...
422
    {
423
        $sql = sprintf(
424
            "SELECT count(*) as [count]
425
             FROM information_schema.columns
426
             WHERE table_name = '%s' AND column_name = '%s'",
427
            $tableName,
428
            $columnName
429
        );
430
        $result = $this->fetchRow($sql);
431
432
        return $result['count'] > 0;
433
    }
434
435
    /**
436
     * {@inheritdoc}
437
     */
438 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...
439
    {
440
        $sql = sprintf(
441
            'ALTER TABLE %s ADD %s %s',
442
            $this->quoteTableName($table->getName()),
443
            $this->quoteColumnName($column->getName()),
444
            $this->getColumnSqlDefinition($column)
445
        );
446
447
        $this->execute($sql);
448
    }
449
450
    /**
451
     * {@inheritdoc}
452
     */
453
    public function renameColumn($tableName, $columnName, $newColumnName)
454
    {
455
        if (!$this->hasColumn($tableName, $columnName)) {
456
            throw new \InvalidArgumentException("The specified column does not exist: $columnName");
457
        }
458
        $this->renameDefault($tableName, $columnName, $newColumnName);
459
        $this->execute(
460
            sprintf(
461
                "EXECUTE sp_rename N'%s.%s', N'%s', 'COLUMN' ",
462
                $tableName,
463
                $columnName,
464
                $newColumnName
465
            )
466
        );
467
    }
468
469
    protected function renameDefault($tableName, $columnName, $newColumnName)
470
    {
471
        $oldConstraintName = "DF_{$tableName}_{$columnName}";
472
        $newConstraintName = "DF_{$tableName}_{$newColumnName}";
473
        $sql = <<<SQL
474
IF (OBJECT_ID('$oldConstraintName', 'D') IS NOT NULL)
475
BEGIN
476
     EXECUTE sp_rename N'%s', N'%s', N'OBJECT'
477
END
478
SQL;
479
        $this->execute(sprintf(
480
            $sql,
481
            $oldConstraintName,
482
            $newConstraintName
483
        ));
484
    }
485
486
    public function changeDefault($tableName, Column $newColumn)
487
    {
488
        $constraintName = "DF_{$tableName}_{$newColumn->getName()}";
489
        $default = $newColumn->getDefault();
490
491
        if ($default === null) {
492
            $default = 'DEFAULT NULL';
493
        } else {
494
            $default = $this->getDefaultValueDefinition($default);
495
        }
496
497
        if (empty($default)) {
498
            return;
499
        }
500
501
        $this->execute(sprintf(
502
            'ALTER TABLE %s ADD CONSTRAINT %s %s FOR %s',
503
            $this->quoteTableName($tableName),
504
            $constraintName,
505
            $default,
506
            $this->quoteColumnName($newColumn->getName())
507
        ));
508
    }
509
510
    /**
511
     * {@inheritdoc}
512
     */
513
    public function changeColumn($tableName, $columnName, Column $newColumn)
514
    {
515
        $columns = $this->getColumns($tableName);
516
        $changeDefault = $newColumn->getDefault() !== $columns[$columnName]->getDefault() || $newColumn->getType() !== $columns[$columnName]->getType();
517
        if ($columnName !== $newColumn->getName()) {
518
            $this->renameColumn($tableName, $columnName, $newColumn->getName());
519
        }
520
521
        if ($changeDefault) {
522
            $this->dropDefaultConstraint($tableName, $newColumn->getName());
523
        }
524
525
        $this->execute(
526
            sprintf(
527
                'ALTER TABLE %s ALTER COLUMN %s %s',
528
                $this->quoteTableName($tableName),
529
                $this->quoteColumnName($newColumn->getName()),
530
                $this->getColumnSqlDefinition($newColumn, false)
531
            )
532
        );
533
        // change column comment if needed
534
        if ($newColumn->getComment()) {
535
            $sql = $this->getColumnCommentSqlDefinition($newColumn, $tableName);
536
            $this->execute($sql);
537
        }
538
539
        if ($changeDefault) {
540
            $this->changeDefault($tableName, $newColumn);
541
        }
542
    }
543
544
    /**
545
     * {@inheritdoc}
546
     */
547
    public function dropColumn($tableName, $columnName)
548
    {
549
        $this->dropDefaultConstraint($tableName, $columnName);
550
551
        $this->execute(
552
            sprintf(
553
                'ALTER TABLE %s DROP COLUMN %s',
554
                $this->quoteTableName($tableName),
555
                $this->quoteColumnName($columnName)
556
            )
557
        );
558
    }
559
560
    protected function dropDefaultConstraint($tableName, $columnName)
561
    {
562
        $defaultConstraint = $this->getDefaultConstraint($tableName, $columnName);
563
564
        if (!$defaultConstraint) {
565
            return;
566
        }
567
568
        $this->dropForeignKey($tableName, $columnName, $defaultConstraint);
569
    }
570
571
    protected function getDefaultConstraint($tableName, $columnName)
572
    {
573
        $sql = "SELECT
574
    default_constraints.name
575
FROM
576
    sys.all_columns
577
578
        INNER JOIN
579
    sys.tables
580
        ON all_columns.object_id = tables.object_id
581
582
        INNER JOIN
583
    sys.schemas
584
        ON tables.schema_id = schemas.schema_id
585
586
        INNER JOIN
587
    sys.default_constraints
588
        ON all_columns.default_object_id = default_constraints.object_id
589
590
WHERE
591
        schemas.name = 'dbo'
592
    AND tables.name = '{$tableName}'
593
    AND all_columns.name = '{$columnName}'";
594
595
        $rows = $this->fetchAll($sql);
596
597
        return empty($rows) ? false : $rows[0]['name'];
598
    }
599
600
    protected function getIndexColums($tableId, $indexId)
601
    {
602
        $sql = "SELECT AC.[name] AS [column_name]
603
FROM sys.[index_columns] IC
604
  INNER JOIN sys.[all_columns] AC ON IC.[column_id] = AC.[column_id]
605
WHERE AC.[object_id] = {$tableId} AND IC.[index_id] = {$indexId}  AND IC.[object_id] = {$tableId}
606
ORDER BY IC.[key_ordinal];";
607
608
        $rows = $this->fetchAll($sql);
609
        $columns = [];
610
        foreach ($rows as $row) {
611
            $columns[] = strtolower($row['column_name']);
612
        }
613
614
        return $columns;
615
    }
616
617
    /**
618
     * Get an array of indexes from a particular table.
619
     *
620
     * @param string $tableName Table Name
621
     * @return array
622
     */
623
    public function getIndexes($tableName)
624
    {
625
        $indexes = [];
626
        $sql = "SELECT I.[name] AS [index_name], I.[index_id] as [index_id], T.[object_id] as [table_id]
627
FROM sys.[tables] AS T
628
  INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
629
WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP'  AND T.[name] = '{$tableName}'
630
ORDER BY T.[name], I.[index_id];";
631
632
        $rows = $this->fetchAll($sql);
633
        foreach ($rows as $row) {
634
            $columns = $this->getIndexColums($row['table_id'], $row['index_id']);
635
            $indexes[$row['index_name']] = ['columns' => $columns];
636
        }
637
638
        return $indexes;
639
    }
640
641
    /**
642
     * {@inheritdoc}
643
     */
644 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...
645
    {
646
        if (is_string($columns)) {
647
            $columns = [$columns]; // str to array
648
        }
649
650
        $columns = array_map('strtolower', $columns);
651
        $indexes = $this->getIndexes($tableName);
652
653
        foreach ($indexes as $index) {
654
            $a = array_diff($columns, $index['columns']);
655
656
            if (empty($a)) {
657
                return true;
658
            }
659
        }
660
661
        return false;
662
    }
663
664
    /**
665
     * {@inheritdoc}
666
     */
667 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...
668
    {
669
        $indexes = $this->getIndexes($tableName);
670
671
        foreach ($indexes as $name => $index) {
672
            if ($name === $indexName) {
673
                 return true;
674
            }
675
        }
676
677
        return false;
678
    }
679
680
    /**
681
     * {@inheritdoc}
682
     */
683
    public function addIndex(Table $table, Index $index)
684
    {
685
        $sql = $this->getIndexSqlDefinition($index, $table->getName());
686
        $this->execute($sql);
687
    }
688
689
    /**
690
     * {@inheritdoc}
691
     */
692 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...
693
    {
694
        if (is_string($columns)) {
695
            $columns = [$columns]; // str to array
696
        }
697
698
        $indexes = $this->getIndexes($tableName);
699
        $columns = array_map('strtolower', $columns);
700
701
        foreach ($indexes as $indexName => $index) {
702
            $a = array_diff($columns, $index['columns']);
703
            if (empty($a)) {
704
                $this->execute(
705
                    sprintf(
706
                        'DROP INDEX %s ON %s',
707
                        $this->quoteColumnName($indexName),
708
                        $this->quoteTableName($tableName)
709
                    )
710
                );
711
712
                return;
713
            }
714
        }
715
    }
716
717
    /**
718
     * {@inheritdoc}
719
     */
720 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...
721
    {
722
        $indexes = $this->getIndexes($tableName);
723
724
        foreach ($indexes as $name => $index) {
725
            if ($name === $indexName) {
726
                $this->execute(
727
                    sprintf(
728
                        'DROP INDEX %s ON %s',
729
                        $this->quoteColumnName($indexName),
730
                        $this->quoteTableName($tableName)
731
                    )
732
                );
733
734
                return;
735
            }
736
        }
737
    }
738
739
    /**
740
     * {@inheritdoc}
741
     */
742 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...
743
    {
744
        if (is_string($columns)) {
745
            $columns = [$columns]; // str to array
746
        }
747
        $foreignKeys = $this->getForeignKeys($tableName);
748
        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...
749
            if (isset($foreignKeys[$constraint])) {
750
                return !empty($foreignKeys[$constraint]);
751
            }
752
753
            return false;
754
        } else {
755
            foreach ($foreignKeys as $key) {
756
                $a = array_diff($columns, $key['columns']);
757
                if (empty($a)) {
758
                    return true;
759
                }
760
            }
761
762
            return false;
763
        }
764
    }
765
766
    /**
767
     * Get an array of foreign keys from a particular table.
768
     *
769
     * @param string $tableName Table Name
770
     * @return array
771
     */
772 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...
773
    {
774
        $foreignKeys = [];
775
        $rows = $this->fetchAll(sprintf(
776
            "SELECT
777
                    tc.constraint_name,
778
                    tc.table_name, kcu.column_name,
779
                    ccu.table_name AS referenced_table_name,
780
                    ccu.column_name AS referenced_column_name
781
                FROM
782
                    information_schema.table_constraints AS tc
783
                    JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
784
                    JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
785
                WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s'
786
                ORDER BY kcu.ordinal_position",
787
            $tableName
788
        ));
789
        foreach ($rows as $row) {
790
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
791
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
792
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
793
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
794
        }
795
796
        return $foreignKeys;
797
    }
798
799
    /**
800
     * {@inheritdoc}
801
     */
802 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...
803
    {
804
        $this->execute(
805
            sprintf(
806
                'ALTER TABLE %s ADD %s',
807
                $this->quoteTableName($table->getName()),
808
                $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
809
            )
810
        );
811
    }
812
813
    /**
814
     * {@inheritdoc}
815
     */
816 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...
817
    {
818
        if (is_string($columns)) {
819
            $columns = [$columns]; // str to array
820
        }
821
822
        if ($constraint) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $constraint of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
823
            $this->execute(
824
                sprintf(
825
                    'ALTER TABLE %s DROP CONSTRAINT %s',
826
                    $this->quoteTableName($tableName),
827
                    $constraint
828
                )
829
            );
830
831
            return;
832
        } else {
833
            foreach ($columns as $column) {
834
                $rows = $this->fetchAll(sprintf(
835
                    "SELECT
836
                    tc.constraint_name,
837
                    tc.table_name, kcu.column_name,
838
                    ccu.table_name AS referenced_table_name,
839
                    ccu.column_name AS referenced_column_name
840
                FROM
841
                    information_schema.table_constraints AS tc
842
                    JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
843
                    JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
844
                WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s' and ccu.column_name='%s'
845
                ORDER BY kcu.ordinal_position",
846
                    $tableName,
847
                    $column
848
                ));
849
                foreach ($rows as $row) {
850
                    $this->dropForeignKey($tableName, $columns, $row['constraint_name']);
851
                }
852
            }
853
        }
854
    }
855
856
    /**
857
     * {@inheritdoc}
858
     */
859
    public function getSqlType($type, $limit = null)
860
    {
861
        switch ($type) {
862
            case static::PHINX_TYPE_STRING:
863
                return ['name' => 'nvarchar', 'limit' => 255];
864
            case static::PHINX_TYPE_CHAR:
865
                return ['name' => 'nchar', 'limit' => 255];
866
            case static::PHINX_TYPE_TEXT:
867
                return ['name' => 'ntext'];
868
            case static::PHINX_TYPE_INTEGER:
869
                return ['name' => 'int'];
870
            case static::PHINX_TYPE_BIG_INTEGER:
871
                return ['name' => 'bigint'];
872
            case static::PHINX_TYPE_FLOAT:
873
                return ['name' => 'float'];
874
            case static::PHINX_TYPE_DECIMAL:
875
                return ['name' => 'decimal'];
876
            case static::PHINX_TYPE_DATETIME:
877
            case static::PHINX_TYPE_TIMESTAMP:
878
                return ['name' => 'datetime'];
879
            case static::PHINX_TYPE_TIME:
880
                return ['name' => 'time'];
881
            case static::PHINX_TYPE_DATE:
882
                return ['name' => 'date'];
883
            case static::PHINX_TYPE_BLOB:
884
            case static::PHINX_TYPE_BINARY:
885
                return ['name' => 'varbinary'];
886
            case static::PHINX_TYPE_BOOLEAN:
887
                return ['name' => 'bit'];
888
            case static::PHINX_TYPE_UUID:
889
                return ['name' => 'uniqueidentifier'];
890
            case static::PHINX_TYPE_FILESTREAM:
891
                return ['name' => 'varbinary', 'limit' => 'max'];
892
            // Geospatial database types
893
            case static::PHINX_TYPE_GEOMETRY:
894
            case static::PHINX_TYPE_POINT:
895
            case static::PHINX_TYPE_LINESTRING:
896
            case static::PHINX_TYPE_POLYGON:
897
                // SQL Server stores all spatial data using a single data type.
898
                // Specific types (point, polygon, etc) are set at insert time.
899
                return ['name' => 'geography'];
900
            default:
901
                throw new \RuntimeException('The type: "' . $type . '" is not supported.');
902
        }
903
    }
904
905
    /**
906
     * Returns Phinx type by SQL type
907
     *
908
     * @param string $sqlType SQL Type definition
909
     * @throws \RuntimeException
910
     * @internal param string $sqlType SQL type
911
     * @returns string Phinx type
912
     */
913
    public function getPhinxType($sqlType)
914
    {
915
        switch ($sqlType) {
916
            case 'nvarchar':
917
            case 'varchar':
918
                return static::PHINX_TYPE_STRING;
919
            case 'char':
920
            case 'nchar':
921
                return static::PHINX_TYPE_CHAR;
922
            case 'text':
923
            case 'ntext':
924
                return static::PHINX_TYPE_TEXT;
925
            case 'int':
926
            case 'integer':
927
                return static::PHINX_TYPE_INTEGER;
928
            case 'decimal':
929
            case 'numeric':
930
            case 'money':
931
                return static::PHINX_TYPE_DECIMAL;
932
            case 'bigint':
933
                return static::PHINX_TYPE_BIG_INTEGER;
934
            case 'real':
935
            case 'float':
936
                return static::PHINX_TYPE_FLOAT;
937
            case 'binary':
938
            case 'image':
939
            case 'varbinary':
940
                return static::PHINX_TYPE_BINARY;
941
            case 'time':
942
                return static::PHINX_TYPE_TIME;
943
            case 'date':
944
                return static::PHINX_TYPE_DATE;
945
            case 'datetime':
946
            case 'timestamp':
947
                return static::PHINX_TYPE_DATETIME;
948
            case 'bit':
949
                return static::PHINX_TYPE_BOOLEAN;
950
            case 'uniqueidentifier':
951
                return static::PHINX_TYPE_UUID;
952
            case 'filestream':
953
                return static::PHINX_TYPE_FILESTREAM;
954
            default:
955
                throw new \RuntimeException('The SqlServer type: "' . $sqlType . '" is not supported');
956
        }
957
    }
958
959
    /**
960
     * {@inheritdoc}
961
     */
962
    public function createDatabase($name, $options = [])
963
    {
964 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...
965
            $this->execute(sprintf('CREATE DATABASE [%s] COLLATE [%s]', $name, $options['collation']));
966
        } else {
967
            $this->execute(sprintf('CREATE DATABASE [%s]', $name));
968
        }
969
        $this->execute(sprintf('USE [%s]', $name));
970
    }
971
972
    /**
973
     * {@inheritdoc}
974
     */
975
    public function hasDatabase($name)
976
    {
977
        $result = $this->fetchRow(
978
            sprintf(
979
                'SELECT count(*) as [count] FROM master.dbo.sysdatabases WHERE [name] = \'%s\'',
980
                $name
981
            )
982
        );
983
984
        return $result['count'] > 0;
985
    }
986
987
    /**
988
     * {@inheritdoc}
989
     */
990
    public function dropDatabase($name)
991
    {
992
        $sql = <<<SQL
993
USE master;
994
IF EXISTS(select * from sys.databases where name=N'$name')
995
ALTER DATABASE [$name] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
996
DROP DATABASE [$name];
997
SQL;
998
        $this->execute($sql);
999
    }
1000
1001
    /**
1002
     * Get the defintion for a `DEFAULT` statement.
1003
     *
1004
     * @param  mixed $default
1005
     * @return string
1006
     */
1007 View Code Duplication
    protected function getDefaultValueDefinition($default)
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...
1008
    {
1009
        if (is_string($default) && 'CURRENT_TIMESTAMP' !== $default) {
1010
            $default = $this->getConnection()->quote($default);
1011
        } elseif (is_bool($default)) {
1012
            $default = $this->castToBool($default);
1013
        }
1014
1015
        return isset($default) ? ' DEFAULT ' . $default : '';
1016
    }
1017
1018
    /**
1019
     * Gets the SqlServer Column Definition for a Column object.
1020
     *
1021
     * @param \Phinx\Db\Table\Column $column Column
1022
     * @return string
1023
     */
1024
    protected function getColumnSqlDefinition(Column $column, $create = true)
1025
    {
1026
        $buffer = [];
1027
1028
        $sqlType = $this->getSqlType($column->getType());
1029
        $buffer[] = strtoupper($sqlType['name']);
1030
        // integers cant have limits in SQlServer
1031
        $noLimits = [
1032
            'bigint',
1033
            'int',
1034
            'tinyint'
1035
        ];
1036
        if (!in_array($sqlType['name'], $noLimits) && ($column->getLimit() || isset($sqlType['limit']))) {
1037
            $buffer[] = sprintf('(%s)', $column->getLimit() ? $column->getLimit() : $sqlType['limit']);
1038
        }
1039 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...
1040
            $buffer[] = '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1041
        }
1042
1043
        $properties = $column->getProperties();
1044
        $buffer[] = $column->getType() === 'filestream' ? 'FILESTREAM' : '';
1045
        $buffer[] = isset($properties['rowguidcol']) ? 'ROWGUIDCOL' : '';
1046
1047
        $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1048
1049
        if ($create === true) {
1050
            if ($column->getDefault() === null && $column->isNull()) {
1051
                $buffer[] = ' DEFAULT NULL';
1052
            } else {
1053
                $buffer[] = $this->getDefaultValueDefinition($column->getDefault());
1054
            }
1055
        }
1056
1057
        if ($column->isIdentity()) {
1058
            $buffer[] = 'IDENTITY(1, 1)';
1059
        }
1060
1061
        return implode(' ', $buffer);
1062
    }
1063
1064
    /**
1065
     * Gets the SqlServer Index Definition for an Index object.
1066
     *
1067
     * @param \Phinx\Db\Table\Index $index Index
1068
     * @return string
1069
     */
1070
    protected function getIndexSqlDefinition(Index $index, $tableName)
1071
    {
1072 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...
1073
            $indexName = $index->getName();
1074
        } else {
1075
            $columnNames = $index->getColumns();
1076
            if (is_string($columnNames)) {
1077
                $columnNames = [$columnNames];
1078
            }
1079
            $indexName = sprintf('%s_%s', $tableName, implode('_', $columnNames));
1080
        }
1081
        $def = sprintf(
1082
            "CREATE %s INDEX %s ON %s (%s);",
1083
            ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1084
            $indexName,
1085
            $this->quoteTableName($tableName),
1086
            '[' . implode('],[', $index->getColumns()) . ']'
1087
        );
1088
1089
        return $def;
1090
    }
1091
1092
    /**
1093
     * Gets the SqlServer Foreign Key Definition for an ForeignKey object.
1094
     *
1095
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1096
     * @return string
1097
     */
1098 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...
1099
    {
1100
        $constraintName = $foreignKey->getConstraint()
1101
            ? $foreignKey->getConstraint()
1102
            : $tableName . '_' . implode('_', $foreignKey->getColumns());
1103
        $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
0 ignored issues
show
Bug introduced by
It seems like $constraintName defined by $foreignKey->getConstrai...reignKey->getColumns()) on line 1100 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...
1104
        $def .= ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")';
1105
        $def .= " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")';
1106
        if ($foreignKey->getOnDelete()) {
1107
            $def .= " ON DELETE {$foreignKey->getOnDelete()}";
1108
        }
1109
        if ($foreignKey->getOnUpdate()) {
1110
            $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1111
        }
1112
1113
        return $def;
1114
    }
1115
1116
    /**
1117
     * {@inheritdoc}
1118
     */
1119
    public function getColumnTypes()
1120
    {
1121
        return array_merge(parent::getColumnTypes(), ['filestream']);
1122
    }
1123
1124
    /**
1125
     * Records a migration being run.
1126
     *
1127
     * @param \Phinx\Migration\MigrationInterface $migration Migration
1128
     * @param string $direction Direction
1129
     * @param int $startTime Start Time
1130
     * @param int $endTime End Time
1131
     * @return \Phinx\Db\Adapter\AdapterInterface
1132
     */
1133
    public function migrated(\Phinx\Migration\MigrationInterface $migration, $direction, $startTime, $endTime)
1134
    {
1135
        $startTime = str_replace(' ', 'T', $startTime);
1136
        $endTime = str_replace(' ', 'T', $endTime);
1137
1138
        return parent::migrated($migration, $direction, $startTime, $endTime);
1139
    }
1140
}
1141