Passed
Pull Request — master (#232)
by Def
08:26 queued 04:35
created

Schema::loadColumnSchema()   F

Complexity

Conditions 27
Paths 500

Size

Total Lines 101
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 59
CRAP Score 27.0033

Importance

Changes 7
Bugs 1 Features 1
Metric Value
cc 27
eloc 63
c 7
b 1
f 1
nc 500
nop 1
dl 0
loc 101
ccs 59
cts 60
cp 0.9833
crap 27.0033
rs 0.6944

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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