Passed
Pull Request — master (#88)
by Wilmer
02:26
created

Schema::findConstraints()   B

Complexity

Conditions 9
Paths 43

Size

Total Lines 69
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 19.125

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 69
ccs 15
cts 30
cp 0.5
crap 19.125
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 JsonException;
8
use PDO;
9
use PDOException;
10
use Throwable;
11
use Yiisoft\Arrays\ArrayHelper;
12
use Yiisoft\Db\Constraint\Constraint;
13
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
14
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
15
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
16
use Yiisoft\Db\Constraint\IndexConstraint;
17
use Yiisoft\Db\Exception\Exception;
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
37
/**
38
 * The class Schema is the class for retrieving metadata from a Mysql database (version 5.7 and above).
39
 *
40
 * @psalm-type ColumnArray = array{
41
 *   table_schema: string,
42
 *   table_name: string,
43
 *   column_name: string,
44
 *   data_type: string,
45
 *   type_type: string|null,
46
 *   character_maximum_length: int,
47
 *   column_comment: string|null,
48
 *   modifier: int,
49
 *   is_nullable: bool,
50
 *   column_default: mixed,
51
 *   is_autoinc: bool,
52
 *   sequence_name: string|null,
53
 *   enum_values: array<array-key, float|int|string>|string|null,
54
 *   numeric_precision: int|null,
55
 *   numeric_scale: int|null,
56
 *   size: string|null,
57
 *   is_pkey: bool|null,
58
 *   dimension: int
59
 * }
60
 *
61
 * @psalm-type ColumnInfoArray = array{
62
 *   field: string,
63
 *   type: string,
64
 *   collation: string|null,
65
 *   null: string,
66
 *   key: string,
67
 *   default: string|null,
68
 *   extra: string,
69
 *   privileges: string,
70
 *   comment: string
71
 * }
72
 *
73
 * @psalm-type RowConstraint = array{
74
 *   constraint_name: string,
75
 *   column_name: string,
76
 *   referenced_table_name: string,
77
 *   referenced_column_name: string
78
 * }
79
 *
80
 * @psalm-type ConstraintArray = array<
81
 *   array-key,
82
 *   array {
83
 *     name: string,
84
 *     column_name: string,
85
 *     type: string,
86
 *     foreign_table_schema: string|null,
87
 *     foreign_table_name: string|null,
88
 *     foreign_column_name: string|null,
89
 *     on_update: string,
90
 *     on_delete: string,
91
 *     check_expr: string
92
 *   }
93
 * >
94
 */
95
final class Schema extends AbstractSchema implements ConstraintFinderInterface
96
{
97
    use ConstraintFinderTrait;
98
99
    /** @var array<array-key, string> $typeMap */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, string> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, string>.
Loading history...
100
    private array $typeMap = [
101
        'tinyint' => self::TYPE_TINYINT,
102
        'bit' => self::TYPE_INTEGER,
103
        'smallint' => self::TYPE_SMALLINT,
104
        'mediumint' => self::TYPE_INTEGER,
105
        'int' => self::TYPE_INTEGER,
106
        'integer' => self::TYPE_INTEGER,
107
        'bigint' => self::TYPE_BIGINT,
108
        'float' => self::TYPE_FLOAT,
109
        'double' => self::TYPE_DOUBLE,
110
        'real' => self::TYPE_FLOAT,
111
        'decimal' => self::TYPE_DECIMAL,
112
        'numeric' => self::TYPE_DECIMAL,
113
        'tinytext' => self::TYPE_TEXT,
114
        'mediumtext' => self::TYPE_TEXT,
115
        'longtext' => self::TYPE_TEXT,
116
        'longblob' => self::TYPE_BINARY,
117
        'blob' => self::TYPE_BINARY,
118
        'text' => self::TYPE_TEXT,
119
        'varchar' => self::TYPE_STRING,
120
        'string' => self::TYPE_STRING,
121
        'char' => self::TYPE_CHAR,
122
        'datetime' => self::TYPE_DATETIME,
123
        'year' => self::TYPE_DATE,
124
        'date' => self::TYPE_DATE,
125
        'time' => self::TYPE_TIME,
126
        'timestamp' => self::TYPE_TIMESTAMP,
127
        'enum' => self::TYPE_STRING,
128
        'varbinary' => self::TYPE_BINARY,
129
        'json' => self::TYPE_JSON,
130
    ];
131
132
    /**
133
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
134
     * case starting and ending characters are different.
135
     */
136
    protected $tableQuoteCharacter = '`';
137
138
    /**
139
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
140
     * and ending characters are different.
141
     */
142
    protected $columnQuoteCharacter = '`';
143
144
    /**
145
     * Resolves the table name and schema name (if any).
146
     *
147
     * @param string $name the table name.
148
     *
149
     * @return TableSchema
150
     *
151
     * {@see TableSchema}
152
     */
153 58
    protected function resolveTableName(string $name): TableSchema
154
    {
155 58
        $resolvedName = new TableSchema();
156
157 58
        $parts = explode('.', str_replace('`', '', $name));
158
159 58
        if (isset($parts[1])) {
160
            $resolvedName->schemaName($parts[0]);
161
            $resolvedName->name($parts[1]);
162
            $resolvedName->comment($this->getTableComment($parts[0], $parts[1]));
163
        } else {
164 58
            $resolvedName->schemaName($this->defaultSchema);
165 58
            $resolvedName->name($name);
166 58
            $resolvedName->comment($this->getTableComment($this->defaultSchema, $name));
167
        }
168
169 58
        $resolvedName->fullName(($resolvedName->getSchemaName() !== $this->defaultSchema ?
170 58
            (string) $resolvedName->getSchemaName() . '.' : '') . (string) $resolvedName->getName());
171
172 58
        return $resolvedName;
173
    }
174
175
    /**
176
     * Returns all table names in the database.
177
     *
178
     * This method should be overridden by child classes in order to support this feature because the default
179
     * implementation simply throws an exception.
180
     *
181
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
182
     *
183
     * @throws Exception|InvalidConfigException|Throwable
184
     *
185
     * @return array all table names in the database. The names have NO schema name prefix.
186
     */
187 3
    protected function findTableNames(string $schema = ''): array
188
    {
189 3
        $sql = 'SHOW TABLES';
190
191 3
        if ($schema !== '') {
192
            $sql .= ' FROM ' . $this->quoteSimpleTableName($schema);
193
        }
194
195 3
        return $this->getDb()->createCommand($sql)->queryColumn();
196
    }
197
198
    /**
199
     * Loads the metadata for the specified table.
200
     *
201
     * @param string $name table name.
202
     *
203
     * @throws Exception|Throwable
204
     *
205
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
206
     */
207 96
    protected function loadTableSchema(string $name): ?TableSchema
208
    {
209 96
        $table = new TableSchema();
210
211 96
        $this->resolveTableNames($table, $name);
212
213 96
        if ($this->findColumns($table)) {
214 90
            $this->findConstraints($table);
215
216 90
            return $table;
217
        }
218
219 15
        return null;
220
    }
221
222
    /**
223
     * Loads a primary key for the given table.
224
     *
225
     * @param string $tableName table name.
226
     *
227
     * @throws Exception|InvalidConfigException|Throwable
228
     *
229
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.*
230
     */
231 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
232
    {
233 31
        $tablePrimaryKey = $this->loadTableConstraints($tableName, 'primaryKey');
234
235 31
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
236
    }
237
238
    /**
239
     * Loads all foreign keys for the given table.
240
     *
241
     * @param string $tableName table name.
242
     *
243
     * @throws Exception|InvalidConfigException|Throwable
244
     *
245
     * @return array|ForeignKeyConstraint[] foreign keys for the given table.
246
     */
247 4
    protected function loadTableForeignKeys(string $tableName): array
248
    {
249 4
        $tableForeignKeys = $this->loadTableConstraints($tableName, 'foreignKeys');
250
251 4
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
252
    }
253
254
    /**
255
     * Loads all indexes for the given table.
256
     *
257
     * @param string $tableName table name.
258
     *
259
     * @throws Exception|InvalidConfigException|Throwable
260
     *
261
     * @return IndexConstraint[] indexes for the given table.
262
     */
263 28
    protected function loadTableIndexes(string $tableName): array
264
    {
265 28
        $sql = <<<'SQL'
266
SELECT
267
    `s`.`INDEX_NAME` AS `name`,
268
    `s`.`COLUMN_NAME` AS `column_name`,
269
    `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
270
    `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
271
FROM `information_schema`.`STATISTICS` AS `s`
272
WHERE `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND `s`.`TABLE_NAME` = :tableName
273
ORDER BY `s`.`SEQ_IN_INDEX` ASC
274
SQL;
275
276 28
        $resolvedName = $this->resolveTableName($tableName);
277
278 28
        $indexes = $this->getDb()->createCommand($sql, [
279 28
            ':schemaName' => $resolvedName->getSchemaName(),
280 28
            ':tableName' => $resolvedName->getName(),
281 28
        ])->queryAll();
282
283
        /** @var array<array-key, array<array-key, mixed>> $indexes */
284 28
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
285 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
286 28
        $result = [];
287
288
        /**
289
         * @psalm-var object|string|null $name
290
         * @psalm-var array<array-key, array<array-key, mixed>> $index
291
         */
292 28
        foreach ($indexes as $name => $index) {
293 28
            $ic = new IndexConstraint();
294
295 28
            $ic->primary((bool) $index[0]['index_is_primary']);
296 28
            $ic->unique((bool) $index[0]['index_is_unique']);
297 28
            $ic->name($name !== 'PRIMARY' ? $name : null);
298 28
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
299
300 28
            $result[] = $ic;
301
        }
302
303 28
        return $result;
304
    }
305
306
    /**
307
     * Loads all unique constraints for the given table.
308
     *
309
     * @param string $tableName table name.
310
     *
311
     * @throws Exception|InvalidConfigException|Throwable
312
     *
313
     * @return array|Constraint[] unique constraints for the given table.
314
     */
315 13
    protected function loadTableUniques(string $tableName): array
316
    {
317 13
        $tableUniques = $this->loadTableConstraints($tableName, 'uniques');
318
319 13
        return is_array($tableUniques) ? $tableUniques : [];
320
    }
321
322
    /**
323
     * Loads all check constraints for the given table.
324
     *
325
     * @param string $tableName table name.
326
     *
327
     * @throws NotSupportedException
328
     *
329
     * @return array check constraints for the given table.
330
     */
331 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

331
    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...
332
    {
333 12
        throw new NotSupportedException('MySQL does not support check constraints.');
334
    }
335
336
    /**
337
     * Loads all default value constraints for the given table.
338
     *
339
     * @param string $tableName table name.
340
     *
341
     * @throws NotSupportedException
342
     *
343
     * @return array default value constraints for the given table.
344
     */
345 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

345
    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...
346
    {
347 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
348
    }
349
350
    /**
351
     * Creates a query builder for the MySQL database.
352
     *
353
     * @return QueryBuilder query builder instance
354
     */
355 68
    public function createQueryBuilder(): QueryBuilder
356
    {
357 68
        return new QueryBuilder($this->getDb());
358
    }
359
360
    /**
361
     * Resolves the table name and schema name (if any).
362
     *
363
     * @param TableSchema $table the table metadata object.
364
     * @param string $name the table name.
365
     */
366 96
    protected function resolveTableNames(TableSchema $table, string $name): void
367
    {
368 96
        $parts = explode('.', str_replace('`', '', $name));
369
370 96
        if (isset($parts[1])) {
371 1
            $table->schemaName($parts[0]);
372 1
            $table->name($parts[1]);
373 1
            $table->fullName((string) $table->getSchemaName() . '.' . (string) $table->getName());
374 1
            $table->comment($this->getTableComment($parts[0], $parts[1]));
375
        } else {
376 95
            $table->name($parts[0]);
377 95
            $table->fullName($parts[0]);
378 95
            $table->comment($this->getTableComment(null, $parts[0]));
379
        }
380 96
    }
381
382
    /**
383
     * Loads the column information into a {@see ColumnSchema} object.
384
     *
385
     * @param array $info column information.
386
     *
387
     * @throws JsonException
388
     *
389
     * @return ColumnSchema the column schema object.
390
     */
391 91
    protected function loadColumnSchema(array $info): ColumnSchema
392
    {
393 91
        $column = $this->createColumnSchema();
394
395
        /** @psalm-var ColumnInfoArray $info */
396 91
        $column->name($info['field']);
397 91
        $column->allowNull($info['null'] === 'YES');
398 91
        $column->primaryKey(strpos($info['key'], 'PRI') !== false);
399 91
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
400 91
        $column->comment($info['comment']);
401 91
        $column->dbType($info['type']);
402 91
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
403 91
        $column->type(self::TYPE_STRING);
404
405 91
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
406 91
            $type = strtolower($matches[1]);
407
408 91
            if (isset($this->typeMap[$type])) {
409 91
                $column->type($this->typeMap[$type]);
410
            }
411
412 91
            if (!empty($matches[2])) {
413 90
                if ($type === 'enum') {
414 20
                    preg_match_all("/'[^']*'/", $matches[2], $values);
415
416 20
                    foreach ($values[0] as $i => $value) {
417 20
                        $values[$i] = trim($value, "'");
418
                    }
419
420 20
                    $column->enumValues($values);
421
                } else {
422 90
                    $values = explode(',', $matches[2]);
423 90
                    $column->precision((int) $values[0]);
424 90
                    $column->size((int) $values[0]);
425
426 90
                    if (isset($values[1])) {
427 32
                        $column->scale((int) $values[1]);
428
                    }
429
430 90
                    if ($column->getSize() === 1 && $type === 'tinyint') {
431 21
                        $column->type('boolean');
432 90
                    } elseif ($type === 'bit') {
433 20
                        if ($column->getSize() > 32) {
434
                            $column->type('bigint');
435 20
                        } elseif ($column->getSize() === 32) {
436
                            $column->type('integer');
437
                        }
438
                    }
439
                }
440
            }
441
        }
442
443 91
        $column->phpType($this->getColumnPhpType($column));
444
445 91
        if (!$column->isPrimaryKey()) {
446
            /**
447
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
448
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
449
             *
450
             * See details here: https://mariadb.com/kb/en/library/now/#description
451
             */
452
            if (
453 88
                ($column->getType() === 'timestamp' || $column->getType() === 'datetime')
454 88
                && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', (string) $info['default'], $matches)
455
            ) {
456 24
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
457 24
                    ? '(' . $matches[1] . ')' : '')));
458 85
            } elseif (isset($type) && $type === 'bit') {
459 20
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
460
            } else {
461 85
                $column->defaultValue($column->phpTypecast($info['default']));
462
            }
463
        }
464
465 91
        return $column;
466
    }
467
468
    /**
469
     * Collects the metadata of table columns.
470
     *
471
     * @param TableSchema $table the table metadata.
472
     *
473
     * @throws Exception|Throwable if DB query fails.
474
     *
475
     * @return bool whether the table exists in the database.
476
     */
477 96
    protected function findColumns(TableSchema $table): bool
478
    {
479 96
        $tableName = $table->getFullName() ?? '';
480
481 96
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName($tableName);
482
483
        try {
484 96
            $columns = $this->getDb()->createCommand($sql)->queryAll();
485 15
        } catch (Exception $e) {
486 15
            $previous = $e->getPrevious();
487
488 15
            if ($previous instanceof PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
489
                /**
490
                 * table does not exist.
491
                 *
492
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
493
                 */
494 15
                return false;
495
            }
496
497
            throw $e;
498
        }
499
500 90
        $slavePdo = $this->getDb()->getSlavePdo();
501
502
        /** @psalm-var ColumnInfoArray $info */
503 90
        foreach ($columns as $info) {
504 90
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_LOWER) {
505 89
                $info = array_change_key_case($info, CASE_LOWER);
506
            }
507
508 90
            $column = $this->loadColumnSchema($info);
509 90
            $table->columns($column->getName(), $column);
510
511 90
            if ($column->isPrimaryKey()) {
512 56
                $table->primaryKey($column->getName());
513 56
                if ($column->isAutoIncrement()) {
514 54
                    $table->sequenceName('');
515
                }
516
            }
517
        }
518
519 90
        return true;
520
    }
521
522
    /**
523
     * Gets the CREATE TABLE sql string.
524
     *
525
     * @param TableSchema $table the table metadata.
526
     *
527
     * @throws Exception|InvalidConfigException|Throwable
528
     *
529
     * @return string $sql the result of 'SHOW CREATE TABLE'.
530
     */
531 1
    protected function getCreateTableSql(TableSchema $table): string
532
    {
533 1
        $tableName = $table->getFullName() ?? '';
534
535
        /** @var array<array-key, string> $row */
536 1
        $row = $this->getDb()->createCommand(
537 1
            'SHOW CREATE TABLE ' . $this->quoteTableName($tableName)
538 1
        )->queryOne();
539
540 1
        if (isset($row['Create Table'])) {
541 1
            $sql = $row['Create Table'];
542
        } else {
543
            $row = array_values($row);
0 ignored issues
show
Bug introduced by
$row of type false is incompatible with the type array expected by parameter $array 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

543
            $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
544
            $sql = $row[1];
545
        }
546
547 1
        return $sql;
548
    }
549
550
    /**
551
     * Collects the foreign key column details for the given table.
552
     *
553
     * @param TableSchema $table the table metadata.
554
     *
555
     * @throws Exception|Throwable
556
     */
557 90
    protected function findConstraints(TableSchema $table): void
558
    {
559 90
        $sql = <<<'SQL'
560
SELECT
561
    `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
562
    `kcu`.`COLUMN_NAME` AS `column_name`,
563
    `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
564
    `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
565
FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
566
JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
567
    (
568
        `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
569
        (`kcu`.`CONSTRAINT_CATALOG` IS NULL AND `rc`.`CONSTRAINT_CATALOG` IS NULL)
570
    ) AND
571
    `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
572
    `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME`
573
WHERE `rc`.`CONSTRAINT_SCHEMA` = database() AND `kcu`.`TABLE_SCHEMA` = database()
574
AND `rc`.`TABLE_NAME` = :tableName AND `kcu`.`TABLE_NAME` = :tableName1
575
SQL;
576
577
        try {
578 90
            $rows = $this->getDb()->createCommand(
579 90
                $sql,
580 90
                [':tableName' => $table->getName(), ':tableName1' => $table->getName()]
581 90
            )->queryAll();
582
583 90
            $constraints = [];
584
585
            /**  @psalm-var RowConstraint $row */
586 90
            foreach ($rows as $row) {
587 24
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
588 24
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
589
            }
590
591 90
            $table->foreignKeys([]);
592
593
            /**
594
             * @var array{referenced_table_name: string, columns: array} $constraint
595
             */
596 90
            foreach ($constraints as $name => $constraint) {
597 24
                $table->foreignKey($name, array_merge(
598 24
                    [$constraint['referenced_table_name']],
599 24
                    $constraint['columns']
600
                ));
601
            }
602
        } catch (Exception $e) {
603
            $previous = $e->getPrevious();
604
605
            if (!$previous instanceof PDOException || strpos($previous->getMessage(), 'SQLSTATE[42S02') === false) {
606
                throw $e;
607
            }
608
609
            // table does not exist, try to determine the foreign keys using the table creation sql
610
            $sql = $this->getCreateTableSql($table);
611
            $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi';
612
613
            if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
614
                foreach ($matches as $match) {
615
                    $fks = array_map('trim', explode(',', str_replace('`', '', $match[1])));
616
                    $pks = array_map('trim', explode(',', str_replace('`', '', $match[3])));
617
                    $constraint = [str_replace('`', '', $match[2])];
618
619
                    foreach ($fks as $k => $name) {
620
                        $constraint[$name] = $pks[$k];
621
                    }
622
623
                    $table->foreignKey(\md5(\serialize($constraint)), $constraint);
624
                }
625
                $table->foreignKeys(array_values($table->getForeignKeys()));
626
            }
627
        }
628 90
    }
629
630
    /**
631
     * Returns all unique indexes for the given table.
632
     *
633
     * Each array element is of the following structure:
634
     *
635
     * ```php
636
     * [
637
     *     'IndexName1' => ['col1' [, ...]],
638
     *     'IndexName2' => ['col2' [, ...]],
639
     * ]
640
     * ```
641
     *
642
     * @param TableSchema $table the table metadata.
643
     *
644
     * @throws Exception|InvalidConfigException|Throwable
645
     *
646
     * @return array all unique indexes for the given table.
647
     */
648 1
    public function findUniqueIndexes(TableSchema $table): array
649
    {
650 1
        $sql = $this->getCreateTableSql($table);
651
652 1
        $uniqueIndexes = [];
653
654 1
        $regexp = '/UNIQUE KEY\s+\`(.+)\`\s*\((\`.+\`)+\)/mi';
655
656 1
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
657 1
            foreach ($matches as $match) {
658 1
                $indexName = $match[1];
659 1
                $indexColumns = array_map('trim', explode('`,`', trim($match[2], '`')));
660 1
                $uniqueIndexes[$indexName] = $indexColumns;
661
            }
662
        }
663
664 1
        return $uniqueIndexes;
665
    }
666
667
    /**
668
     * Create a column schema builder instance giving the type and value precision.
669
     *
670
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
671
     *
672
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
673
     * @param array|int|string $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
674
     *
675
     * @return ColumnSchemaBuilder column schema builder instance
676
     */
677 3
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
678
    {
679 3
        return new ColumnSchemaBuilder($type, $length, $this->getDb());
680
    }
681
682
    public function setDefaultSchema(?string $defaultSchema): void
683
    {
684
        $this->defaultSchema = $defaultSchema;
685
    }
686
687 132
    private function getTableComment(?string $schema, string $name): string
688
    {
689 132
        $sql = "SELECT TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES WHERE table_name='$name'";
690
691 132
        if ($schema !== null) {
692 1
            $sql = "SELECT TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES " .
693 1
                "WHERE table_schema='$schema' AND table_name='$name'";
694
        }
695
696
        /** @psalm-var array<string, string> */
697 132
        $tableinfo = $this->getDb()->createCommand($sql)->queryOne();
698
699 132
        return $tableinfo['TABLE_COMMENT'] ?? '';
700
    }
701
702
    /**
703
     * Loads multiple types of constraints and returns the specified ones.
704
     *
705
     * @param string $tableName table name.
706
     * @param string $returnType return type:
707
     * - primaryKey
708
     * - foreignKeys
709
     * - uniques
710
     *
711
     * @throws Exception|InvalidConfigException|Throwable
712
     *
713
     * @return (Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
714
     *
715
     * @psalm-return Constraint|list<Constraint|ForeignKeyConstraint>|null
716
     */
717 48
    private function loadTableConstraints(string $tableName, string $returnType)
718
    {
719 48
        $sql = <<<'SQL'
720
SELECT
721
    `kcu`.`CONSTRAINT_NAME` AS `name`,
722
    `kcu`.`COLUMN_NAME` AS `column_name`,
723
    `tc`.`CONSTRAINT_TYPE` AS `type`,
724
    CASE
725
        WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
726
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
727
    END AS `foreign_table_schema`,
728
    `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
729
    `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
730
    `rc`.`UPDATE_RULE` AS `on_update`,
731
    `rc`.`DELETE_RULE` AS `on_delete`,
732
    `kcu`.`ORDINAL_POSITION` AS `position`
733
FROM
734
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
735
    `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`,
736
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
737
WHERE
738
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName
739
    AND `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`TABLE_NAME` = :tableName AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
740
    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'
741
UNION
742
SELECT
743
    `kcu`.`CONSTRAINT_NAME` AS `name`,
744
    `kcu`.`COLUMN_NAME` AS `column_name`,
745
    `tc`.`CONSTRAINT_TYPE` AS `type`,
746
    NULL AS `foreign_table_schema`,
747
    NULL AS `foreign_table_name`,
748
    NULL AS `foreign_column_name`,
749
    NULL AS `on_update`,
750
    NULL AS `on_delete`,
751
    `kcu`.`ORDINAL_POSITION` AS `position`
752
FROM
753
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
754
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
755
WHERE
756
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`TABLE_NAME` = :tableName
757
    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')
758
ORDER BY `position` ASC
759
SQL;
760
761 48
        $resolvedName = $this->resolveTableName($tableName);
762
763 48
        $constraints = $this->getDb()->createCommand(
764 48
            $sql,
765
            [
766 48
                ':schemaName' => $resolvedName->getSchemaName(),
767 48
                ':tableName' => $resolvedName->getName(),
768
            ]
769 48
        )->queryAll();
770
771
        /** @var array<array-key, array> $constraints */
772 48
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
773 48
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
774
775 48
        $result = [
776
            'primaryKey' => null,
777
            'foreignKeys' => [],
778
            'uniques' => [],
779
        ];
780
781
        /**
782
         * @var string $type
783
         * @var array $names
784
         */
785 48
        foreach ($constraints as $type => $names) {
786
            /**
787
             * @psalm-var object|string|null $name
788
             * @psalm-var ConstraintArray $constraint
789
             */
790 48
            foreach ($names as $name => $constraint) {
791 48
                switch ($type) {
792 48
                    case 'PRIMARY KEY':
793 37
                        $ct = (new Constraint())
794 37
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
795
796 37
                        $result['primaryKey'] = $ct;
797
798 37
                        break;
799 46
                    case 'FOREIGN KEY':
800 10
                        $fk = (new ForeignKeyConstraint())
801 10
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
802 10
                            ->foreignTableName($constraint[0]['foreign_table_name'])
803 10
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
804 10
                            ->onDelete($constraint[0]['on_delete'])
805 10
                            ->onUpdate($constraint[0]['on_update'])
806 10
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
807 10
                            ->name($name);
808
809 10
                        $result['foreignKeys'][] = $fk;
810
811 10
                        break;
812 37
                    case 'UNIQUE':
813 37
                        $ct = (new Constraint())
814 37
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
815 37
                            ->name($name);
816
817 37
                        $result['uniques'][] = $ct;
818
819 37
                        break;
820
                }
821
            }
822
        }
823
824 48
        foreach ($result as $type => $data) {
825 48
            $this->setTableMetadata($tableName, $type, $data);
826
        }
827
828 48
        return $result[$returnType];
829
    }
830
831
    /**
832
     * Creates a column schema for the database.
833
     *
834
     * This method may be overridden by child classes to create a DBMS-specific column schema.
835
     *
836
     * @return ColumnSchema column schema instance.
837
     */
838 91
    private function createColumnSchema(): ColumnSchema
839
    {
840 91
        return new ColumnSchema();
841
    }
842
}
843