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

Schema::isOldMysql()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 9
ccs 0
cts 5
cp 0
crap 6
rs 10
c 0
b 0
f 0
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