Test Failed
Pull Request — master (#142)
by Def
02:20
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 18.1156

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 29
cp 0.5172
crap 18.1156
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\Connection\ConnectionInterface;
11
use Yiisoft\Db\Constraint\Constraint;
12
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
13
use Yiisoft\Db\Constraint\IndexConstraint;
14
use Yiisoft\Db\Exception\Exception;
15
use Yiisoft\Db\Exception\InvalidConfigException;
16
use Yiisoft\Db\Exception\NotSupportedException;
17
use Yiisoft\Db\Expression\Expression;
18
use Yiisoft\Db\Schema\ColumnSchemaInterface;
19
use Yiisoft\Db\Schema\Schema as AbstractSchema;
20
use Yiisoft\Db\Schema\TableNameInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Schema\TableNameInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
    /**
132
     * Create a column schema builder instance giving the type and value precision.
133 385
     *
134
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
135
     *
136
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
137
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
138
     *
139
     * @return ColumnSchemaBuilder column schema builder instance
140
     *
141
     * @psalm-param string[]|int|string|null $length
142
     */
143
    public function createColumnSchemaBuilder(string $type, array|int|string $length = null): ColumnSchemaBuilder
144
    {
145
        return new ColumnSchemaBuilder($type, $length, $this->db->getQuoter());
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mysql\Schema. Did you maybe forget to declare it?
Loading history...
146
    }
147
148 3
    /**
149
     * Returns all unique indexes for the given table.
150 3
     *
151
     * Each array element is of the following structure:
152
     *
153
     * ```php
154
     * [
155
     *     'IndexName1' => ['col1' [, ...]],
156
     *     'IndexName2' => ['col2' [, ...]],
157
     * ]
158
     * ```
159
     *
160
     * @param TableSchemaInterface $table the table metadata.
161
     *
162
     * @throws Exception|InvalidConfigException|Throwable
163
     *
164
     * @return array all unique indexes for the given table.
165
     */
166
    public function findUniqueIndexes(TableSchemaInterface $table): array
167
    {
168
        $sql = $this->getCreateTableSql($table);
169
170
        $uniqueIndexes = [];
171 1
172
        $regexp = '/UNIQUE KEY\s+`(.+)`\s*\((`.+`)+\)/mi';
173 1
174
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
175 1
            foreach ($matches as $match) {
176
                $indexName = $match[1];
177 1
                $indexColumns = array_map('trim', explode('`,`', trim($match[2], '`')));
178
                $uniqueIndexes[$indexName] = $indexColumns;
179 1
            }
180 1
        }
181 1
182 1
        return $uniqueIndexes;
183 1
    }
184
185
    /**
186
     * @inheritDoc
187 1
     */
188
    public function getLastInsertID(?string $sequenceName = null): string
189
    {
190
        return $this->db->getLastInsertID($sequenceName);
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mysql\Schema. Did you maybe forget to declare it?
Loading history...
191
    }
192
193
    /**
194
     * Returns the actual name of a given table name.
195
     *
196
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
197
     * {@see ConnectionInterface::tablePrefix}.
198
     *
199
     * @param string|TableNameInterface $name the table name to be converted.
200
     *
201
     * @return string the real name of the given table name.
202
     */
203
    public function getRawTableName(string|TableNameInterface $name): string
204
    {
205
        if ($name instanceof TableNameInterface) {
206
            return (string)$name;
207
        }
208 166
209
        if (!str_contains($name, '{{')) {
210 166
            return $name;
211 23
        }
212
213 23
        $name = preg_replace('/{{(.*?)}}/', '\1', $name);
214
        return str_replace('%', $this->db->getTablePrefix(), $name);
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mysql\Schema. Did you maybe forget to declare it?
Loading history...
215
    }
216 166
217
    public function supportsSavepoint(): bool
218
    {
219 5
        return $this->db->isSavepointEnabled();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mysql\Schema. Did you maybe forget to declare it?
Loading history...
220
    }
221 5
222
    /**
223
     * Collects the metadata of table columns.
224
     *
225
     * @param TableSchemaInterface $table the table metadata.
226
     *
227
     * @throws Exception|Throwable if DB query fails.
228
     *
229
     * @return bool whether the table exists in the database.
230
     */
231
    protected function findColumns(TableSchemaInterface $table): bool
232
    {
233 102
        $tableName = $table->getFullName() ?? '';
234
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->db->getQuoter()->quoteTableName($tableName);
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mysql\Schema. Did you maybe forget to declare it?
Loading history...
235 102
236 102
        try {
237
            $columns = $this->db->createCommand($sql)->queryAll();
238
        } catch (Exception $e) {
239 102
            $previous = $e->getPrevious();
240 15
241 15
            if ($previous && str_contains($previous->getMessage(), 'SQLSTATE[42S02')) {
242
                /**
243 15
                 * table does not exist.
244
                 *
245
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
246
                 */
247
                return false;
248
            }
249 15
250
            throw $e;
251
        }
252
253
        /** @psalm-var ColumnInfoArray $info */
254
        foreach ($columns as $info) {
255
            $info = $this->normalizeRowKeyCase($info, false);
256 96
257 96
            $column = $this->loadColumnSchema($info);
258
            $table->columns($column->getName(), $column);
259 96
260 96
            if ($column->isPrimaryKey()) {
261
                $table->primaryKey($column->getName());
262 96
                if ($column->isAutoIncrement()) {
263 61
                    $table->sequenceName('');
264 61
                }
265 59
            }
266
        }
267
268
        return true;
269
    }
270 96
271
    /**
272
     * Collects the foreign key column details for the given table.
273
     *
274
     * @param TableSchemaInterface $table the table metadata.
275
     *
276
     * @throws Exception|Throwable
277
     */
278
    protected function findConstraints(TableSchemaInterface $table): void
279
    {
280 96
        $sql = <<<SQL
281
        SELECT
282 96
            `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
283
            `kcu`.`COLUMN_NAME` AS `column_name`,
284
            `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
285
            `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
286
        FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
287
        JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
288
            (
289
                `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
290
                (
291
                    `kcu`.`CONSTRAINT_CATALOG` IS NULL AND
292
                    `rc`.`CONSTRAINT_CATALOG` IS NULL
293
                )
294
            ) AND
295
            `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
296
            `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME` AND
297
            `kcu`.`TABLE_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
298
            `kcu`.`TABLE_NAME` = `rc`.`TABLE_NAME`
299
        WHERE
300
            `rc`.`CONSTRAINT_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
301
            `rc`.`TABLE_NAME` = :tableName
302
        SQL;
303
304
        try {
305
            $rows = $this->db->createCommand($sql, [
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mysql\Schema. Did you maybe forget to declare it?
Loading history...
306
                ':schemaName' => $table->getSchemaName(),
307 96
                ':tableName' => $table->getName(),
308 96
            ])->queryAll();
309 96
310 96
            $constraints = [];
311
312 96
            /**  @psalm-var RowConstraint $row */
313
            foreach ($rows as $row) {
314
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
315 96
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
316 23
            }
317 23
318
            $table->foreignKeys([]);
319
320 96
            /**
321
             * @var array{referenced_table_name: string, columns: array} $constraint
322
             */
323
            foreach ($constraints as $name => $constraint) {
324
                $table->foreignKey($name, array_merge(
325 96
                    [$constraint['referenced_table_name']],
326 23
                    $constraint['columns']
327 23
                ));
328 23
            }
329
        } catch (Exception $e) {
330
            $previous = $e->getPrevious();
331
332
            if ($previous === null || !str_contains($previous->getMessage(), 'SQLSTATE[42S02')) {
333
                throw $e;
334
            }
335
336
            // table does not exist, try to determine the foreign keys using the table creation sql
337
            $sql = $this->getCreateTableSql($table);
338
            $regexp = '/FOREIGN KEY\s+\(([^)]+)\)\s+REFERENCES\s+([^(^\s]+)\s*\(([^)]+)\)/mi';
339
340
            if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
341
                foreach ($matches as $match) {
342
                    $fks = array_map('trim', explode(',', str_replace('`', '', $match[1])));
343
                    $pks = array_map('trim', explode(',', str_replace('`', '', $match[3])));
344
                    $constraint = [str_replace('`', '', $match[2])];
345
346
                    foreach ($fks as $k => $name) {
347
                        $constraint[$name] = $pks[$k];
348
                    }
349
350
                    $table->foreignKey(md5(serialize($constraint)), $constraint);
351
                }
352
                $table->foreignKeys(array_values($table->getForeignKeys()));
353
            }
354
        }
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|InvalidConfigException|Throwable
366
     *
367
     * @return array All table names in the database. The names have NO schema name prefix.
368
     */
369
    protected function findTableNames(string $schema = ''): array
370
    {
371 7
        $sql = 'SHOW TABLES';
372
373 7
        if ($schema !== '') {
374
            $sql .= ' FROM ' . $this->db->getQuoter()->quoteSimpleTableName($schema);
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mysql\Schema. Did you maybe forget to declare it?
Loading history...
375 7
        }
376
377
        return $this->db->createCommand($sql)->queryColumn();
378
    }
379 7
380
    /**
381
     * Returns the cache key for the specified table name.
382
     *
383
     * @param string $name the table name.
384
     *
385
     * @return array the cache key.
386
     */
387
    protected function getCacheKey(string $name): array
388
    {
389 166
        return array_merge([__CLASS__], $this->db->getCacheKey(), [$name]);
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mysql\Schema. Did you maybe forget to declare it?
Loading history...
390
    }
391 166
392
    /**
393
     * Returns the cache tag name.
394
     *
395
     * This allows {@see refresh()} to invalidate all cached table schemas.
396
     *
397
     * @return string the cache tag name.
398
     */
399
    protected function getCacheTag(): string
400
    {
401 166
        return md5(serialize(array_merge([__CLASS__], $this->db->getCacheKey())));
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mysql\Schema. Did you maybe forget to declare it?
Loading history...
402
    }
403 166
404
    /**
405
     * Gets the CREATE TABLE sql string.
406
     *
407
     * @param TableSchemaInterface $table the table metadata.
408
     *
409
     * @throws Exception|InvalidConfigException|Throwable
410
     *
411
     * @return string $sql the result of 'SHOW CREATE TABLE'.
412
     */
413
    protected function getCreateTableSql(TableSchemaInterface $table): string
414
    {
415 102
        $tableName = $table->getFullName() ?? '';
416
417 102
        try {
418
            /** @var array<array-key, string> $row */
419
            $row = $this->db->createCommand(
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mysql\Schema. Did you maybe forget to declare it?
Loading history...
420
                'SHOW CREATE TABLE ' . $this->db->getQuoter()->quoteTableName($tableName)
421 102
            )->queryOne();
422 102
423 102
            if (isset($row['Create Table'])) {
424
                $sql = $row['Create Table'];
425 96
            } else {
426 94
                $row = array_values($row);
427
                $sql = $row[1];
428 2
            }
429 96
        } catch (Exception) {
430
            $sql = '';
431 15
        }
432 15
433
        return $sql;
434
    }
435 102
436
    /**
437
     * Loads the column information into a {@see ColumnSchemaInterface} object.
438
     *
439
     * @param array $info column information.
440
     *
441
     * @throws JsonException
442
     *
443
     * @return ColumnSchemaInterface the column schema object.
444
     */
445
    protected function loadColumnSchema(array $info): ColumnSchemaInterface
446
    {
447 97
        $column = $this->createColumnSchema();
448
449 97
        /** @psalm-var ColumnInfoArray $info */
450
        $column->name($info['field']);
451
        $column->allowNull($info['null'] === 'YES');
452 97
        $column->primaryKey(str_contains($info['key'], 'PRI'));
453 97
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
454 97
        $column->comment($info['comment']);
455 97
        $column->dbType($info['type']);
456 97
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
457 97
        $column->type(self::TYPE_STRING);
458 97
459 97
        $extra = $info['extra'];
460
        if (str_starts_with($extra, 'DEFAULT_GENERATED')) {
461 97
            $extra = strtoupper(substr($extra, 18));
462 97
        }
463 23
        $column->extra(trim($extra));
464
465 97
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
466
            $type = strtolower($matches[1]);
467 97
468 97
            if (isset($this->typeMap[$type])) {
469
                $column->type($this->typeMap[$type]);
470 97
            }
471 97
472
            if (!empty($matches[2])) {
473
                if ($type === 'enum') {
474 97
                    preg_match_all("/'[^']*'/", $matches[2], $values);
475 89
476 21
                    foreach ($values[0] as $i => $value) {
477
                        $values[$i] = trim($value, "'");
478 21
                    }
479 21
480
                    $column->enumValues($values);
481
                } else {
482 21
                    $values = explode(',', $matches[2]);
483
                    $column->precision((int) $values[0]);
484 89
                    $column->size((int) $values[0]);
485 89
486 89
                    if (isset($values[1])) {
487
                        $column->scale((int) $values[1]);
488 89
                    }
489 33
490
                    if ($column->getSize() === 1 && $type === 'tinyint') {
491
                        $column->type(self::TYPE_BOOLEAN);
492 89
                    } elseif ($type === 'bit') {
493 22
                        if ($column->getSize() > 32) {
494 89
                            $column->type(self::TYPE_BIGINT);
495 21
                        } elseif ($column->getSize() === 32) {
496
                            $column->type(self::TYPE_INTEGER);
497 21
                        }
498
                    }
499
                }
500
            }
501
        }
502
503
        $column->phpType($this->getColumnPhpType($column));
504
505 97
        if (!$column->isPrimaryKey()) {
506
            /**
507 97
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
508
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
509
             *
510
             * See details here: https://mariadb.com/kb/en/library/now/#description
511
             */
512
            if (
513
                ($column->getType() === 'timestamp' || $column->getType() === 'datetime')
514
                && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', (string) $info['default'], $matches)
515 94
            ) {
516 94
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
517
                    ? '(' . $matches[1] . ')' : '')));
518 24
            } elseif (isset($type) && $type === 'bit') {
519 24
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
520 91
            } else {
521 21
                $column->defaultValue($column->phpTypecast($info['default']));
522
            }
523 94
        } elseif ($info['default'] !== null) {
524
            $column->defaultValue($column->phpTypecast($info['default']));
525 61
        }
526 1
527
        return $column;
528
    }
529 97
530
    /**
531
     * Loads all check constraints for the given table.
532
     *
533
     * @param TableNameInterface $tableName table name.
534
     *
535
     * @throws NotSupportedException
536
     *
537
     * @return array check constraints for the given table.
538
     */
539
    protected function loadTableChecks(TableNameInterface $tableName): array
540
    {
541 12
        throw new NotSupportedException('MySQL does not support check constraints.');
542
    }
543 12
544
    /**
545
     * Loads multiple types of constraints and returns the specified ones.
546
     *
547
     * @param TableNameInterface $tableName table name.
548
     * @param string $returnType return type:
549
     * - primaryKey
550
     * - foreignKeys
551
     * - uniques
552
     *
553
     * @throws Exception|InvalidConfigException|Throwable
554
     *
555
     * @return array|Constraint|null (Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
556
     */
557
    private function loadTableConstraints(TableNameInterface $tableName, string $returnType): array|Constraint|null
558
    {
559 51
        $sql = <<<SQL
560
        SELECT
561 51
            `kcu`.`CONSTRAINT_NAME` AS `name`,
562
            `kcu`.`COLUMN_NAME` AS `column_name`,
563
            `tc`.`CONSTRAINT_TYPE` AS `type`,
564
        CASE
565
            WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
566
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
567
        END AS `foreign_table_schema`,
568
            `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
569
            `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
570
            `rc`.`UPDATE_RULE` AS `on_update`,
571
            `rc`.`DELETE_RULE` AS `on_delete`,
572
            `kcu`.`ORDINAL_POSITION` AS `position`
573
        FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
574
        JOIN `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc` ON
575
            `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
576
            `rc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
577
            `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
578
        JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON
579
            `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
580
            `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
581
            `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND
582
            `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY'
583
        WHERE
584
            `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
585
            `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
586
            `kcu`.`TABLE_NAME` = :tableName
587
        UNION
588
        SELECT
589
            `kcu`.`CONSTRAINT_NAME` AS `name`,
590
            `kcu`.`COLUMN_NAME` AS `column_name`,
591
            `tc`.`CONSTRAINT_TYPE` AS `type`,
592
        NULL AS `foreign_table_schema`,
593
        NULL AS `foreign_table_name`,
594
        NULL AS `foreign_column_name`,
595
        NULL AS `on_update`,
596
        NULL AS `on_delete`,
597
            `kcu`.`ORDINAL_POSITION` AS `position`
598
        FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
599
        JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON
600
            `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
601
            `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
602
            `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND
603
            `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE')
604
        WHERE
605
            `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
606
            `kcu`.`TABLE_NAME` = :tableName
607
        ORDER BY `position` ASC
608
        SQL;
609
610
        $resolvedName = $this->resolveTableName($tableName);
611
612 51
        $constraints = $this->db->createCommand($sql, [
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mysql\Schema. Did you maybe forget to declare it?
Loading history...
613
            ':schemaName' => $resolvedName->getSchemaName(),
614 51
            ':tableName' => $resolvedName->getName(),
615 51
        ])->queryAll();
616 51
617 51
        /** @var array<array-key, array> $constraints */
618
        $constraints = $this->normalizeRowKeyCase($constraints, true);
619
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
620 51
621 51
        $result = [
622
            self::PRIMARY_KEY => null,
623 51
            self::FOREIGN_KEYS => [],
624
            self::UNIQUES => [],
625 51
        ];
626 51
627
        /**
628
         * @var string $type
629
         * @var array $names
630
         */
631
        foreach ($constraints as $type => $names) {
632
            /**
633 51
             * @psalm-var object|string|null $name
634
             * @psalm-var ConstraintArray $constraint
635
             */
636
            foreach ($names as $name => $constraint) {
637
                switch ($type) {
638 51
                    case 'PRIMARY KEY':
639 51
                        $result[self::PRIMARY_KEY] = (new Constraint())
640 51
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
641 40
                        break;
642 40
                    case 'FOREIGN KEY':
643 40
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
644 49
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
645 13
                            ->foreignTableName($constraint[0]['foreign_table_name'])
646 13
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
647 13
                            ->onDelete($constraint[0]['on_delete'])
648 13
                            ->onUpdate($constraint[0]['on_update'])
649 13
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
650 13
                            ->name($name);
651 13
                        break;
652 13
                    case 'UNIQUE':
653 13
                        $result[self::UNIQUES][] = (new Constraint())
654 40
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
655 40
                            ->name($name);
656 40
                        break;
657 40
                }
658 40
            }
659
        }
660
661
        foreach ($result as $type => $data) {
662
            $this->setTableMetadata($tableName, $type, $data);
663 51
        }
664 51
665
        return $result[$returnType];
666
    }
667 51
668
    /**
669
     * Loads all default value constraints for the given table.
670
     *
671
     * @param TableNameInterface $tableName table name.
672
     *
673
     * @throws NotSupportedException
674
     *
675
     * @return array default value constraints for the given table.
676
     */
677
    protected function loadTableDefaultValues(TableNameInterface $tableName): array
678
    {
679 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
680
    }
681 12
682
    /**
683
     * Loads all foreign keys for the given table.
684
     *
685
     * @param TableNameInterface $tableName table name.
686
     *
687
     * @throws Exception|InvalidConfigException|Throwable
688
     *
689
     * @return array foreign keys for the given table.
690
     */
691
    protected function loadTableForeignKeys(TableNameInterface $tableName): array
692
    {
693 5
        $tableForeignKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
694
695 5
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
696
    }
697 5
698
    /**
699
     * Loads all indexes for the given table.
700
     *
701
     * @param TableNameInterface $tableName table name.
702
     *
703
     * @throws Exception|InvalidConfigException|Throwable
704
     *
705
     * @return IndexConstraint[] indexes for the given table.
706
     */
707
    protected function loadTableIndexes(TableNameInterface $tableName): array
708
    {
709 29
        $sql = <<<SQL
710
        SELECT
711 29
            `s`.`INDEX_NAME` AS `name`,
712
            `s`.`COLUMN_NAME` AS `column_name`,
713
            `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
714
            `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
715
        FROM `information_schema`.`STATISTICS` AS `s`
716
        WHERE
717
            `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
718
            `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND
719
            `s`.`TABLE_NAME` = :tableName
720
        ORDER BY `s`.`SEQ_IN_INDEX` ASC
721
        SQL;
722
723
        $resolvedName = $this->resolveTableName($tableName);
724
725 29
        $indexes = $this->db->createCommand($sql, [
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mysql\Schema. Did you maybe forget to declare it?
Loading history...
726
            ':schemaName' => $resolvedName->getSchemaName(),
727 29
            ':tableName' => $resolvedName->getName(),
728 29
        ])->queryAll();
729 29
730 29
        /** @var array[] $indexes */
731
        $indexes = $this->normalizeRowKeyCase($indexes, true);
732
        $indexes = ArrayHelper::index($indexes, null, 'name');
733 29
        $result = [];
734 29
735 29
        /**
736
         * @psalm-var object|string|null $name
737
         * @psalm-var array[] $index
738
         */
739
        foreach ($indexes as $name => $index) {
740
            $ic = new IndexConstraint();
741 29
742 29
            $ic->primary((bool) $index[0]['index_is_primary']);
743
            $ic->unique((bool) $index[0]['index_is_unique']);
744 29
            $ic->name($name !== 'PRIMARY' ? $name : null);
745 29
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
746 29
747 29
            $result[] = $ic;
748
        }
749 29
750
        return $result;
751
    }
752 29
753
    /**
754
     * Loads a primary key for the given table.
755
     *
756
     * @param TableNameInterface $tableName table name.
757
     *
758
     * @throws Exception|InvalidConfigException|Throwable
759
     *
760
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.*
761
     */
762
    protected function loadTablePrimaryKey(TableNameInterface $tableName): ?Constraint
763
    {
764 32
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
765
766 32
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
767
    }
768 32
769
    /**
770
     * Loads the metadata for the specified table.
771
     *
772
     * @param TableNameInterface $name table name.
773
     *
774
     * @throws Exception|Throwable
775
     *
776
     * @return TableSchemaInterface|null DBMS-dependent table metadata, `null` if the table does not exist.
777
     */
778
    protected function loadTableSchema(TableNameInterface $name): ?TableSchemaInterface
779
    {
780 102
        $table = $this->resolveTableName($name);
781
        $this->resolveTableCreateSql($table);
782 102
783 102
        if ($this->findColumns($table)) {
784
            $this->findConstraints($table);
785 102
786 96
            return $table;
787
        }
788 96
789
        return null;
790
    }
791 15
792
    /**
793
     * Loads all unique constraints for the given table.
794
     *
795
     * @param TableNameInterface $tableName table name.
796
     *
797
     * @throws Exception|InvalidConfigException|Throwable
798
     *
799
     * @return array unique constraints for the given table.
800
     */
801
    protected function loadTableUniques(TableNameInterface $tableName): array
802
    {
803 14
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
804
805 14
        return is_array($tableUniques) ? $tableUniques : [];
806
    }
807 14
808
    /**
809
     * Changes row's array key case to lower.
810
     *
811
     * @param array $row row's array or an array of row's arrays.
812
     * @param bool $multiple whether multiple rows or a single row passed.
813
     *
814
     * @return array normalized row or rows.
815
     */
816
    protected function normalizeRowKeyCase(array $row, bool $multiple): array
817
    {
818 140
        if ($multiple) {
819
            return array_map(static function (array $row) {
820 140
                return array_change_key_case($row, CASE_LOWER);
821 62
            }, $row);
822 62
        }
823
824
        return array_change_key_case($row, CASE_LOWER);
825
    }
826 96
827
    /**
828
     * Resolves the table name and schema name (if any).
829
     *
830
     * @param TableNameInterface $name the table name.
831
     *
832
     * {@see TableSchemaInterface}
833
     *
834
     * @return TableSchemaInterface
835
     */
836
    protected function resolveTableName(TableNameInterface $name): TableSchemaInterface
837
    {
838 142
        $resolvedName = new TableSchema();
839
840 142
        $resolvedName->schemaName($name->getSchemaName() ?? $this->defaultSchema);
841
        $resolvedName->name($name->getTableName());
842 142
        $resolvedName->fullName($name->getSchemaName() === $this->defaultSchema ? $name->getTableName() : (string) $name);
843
844 142
        return $resolvedName;
845
    }
846
847
    /**
848 142
     * @throws Exception|InvalidConfigException|Throwable
849 142
     */
850
    protected function resolveTableCreateSql(TableSchemaInterface $table): void
851
    {
852 142
        $sql = $this->getCreateTableSql($table);
853 142
        $table->createSql($sql);
854
    }
855 142
856
    /**
857
     * Creates a column schema for the database.
858
     *
859
     * This method may be overridden by child classes to create a DBMS-specific column schema.
860
     *
861 102
     * @return ColumnSchema column schema instance.
862
     */
863 102
    private function createColumnSchema(): ColumnSchema
864 102
    {
865
        return new ColumnSchema();
866
    }
867
}
868