Passed
Pull Request — master (#295)
by
unknown
03:38
created

Schema::findColumns()   B

Complexity

Conditions 10
Paths 29

Size

Total Lines 67
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 13.3855

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

453
                $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
454 145
                $sql = $row[1];
455
            }
456 35
        } catch (Exception) {
457 35
            $sql = '';
458
        }
459
460 162
        return $sql;
461
    }
462
463
    /**
464
     * Loads the column information into a {@see ColumnSchemaInterface} object.
465
     *
466
     * @param array $info The column information.
467
     *
468
     * @throws JsonException
469
     *
470
     * @return ColumnSchemaInterface The column schema object.
471
     *
472
     * @psalm-param ColumnInfoArray $info The column information.
473
     */
474 147
    protected function loadColumnSchema(array $info): ColumnSchemaInterface
475
    {
476 147
        $dbType = $info['type'];
477
478 147
        $column = $this->createColumnSchema($info['field']);
479
480
        /** @psalm-var ColumnInfoArray $info */
481 147
        $column->allowNull($info['null'] === 'YES');
482 147
        $column->primaryKey(str_contains($info['key'], 'PRI'));
483 147
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
484 147
        $column->comment($info['comment']);
485 147
        $column->dbType($dbType);
486 147
        $column->unsigned(stripos($dbType, 'unsigned') !== false);
487 147
        $column->type(self::TYPE_STRING);
488
489 147
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $dbType, $matches)) {
490 147
            $type = $matches[1];
491
492 147
            if (isset($this->typeMap[$type])) {
493 142
                $column->type($this->typeMap[$type]);
494
            }
495
496 147
            if (!empty($matches[2])) {
497 111
                if ($type === 'enum') {
498 27
                    preg_match_all("/'[^']*'/", $matches[2], $values);
499
500 27
                    foreach ($values[0] as $i => $value) {
501 27
                        $values[$i] = trim($value, "'");
502
                    }
503
504 27
                    $column->enumValues($values);
505
                } else {
506 111
                    $values = explode(',', $matches[2]);
507 111
                    $column->precision((int) $values[0]);
508 111
                    $column->size((int) $values[0]);
509
510 111
                    if (isset($values[1])) {
511 42
                        $column->scale((int) $values[1]);
512
                    }
513
514 111
                    if ($type === 'bit') {
515 30
                        if ($column->getSize() === 1) {
516 30
                            $column->type(self::TYPE_BOOLEAN);
517 28
                        } elseif ($column->getSize() > 32) {
518 4
                            $column->type(self::TYPE_BIGINT);
519 28
                        } elseif ($column->getSize() === 32) {
520 4
                            $column->type(self::TYPE_INTEGER);
521
                        }
522
                    }
523
                }
524
            }
525
        }
526
527
        // Chapter 2: crutches for MariaDB {@see https://github.com/yiisoft/yii2/issues/19747}
528 147
        $extra = $info['extra'];
529
        if (
530 147
            empty($extra)
531 147
            && !empty($info['extra_default_value'])
532 147
            && !str_starts_with($info['extra_default_value'], '\'')
533 147
            && in_array($column->getType(), [
534 147
                self::TYPE_CHAR, self::TYPE_STRING, self::TYPE_TEXT,
535 147
                self::TYPE_DATETIME, self::TYPE_TIMESTAMP, self::TYPE_TIME, self::TYPE_DATE,
536 147
            ], true)
537
        ) {
538 1
            $extra = 'DEFAULT_GENERATED';
539
        }
540
541 147
        $column->extra($extra);
542 147
        $column->phpType($this->getColumnPhpType($column));
543 147
        $column->defaultValue($this->normalizeDefaultValue($info['default'], $column));
544
545 147
        if (str_starts_with($extra, 'DEFAULT_GENERATED')) {
546 34
            $column->extra(trim(strtoupper(substr($extra, 18))));
547
        }
548
549 147
        return $column;
550
    }
551
552
    /**
553
     * Converts column's default value according to {@see ColumnSchema::phpType} after retrieval from the database.
554
     *
555
     * @param string|null $defaultValue The default value retrieved from the database.
556
     * @param ColumnSchemaInterface $column The column schema object.
557
     *
558
     * @return mixed The normalized default value.
559
     */
560 147
    private function normalizeDefaultValue(?string $defaultValue, ColumnSchemaInterface $column): mixed
561
    {
562 147
        return match (true) {
563 147
            $defaultValue === null => null,
564
565 147
            $column->isPrimaryKey() => $column->phpTypecast($defaultValue),
566
567 147
            in_array($column->getType(), [self::TYPE_TIMESTAMP, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME], true)
568 147
            && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', $defaultValue, $matches) === 1
0 ignored issues
show
Bug introduced by
It seems like $defaultValue 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

568
            && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', /** @scrutinizer ignore-type */ $defaultValue, $matches) === 1
Loading history...
569 147
                => new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1]) ? '(' . $matches[1] . ')' : '')),
570
571 147
            !empty($column->getExtra()) && !empty($defaultValue) => new Expression($defaultValue),
0 ignored issues
show
Bug introduced by
It seems like $defaultValue can also be of type null; however, parameter $expression of Yiisoft\Db\Expression\Expression::__construct() 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

571
            !empty($column->getExtra()) && !empty($defaultValue) => new Expression(/** @scrutinizer ignore-type */ $defaultValue),
Loading history...
572
573 147
            str_starts_with((string) $column->getDbType(), 'bit')
574 147
                => $column->phpTypecast(bindec(trim($defaultValue, "b'"))),
0 ignored issues
show
Bug introduced by
It seems like $defaultValue can also be of type null; however, parameter $string of trim() 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

574
                => $column->phpTypecast(bindec(trim(/** @scrutinizer ignore-type */ $defaultValue, "b'"))),
Loading history...
575
576 147
            default => $column->phpTypecast($defaultValue),
577 147
        };
578
    }
579
580
    /**
581
     * Loads all check constraints for the given table.
582
     *
583
     * @param string $tableName The table name.
584
     *
585
     * @throws NotSupportedException
586
     *
587
     * @return array Check constraints for the given table.
588
     */
589 16
    protected function loadTableChecks(string $tableName): array
590
    {
591 16
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
592
    }
593
594
    /**
595
     * Loads multiple types of constraints and returns the specified ones.
596
     *
597
     * @param string $tableName table name.
598
     * @param string $returnType return type:
599
     * - primaryKey
600
     * - foreignKeys
601
     * - uniques
602
     *
603
     * @throws Exception
604
     * @throws InvalidConfigException
605
     * @throws Throwable
606
     *
607
     * @psalm-return Constraint[]|ForeignKeyConstraint[]|Constraint|null
608
     */
609 71
    private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null
610
    {
611 71
        $sql = <<<SQL
612
        SELECT
613
            `kcu`.`CONSTRAINT_NAME` AS `name`,
614
            `kcu`.`COLUMN_NAME` AS `column_name`,
615
            `tc`.`CONSTRAINT_TYPE` AS `type`,
616
        CASE
617
            WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
618
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
619
        END AS `foreign_table_schema`,
620
            `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
621
            `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
622
            `rc`.`UPDATE_RULE` AS `on_update`,
623
            `rc`.`DELETE_RULE` AS `on_delete`,
624
            `kcu`.`ORDINAL_POSITION` AS `position`
625
        FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
626
        JOIN `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc` ON
627
            `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
628
            `rc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
629
            `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
630
        JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON
631
            `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
632
            `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
633
            `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND
634
            `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY'
635
        WHERE
636
            `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
637
            `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
638
            `kcu`.`TABLE_NAME` = :tableName
639
        UNION
640
        SELECT
641
            `kcu`.`CONSTRAINT_NAME` AS `name`,
642
            `kcu`.`COLUMN_NAME` AS `column_name`,
643
            `tc`.`CONSTRAINT_TYPE` AS `type`,
644
        NULL AS `foreign_table_schema`,
645
        NULL AS `foreign_table_name`,
646
        NULL AS `foreign_column_name`,
647
        NULL AS `on_update`,
648
        NULL AS `on_delete`,
649
            `kcu`.`ORDINAL_POSITION` AS `position`
650
        FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
651
        JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON
652
            `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
653
            `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
654
            `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND
655
            `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE')
656
        WHERE
657
            `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
658
            `kcu`.`TABLE_NAME` = :tableName
659
        ORDER BY `position` ASC
660 71
        SQL;
661
662 71
        $resolvedName = $this->resolveTableName($tableName);
663 71
        $constraints = $this->db->createCommand($sql, [
664 71
            ':schemaName' => $resolvedName->getSchemaName(),
665 71
            ':tableName' => $resolvedName->getName(),
666 71
        ])->queryAll();
667
668
        /** @psalm-var array[][] $constraints */
669 71
        $constraints = $this->normalizeRowKeyCase($constraints, true);
670 71
        $constraints = DbArrayHelper::index($constraints, null, ['type', 'name']);
671
672 71
        $result = [
673 71
            self::PRIMARY_KEY => null,
674 71
            self::FOREIGN_KEYS => [],
675 71
            self::UNIQUES => [],
676 71
        ];
677
678
        /**
679
         * @psalm-var string $type
680
         * @psalm-var array $names
681
         */
682 71
        foreach ($constraints as $type => $names) {
683
            /**
684
             * @psalm-var object|string|null $name
685
             * @psalm-var ConstraintArray $constraint
686
             */
687 66
            foreach ($names as $name => $constraint) {
688
                switch ($type) {
689 66
                    case 'PRIMARY KEY':
690 47
                        $result[self::PRIMARY_KEY] = (new Constraint())
691 47
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'));
692 47
                        break;
693 58
                    case 'FOREIGN KEY':
694 16
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
695 16
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
696 16
                            ->foreignTableName($constraint[0]['foreign_table_name'])
697 16
                            ->foreignColumnNames(DbArrayHelper::getColumn($constraint, 'foreign_column_name'))
698 16
                            ->onDelete($constraint[0]['on_delete'])
699 16
                            ->onUpdate($constraint[0]['on_update'])
700 16
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'))
701 16
                            ->name($name);
702 16
                        break;
703 48
                    case 'UNIQUE':
704 48
                        $result[self::UNIQUES][] = (new Constraint())
705 48
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'))
706 48
                            ->name($name);
707 48
                        break;
708
                }
709
            }
710
        }
711
712 71
        foreach ($result as $type => $data) {
713 71
            $this->setTableMetadata($tableName, $type, $data);
714
        }
715
716 71
        return $result[$returnType];
717
    }
718
719
    /**
720
     * Loads all default value constraints for the given table.
721
     *
722
     * @param string $tableName The table name.
723
     *
724
     * @throws NotSupportedException
725
     *
726
     * @return array Default value constraints for the given table.
727
     */
728 15
    protected function loadTableDefaultValues(string $tableName): array
729
    {
730 15
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
731
    }
732
733
    /**
734
     * Loads all foreign keys for the given table.
735
     *
736
     * @param string $tableName The table name.
737
     *
738
     * @throws Exception
739
     * @throws InvalidConfigException
740
     * @throws Throwable
741
     *
742
     * @return array Foreign keys for the given table.
743
     */
744 9
    protected function loadTableForeignKeys(string $tableName): array
745
    {
746 9
        $tableForeignKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
747 9
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
748
    }
749
750
    /**
751
     * Loads all indexes for the given table.
752
     *
753
     * @param string $tableName The table name.
754
     *
755
     * @throws Exception
756
     * @throws InvalidConfigException
757
     * @throws Throwable
758
     *
759
     * @return IndexConstraint[] Indexes for the given table.
760
     */
761 38
    protected function loadTableIndexes(string $tableName): array
762
    {
763 38
        $sql = <<<SQL
764
        SELECT
765
            `s`.`INDEX_NAME` AS `name`,
766
            `s`.`COLUMN_NAME` AS `column_name`,
767
            `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
768
            `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
769
        FROM `information_schema`.`STATISTICS` AS `s`
770
        WHERE
771
            `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
772
            `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND
773
            `s`.`TABLE_NAME` = :tableName
774
        ORDER BY `s`.`SEQ_IN_INDEX` ASC
775 38
        SQL;
776
777 38
        $resolvedName = $this->resolveTableName($tableName);
778 38
        $indexes = $this->db->createCommand($sql, [
779 38
            ':schemaName' => $resolvedName->getSchemaName(),
780 38
            ':tableName' => $resolvedName->getName(),
781 38
        ])->queryAll();
782
783
        /** @psalm-var array[] $indexes */
784 38
        $indexes = $this->normalizeRowKeyCase($indexes, true);
785 38
        $indexes = DbArrayHelper::index($indexes, null, ['name']);
786 38
        $result = [];
787
788
        /**
789
         * @psalm-var object|string|null $name
790
         * @psalm-var array[] $index
791
         */
792 38
        foreach ($indexes as $name => $index) {
793 38
            $ic = new IndexConstraint();
794
795 38
            $ic->primary((bool) $index[0]['index_is_primary']);
796 38
            $ic->unique((bool) $index[0]['index_is_unique']);
797 38
            $ic->name($name !== 'PRIMARY' ? $name : null);
798 38
            $ic->columnNames(DbArrayHelper::getColumn($index, 'column_name'));
799
800 38
            $result[] = $ic;
801
        }
802
803 38
        return $result;
804
    }
805
806
    /**
807
     * Loads a primary key for the given table.
808
     *
809
     * @param string $tableName The table name.
810
     *
811
     * @throws Exception
812
     * @throws InvalidConfigException
813
     * @throws Throwable
814
     *
815
     * @return Constraint|null Primary key for the given table, `null` if the table has no primary key.*
816
     */
817 45
    protected function loadTablePrimaryKey(string $tableName): Constraint|null
818
    {
819 45
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
820 45
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
821
    }
822
823
    /**
824
     * Loads the metadata for the specified table.
825
     *
826
     * @param string $name The table name.
827
     *
828
     * @throws Exception
829
     * @throws Throwable
830
     *
831
     * @return TableSchemaInterface|null DBMS-dependent table metadata, `null` if the table doesn't exist.
832
     */
833 162
    protected function loadTableSchema(string $name): TableSchemaInterface|null
834
    {
835 162
        $table = $this->resolveTableName($name);
836 162
        $this->resolveTableCreateSql($table);
837 162
        $this->findTableComment($table);
838
839 162
        if ($this->findColumns($table)) {
840 145
            $this->findConstraints($table);
841
842 145
            return $table;
843
        }
844
845 35
        return null;
846
    }
847
848
    /**
849
     * Loads all unique constraints for the given table.
850
     *
851
     * @param string $tableName The table name.
852
     *
853
     * @throws Exception
854
     * @throws InvalidConfigException
855
     * @throws Throwable
856
     *
857
     * @return array Unique constraints for the given table.
858
     */
859 17
    protected function loadTableUniques(string $tableName): array
860
    {
861 17
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
862 17
        return is_array($tableUniques) ? $tableUniques : [];
863
    }
864
865
    /**
866
     * Resolves the table name and schema name (if any).
867
     *
868
     * @param string $name The table name.
869
     *
870
     * @see TableSchemaInterface
871
     */
872 206
    protected function resolveTableName(string $name): TableSchemaInterface
873
    {
874 206
        $resolvedName = new TableSchema();
875
876 206
        $parts = array_reverse($this->db->getQuoter()->getTableNameParts($name));
877 206
        $resolvedName->name($parts[0] ?? '');
878 206
        $resolvedName->schemaName($parts[1] ?? $this->defaultSchema);
879 206
        $resolvedName->fullName(
880 206
            $resolvedName->getSchemaName() !== $this->defaultSchema ?
881 206
            implode('.', array_reverse($parts)) : $resolvedName->getName()
882 206
        );
883
884 206
        return $resolvedName;
885
    }
886
887
    /**
888
     * @throws Exception
889
     * @throws InvalidConfigException
890
     * @throws Throwable
891
     */
892 162
    protected function resolveTableCreateSql(TableSchemaInterface $table): void
893
    {
894 162
        $sql = $this->getCreateTableSql($table);
895 162
        $table->createSql($sql);
896
    }
897
898
    /**
899
     * Creates a column schema for the database.
900
     *
901
     * This method may be overridden by child classes to create a DBMS-specific column schema.
902
     *
903
     * @param string $name Name of the column.
904
     */
905 147
    private function createColumnSchema(string $name): ColumnSchema
906
    {
907 147
        return new ColumnSchema($name);
908
    }
909
910
    /**
911
     * @throws Exception
912
     * @throws InvalidConfigException
913
     * @throws Throwable
914
     */
915 145
    private function getJsonColumns(TableSchemaInterface $table): array
916
    {
917 145
        $sql = $this->getCreateTableSql($table);
918 145
        $result = [];
919 145
        $regexp = '/json_valid\([\`"](.+)[\`"]\s*\)/mi';
920
921 145
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
922
            foreach ($matches as $match) {
923
                $result[] = $match[1];
924
            }
925
        }
926
927 145
        return $result;
928
    }
929
}
930