Passed
Pull Request — master (#206)
by Wilmer
04:11
created

Schema::loadColumnSchema()   F

Complexity

Conditions 25
Paths 750

Size

Total Lines 95
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 25.1133

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 25
eloc 56
c 1
b 0
f 0
nc 750
nop 1
dl 0
loc 95
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
    private array $columnOverrides = [];
132
    private string|null $currentTable = null;
133
134
    /**
135
     * Create a column schema builder instance giving the type and value precision.
136
     *
137
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
138
     *
139
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
140
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
141
     *
142
     * @return ColumnSchemaBuilder column schema builder instance
143
     *
144
     * @psalm-param string[]|int[]|int|string|null $length
145
     */
146 10
    public function createColumnSchemaBuilder(string $type, array|int|string $length = null): ColumnSchemaBuilder
147
    {
148 10
        return new ColumnSchemaBuilder($type, $length, $this->db->getQuoter());
149
    }
150
151
    /**
152
     * Returns all unique indexes for the given table.
153
     *
154
     * Each array element is of the following structure:
155
     *
156
     * ```php
157
     * [
158
     *     'IndexName1' => ['col1' [, ...]],
159
     *     'IndexName2' => ['col2' [, ...]],
160
     * ]
161
     * ```
162
     *
163
     * @param TableSchemaInterface $table the table metadata.
164
     *
165
     * @throws Exception
166
     * @throws InvalidConfigException
167
     * @throws Throwable
168
     *
169
     * @return array all unique indexes for the given table.
170
     */
171 1
    public function findUniqueIndexes(TableSchemaInterface $table): array
172
    {
173 1
        $sql = $this->getCreateTableSql($table);
174
175 1
        $uniqueIndexes = [];
176
177 1
        $regexp = '/UNIQUE KEY\s+`(.+)`\s*\((`.+`)+\)/mi';
178
179 1
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
180 1
            foreach ($matches as $match) {
181 1
                $indexName = $match[1];
182 1
                $indexColumns = array_map('trim', explode('`,`', trim($match[2], '`')));
183 1
                $uniqueIndexes[$indexName] = $indexColumns;
184
            }
185
        }
186
187 1
        ksort($uniqueIndexes);
188
189 1
        return $uniqueIndexes;
190
    }
191
192
    /**
193
     * Collects the metadata of table columns.
194
     *
195
     * @param TableSchemaInterface $table the table metadata.
196
     *
197
     * @throws Exception
198
     * @throws Throwable if DB query fails.
199
     *
200
     * @return bool whether the table exists in the database.
201
     */
202 147
    protected function findColumns(TableSchemaInterface $table): bool
203
    {
204 147
        $tableName = $table->getFullName() ?? '';
205 147
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->db->getQuoter()->quoteTableName($tableName);
206
207 147
        $this->currentTable = $tableName;
208
209
        try {
210 147
            $columns = $this->db->createCommand($sql)->queryAll();
211
212 130
            $this->jsonColumns[$this->currentTable] = array_merge(
213 130
                $this->columnOverrides[$tableName] ?? [],
214 130
                $this->getJsonColumns($table),
215 130
            );
216 30
        } catch (Exception $e) {
217 30
            $previous = $e->getPrevious();
218
219 30
            if ($previous && str_contains($previous->getMessage(), 'SQLSTATE[42S02')) {
220
                /**
221
                 * table does not exist.
222
                 *
223
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
224
                 */
225 30
                return false;
226
            }
227
228
            throw $e;
229
        }
230
231
        /** @psalm-var ColumnInfoArray $info */
232 130
        foreach ($columns as $info) {
233 130
            $info = $this->normalizeRowKeyCase($info, false);
234
235 130
            $column = $this->loadColumnSchema($info);
236 130
            $table->columns($column->getName(), $column);
237
238 130
            if ($column->isPrimaryKey()) {
239 82
                $table->primaryKey($column->getName());
240 82
                if ($column->isAutoIncrement()) {
241 68
                    $table->sequenceName('');
242
                }
243
            }
244
        }
245
246 130
        return true;
247
    }
248
249
    /**
250
     * Collects the foreign key column details for the given table.
251
     *
252
     * @param TableSchemaInterface $table the table metadata.
253
     *
254
     * @throws Exception
255
     * @throws InvalidConfigException
256
     * @throws Throwable
257
     */
258 130
    protected function findConstraints(TableSchemaInterface $table): void
259
    {
260 130
        $sql = <<<SQL
261
        SELECT
262
            `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
263
            `kcu`.`COLUMN_NAME` AS `column_name`,
264
            `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
265
            `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
266
        FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
267
        JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
268
            (
269
                `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
270
                (
271
                    `kcu`.`CONSTRAINT_CATALOG` IS NULL AND
272
                    `rc`.`CONSTRAINT_CATALOG` IS NULL
273
                )
274
            ) AND
275
            `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
276
            `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME` AND
277
            `kcu`.`TABLE_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
278
            `kcu`.`TABLE_NAME` = `rc`.`TABLE_NAME`
279
        WHERE `rc`.`CONSTRAINT_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `rc`.`TABLE_NAME` = :tableName
280 130
        SQL;
281
282 130
        $constraints = [];
283 130
        $rows = $this->db->createCommand($sql, [
284 130
            ':schemaName' => $table->getSchemaName(),
285 130
            ':tableName' => $table->getName(),
286 130
        ])->queryAll();
287
288
        /**  @psalm-var RowConstraint $row */
289 130
        foreach ($rows as $row) {
290 34
            $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
291 34
            $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
292
        }
293
294 130
        $table->foreignKeys([]);
295
296
        /**
297
         * @var array{referenced_table_name: string, columns: array} $constraint
298
         */
299 130
        foreach ($constraints as $name => $constraint) {
300 34
            $table->foreignKey(
301 34
                $name,
302 34
                array_merge(
303 34
                    [$constraint['referenced_table_name']],
304 34
                    $constraint['columns']
305 34
                ),
306 34
            );
307
        }
308
    }
309
310
    /**
311
     * @throws Exception
312
     * @throws InvalidConfigException
313
     * @throws Throwable
314
     */
315 1
    protected function findSchemaNames(): array
316
    {
317 1
        $sql = <<<SQL
318
        SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
319 1
        SQL;
320
321 1
        return $this->db->createCommand($sql)->queryColumn();
322
    }
323
324
    /**
325
     * @throws Exception
326
     * @throws InvalidConfigException
327
     * @throws Throwable
328
     */
329 147
    protected function findTableComment(TableSchemaInterface $tableSchema): void
330
    {
331 147
        $sql = <<<SQL
332
        SELECT `TABLE_COMMENT`
333
        FROM `INFORMATION_SCHEMA`.`TABLES`
334
        WHERE
335
              `TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
336
              `TABLE_NAME` = :tableName;
337 147
        SQL;
338
339 147
        $comment = $this->db->createCommand($sql, [
340 147
            ':schemaName' => $tableSchema->getSchemaName(),
341 147
            ':tableName' => $tableSchema->getName(),
342 147
        ])->queryScalar();
343
344 147
        $tableSchema->comment(is_string($comment) ? $comment : null);
345
    }
346
347
    /**
348
     * Returns all table names in the database.
349
     *
350
     * This method should be overridden by child classes in order to support this feature because the default
351
     * implementation simply throws an exception.
352
     *
353
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
354
     *
355
     * @throws Exception
356
     * @throws InvalidConfigException
357
     * @throws Throwable
358
     *
359
     * @return array All table names in the database. The names have NO schema name prefix.
360
     */
361 12
    protected function findTableNames(string $schema = ''): array
362
    {
363 12
        $sql = 'SHOW TABLES';
364
365 12
        if ($schema !== '') {
366 1
            $sql .= ' FROM ' . $this->db->getQuoter()->quoteSimpleTableName($schema);
367
        }
368
369 12
        return $this->db->createCommand($sql)->queryColumn();
370
    }
371
372
    /**
373
     * @throws Exception
374
     * @throws InvalidConfigException
375
     * @throws Throwable
376
     */
377 1
    protected function findViewNames(string $schema = ''): array
378
    {
379 1
        $sql = match ($schema) {
380 1
            '' => <<<SQL
381
            SELECT table_name as view FROM information_schema.tables WHERE table_type LIKE 'VIEW' AND table_schema != 'sys'
382 1
            SQL,
383 1
            default => <<<SQL
384 1
            SELECT table_name as view FROM information_schema.tables WHERE table_type LIKE 'VIEW' AND table_schema = '$schema'
385 1
            SQL,
386 1
        };
387
388
        /** @psalm-var string[][] $views */
389 1
        $views = $this->db->createCommand($sql)->queryAll();
390
391 1
        foreach ($views as $key => $view) {
392 1
            $views[$key] = $view['view'];
393
        }
394
395 1
        return $views;
396
    }
397
398
    /**
399
     * Returns the cache key for the specified table name.
400
     *
401
     * @param string $name the table name.
402
     *
403
     * @return array the cache key.
404
     */
405 243
    protected function getCacheKey(string $name): array
406
    {
407 243
        return array_merge([self::class], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
408
    }
409
410
    /**
411
     * Returns the cache tag name.
412
     *
413
     * This allows {@see refresh()} to invalidate all cached table schemas.
414
     *
415
     * @return string the cache tag name.
416
     */
417 244
    protected function getCacheTag(): string
418
    {
419 244
        return md5(serialize(array_merge([self::class], $this->db->getCacheKey())));
420
    }
421
422
    /**
423
     * Gets the CREATE TABLE sql string.
424
     *
425
     * @param TableSchemaInterface $table the table metadata.
426
     *
427
     * @throws Exception
428
     * @throws InvalidConfigException
429
     * @throws Throwable
430
     *
431
     * @return string $sql the result of 'SHOW CREATE TABLE'.
432
     */
433 147
    protected function getCreateTableSql(TableSchemaInterface $table): string
434
    {
435 147
        $tableName = $table->getFullName() ?? '';
436
437
        try {
438
            /** @var array<array-key, string> $row */
439 147
            $row = $this->db->createCommand(
440 147
                'SHOW CREATE TABLE ' . $this->db->getQuoter()->quoteTableName($tableName)
441 147
            )->queryOne();
442
443 130
            if (isset($row['Create Table'])) {
444 128
                $sql = $row['Create Table'];
445
            } else {
446 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

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