Passed
Push — master ( fd9471...8dbd0e )
by Wilmer
27:59 queued 25:06
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 104
    protected function findColumns(TableSchemaInterface $table): bool
193
    {
194 104
        $tableName = $table->getFullName() ?? '';
195 104
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->db->getQuoter()->quoteTableName($tableName);
196
197
        try {
198 104
            $columns = $this->db->createCommand($sql)->queryAll();
199 15
        } catch (Exception $e) {
200 15
            $previous = $e->getPrevious();
201
202 15
            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 15
                return false;
209
            }
210
211
            throw $e;
212
        }
213
214
        /** @psalm-var ColumnInfoArray $info */
215 98
        foreach ($columns as $info) {
216 98
            $info = $this->normalizeRowKeyCase($info, false);
217
218 98
            $column = $this->loadColumnSchema($info);
219 98
            $table->columns($column->getName(), $column);
220
221 98
            if ($column->isPrimaryKey()) {
222 62
                $table->primaryKey($column->getName());
223 62
                if ($column->isAutoIncrement()) {
224 60
                    $table->sequenceName('');
225
                }
226
            }
227
        }
228
229 98
        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 98
    protected function findConstraints(TableSchemaInterface $table): void
240
    {
241 98
        $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 98
            $rows = $this->db->createCommand($sql, [
267 98
                ':schemaName' => $table->getSchemaName(),
268 98
                ':tableName' => $table->getName(),
269 98
            ])->queryAll();
270
271 98
            $constraints = [];
272
273
            /**  @psalm-var RowConstraint $row */
274 98
            foreach ($rows as $row) {
275 27
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
276 27
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
277
            }
278
279 98
            $table->foreignKeys([]);
280
281
            /**
282
             * @var array{referenced_table_name: string, columns: array} $constraint
283
             */
284 98
            foreach ($constraints as $name => $constraint) {
285 27
                $table->foreignKey($name, array_merge(
286 27
                    [$constraint['referenced_table_name']],
287 27
                    $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 173
    protected function getCacheKey(string $name): array
349
    {
350 173
        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 173
    protected function getCacheTag(): string
361
    {
362 173
        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 104
    protected function getCreateTableSql(TableSchemaInterface $table): string
375
    {
376 104
        $tableName = $table->getFullName() ?? '';
377
378
        try {
379
            /** @var array<array-key, string> $row */
380 104
            $row = $this->db->createCommand(
381 104
                'SHOW CREATE TABLE ' . $this->db->getQuoter()->quoteTableName($tableName)
382 104
            )->queryOne();
383
384 98
            if (isset($row['Create Table'])) {
385 96
                $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 98
                $sql = $row[1];
389
            }
390 15
        } catch (Exception) {
391 15
            $sql = '';
392
        }
393
394 104
        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 99
    protected function loadColumnSchema(array $info): ColumnSchemaInterface
407
    {
408 99
        $column = $this->createColumnSchema();
409
410
        /** @psalm-var ColumnInfoArray $info */
411 99
        $column->name($info['field']);
412 99
        $column->allowNull($info['null'] === 'YES');
413 99
        $column->primaryKey(str_contains($info['key'], 'PRI'));
414 99
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
415 99
        $column->comment($info['comment']);
416 99
        $column->dbType($info['type']);
417 99
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
418 99
        $column->type(self::TYPE_STRING);
419
420 99
        $extra = $info['extra'];
421 99
        if (str_starts_with($extra, 'DEFAULT_GENERATED')) {
422 24
            $extra = strtoupper(substr($extra, 18));
423
        }
424 99
        $column->extra(trim($extra));
425
426 99
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
427 99
            $type = strtolower($matches[1]);
428
429 99
            if (isset($this->typeMap[$type])) {
430 99
                $column->type($this->typeMap[$type]);
431
            }
432
433 99
            if (!empty($matches[2])) {
434 91
                if ($type === 'enum') {
435 22
                    preg_match_all("/'[^']*'/", $matches[2], $values);
436
437 22
                    foreach ($values[0] as $i => $value) {
438 22
                        $values[$i] = trim($value, "'");
439
                    }
440
441 22
                    $column->enumValues($values);
442
                } else {
443 91
                    $values = explode(',', $matches[2]);
444 91
                    $column->precision((int) $values[0]);
445 91
                    $column->size((int) $values[0]);
446
447 91
                    if (isset($values[1])) {
448 34
                        $column->scale((int) $values[1]);
449
                    }
450
451 91
                    if ($column->getSize() === 1 && $type === 'tinyint') {
452 23
                        $column->type(self::TYPE_BOOLEAN);
453 91
                    } elseif ($type === 'bit') {
454 22
                        if ($column->getSize() > 32) {
455
                            $column->type(self::TYPE_BIGINT);
456 22
                        } elseif ($column->getSize() === 32) {
457
                            $column->type(self::TYPE_INTEGER);
458
                        }
459
                    }
460
                }
461
            }
462
        }
463
464 99
        $column->phpType($this->getColumnPhpType($column));
465
466 99
        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 96
                ($column->getType() === 'timestamp' || $column->getType() === 'datetime')
475 96
                && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', (string) $info['default'], $matches)
476
            ) {
477 25
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
478 25
                    ? '(' . $matches[1] . ')' : '')));
479 93
            } elseif (isset($type) && $type === 'bit') {
480 22
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
481
            } else {
482 96
                $column->defaultValue($column->phpTypecast($info['default']));
483
            }
484 62
        } elseif ($info['default'] !== null) {
485 1
            $column->defaultValue($column->phpTypecast($info['default']));
486
        }
487
488 99
        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 12
    protected function loadTableChecks(string $tableName): array
501
    {
502 12
        throw new NotSupportedException('MySQL does not support check constraints.');
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 56
    private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null
519
    {
520 56
        $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 56
        $resolvedName = $this->resolveTableName($tableName);
572
573 56
        $constraints = $this->db->createCommand($sql, [
574 56
            ':schemaName' => $resolvedName->getSchemaName(),
575 56
            ':tableName' => $resolvedName->getName(),
576 56
        ])->queryAll();
577
578
        /** @var array<array-key, array> $constraints */
579 56
        $constraints = $this->normalizeRowKeyCase($constraints, true);
580 56
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
581
582 56
        $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 56
        foreach ($constraints as $type => $names) {
593
            /**
594
             * @psalm-var object|string|null $name
595
             * @psalm-var ConstraintArray $constraint
596
             */
597 51
            foreach ($names as $name => $constraint) {
598
                switch ($type) {
599 51
                    case 'PRIMARY KEY':
600 40
                        $result[self::PRIMARY_KEY] = (new Constraint())
601 40
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
602 40
                        break;
603 49
                    case 'FOREIGN KEY':
604 13
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
605 13
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
606 13
                            ->foreignTableName($constraint[0]['foreign_table_name'])
607 13
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
608 13
                            ->onDelete($constraint[0]['on_delete'])
609 13
                            ->onUpdate($constraint[0]['on_update'])
610 13
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
611 13
                            ->name($name);
612 13
                        break;
613 40
                    case 'UNIQUE':
614 40
                        $result[self::UNIQUES][] = (new Constraint())
615 40
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
616 40
                            ->name($name);
617 40
                        break;
618
                }
619
            }
620
        }
621
622 56
        foreach ($result as $type => $data) {
623 56
            $this->setTableMetadata($tableName, $type, $data);
624
        }
625
626 56
        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 12
    protected function loadTableDefaultValues(string $tableName): array
639
    {
640 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
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 5
    protected function loadTableForeignKeys(string $tableName): array
653
    {
654 5
        $tableForeignKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
655
656 5
        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 29
    protected function loadTableIndexes(string $tableName): array
669
    {
670 29
        $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 29
        $resolvedName = $this->resolveTableName($tableName);
685
686 29
        $indexes = $this->db->createCommand($sql, [
687 29
            ':schemaName' => $resolvedName->getSchemaName(),
688 29
            ':tableName' => $resolvedName->getName(),
689 29
        ])->queryAll();
690
691
        /** @var array[] $indexes */
692 29
        $indexes = $this->normalizeRowKeyCase($indexes, true);
693 29
        $indexes = ArrayHelper::index($indexes, null, 'name');
694 29
        $result = [];
695
696
        /**
697
         * @psalm-var object|string|null $name
698
         * @psalm-var array[] $index
699
         */
700 29
        foreach ($indexes as $name => $index) {
701 29
            $ic = new IndexConstraint();
702
703 29
            $ic->primary((bool) $index[0]['index_is_primary']);
704 29
            $ic->unique((bool) $index[0]['index_is_unique']);
705 29
            $ic->name($name !== 'PRIMARY' ? $name : null);
706 29
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
707
708 29
            $result[] = $ic;
709
        }
710
711 29
        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 37
    protected function loadTablePrimaryKey(string $tableName): Constraint|null
724
    {
725 37
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
726
727 37
        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 104
    protected function loadTableSchema(string $name): TableSchemaInterface|null
740
    {
741 104
        $table = $this->resolveTableName($name);
742 104
        $this->resolveTableCreateSql($table);
743
744 104
        if ($this->findColumns($table)) {
745 98
            $this->findConstraints($table);
746
747 98
            return $table;
748
        }
749
750 15
        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 14
    protected function loadTableUniques(string $tableName): array
763
    {
764 14
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
765
766 14
        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 147
    protected function normalizeRowKeyCase(array $row, bool $multiple): array
778
    {
779 147
        if ($multiple) {
780 67
            return array_map(static fn (array $row) => array_change_key_case($row, CASE_LOWER), $row);
781
        }
782
783 98
        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 149
    protected function resolveTableName(string $name): TableSchemaInterface
794
    {
795 149
        $resolvedName = new TableSchema();
796
797 149
        $parts = array_reverse(
798 149
            $this->db->getQuoter()->getTableNameParts($name)
799
        );
800
801 149
        $resolvedName->name($parts[0] ?? '');
802 149
        $resolvedName->schemaName($parts[1] ?? $this->defaultSchema);
803
804 149
        $resolvedName->fullName(
805 149
            $resolvedName->getSchemaName() !== $this->defaultSchema ?
806 149
            implode('.', array_reverse($parts)) : $resolvedName->getName()
807
        );
808
809 149
        return $resolvedName;
810
    }
811
812
    /**
813
     * @throws Exception|InvalidConfigException|Throwable
814
     */
815 104
    protected function resolveTableCreateSql(TableSchemaInterface $table): void
816
    {
817 104
        $sql = $this->getCreateTableSql($table);
818 104
        $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 99
    private function createColumnSchema(): ColumnSchema
829
    {
830 99
        return new ColumnSchema();
831
    }
832
}
833