Passed
Pull Request — master (#160)
by Wilmer
06:50 queued 03:57
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\Constraint\Constraint;
11
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
12
use Yiisoft\Db\Constraint\IndexConstraint;
13
use Yiisoft\Db\Exception\Exception;
14
use Yiisoft\Db\Exception\InvalidConfigException;
15
use Yiisoft\Db\Exception\NotSupportedException;
16
use Yiisoft\Db\Expression\Expression;
17
use Yiisoft\Db\Schema\ColumnSchemaInterface;
18
use Yiisoft\Db\Schema\Schema as AbstractSchema;
19
use Yiisoft\Db\Schema\TableSchemaInterface;
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
    /**
130
     * Create a column schema builder instance giving the type and value precision.
131
     *
132
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
133
     *
134
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
135
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
136
     *
137
     * @return ColumnSchemaBuilder column schema builder instance
138
     *
139
     * @psalm-param string[]|int|string|null $length
140
     */
141 3
    public function createColumnSchemaBuilder(string $type, array|int|string $length = null): ColumnSchemaBuilder
142
    {
143 3
        return new ColumnSchemaBuilder($type, $length, $this->db->getQuoter());
144
    }
145
146
    /**
147
     * Returns all unique indexes for the given table.
148
     *
149
     * Each array element is of the following structure:
150
     *
151
     * ```php
152
     * [
153
     *     'IndexName1' => ['col1' [, ...]],
154
     *     'IndexName2' => ['col2' [, ...]],
155
     * ]
156
     * ```
157
     *
158
     * @param TableSchemaInterface $table the table metadata.
159
     *
160
     * @throws Exception|InvalidConfigException|Throwable
161
     *
162
     * @return array all unique indexes for the given table.
163
     */
164 1
    public function findUniqueIndexes(TableSchemaInterface $table): array
165
    {
166 1
        $sql = $this->getCreateTableSql($table);
167
168 1
        $uniqueIndexes = [];
169
170 1
        $regexp = '/UNIQUE KEY\s+`(.+)`\s*\((`.+`)+\)/mi';
171
172 1
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
173 1
            foreach ($matches as $match) {
174 1
                $indexName = $match[1];
175 1
                $indexColumns = array_map('trim', explode('`,`', trim($match[2], '`')));
176 1
                $uniqueIndexes[$indexName] = $indexColumns;
177
            }
178
        }
179
180 1
        return $uniqueIndexes;
181
    }
182
183
    /**
184
     * Collects the metadata of table columns.
185
     *
186
     * @param TableSchemaInterface $table the table metadata.
187
     *
188
     * @throws Exception|Throwable if DB query fails.
189
     *
190
     * @return bool whether the table exists in the database.
191
     */
192 131
    protected function findColumns(TableSchemaInterface $table): bool
193
    {
194 131
        $tableName = $table->getFullName() ?? '';
195 131
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->db->getQuoter()->quoteTableName($tableName);
196
197
        try {
198 131
            $columns = $this->db->createCommand($sql)->queryAll();
199 24
        } catch (Exception $e) {
200 24
            $previous = $e->getPrevious();
201
202 24
            if ($previous && str_contains($previous->getMessage(), 'SQLSTATE[42S02')) {
203
                /**
204
                 * table does not exist.
205
                 *
206
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
207
                 */
208 24
                return false;
209
            }
210
211
            throw $e;
212
        }
213
214
        /** @psalm-var ColumnInfoArray $info */
215 116
        foreach ($columns as $info) {
216 116
            $info = $this->normalizeRowKeyCase($info, false);
217
218 116
            $column = $this->loadColumnSchema($info);
219 116
            $table->columns($column->getName(), $column);
220
221 116
            if ($column->isPrimaryKey()) {
222 77
                $table->primaryKey($column->getName());
223 77
                if ($column->isAutoIncrement()) {
224 65
                    $table->sequenceName('');
225
                }
226
            }
227
        }
228
229 116
        return true;
230
    }
231
232
    /**
233
     * Collects the foreign key column details for the given table.
234
     *
235
     * @param TableSchemaInterface $table the table metadata.
236
     *
237
     * @throws Exception|Throwable
238
     */
239 116
    protected function findConstraints(TableSchemaInterface $table): void
240
    {
241 116
        $sql = <<<SQL
242
        SELECT
243
            `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
244
            `kcu`.`COLUMN_NAME` AS `column_name`,
245
            `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
246
            `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
247
        FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
248
        JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
249
            (
250
                `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
251
                (
252
                    `kcu`.`CONSTRAINT_CATALOG` IS NULL AND
253
                    `rc`.`CONSTRAINT_CATALOG` IS NULL
254
                )
255
            ) AND
256
            `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
257
            `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME` AND
258
            `kcu`.`TABLE_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
259
            `kcu`.`TABLE_NAME` = `rc`.`TABLE_NAME`
260
        WHERE
261
            `rc`.`CONSTRAINT_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
262
            `rc`.`TABLE_NAME` = :tableName
263
        SQL;
264
265
        try {
266 116
            $rows = $this->db->createCommand($sql, [
267 116
                ':schemaName' => $table->getSchemaName(),
268 116
                ':tableName' => $table->getName(),
269 116
            ])->queryAll();
270
271 116
            $constraints = [];
272
273
            /**  @psalm-var RowConstraint $row */
274 116
            foreach ($rows as $row) {
275 35
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
276 35
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
277
            }
278
279 116
            $table->foreignKeys([]);
280
281
            /**
282
             * @var array{referenced_table_name: string, columns: array} $constraint
283
             */
284 116
            foreach ($constraints as $name => $constraint) {
285 35
                $table->foreignKey($name, array_merge(
286 35
                    [$constraint['referenced_table_name']],
287 35
                    $constraint['columns']
288
                ));
289
            }
290
        } catch (Exception $e) {
291
            $previous = $e->getPrevious();
292
293
            if ($previous === null || !str_contains($previous->getMessage(), 'SQLSTATE[42S02')) {
294
                throw $e;
295
            }
296
297
            // table does not exist, try to determine the foreign keys using the table creation sql
298
            $sql = $this->getCreateTableSql($table);
299
            $regexp = '/FOREIGN KEY\s+\(([^)]+)\)\s+REFERENCES\s+([^(^\s]+)\s*\(([^)]+)\)/mi';
300
301
            if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
302
                foreach ($matches as $match) {
303
                    $fks = array_map('trim', explode(',', str_replace('`', '', $match[1])));
304
                    $pks = array_map('trim', explode(',', str_replace('`', '', $match[3])));
305
                    $constraint = [str_replace('`', '', $match[2])];
306
307
                    foreach ($fks as $k => $name) {
308
                        $constraint[$name] = $pks[$k];
309
                    }
310
311
                    $table->foreignKey(md5(serialize($constraint)), $constraint);
312
                }
313
                $table->foreignKeys(array_values($table->getForeignKeys()));
314
            }
315
        }
316
    }
317
318
    /**
319
     * Returns all table names in the database.
320
     *
321
     * This method should be overridden by child classes in order to support this feature because the default
322
     * implementation simply throws an exception.
323
     *
324
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
325
     *
326
     * @throws Exception|InvalidConfigException|Throwable
327
     *
328
     * @return array All table names in the database. The names have NO schema name prefix.
329
     */
330 7
    protected function findTableNames(string $schema = ''): array
331
    {
332 7
        $sql = 'SHOW TABLES';
333
334 7
        if ($schema !== '') {
335
            $sql .= ' FROM ' . $this->db->getQuoter()->quoteSimpleTableName($schema);
336
        }
337
338 7
        return $this->db->createCommand($sql)->queryColumn();
339
    }
340
341
    /**
342
     * Returns the cache key for the specified table name.
343
     *
344
     * @param string $name the table name.
345
     *
346
     * @return array the cache key.
347
     */
348 200
    protected function getCacheKey(string $name): array
349
    {
350 200
        return array_merge([self::class], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
351
    }
352
353
    /**
354
     * Returns the cache tag name.
355
     *
356
     * This allows {@see refresh()} to invalidate all cached table schemas.
357
     *
358
     * @return string the cache tag name.
359
     */
360 200
    protected function getCacheTag(): string
361
    {
362 200
        return md5(serialize(array_merge([self::class], $this->db->getCacheKey())));
363
    }
364
365
    /**
366
     * Gets the CREATE TABLE sql string.
367
     *
368
     * @param TableSchemaInterface $table the table metadata.
369
     *
370
     * @throws Exception|InvalidConfigException|Throwable
371
     *
372
     * @return string $sql the result of 'SHOW CREATE TABLE'.
373
     */
374 131
    protected function getCreateTableSql(TableSchemaInterface $table): string
375
    {
376 131
        $tableName = $table->getFullName() ?? '';
377
378
        try {
379
            /** @var array<array-key, string> $row */
380 131
            $row = $this->db->createCommand(
381 131
                'SHOW CREATE TABLE ' . $this->db->getQuoter()->quoteTableName($tableName)
382 131
            )->queryOne();
383
384 116
            if (isset($row['Create Table'])) {
385 114
                $sql = $row['Create Table'];
386
            } else {
387 2
                $row = array_values($row);
0 ignored issues
show
Bug introduced by
$row of type null 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

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