Test Failed
Pull Request — master (#180)
by Wilmer
13:58
created

Schema::findConstraints()   B

Complexity

Conditions 9
Paths 43

Size

Total Lines 75
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 21.4523

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 9
eloc 52
c 2
b 0
f 0
nc 43
nop 1
dl 0
loc 75
ccs 13
cts 28
cp 0.4643
crap 21.4523
rs 7.4917

How to fix   Long Method   

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

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