Passed
Push — master ( ab1a00...23465a )
by Wilmer
08:03 queued 04:29
created

Schema::loadColumnSchema()   F

Complexity

Conditions 27
Paths 500

Size

Total Lines 105
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 61
CRAP Score 27.003

Importance

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

455
                $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
456 144
                $sql = $row[1];
457
            }
458 35
        } catch (Exception) {
459 35
            $sql = '';
460
        }
461
462 161
        return $sql;
463
    }
464
465
    /**
466
     * Loads the column information into a {@see ColumnSchemaInterface} object.
467
     *
468
     * @param array $info The column information.
469
     *
470
     * @throws JsonException
471
     *
472
     * @return ColumnSchemaInterface The column schema object.
473
     */
474 146
    protected function loadColumnSchema(array $info): ColumnSchemaInterface
475
    {
476 146
        $column = $this->createColumnSchema($info['field']);
477
478
        /** @psalm-var ColumnInfoArray $info */
479 146
        $column->allowNull($info['null'] === 'YES');
480 146
        $column->primaryKey(str_contains($info['key'], 'PRI'));
481 146
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
482 146
        $column->comment($info['comment']);
483 146
        $column->dbType($info['type']);
484 146
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
0 ignored issues
show
Bug introduced by
It seems like $column->getDbType() can also be of type null; however, parameter $haystack of stripos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

484
        $column->unsigned(stripos(/** @scrutinizer ignore-type */ $column->getDbType(), 'unsigned') !== false);
Loading history...
485 146
        $column->type(self::TYPE_STRING);
486
487 146
        $extra = $info['extra'];
488
489 146
        if (str_starts_with($extra, 'DEFAULT_GENERATED')) {
490 32
            $extra = strtoupper(substr($extra, 18));
491
        }
492 146
        $column->extra(trim($extra));
493
494 146
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
0 ignored issues
show
Bug introduced by
It seems like $column->getDbType() can also be of type null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

494
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', /** @scrutinizer ignore-type */ $column->getDbType(), $matches)) {
Loading history...
495 146
            $type = strtolower($matches[1]);
496
497 146
            if (isset($this->typeMap[$type])) {
498 141
                $column->type($this->typeMap[$type]);
499
            }
500
501 146
            if (!empty($matches[2])) {
502 110
                if ($type === 'enum') {
503 26
                    preg_match_all("/'[^']*'/", $matches[2], $values);
504
505 26
                    foreach ($values[0] as $i => $value) {
506 26
                        $values[$i] = trim($value, "'");
507
                    }
508
509 26
                    $column->enumValues($values);
510
                } else {
511 110
                    $values = explode(',', $matches[2]);
512 110
                    $column->precision((int) $values[0]);
513 110
                    $column->size((int) $values[0]);
514
515 110
                    if (isset($values[1])) {
516 41
                        $column->scale((int) $values[1]);
517
                    }
518
519 110
                    if ($column->getSize() === 1 && $type === 'bit') {
520 29
                        $column->type(self::TYPE_BOOLEAN);
521 110
                    } elseif ($type === 'bit') {
522 27
                        if ($column->getSize() > 32) {
523 4
                            $column->type(self::TYPE_BIGINT);
524 27
                        } elseif ($column->getSize() === 32) {
525 4
                            $column->type(self::TYPE_INTEGER);
526
                        }
527
                    }
528
                }
529
            }
530
        }
531
532 146
        $column->phpType($this->getColumnPhpType($column));
533
534 146
        if (!$column->isPrimaryKey()) {
535
            // Chapter 2: cruthes for MariaDB {@see https://github.com/yiisoft/yii2/issues/19747}
536
            /** @psalm-var string $columnCategory */
537 142
            $columnCategory = $this->createColumn(
538 142
                $column->getType(),
539 142
                $column->getSize()
540 142
            )->getCategoryMap()[$column->getType()] ?? '';
541 142
            $defaultValue = $info['extra_default_value'] ?? '';
542
543
            if (
544 142
                empty($info['extra']) &&
545 142
                !empty($defaultValue) &&
546 142
                in_array($columnCategory, [
547 142
                    AbstractColumn::CATEGORY_STRING,
548 142
                    AbstractColumn::CATEGORY_TIME,
549 142
                ], true)
550 142
                && !str_starts_with($defaultValue, '\'')
551
            ) {
552
                $info['extra'] = 'DEFAULT_GENERATED';
553
            }
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 142
                in_array($column->getType(), [self::TYPE_TIMESTAMP, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME], true)
563 142
                && 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 140
            } elseif (!empty($info['extra']) && !empty($info['default'])) {
568 6
                $column->defaultValue(new Expression($info['default']));
569 139
            } elseif (isset($type) && $type === 'bit' && $column->getType() !== self::TYPE_BOOLEAN) {
570 27
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
571
            } else {
572 142
                $column->defaultValue($column->phpTypecast($info['default']));
573
            }
574 91
        } elseif ($info['default'] !== null) {
575 5
            $column->defaultValue($column->phpTypecast($info['default']));
576
        }
577
578 146
        return $column;
579
    }
580
581
    /**
582
     * Loads all check constraints for the given table.
583
     *
584
     * @param string $tableName The 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
     * @psalm-return Constraint[]|ForeignKeyConstraint[]|Constraint|null
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 71
        $constraints = $this->db->createCommand($sql, [
665 71
            ':schemaName' => $resolvedName->getSchemaName(),
666 71
            ':tableName' => $resolvedName->getName(),
667 71
        ])->queryAll();
668
669
        /** @psalm-var array[][] $constraints */
670 71
        $constraints = $this->normalizeRowKeyCase($constraints, true);
671 71
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
672
673 71
        $result = [
674 71
            self::PRIMARY_KEY => null,
675 71
            self::FOREIGN_KEYS => [],
676 71
            self::UNIQUES => [],
677 71
        ];
678
679
        /**
680
         * @psalm-var string $type
681
         * @psalm-var array $names
682
         */
683 71
        foreach ($constraints as $type => $names) {
684
            /**
685
             * @psalm-var object|string|null $name
686
             * @psalm-var ConstraintArray $constraint
687
             */
688 66
            foreach ($names as $name => $constraint) {
689
                switch ($type) {
690 66
                    case 'PRIMARY KEY':
691 47
                        $result[self::PRIMARY_KEY] = (new Constraint())
692 47
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
693 47
                        break;
694 58
                    case 'FOREIGN KEY':
695 16
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
696 16
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
697 16
                            ->foreignTableName($constraint[0]['foreign_table_name'])
698 16
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
699 16
                            ->onDelete($constraint[0]['on_delete'])
700 16
                            ->onUpdate($constraint[0]['on_update'])
701 16
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
702 16
                            ->name($name);
703 16
                        break;
704 48
                    case 'UNIQUE':
705 48
                        $result[self::UNIQUES][] = (new Constraint())
706 48
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
707 48
                            ->name($name);
708 48
                        break;
709
                }
710
            }
711
        }
712
713 71
        foreach ($result as $type => $data) {
714 71
            $this->setTableMetadata($tableName, $type, $data);
715
        }
716
717 71
        return $result[$returnType];
718
    }
719
720
    /**
721
     * Loads all default value constraints for the given table.
722
     *
723
     * @param string $tableName The table name.
724
     *
725
     * @throws NotSupportedException
726
     *
727
     * @return array Default value constraints for the given table.
728
     */
729 15
    protected function loadTableDefaultValues(string $tableName): array
730
    {
731 15
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
732
    }
733
734
    /**
735
     * Loads all foreign keys for the given table.
736
     *
737
     * @param string $tableName The table name.
738
     *
739
     * @throws Exception
740
     * @throws InvalidConfigException
741
     * @throws Throwable
742
     *
743
     * @return array Foreign keys for the given table.
744
     */
745 9
    protected function loadTableForeignKeys(string $tableName): array
746
    {
747 9
        $tableForeignKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
748 9
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
749
    }
750
751
    /**
752
     * Loads all indexes for the given table.
753
     *
754
     * @param string $tableName The table name.
755
     *
756
     * @throws Exception
757
     * @throws InvalidConfigException
758
     * @throws Throwable
759
     *
760
     * @return IndexConstraint[] Indexes for the given table.
761
     */
762 38
    protected function loadTableIndexes(string $tableName): array
763
    {
764 38
        $sql = <<<SQL
765
        SELECT
766
            `s`.`INDEX_NAME` AS `name`,
767
            `s`.`COLUMN_NAME` AS `column_name`,
768
            `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
769
            `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
770
        FROM `information_schema`.`STATISTICS` AS `s`
771
        WHERE
772
            `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
773
            `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND
774
            `s`.`TABLE_NAME` = :tableName
775
        ORDER BY `s`.`SEQ_IN_INDEX` ASC
776 38
        SQL;
777
778 38
        $resolvedName = $this->resolveTableName($tableName);
779 38
        $indexes = $this->db->createCommand($sql, [
780 38
            ':schemaName' => $resolvedName->getSchemaName(),
781 38
            ':tableName' => $resolvedName->getName(),
782 38
        ])->queryAll();
783
784
        /** @psalm-var array[] $indexes */
785 38
        $indexes = $this->normalizeRowKeyCase($indexes, true);
786 38
        $indexes = ArrayHelper::index($indexes, null, ['name']);
787 38
        $result = [];
788
789
        /**
790
         * @psalm-var object|string|null $name
791
         * @psalm-var array[] $index
792
         */
793 38
        foreach ($indexes as $name => $index) {
794 38
            $ic = new IndexConstraint();
795
796 38
            $ic->primary((bool) $index[0]['index_is_primary']);
797 38
            $ic->unique((bool) $index[0]['index_is_unique']);
798 38
            $ic->name($name !== 'PRIMARY' ? $name : null);
799 38
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
800
801 38
            $result[] = $ic;
802
        }
803
804 38
        return $result;
805
    }
806
807
    /**
808
     * Loads a primary key for the given table.
809
     *
810
     * @param string $tableName The table name.
811
     *
812
     * @throws Exception
813
     * @throws InvalidConfigException
814
     * @throws Throwable
815
     *
816
     * @return Constraint|null Primary key for the given table, `null` if the table has no primary key.*
817
     */
818 45
    protected function loadTablePrimaryKey(string $tableName): Constraint|null
819
    {
820 45
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
821 45
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
822
    }
823
824
    /**
825
     * Loads the metadata for the specified table.
826
     *
827
     * @param string $name The table name.
828
     *
829
     * @throws Exception
830
     * @throws Throwable
831
     *
832
     * @return TableSchemaInterface|null DBMS-dependent table metadata, `null` if the table doesn't exist.
833
     */
834 161
    protected function loadTableSchema(string $name): TableSchemaInterface|null
835
    {
836 161
        $table = $this->resolveTableName($name);
837 161
        $this->resolveTableCreateSql($table);
838 161
        $this->findTableComment($table);
839
840 161
        if ($this->findColumns($table)) {
841 144
            $this->findConstraints($table);
842
843 144
            return $table;
844
        }
845
846 35
        return null;
847
    }
848
849
    /**
850
     * Loads all unique constraints for the given table.
851
     *
852
     * @param string $tableName The table name.
853
     *
854
     * @throws Exception
855
     * @throws InvalidConfigException
856
     * @throws Throwable
857
     *
858
     * @return array Unique constraints for the given table.
859
     */
860 17
    protected function loadTableUniques(string $tableName): array
861
    {
862 17
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
863 17
        return is_array($tableUniques) ? $tableUniques : [];
864
    }
865
866
    /**
867
     * Resolves the table name and schema name (if any).
868
     *
869
     * @param string $name The table name.
870
     *
871
     * @see TableSchemaInterface
872
     */
873 205
    protected function resolveTableName(string $name): TableSchemaInterface
874
    {
875 205
        $resolvedName = new TableSchema();
876
877 205
        $parts = array_reverse($this->db->getQuoter()->getTableNameParts($name));
878 205
        $resolvedName->name($parts[0] ?? '');
879 205
        $resolvedName->schemaName($parts[1] ?? $this->defaultSchema);
880 205
        $resolvedName->fullName(
881 205
            $resolvedName->getSchemaName() !== $this->defaultSchema ?
882 205
            implode('.', array_reverse($parts)) : $resolvedName->getName()
883 205
        );
884
885 205
        return $resolvedName;
886
    }
887
888
    /**
889
     * @throws Exception
890
     * @throws InvalidConfigException
891
     * @throws Throwable
892
     */
893 161
    protected function resolveTableCreateSql(TableSchemaInterface $table): void
894
    {
895 161
        $sql = $this->getCreateTableSql($table);
896 161
        $table->createSql($sql);
897
    }
898
899
    /**
900
     * Creates a column schema for the database.
901
     *
902
     * This method may be overridden by child classes to create a DBMS-specific column schema.
903
     *
904
     * @param string $name Name of the column.
905
     *
906
     * @return ColumnSchema
907
     */
908 146
    private function createColumnSchema(string $name): ColumnSchema
909
    {
910 146
        return new ColumnSchema($name);
911
    }
912
913
    /**
914
     * @throws Exception
915
     * @throws InvalidConfigException
916
     * @throws Throwable
917
     */
918 144
    private function getJsonColumns(TableSchemaInterface $table): array
919
    {
920 144
        $sql = $this->getCreateTableSql($table);
921 144
        $result = [];
922 144
        $regexp = '/json_valid\([\`"](.+)[\`"]\s*\)/mi';
923
924 144
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
925
            foreach ($matches as $match) {
926
                $result[] = $match[1];
927
            }
928
        }
929
930 144
        return $result;
931
    }
932
}
933