Test Failed
Pull Request — master (#142)
by Def
02:20
created

Schema::getLastInsertID()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
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