Passed
Pull Request — master (#244)
by Def
03:59
created

Schema::loadColumnSchema()   F

Complexity

Conditions 27
Paths 500

Size

Total Lines 104
Code Lines 66

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 62
CRAP Score 27.0029

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 27
eloc 66
c 1
b 0
f 0
nc 500
nop 1
dl 0
loc 104
ccs 62
cts 63
cp 0.9841
crap 27.0029
rs 0.6944

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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