Passed
Pull Request — master (#205)
by Wilmer
03:23
created

Schema::loadColumnSchema()   F

Complexity

Conditions 25
Paths 750

Size

Total Lines 98
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 25.1133

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 25
eloc 56
c 4
b 0
f 0
nc 750
nop 1
dl 0
loc 98
ccs 50
cts 53
cp 0.9434
crap 25.1133
rs 0.3472

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

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