Passed
Pull Request — master (#206)
by Wilmer
03:34
created

Schema::findColumns()   B

Complexity

Conditions 10
Paths 12

Size

Total Lines 48
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 10.5258

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 10
eloc 23
c 2
b 0
f 0
nc 12
nop 1
dl 0
loc 48
ccs 19
cts 23
cp 0.8261
crap 10.5258
rs 7.6666

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

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