Passed
Pull Request — master (#139)
by Def
15:56 queued 13:17
created

Schema::findConstraints()   B

Complexity

Conditions 9
Paths 43

Size

Total Lines 75
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 19.125

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

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