Passed
Pull Request — dev (#98)
by Wilmer
43:38 queued 28:30
created

SchemaPDOMysql::findConstraints()   B

Complexity

Conditions 9
Paths 43

Size

Total Lines 75
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 22.8922

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 52
c 1
b 0
f 0
nc 43
nop 1
dl 0
loc 75
ccs 12
cts 27
cp 0.4444
crap 22.8922
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\PDO;
6
7
use JsonException;
8
use PDO;
9
use PDOException;
10
use Throwable;
11
use Yiisoft\Arrays\ArrayHelper;
12
use Yiisoft\Db\Cache\SchemaCache;
13
use Yiisoft\Db\Connection\ConnectionPDOInterface;
14
use Yiisoft\Db\Constraint\Constraint;
15
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
16
use Yiisoft\Db\Constraint\IndexConstraint;
17
use Yiisoft\Db\Exception\Exception;
18
use Yiisoft\Db\Exception\InvalidCallException;
19
use Yiisoft\Db\Exception\InvalidConfigException;
20
use Yiisoft\Db\Exception\NotSupportedException;
21
use Yiisoft\Db\Expression\Expression;
22
use Yiisoft\Db\Mysql\ColumnSchema;
23
use Yiisoft\Db\Mysql\ColumnSchemaBuilder;
24
use Yiisoft\Db\Mysql\TableSchema;
25
use Yiisoft\Db\Schema\Schema;
26
27
use function array_change_key_case;
28
use function array_map;
29
use function array_merge;
30
use function array_values;
31
use function bindec;
32
use function explode;
33
use function md5;
34
use function preg_match;
35
use function preg_match_all;
36
use function serialize;
37
use function str_replace;
38
use function stripos;
39
use function strtolower;
40
use function trim;
41
42
/**
43
 * The class Schema is the class for retrieving metadata from a Mysql database (version 5.7 and above).
44
 *
45
 * @psalm-type ColumnArray = array{
46
 *   table_schema: string,
47
 *   table_name: string,
48
 *   column_name: string,
49
 *   data_type: string,
50
 *   type_type: string|null,
51
 *   character_maximum_length: int,
52
 *   column_comment: string|null,
53
 *   modifier: int,
54
 *   is_nullable: bool,
55
 *   column_default: mixed,
56
 *   is_autoinc: bool,
57
 *   sequence_name: string|null,
58
 *   enum_values: array<array-key, float|int|string>|string|null,
59
 *   numeric_precision: int|null,
60
 *   numeric_scale: int|null,
61
 *   size: string|null,
62
 *   is_pkey: bool|null,
63
 *   dimension: int
64
 * }
65
 *
66
 * @psalm-type ColumnInfoArray = array{
67
 *   field: string,
68
 *   type: string,
69
 *   collation: string|null,
70
 *   null: string,
71
 *   key: string,
72
 *   default: string|null,
73
 *   extra: string,
74
 *   privileges: string,
75
 *   comment: string
76
 * }
77
 *
78
 * @psalm-type RowConstraint = array{
79
 *   constraint_name: string,
80
 *   column_name: string,
81
 *   referenced_table_name: string,
82
 *   referenced_column_name: string
83
 * }
84
 *
85
 * @psalm-type ConstraintArray = array<
86
 *   array-key,
87
 *   array {
88
 *     name: string,
89
 *     column_name: string,
90
 *     type: string,
91
 *     foreign_table_schema: string|null,
92
 *     foreign_table_name: string|null,
93
 *     foreign_column_name: string|null,
94
 *     on_update: string,
95
 *     on_delete: string,
96
 *     check_expr: string
97
 *   }
98
 * >
99
 */
100
final class SchemaPDOMysql extends Schema
101
{
102
    /** @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...
103
    private array $typeMap = [
104
        'tinyint' => self::TYPE_TINYINT,
105
        'bit' => self::TYPE_INTEGER,
106
        'smallint' => self::TYPE_SMALLINT,
107
        'mediumint' => self::TYPE_INTEGER,
108
        'int' => self::TYPE_INTEGER,
109
        'integer' => self::TYPE_INTEGER,
110
        'bigint' => self::TYPE_BIGINT,
111
        'float' => self::TYPE_FLOAT,
112
        'double' => self::TYPE_DOUBLE,
113
        'real' => self::TYPE_FLOAT,
114
        'decimal' => self::TYPE_DECIMAL,
115
        'numeric' => self::TYPE_DECIMAL,
116
        'tinytext' => self::TYPE_TEXT,
117
        'mediumtext' => self::TYPE_TEXT,
118
        'longtext' => self::TYPE_TEXT,
119
        'longblob' => self::TYPE_BINARY,
120
        'blob' => self::TYPE_BINARY,
121
        'text' => self::TYPE_TEXT,
122
        'varchar' => self::TYPE_STRING,
123
        'string' => self::TYPE_STRING,
124
        'char' => self::TYPE_CHAR,
125
        'datetime' => self::TYPE_DATETIME,
126
        'year' => self::TYPE_DATE,
127
        'date' => self::TYPE_DATE,
128
        'time' => self::TYPE_TIME,
129
        'timestamp' => self::TYPE_TIMESTAMP,
130
        'enum' => self::TYPE_STRING,
131
        'varbinary' => self::TYPE_BINARY,
132
        'json' => self::TYPE_JSON,
133
    ];
134
135 372
    public function __construct(private ConnectionPDOInterface $db, SchemaCache $schemaCache)
136
    {
137 372
        parent::__construct($schemaCache);
138
    }
139
140
    /**
141
     * Create a column schema builder instance giving the type and value precision.
142
     *
143
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
144
     *
145
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
146
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
147
     *
148
     * @return ColumnSchemaBuilder column schema builder instance
149
     */
150 3
    public function createColumnSchemaBuilder(string $type, array|int|string $length = null): ColumnSchemaBuilder
151
    {
152 3
        return new ColumnSchemaBuilder($type, $length, $this->db->getQuoter());
153
    }
154
155
    /**
156
     * @throws Exception|InvalidConfigException|Throwable
157
     */
158 1
    public function createSavepoint(string $name): void
159
    {
160 1
        $this->db->createCommand("SAVEPOINT $name")->execute();
161
    }
162
163
    /**
164
     * Returns all unique indexes for the given table.
165
     *
166
     * Each array element is of the following structure:
167
     *
168
     * ```php
169
     * [
170
     *     'IndexName1' => ['col1' [, ...]],
171
     *     'IndexName2' => ['col2' [, ...]],
172
     * ]
173
     * ```
174
     *
175
     * @param TableSchema $table the table metadata.
176
     *
177
     * @throws Exception|InvalidConfigException|Throwable
178
     *
179
     * @return array all unique indexes for the given table.
180
     */
181 1
    public function findUniqueIndexes(TableSchema $table): array
182
    {
183 1
        $sql = $this->getCreateTableSql($table);
184
185 1
        $uniqueIndexes = [];
186
187 1
        $regexp = '/UNIQUE KEY\s+`(.+)`\s*\((`.+`)+\)/mi';
188
189 1
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
190 1
            foreach ($matches as $match) {
191 1
                $indexName = $match[1];
192 1
                $indexColumns = array_map('trim', explode('`,`', trim($match[2], '`')));
193 1
                $uniqueIndexes[$indexName] = $indexColumns;
194
            }
195
        }
196
197 1
        return $uniqueIndexes;
198
    }
199
200
    /**
201
     * Returns the ID of the last inserted row or sequence value.
202
     *
203
     * @param string $sequenceName name of the sequence object (required by some DBMS)
204
     *
205
     * @throws InvalidCallException if the DB connection is not active
206
     *
207
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
208
     *
209
     * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
210
     */
211 3
    public function getLastInsertID(string $sequenceName = ''): string
212
    {
213 3
        $pdo = $this->db->getPDO();
214
215 3
        if ($this->db->isActive() && $pdo !== null) {
216 3
            return $pdo->lastInsertId(
217 3
                $sequenceName === '' ? null : $this->db->getQuoter()->quoteTableName($sequenceName)
218
            );
219
        }
220
221
        throw new InvalidCallException('DB Connection is not active.');
222
    }
223
224
    /**
225
     * Returns the actual name of a given table name.
226
     *
227
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
228
     * {@see ConnectionInterface::tablePrefix}.
229
     *
230
     * @param string $name the table name to be converted.
231
     *
232
     * @return string the real name of the given table name.
233
     */
234 154
    public function getRawTableName(string $name): string
235
    {
236 154
        if (str_contains($name, '{{')) {
237 23
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
238
239 23
            return str_replace('%', $this->db->getTablePrefix(), $name);
240
        }
241
242 154
        return $name;
243
    }
244
245 1
    public function rollBackSavepoint(string $name): void
246
    {
247 1
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
248
    }
249
250
    /**
251
     * @throws Exception|InvalidConfigException|Throwable
252
     */
253
    public function releaseSavepoint(string $name): void
254
    {
255
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
256
    }
257
258 2
    public function setTransactionIsolationLevel(string $level): void
259
    {
260 2
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
261
    }
262
263 2
    public function supportsSavepoint(): bool
264
    {
265 2
        return $this->db->isSavepointEnabled();
266
    }
267
268
    /**
269
     * Collects the metadata of table columns.
270
     *
271
     * @param TableSchema $table the table metadata.
272
     *
273
     * @throws Exception|Throwable if DB query fails.
274
     *
275
     * @return bool whether the table exists in the database.
276
     */
277 94
    protected function findColumns(TableSchema $table): bool
278
    {
279 94
        $tableName = $table->getFullName() ?? '';
280 94
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->db->getQuoter()->quoteTableName($tableName);
281
282
        try {
283 94
            $columns = $this->db->createCommand($sql)->queryAll();
284 15
        } catch (Exception $e) {
285 15
            $previous = $e->getPrevious();
286
287 15
            if ($previous instanceof PDOException && str_contains($previous->getMessage(), 'SQLSTATE[42S02')) {
288
                /**
289
                 * table does not exist.
290
                 *
291
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
292
                 */
293 15
                return false;
294
            }
295
296
            throw $e;
297
        }
298
299 88
        $slavePdo = $this->db->getSlavePdo();
300
301
        /** @psalm-var ColumnInfoArray $info */
302 88
        foreach ($columns as $info) {
303 88
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_LOWER) {
304 87
                $info = array_change_key_case($info, CASE_LOWER);
305
            }
306
307 88
            $column = $this->loadColumnSchema($info);
308 88
            $table->columns($column->getName(), $column);
309
310 88
            if ($column->isPrimaryKey()) {
311 54
                $table->primaryKey($column->getName());
312 54
                if ($column->isAutoIncrement()) {
313 52
                    $table->sequenceName('');
314
                }
315
            }
316
        }
317
318 88
        return true;
319
    }
320
321
    /**
322
     * Collects the foreign key column details for the given table.
323
     *
324
     * @param TableSchema $table the table metadata.
325
     *
326
     * @throws Exception|Throwable
327
     */
328 88
    protected function findConstraints(TableSchema $table): void
329
    {
330 88
        $sql = <<<SQL
331
        SELECT
332
            `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
333
            `kcu`.`COLUMN_NAME` AS `column_name`,
334
            `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
335
            `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
336
        FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
337
        JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
338
            (
339
                `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
340
                (
341
                    `kcu`.`CONSTRAINT_CATALOG` IS NULL AND
342
                    `rc`.`CONSTRAINT_CATALOG` IS NULL
343
                )
344
            ) AND
345
            `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
346
            `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME` AND
347
            `kcu`.`TABLE_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
348
            `kcu`.`TABLE_NAME` = `rc`.`TABLE_NAME`
349
        WHERE
350
            `rc`.`CONSTRAINT_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
351
            `rc`.`TABLE_NAME` = :tableName
352
        SQL;
353
354
        try {
355 88
            $rows = $this->db->createCommand($sql, [
356 88
                ':schemaName' => $table->getSchemaName(),
357 88
                ':tableName' => $table->getName(),
358
            ])->queryAll();
359
360 88
            $constraints = [];
361
362
            /**  @psalm-var RowConstraint $row */
363 88
            foreach ($rows as $row) {
364 24
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
365 24
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
366
            }
367
368 88
            $table->foreignKeys([]);
369
370
            /**
371
             * @var array{referenced_table_name: string, columns: array} $constraint
372
             */
373 88
            foreach ($constraints as $name => $constraint) {
374 24
                $table->foreignKey($name, array_merge(
375
                    [$constraint['referenced_table_name']],
376
                    $constraint['columns']
377
                ));
378
            }
379
        } catch (Exception $e) {
380
            $previous = $e->getPrevious();
381
382
            if (!$previous instanceof PDOException || !str_contains($previous->getMessage(), 'SQLSTATE[42S02')) {
383
                throw $e;
384
            }
385
386
            // table does not exist, try to determine the foreign keys using the table creation sql
387
            $sql = $this->getCreateTableSql($table);
388
            $regexp = '/FOREIGN KEY\s+\(([^)]+)\)\s+REFERENCES\s+([^(^\s]+)\s*\(([^)]+)\)/mi';
389
390
            if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
391
                foreach ($matches as $match) {
392
                    $fks = array_map('trim', explode(',', str_replace('`', '', $match[1])));
393
                    $pks = array_map('trim', explode(',', str_replace('`', '', $match[3])));
394
                    $constraint = [str_replace('`', '', $match[2])];
395
396
                    foreach ($fks as $k => $name) {
397
                        $constraint[$name] = $pks[$k];
398
                    }
399
400
                    $table->foreignKey(md5(serialize($constraint)), $constraint);
401
                }
402
                $table->foreignKeys(array_values($table->getForeignKeys()));
403
            }
404
        }
405
    }
406
407
    /**
408
     * Returns all table names in the database.
409
     *
410
     * This method should be overridden by child classes in order to support this feature because the default
411
     * implementation simply throws an exception.
412
     *
413
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
414
     *
415
     * @throws Exception|InvalidConfigException|Throwable
416
     *
417
     * @return array all table names in the database. The names have NO schema name prefix.
418
     */
419 3
    protected function findTableNames(string $schema = ''): array
420
    {
421 3
        $sql = 'SHOW TABLES';
422
423 3
        if ($schema !== '') {
424
            $sql .= ' FROM ' . $this->db->getQuoter()->quoteSimpleTableName($schema);
425
        }
426
427 3
        return $this->db->createCommand($sql)->queryColumn();
428
    }
429
430
    /**
431
     * Returns the cache key for the specified table name.
432
     *
433
     * @param string $name the table name.
434
     *
435
     * @return array the cache key.
436
     */
437 154
    protected function getCacheKey(string $name): array
438
    {
439
        return [
440
            __CLASS__,
441 154
            $this->db->getDriver()->getDsn(),
442 154
            $this->db->getDriver()->getUsername(),
443 154
            $this->getRawTableName($name),
444
        ];
445
    }
446
447
    /**
448
     * Returns the cache tag name.
449
     *
450
     * This allows {@see refresh()} to invalidate all cached table schemas.
451
     *
452
     * @return string the cache tag name.
453
     */
454 154
    protected function getCacheTag(): string
455
    {
456 154
        return md5(serialize([
457
            __CLASS__,
458 154
            $this->db->getDriver()->getDsn(),
459 154
            $this->db->getDriver()->getUsername(),
460
        ]));
461
    }
462
463
    /**
464
     * Gets the CREATE TABLE sql string.
465
     *
466
     * @param TableSchema $table the table metadata.
467
     *
468
     * @throws Exception|InvalidConfigException|Throwable
469
     *
470
     * @return string $sql the result of 'SHOW CREATE TABLE'.
471
     */
472 1
    protected function getCreateTableSql(TableSchema $table): string
473
    {
474 1
        $tableName = $table->getFullName() ?? '';
475
476
        /** @var array<array-key, string> $row */
477 1
        $row = $this->db->createCommand(
478 1
            'SHOW CREATE TABLE ' . $this->db->getQuoter()->quoteTableName($tableName)
479
        )->queryOne();
480
481 1
        if (isset($row['Create Table'])) {
482 1
            $sql = $row['Create Table'];
483
        } else {
484
            $row = array_values($row);
485
            $sql = $row[1];
486
        }
487
488 1
        return $sql;
489
    }
490
491
    /**
492
     * @throws Exception|InvalidCallException|InvalidConfigException|Throwable
493
     */
494
    public function insert(string $table, array $columns): bool|array
495
    {
496
        $command = $this->db->createCommand()->insert($table, $columns);
497
        $tablePrimaryKey = [];
498
499
        if (!$command->execute()) {
500
            return false;
501
        }
502
503
        $tableSchema = $this->getTableSchema($table);
504
        $result = [];
505
506
        if ($tableSchema !== null) {
507
            $tablePrimaryKey = $tableSchema->getPrimaryKey();
508
        }
509
510
        foreach ($tablePrimaryKey as $name) {
511
            if ($tableSchema?->getColumn($name)?->isAutoIncrement()) {
512
                $result[$name] = $this->getLastInsertID((string) $tableSchema?->getSequenceName());
513
                break;
514
            }
515
516
            /** @var mixed */
517
            $result[$name] = $columns[$name] ?? $tableSchema?->getColumn($name)?->getDefaultValue();
518
        }
519
520
        return $result;
521
    }
522
523
    /**
524
     * Loads the column information into a {@see ColumnSchema} object.
525
     *
526
     * @param array $info column information.
527
     *
528
     * @throws JsonException
529
     *
530
     * @return ColumnSchema the column schema object.
531
     */
532 89
    protected function loadColumnSchema(array $info): ColumnSchema
533
    {
534 89
        $column = $this->createColumnSchema();
535
536
        /** @psalm-var ColumnInfoArray $info */
537 89
        $column->name($info['field']);
538 89
        $column->allowNull($info['null'] === 'YES');
539 89
        $column->primaryKey(str_contains($info['key'], 'PRI'));
540 89
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
541 89
        $column->comment($info['comment']);
542 89
        $column->dbType($info['type']);
543 89
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
544 89
        $column->type(self::TYPE_STRING);
545
546 89
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
547 89
            $type = strtolower($matches[1]);
548
549 89
            if (isset($this->typeMap[$type])) {
550 89
                $column->type($this->typeMap[$type]);
551
            }
552
553 89
            if (!empty($matches[2])) {
554 81
                if ($type === 'enum') {
555 20
                    preg_match_all("/'[^']*'/", $matches[2], $values);
556
557 20
                    foreach ($values[0] as $i => $value) {
558 20
                        $values[$i] = trim($value, "'");
559
                    }
560
561 20
                    $column->enumValues($values);
562
                } else {
563 81
                    $values = explode(',', $matches[2]);
564 81
                    $column->precision((int) $values[0]);
565 81
                    $column->size((int) $values[0]);
566
567 81
                    if (isset($values[1])) {
568 32
                        $column->scale((int) $values[1]);
569
                    }
570
571 81
                    if ($column->getSize() === 1 && $type === 'tinyint') {
572 21
                        $column->type('boolean');
573 81
                    } elseif ($type === 'bit') {
574 20
                        if ($column->getSize() > 32) {
575
                            $column->type('bigint');
576 20
                        } elseif ($column->getSize() === 32) {
577
                            $column->type('integer');
578
                        }
579
                    }
580
                }
581
            }
582
        }
583
584 89
        $column->phpType($this->getColumnPhpType($column));
585
586 89
        if (!$column->isPrimaryKey()) {
587
            /**
588
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
589
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
590
             *
591
             * See details here: https://mariadb.com/kb/en/library/now/#description
592
             */
593
            if (
594 86
                ($column->getType() === 'timestamp' || $column->getType() === 'datetime')
595 86
                && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', (string) $info['default'], $matches)
596
            ) {
597 23
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
598 23
                    ? '(' . $matches[1] . ')' : '')));
599 83
            } elseif (isset($type) && $type === 'bit') {
600 20
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
601
            } else {
602 83
                $column->defaultValue($column->phpTypecast($info['default']));
603
            }
604
        }
605
606 89
        return $column;
607
    }
608
609
    /**
610
     * Loads all check constraints for the given table.
611
     *
612
     * @param string $tableName table name.
613
     *
614
     * @throws NotSupportedException
615
     *
616
     * @return array check constraints for the given table.
617
     */
618 12
    protected function loadTableChecks(string $tableName): array
619
    {
620 12
        throw new NotSupportedException('MySQL does not support check constraints.');
621
    }
622
623
    /**
624
     * Loads multiple types of constraints and returns the specified ones.
625
     *
626
     * @param string $tableName table name.
627
     * @param string $returnType return type:
628
     * - primaryKey
629
     * - foreignKeys
630
     * - uniques
631
     *
632
     * @throws Exception|InvalidConfigException|Throwable
633
     *
634
     * @return array|Constraint|null (Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
635
     */
636 48
    private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null
637
    {
638 48
        $sql = <<<SQL
639
        SELECT
640
            `kcu`.`CONSTRAINT_NAME` AS `name`,
641
            `kcu`.`COLUMN_NAME` AS `column_name`,
642
            `tc`.`CONSTRAINT_TYPE` AS `type`,
643
        CASE
644
            WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
645
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
646
        END AS `foreign_table_schema`,
647
            `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
648
            `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
649
            `rc`.`UPDATE_RULE` AS `on_update`,
650
            `rc`.`DELETE_RULE` AS `on_delete`,
651
            `kcu`.`ORDINAL_POSITION` AS `position`
652
        FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
653
        JOIN `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc` ON
654
            `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
655
            `rc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
656
            `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
657
        JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON
658
            `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
659
            `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
660
            `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND
661
            `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY'
662
        WHERE
663
            `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
664
            `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
665
            `kcu`.`TABLE_NAME` = :tableName
666
        UNION
667
        SELECT
668
            `kcu`.`CONSTRAINT_NAME` AS `name`,
669
            `kcu`.`COLUMN_NAME` AS `column_name`,
670
            `tc`.`CONSTRAINT_TYPE` AS `type`,
671
        NULL AS `foreign_table_schema`,
672
        NULL AS `foreign_table_name`,
673
        NULL AS `foreign_column_name`,
674
        NULL AS `on_update`,
675
        NULL AS `on_delete`,
676
            `kcu`.`ORDINAL_POSITION` AS `position`
677
        FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
678
        JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON
679
            `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
680
            `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
681
            `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND
682
            `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE')
683
        WHERE
684
            `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
685
            `kcu`.`TABLE_NAME` = :tableName
686
        ORDER BY `position` ASC
687
        SQL;
688
689 48
        $resolvedName = $this->resolveTableName($tableName);
690
691 48
        $constraints = $this->db->createCommand($sql, [
692 48
            ':schemaName' => $resolvedName->getSchemaName(),
693 48
            ':tableName' => $resolvedName->getName(),
694
        ])->queryAll();
695
696
        /** @var array<array-key, array> $constraints */
697 48
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
698 48
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
699
700 48
        $result = [
701
            'primaryKey' => null,
702
            'foreignKeys' => [],
703
            'uniques' => [],
704
        ];
705
706
        /**
707
         * @var string $type
708
         * @var array $names
709
         */
710 48
        foreach ($constraints as $type => $names) {
711
            /**
712
             * @psalm-var object|string|null $name
713
             * @psalm-var ConstraintArray $constraint
714
             */
715 48
            foreach ($names as $name => $constraint) {
716 48
                switch ($type) {
717 48
                    case 'PRIMARY KEY':
718 37
                        $ct = (new Constraint())
719 37
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
720
721 37
                        $result['primaryKey'] = $ct;
722
723 37
                        break;
724 46
                    case 'FOREIGN KEY':
725 10
                        $fk = (new ForeignKeyConstraint())
726
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
727
                            ->foreignTableName($constraint[0]['foreign_table_name'])
728 10
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
729
                            ->onDelete($constraint[0]['on_delete'])
730
                            ->onUpdate($constraint[0]['on_update'])
731 10
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
732
                            ->name($name);
733
734 10
                        $result['foreignKeys'][] = $fk;
735
736 10
                        break;
737 37
                    case 'UNIQUE':
738 37
                        $ct = (new Constraint())
739 37
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
740
                            ->name($name);
741
742 37
                        $result['uniques'][] = $ct;
743
744 37
                        break;
745
                }
746
            }
747
        }
748
749 48
        foreach ($result as $type => $data) {
750 48
            $this->setTableMetadata($tableName, $type, $data);
751
        }
752
753 48
        return $result[$returnType];
754
    }
755
756
    /**
757
     * Loads all default value constraints for the given table.
758
     *
759
     * @param string $tableName table name.
760
     *
761
     * @throws NotSupportedException
762
     *
763
     * @return array default value constraints for the given table.
764
     */
765 12
    protected function loadTableDefaultValues(string $tableName): array
766
    {
767 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
768
    }
769
770
    /**
771
     * Loads all foreign keys for the given table.
772
     *
773
     * @param string $tableName table name.
774
     *
775
     * @throws Exception|InvalidConfigException|Throwable
776
     *
777
     * @return array foreign keys for the given table.
778
     */
779 4
    protected function loadTableForeignKeys(string $tableName): array
780
    {
781 4
        $tableForeignKeys = $this->loadTableConstraints($tableName, 'foreignKeys');
782
783 4
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
784
    }
785
786
    /**
787
     * Loads all indexes for the given table.
788
     *
789
     * @param string $tableName table name.
790
     *
791
     * @throws Exception|InvalidConfigException|Throwable
792
     *
793
     * @return IndexConstraint[] indexes for the given table.
794
     */
795 28
    protected function loadTableIndexes(string $tableName): array
796
    {
797 28
        $sql = <<<SQL
798
        SELECT
799
            `s`.`INDEX_NAME` AS `name`,
800
            `s`.`COLUMN_NAME` AS `column_name`,
801
            `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
802
            `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
803
        FROM `information_schema`.`STATISTICS` AS `s`
804
        WHERE
805
            `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
806
            `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND
807
            `s`.`TABLE_NAME` = :tableName
808
        ORDER BY `s`.`SEQ_IN_INDEX` ASC
809
        SQL;
810
811 28
        $resolvedName = $this->resolveTableName($tableName);
812
813 28
        $indexes = $this->db->createCommand($sql, [
814 28
            ':schemaName' => $resolvedName->getSchemaName(),
815 28
            ':tableName' => $resolvedName->getName(),
816
        ])->queryAll();
817
818
        /** @var array[] $indexes */
819 28
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
820 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
821 28
        $result = [];
822
823
        /**
824
         * @psalm-var object|string|null $name
825
         * @psalm-var array[] $index
826
         */
827 28
        foreach ($indexes as $name => $index) {
828 28
            $ic = new IndexConstraint();
829
830 28
            $ic->primary((bool) $index[0]['index_is_primary']);
831 28
            $ic->unique((bool) $index[0]['index_is_unique']);
832 28
            $ic->name($name !== 'PRIMARY' ? $name : null);
833 28
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
834
835 28
            $result[] = $ic;
836
        }
837
838 28
        return $result;
839
    }
840
841
    /**
842
     * Loads a primary key for the given table.
843
     *
844
     * @param string $tableName table name.
845
     *
846
     * @throws Exception|InvalidConfigException|Throwable
847
     *
848
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.*
849
     */
850 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
851
    {
852 31
        $tablePrimaryKey = $this->loadTableConstraints($tableName, 'primaryKey');
853
854 31
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
855
    }
856
857
    /**
858
     * Loads the metadata for the specified table.
859
     *
860
     * @param string $name table name.
861
     *
862
     * @throws Exception|Throwable
863
     *
864
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
865
     */
866 94
    protected function loadTableSchema(string $name): ?TableSchema
867
    {
868 94
        $table = new TableSchema();
869
870 94
        $this->resolveTableNames($table, $name);
871
872 94
        if ($this->findColumns($table)) {
873 88
            $this->findConstraints($table);
874
875 88
            return $table;
876
        }
877
878 15
        return null;
879
    }
880
881
    /**
882
     * Loads all unique constraints for the given table.
883
     *
884
     * @param string $tableName table name.
885
     *
886
     * @throws Exception|InvalidConfigException|Throwable
887
     *
888
     * @return array unique constraints for the given table.
889
     */
890 13
    protected function loadTableUniques(string $tableName): array
891
    {
892 13
        $tableUniques = $this->loadTableConstraints($tableName, 'uniques');
893
894 13
        return is_array($tableUniques) ? $tableUniques : [];
895
    }
896
897
    /**
898
     * Changes row's array key case to lower if PDO's one is set to uppercase.
899
     *
900
     * @param array $row row's array or an array of row's arrays.
901
     * @param bool $multiple whether multiple rows or a single row passed.
902
     *
903
     * @throws \Exception
904
     *
905
     * @return array normalized row or rows.
906
     */
907 58
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
908
    {
909 58
        if ($this->db->getSlavePdo()?->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_UPPER) {
910 46
            return $row;
911
        }
912
913 12
        if ($multiple) {
914 12
            return array_map(static function (array $row) {
915 12
                return array_change_key_case($row, CASE_LOWER);
916
            }, $row);
917
        }
918
919
        return array_change_key_case($row, CASE_LOWER);
920
    }
921
922
    /**
923
     * Resolves the table name and schema name (if any).
924
     *
925
     * @param string $name the table name.
926
     *
927
     * @return TableSchema
928
     *
929
     * {@see TableSchema}
930
     */
931 58
    protected function resolveTableName(string $name): TableSchema
932
    {
933 58
        $resolvedName = new TableSchema();
934
935 58
        $parts = explode('.', str_replace('`', '', $name));
936
937 58
        if (isset($parts[1])) {
938
            $resolvedName->schemaName($parts[0]);
939
            $resolvedName->name($parts[1]);
940
        } else {
941 58
            $resolvedName->schemaName($this->defaultSchema);
942 58
            $resolvedName->name($name);
943
        }
944
945 58
        $resolvedName->fullName(($resolvedName->getSchemaName() !== $this->defaultSchema ?
946 58
            (string) $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName());
947
948 58
        return $resolvedName;
949
    }
950
951
    /**
952
     * Resolves the table name and schema name (if any).
953
     *
954
     * @param TableSchema $table the table metadata object.
955
     * @param string $name the table name.
956
     */
957 94
    protected function resolveTableNames(TableSchema $table, string $name): void
958
    {
959 94
        $parts = explode('.', str_replace('`', '', $name));
960
961 94
        if (isset($parts[1])) {
962
            $table->schemaName($parts[0]);
963
            $table->name($parts[1]);
964
            $table->fullName((string) $table->getSchemaName() . '.' . $table->getName());
965
        } else {
966 94
            $table->name($parts[0]);
967 94
            $table->fullName($parts[0]);
968
        }
969
    }
970
971
    /**
972
     * Creates a column schema for the database.
973
     *
974
     * This method may be overridden by child classes to create a DBMS-specific column schema.
975
     *
976
     * @return ColumnSchema column schema instance.
977
     */
978 89
    private function createColumnSchema(): ColumnSchema
979
    {
980 89
        return new ColumnSchema();
981
    }
982
}
983