Passed
Pull Request — master (#88)
by Wilmer
18:45 queued 16:07
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 132
    private function getTableComment(?string $schema, string $name): string
683
    {
684 132
        $sql = "SELECT TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES WHERE table_name='$name'";
685
686 132
        if ($schema !== null) {
687 1
            $sql = 'SELECT TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES ' .
688 1
                "WHERE table_schema='$schema' AND table_name='$name'";
689
        }
690
691
        /** @psalm-var array<string, string> */
692 132
        $tableinfo = $this->getDb()->createCommand($sql)->queryOne();
693
694 132
        return $tableinfo['TABLE_COMMENT'] ?? '';
695
    }
696
697
    /**
698
     * Loads multiple types of constraints and returns the specified ones.
699
     *
700
     * @param string $tableName table name.
701
     * @param string $returnType return type:
702
     * - primaryKey
703
     * - foreignKeys
704
     * - uniques
705
     *
706
     * @throws Exception|InvalidConfigException|Throwable
707
     *
708
     * @return (Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
709
     *
710
     * @psalm-return Constraint|list<Constraint|ForeignKeyConstraint>|null
711
     */
712 48
    private function loadTableConstraints(string $tableName, string $returnType)
713
    {
714 48
        $sql = <<<'SQL'
715
SELECT
716
    `kcu`.`CONSTRAINT_NAME` AS `name`,
717
    `kcu`.`COLUMN_NAME` AS `column_name`,
718
    `tc`.`CONSTRAINT_TYPE` AS `type`,
719
    CASE
720
        WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
721
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
722
    END AS `foreign_table_schema`,
723
    `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
724
    `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
725
    `rc`.`UPDATE_RULE` AS `on_update`,
726
    `rc`.`DELETE_RULE` AS `on_delete`,
727
    `kcu`.`ORDINAL_POSITION` AS `position`
728
FROM
729
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
730
    `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`,
731
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
732
WHERE
733
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName
734
    AND `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`TABLE_NAME` = :tableName AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
735
    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'
736
UNION
737
SELECT
738
    `kcu`.`CONSTRAINT_NAME` AS `name`,
739
    `kcu`.`COLUMN_NAME` AS `column_name`,
740
    `tc`.`CONSTRAINT_TYPE` AS `type`,
741
    NULL AS `foreign_table_schema`,
742
    NULL AS `foreign_table_name`,
743
    NULL AS `foreign_column_name`,
744
    NULL AS `on_update`,
745
    NULL AS `on_delete`,
746
    `kcu`.`ORDINAL_POSITION` AS `position`
747
FROM
748
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
749
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
750
WHERE
751
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`TABLE_NAME` = :tableName
752
    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')
753
ORDER BY `position` ASC
754
SQL;
755
756 48
        $resolvedName = $this->resolveTableName($tableName);
757
758 48
        $constraints = $this->getDb()->createCommand(
759 48
            $sql,
760
            [
761 48
                ':schemaName' => $resolvedName->getSchemaName(),
762 48
                ':tableName' => $resolvedName->getName(),
763
            ]
764 48
        )->queryAll();
765
766
        /** @var array<array-key, array> $constraints */
767 48
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
768 48
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
769
770 48
        $result = [
771
            'primaryKey' => null,
772
            'foreignKeys' => [],
773
            'uniques' => [],
774
        ];
775
776
        /**
777
         * @var string $type
778
         * @var array $names
779
         */
780 48
        foreach ($constraints as $type => $names) {
781
            /**
782
             * @psalm-var object|string|null $name
783
             * @psalm-var ConstraintArray $constraint
784
             */
785 48
            foreach ($names as $name => $constraint) {
786 48
                switch ($type) {
787 48
                    case 'PRIMARY KEY':
788 37
                        $ct = (new Constraint())
789 37
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
790
791 37
                        $result['primaryKey'] = $ct;
792
793 37
                        break;
794 46
                    case 'FOREIGN KEY':
795 10
                        $fk = (new ForeignKeyConstraint())
796 10
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
797 10
                            ->foreignTableName($constraint[0]['foreign_table_name'])
798 10
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
799 10
                            ->onDelete($constraint[0]['on_delete'])
800 10
                            ->onUpdate($constraint[0]['on_update'])
801 10
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
802 10
                            ->name($name);
803
804 10
                        $result['foreignKeys'][] = $fk;
805
806 10
                        break;
807 37
                    case 'UNIQUE':
808 37
                        $ct = (new Constraint())
809 37
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
810 37
                            ->name($name);
811
812 37
                        $result['uniques'][] = $ct;
813
814 37
                        break;
815
                }
816
            }
817
        }
818
819 48
        foreach ($result as $type => $data) {
820 48
            $this->setTableMetadata($tableName, $type, $data);
821
        }
822
823 48
        return $result[$returnType];
824
    }
825
826
    /**
827
     * Creates a column schema for the database.
828
     *
829
     * This method may be overridden by child classes to create a DBMS-specific column schema.
830
     *
831
     * @return ColumnSchema column schema instance.
832
     */
833 91
    private function createColumnSchema(): ColumnSchema
834
    {
835 91
        return new ColumnSchema();
836
    }
837
}
838