Passed
Push — master ( 8a7a3b...05c121 )
by Wilmer
03:53
created

Schema::findColumns()   B

Complexity

Conditions 10
Paths 29

Size

Total Lines 66
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 13.3855

Importance

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

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