Passed
Push — master ( 9db293...04a6a6 )
by Def
03:54
created

Schema::findColumns()   B

Complexity

Conditions 10
Paths 29

Size

Total Lines 67
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 13.3855

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 10
eloc 33
c 2
b 0
f 0
nc 29
nop 1
dl 0
loc 67
ccs 23
cts 34
cp 0.6765
crap 13.3855
rs 7.6666

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\Driver\Pdo\AbstractPdoSchema;
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\Helper\DbArrayHelper;
18
use Yiisoft\Db\Schema\Builder\ColumnInterface;
19
use Yiisoft\Db\Schema\ColumnSchemaInterface;
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
 * Implements MySQL, MariaDB specific schema, supporting MySQL Server 5.7, MariaDB Server 10.4 and higher.
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
 * @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
 *   extra_default_value: string|null,
70
 *   privileges: string,
71
 *   comment: string
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
 * @psalm-type ConstraintArray = array<
80
 *   array-key,
81
 *   array {
82
 *     name: string,
83
 *     column_name: string,
84
 *     type: string,
85
 *     foreign_table_schema: string|null,
86
 *     foreign_table_name: string|null,
87
 *     foreign_column_name: string|null,
88
 *     on_update: string,
89
 *     on_delete: string,
90
 *     check_expr: string
91
 *   }
92
 * >
93
 */
94
final class Schema extends AbstractPdoSchema
95
{
96
    /**
97
     * @var array Mapping from physical column types (keys) to abstract column types (values).
98
     *
99
     * @psalm-var string[]
100
     */
101
    private array $typeMap = [
102
        'tinyint' => self::TYPE_TINYINT,
103
        'bit' => self::TYPE_INTEGER,
104
        'smallint' => self::TYPE_SMALLINT,
105
        'mediumint' => self::TYPE_INTEGER,
106
        'int' => self::TYPE_INTEGER,
107
        'integer' => self::TYPE_INTEGER,
108
        'bigint' => self::TYPE_BIGINT,
109
        'float' => self::TYPE_FLOAT,
110
        'double' => self::TYPE_DOUBLE,
111
        'real' => self::TYPE_FLOAT,
112
        'decimal' => self::TYPE_DECIMAL,
113
        'numeric' => self::TYPE_DECIMAL,
114
        'tinytext' => self::TYPE_TEXT,
115
        'mediumtext' => self::TYPE_TEXT,
116
        'longtext' => self::TYPE_TEXT,
117
        'longblob' => self::TYPE_BINARY,
118
        'blob' => self::TYPE_BINARY,
119
        'text' => self::TYPE_TEXT,
120
        'varchar' => self::TYPE_STRING,
121
        'string' => self::TYPE_STRING,
122
        'char' => self::TYPE_CHAR,
123
        'datetime' => self::TYPE_DATETIME,
124
        'year' => self::TYPE_DATE,
125
        'date' => self::TYPE_DATE,
126
        'time' => self::TYPE_TIME,
127
        'timestamp' => self::TYPE_TIMESTAMP,
128
        'enum' => self::TYPE_STRING,
129
        'varbinary' => self::TYPE_BINARY,
130
        'json' => self::TYPE_JSON,
131
    ];
132
133 16
    public function createColumn(string $type, array|int|string $length = null): ColumnInterface
134
    {
135 16
        return new Column($type, $length);
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 1
        $uniqueIndexes = [];
162 1
        $regexp = '/UNIQUE KEY\s+[`"](.+)[`"]\s*\(([`"].+[`"])+\)/mi';
163
164 1
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
165 1
            foreach ($matches as $match) {
166 1
                $indexName = $match[1];
167 1
                $indexColumns = array_map('trim', preg_split('/[`"],[`"]/', trim($match[2], '`"')));
168 1
                $uniqueIndexes[$indexName] = $indexColumns;
169
            }
170
        }
171
172 1
        ksort($uniqueIndexes);
173
174 1
        return $uniqueIndexes;
175
    }
176
177
    /**
178
     * Collects the metadata of table columns.
179
     *
180
     * @param TableSchemaInterface $table The table metadata.
181
     *
182
     * @throws Exception
183
     * @throws Throwable If DB query fails.
184
     *
185
     * @return bool Whether the table exists in the database.
186
     */
187 162
    protected function findColumns(TableSchemaInterface $table): bool
188
    {
189 162
        $tableName = $table->getFullName() ?? '';
190 162
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->db->getQuoter()->quoteTableName($tableName);
191
192
        try {
193 162
            $columns = $this->db->createCommand($sql)->queryAll();
194
            // Chapter 1: crutches for MariaDB. {@see https://github.com/yiisoft/yii2/issues/19747}
195 145
            $columnsExtra = [];
196 145
            if (str_contains($this->db->getServerVersion(), 'MariaDB')) {
197
                /** @psalm-var array[] $columnsExtra */
198
                $columnsExtra = $this->db->createCommand(
199
                    <<<SQL
200
                    SELECT `COLUMN_NAME` as name,`COLUMN_DEFAULT` as default_value
201
                    FROM INFORMATION_SCHEMA.COLUMNS
202
                    WHERE TABLE_SCHEMA = COALESCE(:schemaName, DATABASE()) AND TABLE_NAME = :tableName
203
                    SQL ,
204
                    [
205
                        ':schemaName' => $table->getSchemaName(),
206
                        ':tableName' => $table->getName(),
207
                    ]
208
                )->queryAll();
209
                /** @psalm-var string[] $cols */
210 145
                foreach ($columnsExtra as $cols) {
211
                    $columnsExtra[$cols['name']] = $cols['default_value'];
212
                }
213
            }
214 35
        } catch (Exception $e) {
215 35
            $previous = $e->getPrevious();
216
217 35
            if ($previous && str_contains($previous->getMessage(), 'SQLSTATE[42S02')) {
218
                /**
219
                 * The table doesn't exist.
220
                 *
221
                 * @link https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
222
                 */
223 35
                return false;
224
            }
225
226
            throw $e;
227
        }
228
229 145
        $jsonColumns = $this->getJsonColumns($table);
230
231
        /** @psalm-var ColumnInfoArray $info */
232 145
        foreach ($columns as $info) {
233 145
            $info = $this->normalizeRowKeyCase($info, false);
234
235 145
            $info['extra_default_value'] = $columnsExtra[(string) $info['field']] ?? '';
236
237 145
            if (in_array($info['field'], $jsonColumns, true)) {
238
                $info['type'] = self::TYPE_JSON;
239
            }
240
241
            /** @psalm-var ColumnInfoArray $info */
242 145
            $column = $this->loadColumnSchema($info);
243 145
            $table->column($column->getName(), $column);
244
245 145
            if ($column->isPrimaryKey()) {
246 91
                $table->primaryKey($column->getName());
247 91
                if ($column->isAutoIncrement()) {
248 73
                    $table->sequenceName('');
249
                }
250
            }
251
        }
252
253 145
        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 145
    protected function findConstraints(TableSchemaInterface $table): void
266
    {
267 145
        $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 145
        SQL;
288
289 145
        $constraints = [];
290 145
        $rows = $this->db->createCommand($sql, [
291 145
            ':schemaName' => $table->getSchemaName(),
292 145
            ':tableName' => $table->getName(),
293 145
        ])->queryAll();
294
295
        /**  @psalm-var RowConstraint $row */
296 145
        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 145
        $table->foreignKeys([]);
302
303
        /**
304
         * @psalm-var array{referenced_table_name: string, columns: array} $constraint
305
         */
306 145
        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 162
    protected function findTableComment(TableSchemaInterface $tableSchema): void
337
    {
338 162
        $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 162
        SQL;
345
346 162
        $comment = $this->db->createCommand($sql, [
347 162
            ':schemaName' => $tableSchema->getSchemaName(),
348 162
            ':tableName' => $tableSchema->getName(),
349 162
        ])->queryScalar();
350
351 162
        $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 to support this feature because the default implementation
358
     * simply throws an exception.
359
     *
360
     * @param string $schema The schema of the tables.
361
     * Defaults to empty string, meaning the current or default schema.
362
     *
363
     * @throws Exception
364
     * @throws InvalidConfigException
365
     * @throws Throwable
366
     *
367
     * @return array All tables name in the database. The names have NO schema name prefix.
368
     */
369 12
    protected function findTableNames(string $schema = ''): array
370
    {
371 12
        $sql = 'SHOW TABLES';
372
373 12
        if ($schema !== '') {
374 1
            $sql .= ' FROM ' . $this->db->getQuoter()->quoteSimpleTableName($schema);
375
        }
376
377 12
        return $this->db->createCommand($sql)->queryColumn();
378
    }
379
380
    /**
381
     * @throws Exception
382
     * @throws InvalidConfigException
383
     * @throws Throwable
384
     */
385 1
    protected function findViewNames(string $schema = ''): array
386
    {
387 1
        $sql = match ($schema) {
388 1
            '' => <<<SQL
389
            SELECT table_name as view FROM information_schema.tables WHERE table_type LIKE 'VIEW' AND table_schema != 'sys' order by table_name
390 1
            SQL,
391 1
            default => <<<SQL
392 1
            SELECT table_name as view FROM information_schema.tables WHERE table_type LIKE 'VIEW' AND table_schema = '$schema' order by table_name
393 1
            SQL,
394 1
        };
395
396
        /** @psalm-var string[][] $views */
397 1
        $views = $this->db->createCommand($sql)->queryAll();
398
399 1
        foreach ($views as $key => $view) {
400 1
            $views[$key] = $view['view'];
401
        }
402
403 1
        return $views;
404
    }
405
406
    /**
407
     * Returns the cache key for the specified table name.
408
     *
409
     * @param string $name The table name.
410
     *
411
     * @return array The cache key.
412
     */
413 260
    protected function getCacheKey(string $name): array
414
    {
415 260
        return array_merge([self::class], $this->generateCacheKey(), [$this->getRawTableName($name)]);
416
    }
417
418
    /**
419
     * Returns the cache tag name.
420
     *
421
     * This allows {@see refresh()} to invalidate all cached table schemas.
422
     *
423
     * @return string The cache tag name.
424
     */
425 207
    protected function getCacheTag(): string
426
    {
427 207
        return md5(serialize(array_merge([self::class], $this->generateCacheKey())));
428
    }
429
430
    /**
431
     * Gets the `CREATE TABLE` SQL string.
432
     *
433
     * @param TableSchemaInterface $table The table metadata.
434
     *
435
     * @throws Exception
436
     * @throws InvalidConfigException
437
     * @throws Throwable
438
     *
439
     * @return string $sql The result of `SHOW CREATE TABLE`.
440
     */
441 162
    protected function getCreateTableSql(TableSchemaInterface $table): string
442
    {
443 162
        $tableName = $table->getFullName() ?? '';
444
445
        try {
446
            /** @psalm-var array<array-key, string> $row */
447 162
            $row = $this->db->createCommand(
448 162
                'SHOW CREATE TABLE ' . $this->db->getQuoter()->quoteTableName($tableName)
449 162
            )->queryOne();
450
451 145
            if (isset($row['Create Table'])) {
452 143
                $sql = $row['Create Table'];
453
            } else {
454 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

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