Passed
Pull Request — master (#232)
by Def
03:50
created

Schema::findColumns()   C

Complexity

Conditions 12
Paths 46

Size

Total Lines 65
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 17.3317

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 12
eloc 34
c 2
b 0
f 1
nc 46
nop 1
dl 0
loc 65
ccs 24
cts 36
cp 0.6667
crap 17.3317
rs 6.9666

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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