Passed
Push — dev ( fed250...26cef6 )
by Def
32:10 queued 29:32
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 Throwable;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Db\Cache\SchemaCache;
11
use Yiisoft\Db\Connection\ConnectionInterface;
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 md5;
28
use function preg_match;
29
use function preg_match_all;
30
use function serialize;
31
use function str_replace;
32
use function stripos;
33
use function strtolower;
34
use function trim;
35
36
/**
37
 * The class Schema is the class for retrieving metadata from a Mysql database (version 5.7 and above).
38
 *
39
 * @psalm-type ColumnArray = array{
40
 *   table_schema: string,
41
 *   table_name: string,
42
 *   column_name: string,
43
 *   data_type: string,
44
 *   type_type: string|null,
45
 *   character_maximum_length: int,
46
 *   column_comment: string|null,
47
 *   modifier: int,
48
 *   is_nullable: bool,
49
 *   column_default: mixed,
50
 *   is_autoinc: bool,
51
 *   sequence_name: string|null,
52
 *   enum_values: array<array-key, float|int|string>|string|null,
53
 *   numeric_precision: int|null,
54
 *   numeric_scale: int|null,
55
 *   size: string|null,
56
 *   is_pkey: bool|null,
57
 *   dimension: int
58
 * }
59
 *
60
 * @psalm-type ColumnInfoArray = array{
61
 *   field: string,
62
 *   type: string,
63
 *   collation: string|null,
64
 *   null: string,
65
 *   key: string,
66
 *   default: string|null,
67
 *   extra: string,
68
 *   privileges: string,
69
 *   comment: string
70
 * }
71
 *
72
 * @psalm-type RowConstraint = array{
73
 *   constraint_name: string,
74
 *   column_name: string,
75
 *   referenced_table_name: string,
76
 *   referenced_column_name: string
77
 * }
78
 *
79
 * @psalm-type ConstraintArray = array<
80
 *   array-key,
81
 *   array {
82
 *     name: string,
83
 *     column_name: string,
84
 *     type: string,
85
 *     foreign_table_schema: string|null,
86
 *     foreign_table_name: string|null,
87
 *     foreign_column_name: string|null,
88
 *     on_update: string,
89
 *     on_delete: string,
90
 *     check_expr: string
91
 *   }
92
 * >
93
 */
94
final class Schema extends AbstractSchema
95
{
96
    /** @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...
97
    private array $typeMap = [
98
        'tinyint' => self::TYPE_TINYINT,
99
        'bit' => self::TYPE_INTEGER,
100
        'smallint' => self::TYPE_SMALLINT,
101
        'mediumint' => self::TYPE_INTEGER,
102
        'int' => self::TYPE_INTEGER,
103
        'integer' => self::TYPE_INTEGER,
104
        'bigint' => self::TYPE_BIGINT,
105
        'float' => self::TYPE_FLOAT,
106
        'double' => self::TYPE_DOUBLE,
107
        'real' => self::TYPE_FLOAT,
108
        'decimal' => self::TYPE_DECIMAL,
109
        'numeric' => self::TYPE_DECIMAL,
110
        'tinytext' => self::TYPE_TEXT,
111
        'mediumtext' => self::TYPE_TEXT,
112
        'longtext' => self::TYPE_TEXT,
113
        'longblob' => self::TYPE_BINARY,
114
        'blob' => self::TYPE_BINARY,
115
        'text' => self::TYPE_TEXT,
116
        'varchar' => self::TYPE_STRING,
117
        'string' => self::TYPE_STRING,
118
        'char' => self::TYPE_CHAR,
119
        'datetime' => self::TYPE_DATETIME,
120
        'year' => self::TYPE_DATE,
121
        'date' => self::TYPE_DATE,
122
        'time' => self::TYPE_TIME,
123
        'timestamp' => self::TYPE_TIMESTAMP,
124
        'enum' => self::TYPE_STRING,
125
        'varbinary' => self::TYPE_BINARY,
126
        'json' => self::TYPE_JSON,
127
    ];
128
129 378
    public function __construct(private ConnectionInterface $db, SchemaCache $schemaCache)
130
    {
131 378
        parent::__construct($schemaCache);
132
    }
133
134
    /**
135
     * Create a column schema builder instance giving the type and value precision.
136
     *
137
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
138
     *
139
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
140
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
141
     *
142
     * @return ColumnSchemaBuilder column schema builder instance
143
     *
144
     * @psalm-param string[]|int|string|null $length
145
     */
146 3
    public function createColumnSchemaBuilder(string $type, array|int|string $length = null): ColumnSchemaBuilder
147
    {
148 3
        return new ColumnSchemaBuilder($type, $length, $this->db->getQuoter());
149
    }
150
151
    /**
152
     * Returns all unique indexes for the given table.
153
     *
154
     * Each array element is of the following structure:
155
     *
156
     * ```php
157
     * [
158
     *     'IndexName1' => ['col1' [, ...]],
159
     *     'IndexName2' => ['col2' [, ...]],
160
     * ]
161
     * ```
162
     *
163
     * @param TableSchema $table the table metadata.
164
     *
165
     * @throws Exception|InvalidConfigException|Throwable
166
     *
167
     * @return array all unique indexes for the given table.
168
     */
169 1
    public function findUniqueIndexes(TableSchema $table): array
170
    {
171 1
        $sql = $this->getCreateTableSql($table);
172
173 1
        $uniqueIndexes = [];
174
175 1
        $regexp = '/UNIQUE KEY\s+`(.+)`\s*\((`.+`)+\)/mi';
176
177 1
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
178 1
            foreach ($matches as $match) {
179 1
                $indexName = $match[1];
180 1
                $indexColumns = array_map('trim', explode('`,`', trim($match[2], '`')));
181 1
                $uniqueIndexes[$indexName] = $indexColumns;
182
            }
183
        }
184
185 1
        return $uniqueIndexes;
186
    }
187
188
    /**
189
     * @inheritDoc
190
     */
191
    public function getLastInsertID(?string $sequenceName = null): string
192
    {
193
        return $this->db->getLastInsertID($sequenceName);
194
    }
195
196
    /**
197
     * Returns the actual name of a given table name.
198
     *
199
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
200
     * {@see ConnectionInterface::tablePrefix}.
201
     *
202
     * @param string $name the table name to be converted.
203
     *
204
     * @return string the real name of the given table name.
205
     */
206 158
    public function getRawTableName(string $name): string
207
    {
208 158
        if (str_contains($name, '{{')) {
209 23
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
210
211 23
            return str_replace('%', $this->db->getTablePrefix(), $name);
212
        }
213
214 158
        return $name;
215
    }
216
217 5
    public function supportsSavepoint(): bool
218
    {
219 5
        return $this->db->isSavepointEnabled();
220
    }
221
222
    /**
223
     * Collects the metadata of table columns.
224
     *
225
     * @param TableSchema $table the table metadata.
226
     *
227
     * @throws Exception|Throwable if DB query fails.
228
     *
229
     * @return bool whether the table exists in the database.
230
     */
231 98
    protected function findColumns(TableSchema $table): bool
232
    {
233 98
        $tableName = $table->getFullName() ?? '';
234 98
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->db->getQuoter()->quoteTableName($tableName);
235
236
        try {
237 98
            $columns = $this->db->createCommand($sql)->queryAll();
238 15
        } catch (Exception $e) {
239 15
            $previous = $e->getPrevious();
240
241 15
            if ($previous && str_contains($previous->getMessage(), 'SQLSTATE[42S02')) {
242
                /**
243
                 * table does not exist.
244
                 *
245
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
246
                 */
247 15
                return false;
248
            }
249
250
            throw $e;
251
        }
252
253
        /** @psalm-var ColumnInfoArray $info */
254 92
        foreach ($columns as $info) {
255 92
            $info = $this->normalizeRowKeyCase($info, false);
256
257 92
            $column = $this->loadColumnSchema($info);
258 92
            $table->columns($column->getName(), $column);
259
260 92
            if ($column->isPrimaryKey()) {
261 58
                $table->primaryKey($column->getName());
262 58
                if ($column->isAutoIncrement()) {
263 56
                    $table->sequenceName('');
264
                }
265
            }
266
        }
267
268 92
        return true;
269
    }
270
271
    /**
272
     * Collects the foreign key column details for the given table.
273
     *
274
     * @param TableSchema $table the table metadata.
275
     *
276
     * @throws Exception|Throwable
277
     */
278 92
    protected function findConstraints(TableSchema $table): void
279
    {
280 92
        $sql = <<<SQL
281
        SELECT
282
            `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
283
            `kcu`.`COLUMN_NAME` AS `column_name`,
284
            `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
285
            `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
286
        FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
287
        JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
288
            (
289
                `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
290
                (
291
                    `kcu`.`CONSTRAINT_CATALOG` IS NULL AND
292
                    `rc`.`CONSTRAINT_CATALOG` IS NULL
293
                )
294
            ) AND
295
            `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
296
            `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME` AND
297
            `kcu`.`TABLE_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
298
            `kcu`.`TABLE_NAME` = `rc`.`TABLE_NAME`
299
        WHERE
300
            `rc`.`CONSTRAINT_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
301
            `rc`.`TABLE_NAME` = :tableName
302
        SQL;
303
304
        try {
305 92
            $rows = $this->db->createCommand($sql, [
306 92
                ':schemaName' => $table->getSchemaName(),
307 92
                ':tableName' => $table->getName(),
308 92
            ])->queryAll();
309
310 92
            $constraints = [];
311
312
            /**  @psalm-var RowConstraint $row */
313 92
            foreach ($rows as $row) {
314 25
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
315 25
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
316
            }
317
318 92
            $table->foreignKeys([]);
319
320
            /**
321
             * @var array{referenced_table_name: string, columns: array} $constraint
322
             */
323 92
            foreach ($constraints as $name => $constraint) {
324 25
                $table->foreignKey($name, array_merge(
325 25
                    [$constraint['referenced_table_name']],
326 25
                    $constraint['columns']
327
                ));
328
            }
329
        } catch (Exception $e) {
330
            $previous = $e->getPrevious();
331
332
            if ($previous === null || !str_contains($previous->getMessage(), 'SQLSTATE[42S02')) {
333
                throw $e;
334
            }
335
336
            // table does not exist, try to determine the foreign keys using the table creation sql
337
            $sql = $this->getCreateTableSql($table);
338
            $regexp = '/FOREIGN KEY\s+\(([^)]+)\)\s+REFERENCES\s+([^(^\s]+)\s*\(([^)]+)\)/mi';
339
340
            if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
341
                foreach ($matches as $match) {
342
                    $fks = array_map('trim', explode(',', str_replace('`', '', $match[1])));
343
                    $pks = array_map('trim', explode(',', str_replace('`', '', $match[3])));
344
                    $constraint = [str_replace('`', '', $match[2])];
345
346
                    foreach ($fks as $k => $name) {
347
                        $constraint[$name] = $pks[$k];
348
                    }
349
350
                    $table->foreignKey(md5(serialize($constraint)), $constraint);
351
                }
352
                $table->foreignKeys(array_values($table->getForeignKeys()));
353
            }
354
        }
355
    }
356
357
    /**
358
     * Returns all table names in the database.
359
     *
360
     * This method should be overridden by child classes in order to support this feature because the default
361
     * implementation simply throws an exception.
362
     *
363
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
364
     *
365
     * @throws Exception|InvalidConfigException|Throwable
366
     *
367
     * @return array all table names in the database. The names have NO schema name prefix.
368
     */
369 3
    protected function findTableNames(string $schema = ''): array
370
    {
371 3
        $sql = 'SHOW TABLES';
372
373 3
        if ($schema !== '') {
374
            $sql .= ' FROM ' . $this->db->getQuoter()->quoteSimpleTableName($schema);
375
        }
376
377 3
        $tableNames = $this->db->createCommand($sql)->queryColumn();
378 3
        if (!$tableNames) {
379
            return [];
380
        }
381
382 3
        return $tableNames;
383
    }
384
385
    /**
386
     * Returns the cache key for the specified table name.
387
     *
388
     * @param string $name the table name.
389
     *
390
     * @return array the cache key.
391
     */
392 158
    protected function getCacheKey(string $name): array
393
    {
394 158
        return array_merge([__CLASS__], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
395
    }
396
397
    /**
398
     * Returns the cache tag name.
399
     *
400
     * This allows {@see refresh()} to invalidate all cached table schemas.
401
     *
402
     * @return string the cache tag name.
403
     */
404 158
    protected function getCacheTag(): string
405
    {
406 158
        return md5(serialize(array_merge([__CLASS__], $this->db->getCacheKey())));
407
    }
408
409
    /**
410
     * Gets the CREATE TABLE sql string.
411
     *
412
     * @param TableSchema $table the table metadata.
413
     *
414
     * @throws Exception|InvalidConfigException|Throwable
415
     *
416
     * @return string $sql the result of 'SHOW CREATE TABLE'.
417
     */
418 1
    protected function getCreateTableSql(TableSchema $table): string
419
    {
420 1
        $tableName = $table->getFullName() ?? '';
421
422
        /** @var array<array-key, string> $row */
423 1
        $row = $this->db->createCommand(
424 1
            'SHOW CREATE TABLE ' . $this->db->getQuoter()->quoteTableName($tableName)
425 1
        )->queryOne();
426
427 1
        if (isset($row['Create Table'])) {
428 1
            $sql = $row['Create Table'];
429
        } else {
430
            $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

430
            $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
431
            $sql = $row[1];
432
        }
433
434 1
        return $sql;
435
    }
436
437
    /**
438
     * Loads the column information into a {@see ColumnSchema} object.
439
     *
440
     * @param array $info column information.
441
     *
442
     * @throws JsonException
443
     *
444
     * @return ColumnSchema the column schema object.
445
     */
446 93
    protected function loadColumnSchema(array $info): ColumnSchema
447
    {
448 93
        $column = $this->createColumnSchema();
449
450
        /** @psalm-var ColumnInfoArray $info */
451 93
        $column->name($info['field']);
452 93
        $column->allowNull($info['null'] === 'YES');
453 93
        $column->primaryKey(str_contains($info['key'], 'PRI'));
454 93
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
455 93
        $column->comment($info['comment']);
456 93
        $column->dbType($info['type']);
457 93
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
458 93
        $column->type(self::TYPE_STRING);
459
460 93
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
461 93
            $type = strtolower($matches[1]);
462
463 93
            if (isset($this->typeMap[$type])) {
464 93
                $column->type($this->typeMap[$type]);
465
            }
466
467 93
            if (!empty($matches[2])) {
468 85
                if ($type === 'enum') {
469 20
                    preg_match_all("/'[^']*'/", $matches[2], $values);
470
471 20
                    foreach ($values[0] as $i => $value) {
472 20
                        $values[$i] = trim($value, "'");
473
                    }
474
475 20
                    $column->enumValues($values);
476
                } else {
477 85
                    $values = explode(',', $matches[2]);
478 85
                    $column->precision((int) $values[0]);
479 85
                    $column->size((int) $values[0]);
480
481 85
                    if (isset($values[1])) {
482 32
                        $column->scale((int) $values[1]);
483
                    }
484
485 85
                    if ($column->getSize() === 1 && $type === 'tinyint') {
486 21
                        $column->type('boolean');
487 85
                    } elseif ($type === 'bit') {
488 20
                        if ($column->getSize() > 32) {
489
                            $column->type('bigint');
490 20
                        } elseif ($column->getSize() === 32) {
491
                            $column->type('integer');
492
                        }
493
                    }
494
                }
495
            }
496
        }
497
498 93
        $column->phpType($this->getColumnPhpType($column));
499
500 93
        if (!$column->isPrimaryKey()) {
501
            /**
502
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
503
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
504
             *
505
             * See details here: https://mariadb.com/kb/en/library/now/#description
506
             */
507
            if (
508 90
                ($column->getType() === 'timestamp' || $column->getType() === 'datetime')
509 90
                && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', (string) $info['default'], $matches)
510
            ) {
511 23
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
512 23
                    ? '(' . $matches[1] . ')' : '')));
513 87
            } elseif (isset($type) && $type === 'bit') {
514 20
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
515
            } else {
516 87
                $column->defaultValue($column->phpTypecast($info['default']));
517
            }
518
        }
519
520 93
        return $column;
521
    }
522
523
    /**
524
     * Loads all check constraints for the given table.
525
     *
526
     * @param string $tableName table name.
527
     *
528
     * @throws NotSupportedException
529
     *
530
     * @return array check constraints for the given table.
531
     */
532 12
    protected function loadTableChecks(string $tableName): array
533
    {
534 12
        throw new NotSupportedException('MySQL does not support check constraints.');
535
    }
536
537
    /**
538
     * Loads multiple types of constraints and returns the specified ones.
539
     *
540
     * @param string $tableName table name.
541
     * @param string $returnType return type:
542
     * - primaryKey
543
     * - foreignKeys
544
     * - uniques
545
     *
546
     * @throws Exception|InvalidConfigException|Throwable
547
     *
548
     * @return array|Constraint|null (Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
549
     */
550 48
    private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null
551
    {
552 48
        $sql = <<<SQL
553
        SELECT
554
            `kcu`.`CONSTRAINT_NAME` AS `name`,
555
            `kcu`.`COLUMN_NAME` AS `column_name`,
556
            `tc`.`CONSTRAINT_TYPE` AS `type`,
557
        CASE
558
            WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
559
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
560
        END AS `foreign_table_schema`,
561
            `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
562
            `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
563
            `rc`.`UPDATE_RULE` AS `on_update`,
564
            `rc`.`DELETE_RULE` AS `on_delete`,
565
            `kcu`.`ORDINAL_POSITION` AS `position`
566
        FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
567
        JOIN `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc` ON
568
            `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
569
            `rc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
570
            `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
571
        JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON
572
            `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
573
            `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
574
            `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND
575
            `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY'
576
        WHERE
577
            `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
578
            `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
579
            `kcu`.`TABLE_NAME` = :tableName
580
        UNION
581
        SELECT
582
            `kcu`.`CONSTRAINT_NAME` AS `name`,
583
            `kcu`.`COLUMN_NAME` AS `column_name`,
584
            `tc`.`CONSTRAINT_TYPE` AS `type`,
585
        NULL AS `foreign_table_schema`,
586
        NULL AS `foreign_table_name`,
587
        NULL AS `foreign_column_name`,
588
        NULL AS `on_update`,
589
        NULL AS `on_delete`,
590
            `kcu`.`ORDINAL_POSITION` AS `position`
591
        FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
592
        JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON
593
            `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
594
            `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
595
            `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND
596
            `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE')
597
        WHERE
598
            `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
599
            `kcu`.`TABLE_NAME` = :tableName
600
        ORDER BY `position` ASC
601
        SQL;
602
603 48
        $resolvedName = $this->resolveTableName($tableName);
604
605 48
        $constraints = $this->db->createCommand($sql, [
606 48
            ':schemaName' => $resolvedName->getSchemaName(),
607 48
            ':tableName' => $resolvedName->getName(),
608 48
        ])->queryAll();
609
610
        /** @var array<array-key, array> $constraints */
611 48
        $constraints = $this->normalizeRowKeyCase($constraints, true);
612 48
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
613
614 48
        $result = [
615
            'primaryKey' => null,
616
            'foreignKeys' => [],
617
            'uniques' => [],
618
        ];
619
620
        /**
621
         * @var string $type
622
         * @var array $names
623
         */
624 48
        foreach ($constraints as $type => $names) {
625
            /**
626
             * @psalm-var object|string|null $name
627
             * @psalm-var ConstraintArray $constraint
628
             */
629 48
            foreach ($names as $name => $constraint) {
630 48
                switch ($type) {
631 48
                    case 'PRIMARY KEY':
632 37
                        $ct = (new Constraint())
633 37
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
634
635 37
                        $result['primaryKey'] = $ct;
636
637 37
                        break;
638 46
                    case 'FOREIGN KEY':
639 10
                        $fk = (new ForeignKeyConstraint())
640 10
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
641 10
                            ->foreignTableName($constraint[0]['foreign_table_name'])
642 10
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
643 10
                            ->onDelete($constraint[0]['on_delete'])
644 10
                            ->onUpdate($constraint[0]['on_update'])
645 10
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
646 10
                            ->name($name);
647
648 10
                        $result['foreignKeys'][] = $fk;
649
650 10
                        break;
651 37
                    case 'UNIQUE':
652 37
                        $ct = (new Constraint())
653 37
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
654 37
                            ->name($name);
655
656 37
                        $result['uniques'][] = $ct;
657
658 37
                        break;
659
                }
660
            }
661
        }
662
663 48
        foreach ($result as $type => $data) {
664 48
            $this->setTableMetadata($tableName, $type, $data);
665
        }
666
667 48
        return $result[$returnType];
668
    }
669
670
    /**
671
     * Loads all default value constraints for the given table.
672
     *
673
     * @param string $tableName table name.
674
     *
675
     * @throws NotSupportedException
676
     *
677
     * @return array default value constraints for the given table.
678
     */
679 12
    protected function loadTableDefaultValues(string $tableName): array
680
    {
681 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
682
    }
683
684
    /**
685
     * Loads all foreign keys for the given table.
686
     *
687
     * @param string $tableName table name.
688
     *
689
     * @throws Exception|InvalidConfigException|Throwable
690
     *
691
     * @return array foreign keys for the given table.
692
     */
693 4
    protected function loadTableForeignKeys(string $tableName): array
694
    {
695 4
        $tableForeignKeys = $this->loadTableConstraints($tableName, 'foreignKeys');
696
697 4
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
698
    }
699
700
    /**
701
     * Loads all indexes for the given table.
702
     *
703
     * @param string $tableName table name.
704
     *
705
     * @throws Exception|InvalidConfigException|Throwable
706
     *
707
     * @return IndexConstraint[] indexes for the given table.
708
     */
709 28
    protected function loadTableIndexes(string $tableName): array
710
    {
711 28
        $sql = <<<SQL
712
        SELECT
713
            `s`.`INDEX_NAME` AS `name`,
714
            `s`.`COLUMN_NAME` AS `column_name`,
715
            `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
716
            `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
717
        FROM `information_schema`.`STATISTICS` AS `s`
718
        WHERE
719
            `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
720
            `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND
721
            `s`.`TABLE_NAME` = :tableName
722
        ORDER BY `s`.`SEQ_IN_INDEX` ASC
723
        SQL;
724
725 28
        $resolvedName = $this->resolveTableName($tableName);
726
727 28
        $indexes = $this->db->createCommand($sql, [
728 28
            ':schemaName' => $resolvedName->getSchemaName(),
729 28
            ':tableName' => $resolvedName->getName(),
730 28
        ])->queryAll();
731
732
        /** @var array[] $indexes */
733 28
        $indexes = $this->normalizeRowKeyCase($indexes, true);
734 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
735 28
        $result = [];
736
737
        /**
738
         * @psalm-var object|string|null $name
739
         * @psalm-var array[] $index
740
         */
741 28
        foreach ($indexes as $name => $index) {
742 28
            $ic = new IndexConstraint();
743
744 28
            $ic->primary((bool) $index[0]['index_is_primary']);
745 28
            $ic->unique((bool) $index[0]['index_is_unique']);
746 28
            $ic->name($name !== 'PRIMARY' ? $name : null);
747 28
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
748
749 28
            $result[] = $ic;
750
        }
751
752 28
        return $result;
753
    }
754
755
    /**
756
     * Loads a primary key for the given table.
757
     *
758
     * @param string $tableName table name.
759
     *
760
     * @throws Exception|InvalidConfigException|Throwable
761
     *
762
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.*
763
     */
764 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
765
    {
766 31
        $tablePrimaryKey = $this->loadTableConstraints($tableName, 'primaryKey');
767
768 31
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
769
    }
770
771
    /**
772
     * Loads the metadata for the specified table.
773
     *
774
     * @param string $name table name.
775
     *
776
     * @throws Exception|Throwable
777
     *
778
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
779
     */
780 98
    protected function loadTableSchema(string $name): ?TableSchema
781
    {
782 98
        $table = new TableSchema();
783
784 98
        $this->resolveTableNames($table, $name);
785
786 98
        if ($this->findColumns($table)) {
787 92
            $this->findConstraints($table);
788
789 92
            return $table;
790
        }
791
792 15
        return null;
793
    }
794
795
    /**
796
     * Loads all unique constraints for the given table.
797
     *
798
     * @param string $tableName table name.
799
     *
800
     * @throws Exception|InvalidConfigException|Throwable
801
     *
802
     * @return array unique constraints for the given table.
803
     */
804 13
    protected function loadTableUniques(string $tableName): array
805
    {
806 13
        $tableUniques = $this->loadTableConstraints($tableName, 'uniques');
807
808 13
        return is_array($tableUniques) ? $tableUniques : [];
809
    }
810
811
    /**
812
     * Changes row's array key case to lower.
813
     *
814
     * @param array $row row's array or an array of row's arrays.
815
     * @param bool $multiple whether multiple rows or a single row passed.
816
     *
817
     * @return array normalized row or rows.
818
     */
819 132
    protected function normalizeRowKeyCase(array $row, bool $multiple): array
820
    {
821 132
        if ($multiple) {
822 58
            return array_map(static function (array $row) {
823 58
                return array_change_key_case($row, CASE_LOWER);
824
            }, $row);
825
        }
826
827 92
        return array_change_key_case($row, CASE_LOWER);
828
    }
829
830
    /**
831
     * Resolves the table name and schema name (if any).
832
     *
833
     * @param string $name the table name.
834
     *
835
     * @return TableSchema
836
     *
837
     * {@see TableSchema}
838
     */
839 58
    protected function resolveTableName(string $name): TableSchema
840
    {
841 58
        $resolvedName = new TableSchema();
842
843 58
        $parts = explode('.', str_replace('`', '', $name));
844
845 58
        if (isset($parts[1])) {
846
            $resolvedName->schemaName($parts[0]);
847
            $resolvedName->name($parts[1]);
848
        } else {
849 58
            $resolvedName->schemaName($this->defaultSchema);
850 58
            $resolvedName->name($name);
851
        }
852
853 58
        $resolvedName->fullName(($resolvedName->getSchemaName() !== $this->defaultSchema ?
854 58
            (string) $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName());
855
856 58
        return $resolvedName;
857
    }
858
859
    /**
860
     * Resolves the table name and schema name (if any).
861
     *
862
     * @param TableSchema $table the table metadata object.
863
     * @param string $name the table name.
864
     */
865 98
    protected function resolveTableNames(TableSchema $table, string $name): void
866
    {
867 98
        $parts = explode('.', str_replace('`', '', $name));
868
869 98
        if (isset($parts[1])) {
870
            $table->schemaName($parts[0]);
871
            $table->name($parts[1]);
872
            $table->fullName((string) $table->getSchemaName() . '.' . $table->getName());
873
        } else {
874 98
            $table->name($parts[0]);
875 98
            $table->fullName($parts[0]);
876
        }
877
    }
878
879
    /**
880
     * Creates a column schema for the database.
881
     *
882
     * This method may be overridden by child classes to create a DBMS-specific column schema.
883
     *
884
     * @return ColumnSchema column schema instance.
885
     */
886 93
    private function createColumnSchema(): ColumnSchema
887
    {
888 93
        return new ColumnSchema();
889
    }
890
}
891