Passed
Push — master ( 2d168f...0cc03e )
by Wilmer
12:04 queued 10:25
created

Schema::loadColumnSchema()   D

Complexity

Conditions 19
Paths 100

Size

Total Lines 74
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 19.0389

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 19
eloc 44
c 1
b 0
f 0
nc 100
nop 1
dl 0
loc 74
ccs 40
cts 42
cp 0.9524
crap 19.0389
rs 4.5166

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
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\Constraint\Constraint;
11
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
12
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
13
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
14
use Yiisoft\Db\Constraint\IndexConstraint;
15
use Yiisoft\Db\Exception\Exception;
16
use Yiisoft\Db\Exception\InvalidArgumentException;
17
use Yiisoft\Db\Exception\InvalidConfigException;
18
use Yiisoft\Db\Exception\NotSupportedException;
19
use Yiisoft\Db\Expression\Expression;
20
use Yiisoft\Db\Schema\Schema as AbstractSchema;
21
22
use function array_change_key_case;
23
use function array_map;
24
use function array_merge;
25
use function array_values;
26
use function bindec;
27
use function explode;
28
use function preg_match;
29
use function preg_match_all;
30
use function str_replace;
31
use function stripos;
32
use function strpos;
33
use function strtolower;
34
use function trim;
35
use function version_compare;
36
37
final class Schema extends AbstractSchema implements ConstraintFinderInterface
38
{
39
    use ConstraintFinderTrait;
40
41
    private bool $oldMysql;
42
    private array $typeMap = [
43
        'tinyint' => self::TYPE_TINYINT,
44
        'bit' => self::TYPE_INTEGER,
45
        'smallint' => self::TYPE_SMALLINT,
46
        'mediumint' => self::TYPE_INTEGER,
47
        'int' => self::TYPE_INTEGER,
48
        'integer' => self::TYPE_INTEGER,
49
        'bigint' => self::TYPE_BIGINT,
50
        'float' => self::TYPE_FLOAT,
51
        'double' => self::TYPE_DOUBLE,
52
        'real' => self::TYPE_FLOAT,
53
        'decimal' => self::TYPE_DECIMAL,
54
        'numeric' => self::TYPE_DECIMAL,
55
        'tinytext' => self::TYPE_TEXT,
56
        'mediumtext' => self::TYPE_TEXT,
57
        'longtext' => self::TYPE_TEXT,
58
        'longblob' => self::TYPE_BINARY,
59
        'blob' => self::TYPE_BINARY,
60
        'text' => self::TYPE_TEXT,
61
        'varchar' => self::TYPE_STRING,
62
        'string' => self::TYPE_STRING,
63
        'char' => self::TYPE_CHAR,
64
        'datetime' => self::TYPE_DATETIME,
65
        'year' => self::TYPE_DATE,
66
        'date' => self::TYPE_DATE,
67
        'time' => self::TYPE_TIME,
68
        'timestamp' => self::TYPE_TIMESTAMP,
69
        'enum' => self::TYPE_STRING,
70
        'varbinary' => self::TYPE_BINARY,
71
        'json' => self::TYPE_JSON,
72
    ];
73
74
    /**
75
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
76
     * case starting and ending characters are different.
77
     */
78
    protected $tableQuoteCharacter = '`';
79
80
    /**
81
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
82
     * and ending characters are different.
83
     */
84
    protected $columnQuoteCharacter = '`';
85
86
    /**
87
     * Resolves the table name and schema name (if any).
88
     *
89
     * @param string $name the table name.
90
     *
91
     * @return TableSchema
92
     *
93
     * {@see TableSchema}
94
     */
95 58
    protected function resolveTableName(string $name): TableSchema
96
    {
97 58
        $resolvedName = new TableSchema();
98
99 58
        $parts = explode('.', str_replace('`', '', $name));
100
101 58
        if (isset($parts[1])) {
102
            $resolvedName->schemaName($parts[0]);
103
            $resolvedName->name($parts[1]);
104
        } else {
105 58
            $resolvedName->schemaName($this->defaultSchema);
106 58
            $resolvedName->name($name);
107
        }
108
109 58
        $resolvedName->fullName(($resolvedName->getSchemaName() !== $this->defaultSchema ?
110 58
            $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName());
111
112 58
        return $resolvedName;
113
    }
114
115
    /**
116
     * Returns all table names in the database.
117
     *
118
     * This method should be overridden by child classes in order to support this feature because the default
119
     * implementation simply throws an exception.
120
     *
121
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
122
     *
123
     * @throws Exception
124
     * @throws InvalidConfigException
125
     * @throws InvalidArgumentException
126
     *
127
     * @return array all table names in the database. The names have NO schema name prefix.
128
     */
129 3
    protected function findTableNames(string $schema = ''): array
130
    {
131 3
        $sql = 'SHOW TABLES';
132
133 3
        if ($schema !== '') {
134
            $sql .= ' FROM ' . $this->quoteSimpleTableName($schema);
135
        }
136
137 3
        return $this->getDb()->createCommand($sql)->queryColumn();
138
    }
139
140
    /**
141
     * Loads the metadata for the specified table.
142
     *
143
     * @param string $name table name.
144
     *
145
     * @throws Exception
146
     *
147
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
148
     */
149 82
    protected function loadTableSchema(string $name): ?TableSchema
150
    {
151 82
        $table = new TableSchema();
152
153 82
        $this->resolveTableNames($table, $name);
154
155 82
        if ($this->findColumns($table)) {
156 77
            $this->findConstraints($table);
157
158 77
            return $table;
159
        }
160
161 13
        return null;
162
    }
163
164
    /**
165
     * Loads a primary key for the given table.
166
     *
167
     * @param string $tableName table name.
168
     *
169
     * @throws Exception
170
     * @throws InvalidArgumentException
171
     * @throws InvalidConfigException
172
     *
173
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
174
     */
175 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
176
    {
177 31
        return $this->loadTableConstraints($tableName, 'primaryKey');
178
    }
179
180
    /**
181
     * Loads all foreign keys for the given table.
182
     *
183
     * @param string $tableName table name.
184
     *
185
     * @throws Exception
186
     * @throws InvalidArgumentException
187
     * @throws InvalidConfigException
188
     *
189
     * @return ForeignKeyConstraint[] foreign keys for the given table.
190
     */
191 4
    protected function loadTableForeignKeys(string $tableName): array
192
    {
193 4
        return $this->loadTableConstraints($tableName, 'foreignKeys');
194
    }
195
196
    /**
197
     * Loads all indexes for the given table.
198
     *
199
     * @param string $tableName table name.
200
     *
201
     * @throws Exception
202
     * @throws InvalidArgumentException
203
     * @throws InvalidConfigException
204
     *
205
     * @return IndexConstraint[] indexes for the given table.
206
     */
207 28
    protected function loadTableIndexes(string $tableName): array
208
    {
209 28
        static $sql = <<<'SQL'
210
SELECT
211
    `s`.`INDEX_NAME` AS `name`,
212
    `s`.`COLUMN_NAME` AS `column_name`,
213
    `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
214
    `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
215
FROM `information_schema`.`STATISTICS` AS `s`
216
WHERE `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND `s`.`TABLE_NAME` = :tableName
217
ORDER BY `s`.`SEQ_IN_INDEX` ASC
218
SQL;
219
220 28
        $resolvedName = $this->resolveTableName($tableName);
221
222 28
        $indexes = $this->getDb()->createCommand($sql, [
223 28
            ':schemaName' => $resolvedName->getSchemaName(),
224 28
            ':tableName' => $resolvedName->getName(),
225 28
        ])->queryAll();
226
227 28
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
228 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
229 28
        $result = [];
230
231 28
        foreach ($indexes as $name => $index) {
232 28
            $ic = new IndexConstraint();
233
234 28
            $ic->primary((bool) $index[0]['index_is_primary']);
235 28
            $ic->unique((bool) $index[0]['index_is_unique']);
236 28
            $ic->name($name !== 'PRIMARY' ? $name : null);
237 28
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
238
239 28
            $result[] = $ic;
240
        }
241
242 28
        return $result;
243
    }
244
245
    /**
246
     * Loads all unique constraints for the given table.
247
     *
248
     * @param string $tableName table name.
249
     *
250
     * @throws Exception
251
     * @throws InvalidArgumentException
252
     * @throws InvalidConfigException
253
     *
254
     * @return Constraint[] unique constraints for the given table.
255
     */
256 13
    protected function loadTableUniques(string $tableName): array
257
    {
258 13
        return $this->loadTableConstraints($tableName, 'uniques');
259
    }
260
261
    /**
262
     * Loads all check constraints for the given table.
263
     *
264
     * @param string $tableName table name.
265
     *
266
     * @throws NotSupportedException
267
     *
268
     * @return CheckConstraint[] check constraints for the given table.
269
     */
270 12
    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

270
    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...
271
    {
272 12
        throw new NotSupportedException('MySQL does not support check constraints.');
273
    }
274
275
    /**
276
     * Loads all default value constraints for the given table.
277
     *
278
     * @param string $tableName table name.
279
     *
280
     * @throws NotSupportedException
281
     *
282
     * @return DefaultValueConstraint[] default value constraints for the given table.
283
     */
284 12
    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

284
    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...
285
    {
286 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
287
    }
288
289
    /**
290
     * Creates a query builder for the MySQL database.
291
     *
292
     * @return QueryBuilder query builder instance
293
     */
294 53
    public function createQueryBuilder(): QueryBuilder
295
    {
296 53
        return new QueryBuilder($this->getDb());
0 ignored issues
show
Bug introduced by
It seems like $this->getDb() can also be of type null; however, parameter $db of Yiisoft\Db\Mysql\QueryBuilder::__construct() does only seem to accept Yiisoft\Db\Connection\Connection, 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

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

413
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName(/** @scrutinizer ignore-type */ $table->getFullName());
Loading history...
414
415
        try {
416 82
            $columns = $this->getDb()->createCommand($sql)->queryAll();
417 13
        } catch (Exception $e) {
418 13
            $previous = $e->getPrevious();
419
420 13
            if ($previous instanceof PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
421
                /**
422
                 * table does not exist.
423
                 *
424
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
425
                 */
426 13
                return false;
427
            }
428
429
            throw $e;
430
        }
431
432 77
        foreach ($columns as $info) {
433 77
            if ($this->getDb()->getSlavePdo()->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_LOWER) {
434 76
                $info = array_change_key_case($info, CASE_LOWER);
435
            }
436
437 77
            $column = $this->loadColumnSchema($info);
438 77
            $table->columns($column->getName(), $column);
439
440 77
            if ($column->isPrimaryKey()) {
441 53
                $table->primaryKey($column->getName());
442 53
                if ($column->isAutoIncrement()) {
443 51
                    $table->sequenceName('');
444
                }
445
            }
446
        }
447
448 77
        return true;
449
    }
450
451
    /**
452
     * Gets the CREATE TABLE sql string.
453
     *
454
     * @param TableSchema $table the table metadata.
455
     *
456
     * @throws Exception
457
     * @throws InvalidArgumentException
458
     * @throws InvalidConfigException
459
     *
460
     * @return string $sql the result of 'SHOW CREATE TABLE'.
461
     */
462
    protected function getCreateTableSql($table): string
463
    {
464
        $row = $this->getDb()->createCommand(
465
            '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

465
            'SHOW CREATE TABLE ' . $this->quoteTableName(/** @scrutinizer ignore-type */ $table->getFullName())
Loading history...
466
        )->queryOne();
467
468
        if (isset($row['Create Table'])) {
469
            $sql = $row['Create Table'];
470
        } else {
471
            $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

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