Passed
Pull Request — master (#125)
by
unknown
11:00
created

Schema::loadColumnSchema()   D

Complexity

Conditions 19
Paths 100

Size

Total Lines 75
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 19.0389

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mysql;
6
7
use 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\ForeignKeyConstraint;
14
use Yiisoft\Db\Constraint\IndexConstraint;
15
use Yiisoft\Db\Exception\Exception;
16
use Yiisoft\Db\Exception\InvalidConfigException;
17
use Yiisoft\Db\Exception\NotSupportedException;
18
use Yiisoft\Db\Expression\Expression;
19
use Yiisoft\Db\Schema\Schema as AbstractSchema;
20
21
use function array_change_key_case;
22
use function array_map;
23
use function array_merge;
24
use function array_values;
25
use function bindec;
26
use function explode;
27
use function preg_match;
28
use function preg_match_all;
29
use function str_replace;
30
use function stripos;
31
use function strpos;
32
use function strtolower;
33
use function trim;
34
35
/**
36
 * The class Schema is the class for retrieving metadata from a Mysql database (version 5.7 and above).
37
 *
38
 * @psalm-type ColumnArray = array{
39
 *   table_schema: string,
40
 *   table_name: string,
41
 *   column_name: string,
42
 *   data_type: string,
43
 *   type_type: string|null,
44
 *   character_maximum_length: int,
45
 *   column_comment: string|null,
46
 *   modifier: int,
47
 *   is_nullable: bool,
48
 *   column_default: mixed,
49
 *   is_autoinc: bool,
50
 *   sequence_name: string|null,
51
 *   enum_values: array<array-key, float|int|string>|string|null,
52
 *   numeric_precision: int|null,
53
 *   numeric_scale: int|null,
54
 *   size: string|null,
55
 *   is_pkey: bool|null,
56
 *   dimension: int
57
 * }
58
 *
59
 * @psalm-type ColumnInfoArray = array{
60
 *   field: string,
61
 *   type: string,
62
 *   collation: string|null,
63
 *   null: string,
64
 *   key: string,
65
 *   default: string|null,
66
 *   extra: string,
67
 *   privileges: string,
68
 *   comment: string
69
 * }
70
 *
71
 * @psalm-type RowConstraint = array{
72
 *   constraint_name: string,
73
 *   column_name: string,
74
 *   referenced_table_name: string,
75
 *   referenced_column_name: string
76
 * }
77
 *
78
 * @psalm-type ConstraintArray = array<
79
 *   array-key,
80
 *   array {
81
 *     name: string,
82
 *     column_name: string,
83
 *     type: string,
84
 *     foreign_table_schema: string|null,
85
 *     foreign_table_name: string|null,
86
 *     foreign_column_name: string|null,
87
 *     on_update: string,
88
 *     on_delete: string,
89
 *     check_expr: string
90
 *   }
91
 * >
92
 */
93
final class Schema extends AbstractSchema
94
{
95
    /** @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...
96
    private array $typeMap = [
97
        'tinyint' => self::TYPE_TINYINT,
98
        'bit' => self::TYPE_INTEGER,
99
        'smallint' => self::TYPE_SMALLINT,
100
        'mediumint' => self::TYPE_INTEGER,
101
        'int' => self::TYPE_INTEGER,
102
        'integer' => self::TYPE_INTEGER,
103
        'bigint' => self::TYPE_BIGINT,
104
        'float' => self::TYPE_FLOAT,
105
        'double' => self::TYPE_DOUBLE,
106
        'real' => self::TYPE_FLOAT,
107
        'decimal' => self::TYPE_DECIMAL,
108
        'numeric' => self::TYPE_DECIMAL,
109
        'tinytext' => self::TYPE_TEXT,
110
        'mediumtext' => self::TYPE_TEXT,
111
        'longtext' => self::TYPE_TEXT,
112
        'longblob' => self::TYPE_BINARY,
113
        'blob' => self::TYPE_BINARY,
114
        'text' => self::TYPE_TEXT,
115
        'varchar' => self::TYPE_STRING,
116
        'string' => self::TYPE_STRING,
117
        'char' => self::TYPE_CHAR,
118
        'datetime' => self::TYPE_DATETIME,
119
        'year' => self::TYPE_DATE,
120
        'date' => self::TYPE_DATE,
121
        'time' => self::TYPE_TIME,
122
        'timestamp' => self::TYPE_TIMESTAMP,
123
        'enum' => self::TYPE_STRING,
124
        'varbinary' => self::TYPE_BINARY,
125
        'json' => self::TYPE_JSON,
126
    ];
127
128
    /**
129
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
130
     * case starting and ending characters are different.
131
     */
132
    protected $tableQuoteCharacter = '`';
133
134
    /**
135
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
136
     * and ending characters are different.
137
     */
138
    protected $columnQuoteCharacter = '`';
139
140
    /**
141
     * Resolves the table name and schema name (if any).
142
     *
143
     * @param string $name the table name.
144
     *
145
     * @return TableSchema
146
     *
147
     * {@see TableSchema}
148
     */
149 58
    protected function resolveTableName(string $name): TableSchema
150
    {
151 58
        $resolvedName = new TableSchema();
152
153 58
        $parts = explode('.', str_replace('`', '', $name));
154
155 58
        if (isset($parts[1])) {
156
            $resolvedName->schemaName($parts[0]);
157
            $resolvedName->name($parts[1]);
158
        } else {
159 58
            $resolvedName->schemaName($this->defaultSchema);
160 58
            $resolvedName->name($name);
161
        }
162
163 58
        $resolvedName->fullName(($resolvedName->getSchemaName() !== $this->defaultSchema ?
164 58
                (string) $resolvedName->getSchemaName() . '.' : '') . (string) $resolvedName->getName());
165
166 58
        return $resolvedName;
167
    }
168
169
    /**
170
     * Returns all table names in the database.
171
     *
172
     * This method should be overridden by child classes in order to support this feature because the default
173
     * implementation simply throws an exception.
174
     *
175
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
176
     *
177
     * @throws Exception|InvalidConfigException|Throwable
178
     *
179
     * @return array all table names in the database. The names have NO schema name prefix.
180
     */
181 3
    protected function findTableNames(string $schema = ''): array
182
    {
183 3
        $sql = 'SHOW TABLES';
184
185 3
        if ($schema !== '') {
186
            $sql .= ' FROM ' . $this->quoteSimpleTableName($schema);
187
        }
188
189
        return $this
190 3
            ->getDb()
191 3
            ->createCommand($sql)
192 3
            ->queryColumn();
193
    }
194
195
    /**
196
     * Loads the metadata for the specified table.
197
     *
198
     * @param string $name table name.
199
     *
200
     * @throws Exception|Throwable
201
     *
202
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
203
     */
204 94
    protected function loadTableSchema(string $name): ?TableSchema
205
    {
206 94
        $table = new TableSchema();
207
208 94
        $this->resolveTableNames($table, $name);
209
210 94
        if ($this->findColumns($table)) {
211 88
            $this->findConstraints($table);
212
213 88
            return $table;
214
        }
215
216 15
        return null;
217
    }
218
219
    /**
220
     * Loads a primary key for the given table.
221
     *
222
     * @param string $tableName table name.
223
     *
224
     * @throws Exception|InvalidConfigException|Throwable
225
     *
226
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.*
227
     */
228 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
229
    {
230 31
        $tablePrimaryKey = $this->loadTableConstraints($tableName, 'primaryKey');
231
232 31
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
233
    }
234
235
    /**
236
     * Loads all foreign keys for the given table.
237
     *
238
     * @param string $tableName table name.
239
     *
240
     * @throws Exception|InvalidConfigException|Throwable
241
     *
242
     * @return array|ForeignKeyConstraint[] foreign keys for the given table.
243
     */
244 4
    protected function loadTableForeignKeys(string $tableName): array
245
    {
246 4
        $tableForeignKeys = $this->loadTableConstraints($tableName, 'foreignKeys');
247
248 4
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
249
    }
250
251
    /**
252
     * Loads all indexes for the given table.
253
     *
254
     * @param string $tableName table name.
255
     *
256
     * @throws Exception|InvalidConfigException|Throwable
257
     *
258
     * @return IndexConstraint[] indexes for the given table.
259
     */
260 28
    protected function loadTableIndexes(string $tableName): array
261
    {
262 28
        $sql = <<<'SQL'
263
SELECT
264
    `s`.`INDEX_NAME` AS `name`,
265
    `s`.`COLUMN_NAME` AS `column_name`,
266
    `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
267
    `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
268
FROM `information_schema`.`STATISTICS` AS `s`
269
WHERE
270
    `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
271
    `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND
272
    `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
279 28
            ->getDb()
280 28
            ->createCommand($sql, [
281 28
                ':schemaName' => $resolvedName->getSchemaName(),
282 28
                ':tableName' => $resolvedName->getName(),
283 28
            ])->queryAll();
284
285
        /** @var array<array-key, array<array-key, mixed>> $indexes */
286 28
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
287 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
288 28
        $result = [];
289
290
        /**
291
         * @psalm-var object|string|null $name
292
         * @psalm-var array<array-key, array<array-key, mixed>> $index
293
         */
294 28
        foreach ($indexes as $name => $index) {
295 28
            $ic = new IndexConstraint();
296
297 28
            $ic->primary((bool) $index[0]['index_is_primary']);
298 28
            $ic->unique((bool) $index[0]['index_is_unique']);
299 28
            $ic->name($name !== 'PRIMARY' ? $name : null);
300 28
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
301
302 28
            $result[] = $ic;
303
        }
304
305 28
        return $result;
306
    }
307
308
    /**
309
     * Loads all unique constraints for the given table.
310
     *
311
     * @param string $tableName table name.
312
     *
313
     * @throws Exception|InvalidConfigException|Throwable
314
     *
315
     * @return array|Constraint[] unique constraints for the given table.
316
     */
317 13
    protected function loadTableUniques(string $tableName): array
318
    {
319 13
        $tableUniques = $this->loadTableConstraints($tableName, 'uniques');
320
321 13
        return is_array($tableUniques) ? $tableUniques : [];
322
    }
323
324
    /**
325
     * Loads all check constraints for the given table.
326
     *
327
     * @param string $tableName table name.
328
     *
329
     * @throws NotSupportedException
330
     *
331
     * @return array check constraints for the given table.
332
     */
333 12
    protected function loadTableChecks(string $tableName): array
334
    {
335 12
        throw new NotSupportedException('MySQL does not support check constraints.');
336
    }
337
338
    /**
339
     * Loads all default value constraints for the given table.
340
     *
341
     * @param string $tableName table name.
342
     *
343
     * @throws NotSupportedException
344
     *
345
     * @return array default value constraints for the given table.
346
     */
347 12
    protected function loadTableDefaultValues(string $tableName): array
348
    {
349 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
350
    }
351
352
    /**
353
     * Creates a query builder for the MySQL database.
354
     *
355
     * @return QueryBuilder query builder instance
356
     */
357 68
    public function createQueryBuilder(): QueryBuilder
358
    {
359 68
        return new QueryBuilder($this->getDb());
360
    }
361
362
    /**
363
     * Resolves the table name and schema name (if any).
364
     *
365
     * @param TableSchema $table the table metadata object.
366
     * @param string $name the table name.
367
     */
368 94
    protected function resolveTableNames(TableSchema $table, string $name): void
369
    {
370 94
        $parts = explode('.', str_replace('`', '', $name));
371
372 94
        if (isset($parts[1])) {
373
            $table->schemaName($parts[0]);
374
            $table->name($parts[1]);
375
            $table->fullName((string) $table->getSchemaName() . '.' . (string) $table->getName());
376
        } else {
377 94
            $table->name($parts[0]);
378 94
            $table->fullName($parts[0]);
379
        }
380
    }
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 89
    protected function loadColumnSchema(array $info): ColumnSchema
392
    {
393 89
        $column = $this->createColumnSchema();
394
395
        /** @psalm-var ColumnInfoArray $info */
396 89
        $column->name($info['field']);
397 89
        $column->allowNull($info['null'] === 'YES');
398 89
        $column->primaryKey(strpos($info['key'], 'PRI') !== false);
399 89
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
400 89
        $column->comment($info['comment']);
401 89
        $column->dbType($info['type']);
402 89
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
403 89
        $column->type(self::TYPE_STRING);
404
405 89
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
406 89
            $type = strtolower($matches[1]);
407
408 89
            if (isset($this->typeMap[$type])) {
409 89
                $column->type($this->typeMap[$type]);
410
            }
411
412 89
            if (!empty($matches[2])) {
413 88
                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 88
                    $values = explode(',', $matches[2]);
423 88
                    $column->precision((int) $values[0]);
424 88
                    $column->size((int) $values[0]);
425
426 88
                    if (isset($values[1])) {
427 32
                        $column->scale((int) $values[1]);
428
                    }
429
430 88
                    if ($column->getSize() === 1 && $type === 'tinyint') {
431 21
                        $column->type('boolean');
432 88
                    } 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 89
        $column->phpType($this->getColumnPhpType($column));
444
445 89
        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 86
                ($column->getType() === 'timestamp' || $column->getType() === 'datetime')
454 86
                && 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 83
            } elseif (isset($type) && $type === 'bit') {
459 20
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
460
            } else {
461 83
                $column->defaultValue($column->phpTypecast($info['default']));
462
            }
463
        }
464
465 89
        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 94
    protected function findColumns(TableSchema $table): bool
478
    {
479 94
        $tableName = $table->getFullName() ?? '';
480
481 94
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName($tableName);
482
483
        try {
484 88
            $columns = $this
485 94
                ->getDb()
486 94
                ->createCommand($sql)
487 94
                ->queryAll();
488 15
        } catch (Exception $e) {
489 15
            $previous = $e->getPrevious();
490
491 15
            if ($previous instanceof PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
492
                /**
493
                 * table does not exist.
494
                 *
495
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
496
                 */
497 15
                return false;
498
            }
499
500
            throw $e;
501
        }
502
503 88
        $slavePdo = $this
504 88
            ->getDb()
505 88
            ->getSlavePdo();
506
507
        /** @psalm-var ColumnInfoArray $info */
508 88
        foreach ($columns as $info) {
509 88
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_LOWER) {
510 87
                $info = array_change_key_case($info, CASE_LOWER);
511
            }
512
513 88
            $column = $this->loadColumnSchema($info);
514 88
            $table->columns($column->getName(), $column);
515
516 88
            if ($column->isPrimaryKey()) {
517 54
                $table->primaryKey($column->getName());
518 54
                if ($column->isAutoIncrement()) {
519 52
                    $table->sequenceName('');
520
                }
521
            }
522
        }
523
524 88
        return true;
525
    }
526
527
    /**
528
     * Gets the CREATE TABLE sql string.
529
     *
530
     * @param TableSchema $table the table metadata.
531
     *
532
     * @throws Exception|InvalidConfigException|Throwable
533
     *
534
     * @return string $sql the result of 'SHOW CREATE TABLE'.
535
     */
536 1
    protected function getCreateTableSql(TableSchema $table): string
537
    {
538 1
        $tableName = $table->getFullName() ?? '';
539
540
        /** @var array<array-key, string> $row */
541 1
        $row = $this
542 1
            ->getDb()
543 1
            ->createCommand(
544 1
                'SHOW CREATE TABLE ' . $this->quoteTableName($tableName)
545 1
            )->queryOne();
546
547 1
        if (isset($row['Create Table'])) {
548 1
            $sql = $row['Create Table'];
549
        } else {
550
            $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

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