Passed
Pull Request — master (#114)
by Wilmer
02:37
created

Schema::findConstraints()   B

Complexity

Conditions 9
Paths 43

Size

Total Lines 75
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 19.125

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 9
eloc 52
c 2
b 0
f 0
nc 43
nop 1
dl 0
loc 75
ccs 15
cts 30
cp 0.5
crap 19.125
rs 7.4917

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\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 3
        return $this->getDb()->createCommand($sql)->queryColumn();
190
    }
191
192
    /**
193
     * Loads the metadata for the specified table.
194
     *
195
     * @param string $name table name.
196
     *
197
     * @throws Exception|Throwable
198
     *
199
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
200
     */
201 94
    protected function loadTableSchema(string $name): ?TableSchema
202
    {
203 94
        $table = new TableSchema();
204
205 94
        $this->resolveTableNames($table, $name);
206
207 94
        if ($this->findColumns($table)) {
208 88
            $this->findConstraints($table);
209
210 88
            return $table;
211
        }
212
213 15
        return null;
214
    }
215
216
    /**
217
     * Loads a primary key for the given table.
218
     *
219
     * @param string $tableName table name.
220
     *
221
     * @throws Exception|InvalidConfigException|Throwable
222
     *
223
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.*
224
     */
225 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
226
    {
227 31
        $tablePrimaryKey = $this->loadTableConstraints($tableName, 'primaryKey');
228
229 31
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
230
    }
231
232
    /**
233
     * Loads all foreign keys for the given table.
234
     *
235
     * @param string $tableName table name.
236
     *
237
     * @throws Exception|InvalidConfigException|Throwable
238
     *
239
     * @return array|ForeignKeyConstraint[] foreign keys for the given table.
240
     */
241 4
    protected function loadTableForeignKeys(string $tableName): array
242
    {
243 4
        $tableForeignKeys = $this->loadTableConstraints($tableName, 'foreignKeys');
244
245 4
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
246
    }
247
248
    /**
249
     * Loads all indexes for the given table.
250
     *
251
     * @param string $tableName table name.
252
     *
253
     * @throws Exception|InvalidConfigException|Throwable
254
     *
255
     * @return IndexConstraint[] indexes for the given table.
256
     */
257 28
    protected function loadTableIndexes(string $tableName): array
258
    {
259 28
        $sql = <<<'SQL'
260
SELECT
261
    `s`.`INDEX_NAME` AS `name`,
262
    `s`.`COLUMN_NAME` AS `column_name`,
263
    `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
264
    `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
265
FROM `information_schema`.`STATISTICS` AS `s`
266
WHERE
267
    `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
268
    `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND
269
    `s`.`TABLE_NAME` = :tableName
270
ORDER BY `s`.`SEQ_IN_INDEX` ASC
271
SQL;
272
273 28
        $resolvedName = $this->resolveTableName($tableName);
274
275 28
        $indexes = $this->getDb()->createCommand($sql, [
276 28
            ':schemaName' => $resolvedName->getSchemaName(),
277 28
            ':tableName' => $resolvedName->getName(),
278 28
        ])->queryAll();
279
280
        /** @var array<array-key, array<array-key, mixed>> $indexes */
281 28
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
282 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
283 28
        $result = [];
284
285
        /**
286
         * @psalm-var object|string|null $name
287
         * @psalm-var array<array-key, array<array-key, mixed>> $index
288
         */
289 28
        foreach ($indexes as $name => $index) {
290 28
            $ic = new IndexConstraint();
291
292 28
            $ic->primary((bool) $index[0]['index_is_primary']);
293 28
            $ic->unique((bool) $index[0]['index_is_unique']);
294 28
            $ic->name($name !== 'PRIMARY' ? $name : null);
295 28
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
296
297 28
            $result[] = $ic;
298
        }
299
300 28
        return $result;
301
    }
302
303
    /**
304
     * Loads all unique constraints for the given table.
305
     *
306
     * @param string $tableName table name.
307
     *
308
     * @throws Exception|InvalidConfigException|Throwable
309
     *
310
     * @return array|Constraint[] unique constraints for the given table.
311
     */
312 13
    protected function loadTableUniques(string $tableName): array
313
    {
314 13
        $tableUniques = $this->loadTableConstraints($tableName, 'uniques');
315
316 13
        return is_array($tableUniques) ? $tableUniques : [];
317
    }
318
319
    /**
320
     * Loads all check constraints for the given table.
321
     *
322
     * @param string $tableName table name.
323
     *
324
     * @throws NotSupportedException
325
     *
326
     * @return array check constraints for the given table.
327
     */
328 12
    protected function loadTableChecks(string $tableName): array
329
    {
330 12
        throw new NotSupportedException('MySQL does not support check constraints.');
331
    }
332
333
    /**
334
     * Loads all default value constraints for the given table.
335
     *
336
     * @param string $tableName table name.
337
     *
338
     * @throws NotSupportedException
339
     *
340
     * @return array default value constraints for the given table.
341
     */
342 12
    protected function loadTableDefaultValues(string $tableName): array
343
    {
344 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
345
    }
346
347
    /**
348
     * Creates a query builder for the MySQL database.
349
     *
350
     * @return QueryBuilder query builder instance
351
     */
352 68
    public function createQueryBuilder(): QueryBuilder
353
    {
354 68
        return new QueryBuilder($this->getDb());
355
    }
356
357
    /**
358
     * Resolves the table name and schema name (if any).
359
     *
360
     * @param TableSchema $table the table metadata object.
361
     * @param string $name the table name.
362
     */
363 94
    protected function resolveTableNames(TableSchema $table, string $name): void
364
    {
365 94
        $parts = explode('.', str_replace('`', '', $name));
366
367 94
        if (isset($parts[1])) {
368
            $table->schemaName($parts[0]);
369
            $table->name($parts[1]);
370
            $table->fullName((string) $table->getSchemaName() . '.' . (string) $table->getName());
371
        } else {
372 94
            $table->name($parts[0]);
373 94
            $table->fullName($parts[0]);
374
        }
375
    }
376
377
    /**
378
     * Loads the column information into a {@see ColumnSchema} object.
379
     *
380
     * @param array $info column information.
381
     *
382
     * @throws JsonException
383
     *
384
     * @return ColumnSchema the column schema object.
385
     */
386 89
    protected function loadColumnSchema(array $info): ColumnSchema
387
    {
388 89
        $column = $this->createColumnSchema();
389
390
        /** @psalm-var ColumnInfoArray $info */
391 89
        $column->name($info['field']);
392 89
        $column->allowNull($info['null'] === 'YES');
393 89
        $column->primaryKey(strpos($info['key'], 'PRI') !== false);
394 89
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
395 89
        $column->comment($info['comment']);
396 89
        $column->dbType($info['type']);
397 89
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
398 89
        $column->type(self::TYPE_STRING);
399
400 89
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
401 89
            $type = strtolower($matches[1]);
402
403 89
            if (isset($this->typeMap[$type])) {
404 89
                $column->type($this->typeMap[$type]);
405
            }
406
407 89
            if (!empty($matches[2])) {
408 88
                if ($type === 'enum') {
409 20
                    preg_match_all("/'[^']*'/", $matches[2], $values);
410
411 20
                    foreach ($values[0] as $i => $value) {
412 20
                        $values[$i] = trim($value, "'");
413
                    }
414
415 20
                    $column->enumValues($values);
416
                } else {
417 88
                    $values = explode(',', $matches[2]);
418 88
                    $column->precision((int) $values[0]);
419 88
                    $column->size((int) $values[0]);
420
421 88
                    if (isset($values[1])) {
422 32
                        $column->scale((int) $values[1]);
423
                    }
424
425 88
                    if ($column->getSize() === 1 && $type === 'tinyint') {
426 21
                        $column->type('boolean');
427 88
                    } elseif ($type === 'bit') {
428 20
                        if ($column->getSize() > 32) {
429
                            $column->type('bigint');
430 20
                        } elseif ($column->getSize() === 32) {
431
                            $column->type('integer');
432
                        }
433
                    }
434
                }
435
            }
436
        }
437
438 89
        $column->phpType($this->getColumnPhpType($column));
439
440 89
        if (!$column->isPrimaryKey()) {
441
            /**
442
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
443
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
444
             *
445
             * See details here: https://mariadb.com/kb/en/library/now/#description
446
             */
447
            if (
448 86
                ($column->getType() === 'timestamp' || $column->getType() === 'datetime')
449 86
                && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', (string) $info['default'], $matches)
450
            ) {
451 24
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
452 24
                    ? '(' . $matches[1] . ')' : '')));
453 83
            } elseif (isset($type) && $type === 'bit') {
454 20
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
455
            } else {
456 83
                $column->defaultValue($column->phpTypecast($info['default']));
457
            }
458
        }
459
460 89
        return $column;
461
    }
462
463
    /**
464
     * Collects the metadata of table columns.
465
     *
466
     * @param TableSchema $table the table metadata.
467
     *
468
     * @throws Exception|Throwable if DB query fails.
469
     *
470
     * @return bool whether the table exists in the database.
471
     */
472 94
    protected function findColumns(TableSchema $table): bool
473
    {
474 94
        $tableName = $table->getFullName() ?? '';
475
476 94
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName($tableName);
477
478
        try {
479 94
            $columns = $this->getDb()->createCommand($sql)->queryAll();
480 15
        } catch (Exception $e) {
481 15
            $previous = $e->getPrevious();
482
483 15
            if ($previous instanceof PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
484
                /**
485
                 * table does not exist.
486
                 *
487
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
488
                 */
489 15
                return false;
490
            }
491
492
            throw $e;
493
        }
494
495 88
        $slavePdo = $this->getDb()->getSlavePdo();
496
497
        /** @psalm-var ColumnInfoArray $info */
498 88
        foreach ($columns as $info) {
499 88
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_LOWER) {
500 87
                $info = array_change_key_case($info, CASE_LOWER);
501
            }
502
503 88
            $column = $this->loadColumnSchema($info);
504 88
            $table->columns($column->getName(), $column);
505
506 88
            if ($column->isPrimaryKey()) {
507 54
                $table->primaryKey($column->getName());
508 54
                if ($column->isAutoIncrement()) {
509 52
                    $table->sequenceName('');
510
                }
511
            }
512
        }
513
514 88
        return true;
515
    }
516
517
    /**
518
     * Gets the CREATE TABLE sql string.
519
     *
520
     * @param TableSchema $table the table metadata.
521
     *
522
     * @throws Exception|InvalidConfigException|Throwable
523
     *
524
     * @return string $sql the result of 'SHOW CREATE TABLE'.
525
     */
526 1
    protected function getCreateTableSql(TableSchema $table): string
527
    {
528 1
        $tableName = $table->getFullName() ?? '';
529
530
        /** @var array<array-key, string> $row */
531 1
        $row = $this->getDb()->createCommand(
532 1
            'SHOW CREATE TABLE ' . $this->quoteTableName($tableName)
533 1
        )->queryOne();
534
535 1
        if (isset($row['Create Table'])) {
536 1
            $sql = $row['Create Table'];
537
        } else {
538
            $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

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