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

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

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

890
        $table->/** @scrutinizer ignore-call */ 
891
                createSql($sql);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
891
    }
892
893
    /**
894
     * Creates a column schema for the database.
895
     *
896
     * This method may be overridden by child classes to create a DBMS-specific column schema.
897
     *
898
     * @return ColumnSchema column schema instance.
899
     */
900
    private function createColumnSchema(): ColumnSchema
901
    {
902
        return new ColumnSchema();
903
    }
904
}
905