Passed
Pull Request — master (#50)
by Wilmer
12:16
created

Schema::findConstraints()   B

Complexity

Conditions 9
Paths 43

Size

Total Lines 65
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 21.4523

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 46
c 1
b 0
f 0
nc 43
nop 1
dl 0
loc 65
ccs 13
cts 28
cp 0.4643
crap 21.4523
rs 7.6226

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mysql;
6
7
use PDO;
8
use PDOException;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Db\Connection\ConnectionInterface;
11
use Yiisoft\Db\Constraint\Constraint;
12
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
13
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
14
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
15
use Yiisoft\Db\Constraint\IndexConstraint;
16
use Yiisoft\Db\Exception\Exception;
17
use Yiisoft\Db\Exception\InvalidArgumentException;
18
use Yiisoft\Db\Exception\InvalidConfigException;
19
use Yiisoft\Db\Exception\NotSupportedException;
20
use Yiisoft\Db\Expression\Expression;
21
use Yiisoft\Db\Schema\Schema as AbstractSchema;
22
23
use function array_change_key_case;
24
use function array_map;
25
use function array_merge;
26
use function array_values;
27
use function bindec;
28
use function explode;
29
use function preg_match;
30
use function preg_match_all;
31
use function str_replace;
32
use function stripos;
33
use function strpos;
34
use function strtolower;
35
use function trim;
36
use function version_compare;
37
38
final class Schema extends AbstractSchema implements ConstraintFinderInterface
39
{
40
    use ConstraintFinderTrait;
41
42
    private bool $oldMysql;
43
    private array $typeMap = [
44
        'tinyint' => self::TYPE_TINYINT,
45
        'bit' => self::TYPE_INTEGER,
46
        'smallint' => self::TYPE_SMALLINT,
47
        'mediumint' => self::TYPE_INTEGER,
48
        'int' => self::TYPE_INTEGER,
49
        'integer' => self::TYPE_INTEGER,
50
        'bigint' => self::TYPE_BIGINT,
51
        'float' => self::TYPE_FLOAT,
52
        'double' => self::TYPE_DOUBLE,
53
        'real' => self::TYPE_FLOAT,
54
        'decimal' => self::TYPE_DECIMAL,
55
        'numeric' => self::TYPE_DECIMAL,
56
        'tinytext' => self::TYPE_TEXT,
57
        'mediumtext' => self::TYPE_TEXT,
58
        'longtext' => self::TYPE_TEXT,
59
        'longblob' => self::TYPE_BINARY,
60
        'blob' => self::TYPE_BINARY,
61
        'text' => self::TYPE_TEXT,
62
        'varchar' => self::TYPE_STRING,
63
        'string' => self::TYPE_STRING,
64
        'char' => self::TYPE_CHAR,
65
        'datetime' => self::TYPE_DATETIME,
66
        'year' => self::TYPE_DATE,
67
        'date' => self::TYPE_DATE,
68
        'time' => self::TYPE_TIME,
69
        'timestamp' => self::TYPE_TIMESTAMP,
70
        'enum' => self::TYPE_STRING,
71
        'varbinary' => self::TYPE_BINARY,
72
        'json' => self::TYPE_JSON,
73
    ];
74
75
    /**
76
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
77
     * case starting and ending characters are different.
78
     */
79
    protected $tableQuoteCharacter = '`';
80
81
    /**
82
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
83
     * and ending characters are different.
84
     */
85
    protected $columnQuoteCharacter = '`';
86
87
    /** @var Connection $db */
88
    private ConnectionInterface $db;
89
90
    public function __construct(ConnectionInterface $db)
91
    {
92
        $this->db = $db;
0 ignored issues
show
Documentation Bug introduced by
$db is of type Yiisoft\Db\Connection\ConnectionInterface, but the property $db was declared to be of type Yiisoft\Db\Mysql\Connection. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
93
94
        parent::__construct($db);
95 58
    }
96
97 58
    /**
98
     * Resolves the table name and schema name (if any).
99 58
     *
100
     * @param string $name the table name.
101 58
     *
102
     * @return TableSchema
103
     *
104
     * {@see TableSchema}
105 58
     */
106 58
    protected function resolveTableName(string $name): TableSchema
107
    {
108
        $resolvedName = new TableSchema();
109 58
110 58
        $parts = explode('.', str_replace('`', '', $name));
111
112 58
        if (isset($parts[1])) {
113
            $resolvedName->schemaName($parts[0]);
114
            $resolvedName->name($parts[1]);
115
        } else {
116
            $resolvedName->schemaName($this->defaultSchema);
117
            $resolvedName->name($name);
118
        }
119
120
        $resolvedName->fullName(($resolvedName->getSchemaName() !== $this->defaultSchema ?
121
            $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName());
122
123
        return $resolvedName;
124
    }
125
126
    /**
127
     * Returns all table names in the database.
128
     *
129 3
     * This method should be overridden by child classes in order to support this feature because the default
130
     * implementation simply throws an exception.
131 3
     *
132
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
133 3
     *
134
     * @throws Exception
135
     * @throws InvalidConfigException
136
     * @throws InvalidArgumentException
137 3
     *
138
     * @return array all table names in the database. The names have NO schema name prefix.
139
     */
140
    protected function findTableNames(string $schema = ''): array
141
    {
142
        $sql = 'SHOW TABLES';
143
144
        if ($schema !== '') {
145
            $sql .= ' FROM ' . $this->quoteSimpleTableName($schema);
146
        }
147
148
        return $this->db->createCommand($sql)->queryColumn();
149 82
    }
150
151 82
    /**
152
     * Loads the metadata for the specified table.
153 82
     *
154
     * @param string $name table name.
155 82
     *
156 77
     * @throws Exception
157
     *
158 77
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
159
     */
160
    protected function loadTableSchema(string $name): ?TableSchema
161 13
    {
162
        $table = new TableSchema();
163
164
        $this->resolveTableNames($table, $name);
165
166
        if ($this->findColumns($table)) {
167
            $this->findConstraints($table);
168
169
            return $table;
170
        }
171
172
        return null;
173
    }
174
175 31
    /**
176
     * Loads a primary key for the given table.
177 31
     *
178
     * @param string $tableName table name.
179
     *
180
     * @throws Exception
181
     * @throws InvalidArgumentException
182
     * @throws InvalidConfigException
183
     *
184
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
185
     */
186
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
187
    {
188
        return $this->loadTableConstraints($tableName, 'primaryKey');
189
    }
190
191 4
    /**
192
     * Loads all foreign keys for the given table.
193 4
     *
194
     * @param string $tableName table name.
195
     *
196
     * @throws Exception
197
     * @throws InvalidArgumentException
198
     * @throws InvalidConfigException
199
     *
200
     * @return ForeignKeyConstraint[] foreign keys for the given table.
201
     */
202
    protected function loadTableForeignKeys(string $tableName): array
203
    {
204
        return $this->loadTableConstraints($tableName, 'foreignKeys');
205
    }
206
207 28
    /**
208
     * Loads all indexes for the given table.
209 28
     *
210
     * @param string $tableName table name.
211
     *
212
     * @throws Exception
213
     * @throws InvalidArgumentException
214
     * @throws InvalidConfigException
215
     *
216
     * @return IndexConstraint[] indexes for the given table.
217
     */
218
    protected function loadTableIndexes(string $tableName): array
219
    {
220 28
        static $sql = <<<'SQL'
221
SELECT
222 28
    `s`.`INDEX_NAME` AS `name`,
223 28
    `s`.`COLUMN_NAME` AS `column_name`,
224 28
    `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
225 28
    `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
226
FROM `information_schema`.`STATISTICS` AS `s`
227 28
WHERE `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND `s`.`TABLE_NAME` = :tableName
228 28
ORDER BY `s`.`SEQ_IN_INDEX` ASC
229 28
SQL;
230
231 28
        $resolvedName = $this->resolveTableName($tableName);
232 28
233
        $indexes = $this->db->createCommand($sql, [
234 28
            ':schemaName' => $resolvedName->getSchemaName(),
235 28
            ':tableName' => $resolvedName->getName(),
236 28
        ])->queryAll();
237 28
238
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
239 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
240
        $result = [];
241
242 28
        foreach ($indexes as $name => $index) {
243
            $ic = new IndexConstraint();
244
245
            $ic->primary((bool) $index[0]['index_is_primary']);
246
            $ic->unique((bool) $index[0]['index_is_unique']);
247
            $ic->name($name !== 'PRIMARY' ? $name : null);
248
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
249
250
            $result[] = $ic;
251
        }
252
253
        return $result;
254
    }
255
256 13
    /**
257
     * Loads all unique constraints for the given table.
258 13
     *
259
     * @param string $tableName table name.
260
     *
261
     * @throws Exception
262
     * @throws InvalidArgumentException
263
     * @throws InvalidConfigException
264
     *
265
     * @return Constraint[] unique constraints for the given table.
266
     */
267
    protected function loadTableUniques(string $tableName): array
268
    {
269
        return $this->loadTableConstraints($tableName, 'uniques');
270 12
    }
271
272 12
    /**
273
     * Loads all check constraints for the given table.
274
     *
275
     * @param string $tableName table name.
276
     *
277
     * @throws NotSupportedException
278
     *
279
     * @return array check constraints for the given table.
280
     */
281
    protected function loadTableChecks(string $tableName): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

281
    protected function loadTableChecks(/** @scrutinizer ignore-unused */ string $tableName): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
282
    {
283
        throw new NotSupportedException('MySQL does not support check constraints.');
284 12
    }
285
286 12
    /**
287
     * Loads all default value constraints for the given table.
288
     *
289
     * @param string $tableName table name.
290
     *
291
     * @throws NotSupportedException
292
     *
293
     * @return array default value constraints for the given table.
294 53
     */
295
    protected function loadTableDefaultValues(string $tableName): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

295
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ string $tableName): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
296 53
    {
297
        throw new NotSupportedException('MySQL does not support default value constraints.');
298
    }
299
300
    /**
301
     * Creates a query builder for the MySQL database.
302
     *
303
     * @return QueryBuilder query builder instance
304
     */
305 82
    public function createQueryBuilder(): QueryBuilder
306
    {
307 82
        return new QueryBuilder($this->db);
308
    }
309 82
310
    /**
311
     * Resolves the table name and schema name (if any).
312
     *
313
     * @param TableSchema $table the table metadata object.
314 82
     * @param string $name the table name.
315 82
     */
316
    protected function resolveTableNames($table, $name): void
317 82
    {
318
        $parts = explode('.', str_replace('`', '', $name));
319
320
        if (isset($parts[1])) {
321
            $table->schemaName($parts[0]);
322
            $table->name($parts[1]);
323
            $table->fullName($table->getSchemaName() . '.' . $table->getName());
324
        } else {
325
            $table->name($parts[0]);
326 78
            $table->fullName($parts[0]);
327
        }
328 78
    }
329
330 78
    /**
331 78
     * Loads the column information into a {@see ColumnSchema} object.
332 78
     *
333 78
     * @param array $info column information.
334 78
     *
335 78
     * @return ColumnSchema the column schema object.
336 78
     */
337 78
    protected function loadColumnSchema(array $info): ColumnSchema
338
    {
339 78
        $column = $this->createColumnSchema();
340 78
341
        $column->name($info['field']);
342 78
        $column->allowNull($info['null'] === 'YES');
343 78
        $column->primaryKey(strpos($info['key'], 'PRI') !== false);
344
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
345
        $column->comment($info['comment']);
346 78
        $column->dbType($info['type']);
347 77
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
348 20
        $column->type(self::TYPE_STRING);
349
350 20
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
351 20
            $type = strtolower($matches[1]);
352
353
            if (isset($this->typeMap[$type])) {
354 20
                $column->type($this->typeMap[$type]);
355
            }
356 77
357 77
            if (!empty($matches[2])) {
358 77
                if ($type === 'enum') {
359
                    preg_match_all("/'[^']*'/", $matches[2], $values);
360 77
361 24
                    foreach ($values[0] as $i => $value) {
362
                        $values[$i] = trim($value, "'");
363
                    }
364 77
365 21
                    $column->enumValues($values);
366 77
                } else {
367 20
                    $values = explode(',', $matches[2]);
368
                    $column->precision((int) $values[0]);
369 20
                    $column->size((int) $values[0]);
370
371
                    if (isset($values[1])) {
372
                        $column->scale((int) $values[1]);
373
                    }
374
375
                    if ($column->getSize() === 1 && $type === 'tinyint') {
376
                        $column->type('boolean');
377 78
                    } elseif ($type === 'bit') {
378
                        if ($column->getSize() > 32) {
379 78
                            $column->type('bigint');
380
                        } elseif ($column->getSize() === 32) {
381
                            $column->type('integer');
382
                        }
383
                    }
384
                }
385
            }
386
        }
387 75
388 75
        $column->phpType($this->getColumnPhpType($column));
389
390 24
        if (!$column->isPrimaryKey()) {
391 24
            /**
392 72
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
393 20
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
394
             *
395 72
             * See details here: https://mariadb.com/kb/en/library/now/#description
396
             */
397
            if (
398
                ($column->getType() === 'timestamp' || $column->getType() === 'datetime')
399 78
                && preg_match('/^current_timestamp(?:\(([0-9]*)\))?$/i', (string) $info['default'], $matches)
400
            ) {
401
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
402
                    ? '(' . $matches[1] . ')' : '')));
403
            } elseif (isset($type) && $type === 'bit') {
404
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
405
            } else {
406
                $column->defaultValue($column->phpTypecast($info['default']));
407
            }
408
        }
409
410
        return $column;
411 82
    }
412
413 82
    /**
414
     * Collects the metadata of table columns.
415
     *
416 82
     * @param TableSchema $table the table metadata.
417 13
     *
418 13
     * @throws Exception if DB query fails.
419
     *
420 13
     * @return bool whether the table exists in the database.
421
     */
422
    protected function findColumns($table): bool
423
    {
424
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName($table->getFullName());
0 ignored issues
show
Bug introduced by
It seems like $table->getFullName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteTableName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

424
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName(/** @scrutinizer ignore-type */ $table->getFullName());
Loading history...
425
426 13
        try {
427
            $columns = $this->db->createCommand($sql)->queryAll();
428
        } catch (Exception $e) {
429
            $previous = $e->getPrevious();
430
431
            if ($previous instanceof PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
432 77
                /**
433 77
                 * table does not exist.
434 76
                 *
435
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
436
                 */
437 77
                return false;
438 77
            }
439
440 77
            throw $e;
441 53
        }
442 53
443 51
        foreach ($columns as $info) {
444
            if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_LOWER) {
445
                $info = array_change_key_case($info, CASE_LOWER);
446
            }
447
448 77
            $column = $this->loadColumnSchema($info);
449
            $table->columns($column->getName(), $column);
450
451
            if ($column->isPrimaryKey()) {
452
                $table->primaryKey($column->getName());
453
                if ($column->isAutoIncrement()) {
454
                    $table->sequenceName('');
455
                }
456
            }
457
        }
458
459
        return true;
460
    }
461
462
    /**
463
     * Gets the CREATE TABLE sql string.
464
     *
465
     * @param TableSchema $table the table metadata.
466
     *
467
     * @throws Exception
468
     * @throws InvalidArgumentException
469
     * @throws InvalidConfigException
470
     *
471
     * @return string $sql the result of 'SHOW CREATE TABLE'.
472
     */
473
    protected function getCreateTableSql($table): string
474
    {
475
        $row = $this->db->createCommand(
476
            'SHOW CREATE TABLE ' . $this->quoteTableName($table->getFullName())
0 ignored issues
show
Bug introduced by
It seems like $table->getFullName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteTableName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

476
            'SHOW CREATE TABLE ' . $this->quoteTableName(/** @scrutinizer ignore-type */ $table->getFullName())
Loading history...
477
        )->queryOne();
478
479
        if (isset($row['Create Table'])) {
480
            $sql = $row['Create Table'];
481
        } else {
482
            $row = array_values($row);
0 ignored issues
show
Bug introduced by
$row of type false is incompatible with the type array expected by parameter $input of array_values(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

482
            $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
483
            $sql = $row[1];
484
        }
485 77
486
        return $sql;
487
    }
488 77
489
    /**
490
     * Collects the foreign key column details for the given table.
491
     *
492
     * @param TableSchema $table the table metadata.
493
     *
494
     * @throws Exception
495
     */
496
    protected function findConstraints($table)
497
    {
498
        $sql = <<<'SQL'
499
SELECT
500
    `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
501
    `kcu`.`COLUMN_NAME` AS `column_name`,
502
    `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
503
    `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
504
FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
505
JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
506 77
    (
507
        `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
508 77
        (`kcu`.`CONSTRAINT_CATALOG` IS NULL AND `rc`.`CONSTRAINT_CATALOG` IS NULL)
509 77
    ) AND
510
    `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
511 77
    `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME`
512
WHERE `rc`.`CONSTRAINT_SCHEMA` = database() AND `kcu`.`TABLE_SCHEMA` = database()
513 77
AND `rc`.`TABLE_NAME` = :tableName AND `kcu`.`TABLE_NAME` = :tableName1
514 24
SQL;
515 24
516
        try {
517
            $rows = $this->db->createCommand(
518 77
                $sql,
519
                [':tableName' => $table->getName(), ':tableName1' => $table->getName()]
520 77
            )->queryAll();
521 24
522 24
            $constraints = [];
523 24
524
            foreach ($rows as $row) {
525
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
526
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
527
            }
528
529
            $table->foreignKeys([]);
530
531
            foreach ($constraints as $name => $constraint) {
532
                $table->foreignKey($name, array_merge(
533
                    [$constraint['referenced_table_name']],
534
                    $constraint['columns']
535
                ));
536
            }
537
        } catch (Exception $e) {
538
            $previous = $e->getPrevious();
539
540
            if (!$previous instanceof PDOException || strpos($previous->getMessage(), 'SQLSTATE[42S02') === false) {
541
                throw $e;
542
            }
543
544
            // table does not exist, try to determine the foreign keys using the table creation sql
545
            $sql = $this->getCreateTableSql($table);
546
            $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi';
547
548
            if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
549
                foreach ($matches as $match) {
550
                    $fks = array_map('trim', explode(',', str_replace('`', '', $match[1])));
551
                    $pks = array_map('trim', explode(',', str_replace('`', '', $match[3])));
552 77
                    $constraint = [str_replace('`', '', $match[2])];
553
554
                    foreach ($fks as $k => $name) {
555
                        $constraint[$name] = $pks[$k];
556
                    }
557
558
                    $table->foreignKey(\md5(\serialize($constraint)), $constraint);
559
                }
560
                $table->foreignKeys(array_values($table->getForeignKeys()));
561
            }
562
        }
563
    }
564
565
    /**
566
     * Returns all unique indexes for the given table.
567
     *
568
     * Each array element is of the following structure:
569
     *
570
     * ```php
571
     * [
572
     *     'IndexName1' => ['col1' [, ...]],
573
     *     'IndexName2' => ['col2' [, ...]],
574
     * ]
575
     * ```
576
     *
577
     * @param TableSchema $table the table metadata.
578
     *
579
     * @throws Exception
580
     * @throws InvalidArgumentException
581
     * @throws InvalidConfigException
582
     *
583
     * @return array all unique indexes for the given table.
584
     */
585
    public function findUniqueIndexes($table): array
586
    {
587
        $sql = $this->getCreateTableSql($table);
588
589
        $uniqueIndexes = [];
590
591
        $regexp = '/UNIQUE KEY\s+\`(.+)\`\s*\((\`.+\`)+\)/mi';
592
593
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
594
            foreach ($matches as $match) {
595
                $indexName = $match[1];
596
                $indexColumns = array_map('trim', explode('`,`', trim($match[2], '`')));
597
                $uniqueIndexes[$indexName] = $indexColumns;
598
            }
599
        }
600
601
        return $uniqueIndexes;
602
    }
603 3
604
    /**
605 3
     * Create a column schema builder instance giving the type and value precision.
606
     *
607
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
608
     *
609
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
610
     * @param int|string|array $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
611
     *
612
     * @return ColumnSchemaBuilder column schema builder instance
613
     */
614
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
615
    {
616
        return new ColumnSchemaBuilder($type, $length, $this->db);
617
    }
618
619
    /**
620
     * @throws InvalidConfigException
621
     * @throws Exception
622
     *
623
     * @return bool whether the version of the MySQL being used is older than 5.1.
624
     */
625
    protected function isOldMysql(): bool
626
    {
627
        if ($this->oldMysql === null) {
628
            $version = $this->db->getSlavePdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
629
630
            $this->oldMysql = version_compare($version, '5.1', '<=');
631
        }
632
633
        return $this->oldMysql;
634
    }
635
636
    /**
637
     * Loads multiple types of constraints and returns the specified ones.
638
     *
639
     * @param string $tableName table name.
640 48
     * @param string $returnType return type:
641
     * - primaryKey
642 48
     * - foreignKeys
643
     * - uniques
644
     *
645
     * @throws Exception
646
     * @throws InvalidArgumentException
647
     * @throws InvalidConfigException
648
     *
649
     * @return mixed constraints.
650
     */
651
    private function loadTableConstraints(string $tableName, string $returnType)
652
    {
653
        static $sql = <<<'SQL'
654
SELECT
655
    `kcu`.`CONSTRAINT_NAME` AS `name`,
656
    `kcu`.`COLUMN_NAME` AS `column_name`,
657
    `tc`.`CONSTRAINT_TYPE` AS `type`,
658
    CASE
659
        WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
660
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
661
    END AS `foreign_table_schema`,
662
    `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
663
    `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
664
    `rc`.`UPDATE_RULE` AS `on_update`,
665
    `rc`.`DELETE_RULE` AS `on_delete`,
666
    `kcu`.`ORDINAL_POSITION` AS `position`
667
FROM
668
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
669
    `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`,
670
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
671
WHERE
672
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName
673
    AND `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`TABLE_NAME` = :tableName AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
674
    AND `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`TABLE_NAME` = :tableName AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY'
675
UNION
676
SELECT
677
    `kcu`.`CONSTRAINT_NAME` AS `name`,
678
    `kcu`.`COLUMN_NAME` AS `column_name`,
679
    `tc`.`CONSTRAINT_TYPE` AS `type`,
680
    NULL AS `foreign_table_schema`,
681
    NULL AS `foreign_table_name`,
682
    NULL AS `foreign_column_name`,
683
    NULL AS `on_update`,
684 48
    NULL AS `on_delete`,
685
    `kcu`.`ORDINAL_POSITION` AS `position`
686 48
FROM
687
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
688
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
689 48
WHERE
690 48
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`TABLE_NAME` = :tableName
691
    AND `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`TABLE_NAME` = :tableName AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE')
692 48
ORDER BY `position` ASC
693
SQL;
694 48
695 48
        $resolvedName = $this->resolveTableName($tableName);
696
697
        $constraints = $this->db->createCommand(
698 48
            $sql,
699
            [
700
                ':schemaName' => $resolvedName->getSchemaName(),
701
                ':tableName' => $resolvedName->getName(),
702
            ]
703 48
        )->queryAll();
704 48
705
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
706 48
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
707 37
708 37
        $result = [
709
            'primaryKey' => null,
710 37
            'foreignKeys' => [],
711
            'uniques' => [],
712 37
        ];
713 46
714 10
        foreach ($constraints as $type => $names) {
715 10
            foreach ($names as $name => $constraint) {
716 10
                switch ($type) {
717 10
                    case 'PRIMARY KEY':
718 10
                        $ct = (new Constraint())
719 10
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
720 10
721 10
                        $result['primaryKey'] = $ct;
722
723 10
                        break;
724
                    case 'FOREIGN KEY':
725 10
                        $fk = (new ForeignKeyConstraint())
726 37
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
727 37
                            ->foreignTableName($constraint[0]['foreign_table_name'])
728 37
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
729 37
                            ->onDelete($constraint[0]['on_delete'])
730
                            ->onUpdate($constraint[0]['on_update'])
731 37
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
732
                            ->name($name);
733 37
734
                        $result['foreignKeys'][] = $fk;
735
736
                        break;
737
                    case 'UNIQUE':
738 48
                        $ct = (new Constraint())
739 48
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
740
                            ->name($name);
741
742 48
                        $result['uniques'][] = $ct;
743
744
                        break;
745
                }
746
            }
747
        }
748
749
        foreach ($result as $type => $data) {
750
            $this->setTableMetadata($tableName, $type, $data);
751
        }
752 78
753
        return $result[$returnType];
754 78
    }
755
756
    /**
757
     * Creates a column schema for the database.
758
     *
759
     * This method may be overridden by child classes to create a DBMS-specific column schema.
760
     *
761
     * @return ColumnSchema column schema instance.
762
     */
763
    private function createColumnSchema(): ColumnSchema
764
    {
765
        return new ColumnSchema();
766
    }
767
}
768