Passed
Pull Request — master (#40)
by Wilmer
14:04
created

MysqlSchema::getCreateTableSql()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 2
nop 1
dl 0
loc 14
rs 9.9666
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mysql\Schema;
6
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\Db\Constraint\Constraint;
9
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
10
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
11
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
12
use Yiisoft\Db\Constraint\IndexConstraint;
13
use Yiisoft\Db\Exception\Exception;
14
use Yiisoft\Db\Exception\InvalidArgumentException;
15
use Yiisoft\Db\Exception\InvalidConfigException;
16
use Yiisoft\Db\Exception\NotSupportedException;
17
use Yiisoft\Db\Expression\Expression;
18
use Yiisoft\Db\Mysql\Query\MysqlQueryBuilder;
19
use Yiisoft\Db\Schema\Schema;
20
21
final class MysqlSchema extends Schema implements ConstraintFinderInterface
22
{
23
    use ConstraintFinderTrait;
24
25
    private bool $oldMysql;
26
    private array $typeMap = [
27
        'tinyint' => self::TYPE_TINYINT,
28
        'bit' => self::TYPE_INTEGER,
29
        'smallint' => self::TYPE_SMALLINT,
30
        'mediumint' => self::TYPE_INTEGER,
31
        'int' => self::TYPE_INTEGER,
32
        'integer' => self::TYPE_INTEGER,
33
        'bigint' => self::TYPE_BIGINT,
34
        'float' => self::TYPE_FLOAT,
35
        'double' => self::TYPE_DOUBLE,
36
        'real' => self::TYPE_FLOAT,
37
        'decimal' => self::TYPE_DECIMAL,
38
        'numeric' => self::TYPE_DECIMAL,
39
        'tinytext' => self::TYPE_TEXT,
40
        'mediumtext' => self::TYPE_TEXT,
41
        'longtext' => self::TYPE_TEXT,
42
        'longblob' => self::TYPE_BINARY,
43
        'blob' => self::TYPE_BINARY,
44
        'text' => self::TYPE_TEXT,
45
        'varchar' => self::TYPE_STRING,
46
        'string' => self::TYPE_STRING,
47
        'char' => self::TYPE_CHAR,
48
        'datetime' => self::TYPE_DATETIME,
49
        'year' => self::TYPE_DATE,
50
        'date' => self::TYPE_DATE,
51
        'time' => self::TYPE_TIME,
52
        'timestamp' => self::TYPE_TIMESTAMP,
53
        'enum' => self::TYPE_STRING,
54
        'varbinary' => self::TYPE_BINARY,
55
        'json' => self::TYPE_JSON,
56
    ];
57
58
    /**
59
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
60
     * case starting and ending characters are different.
61
     */
62
    protected $tableQuoteCharacter = '`';
63
64
    /**
65
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
66
     * and ending characters are different.
67
     */
68
    protected $columnQuoteCharacter = '`';
69
70
    /**
71
     * Resolves the table name and schema name (if any).
72
     *
73
     * @param string $name the table name.
74
     *
75
     * @return MysqlTableSchema
76
     *
77
     * {@see MysqlTableSchema}
78
     */
79
    protected function resolveTableName(string $name): MysqlTableSchema
80
    {
81
        $resolvedName = new MysqlTableSchema();
82
83
        $parts = \explode('.', \str_replace('`', '', $name));
84
85
        if (isset($parts[1])) {
86
            $resolvedName->schemaName($parts[0]);
87
            $resolvedName->name($parts[1]);
88
        } else {
89
            $resolvedName->schemaName($this->defaultSchema);
90
            $resolvedName->name($name);
91
        }
92
93
        $resolvedName->fullName(($resolvedName->getSchemaName() !== $this->defaultSchema ?
94
            $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName());
95
96
        return $resolvedName;
97
    }
98
99
    /**
100
     * Returns all table names in the database.
101
     *
102
     * This method should be overridden by child classes in order to support this feature because the default
103
     * implementation simply throws an exception.
104
     *
105
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
106
     *
107
     * @throws Exception
108
     * @throws InvalidConfigException
109
     * @throws InvalidArgumentException
110
     *
111
     * @return array all table names in the database. The names have NO schema name prefix.
112
     */
113
    protected function findTableNames(string $schema = ''): array
114
    {
115
        $sql = 'SHOW TABLES';
116
117
        if ($schema !== '') {
118
            $sql .= ' FROM ' . $this->quoteSimpleTableName($schema);
119
        }
120
121
        return $this->getDb()->createCommand($sql)->queryColumn();
122
    }
123
124
    /**
125
     * Loads the metadata for the specified table.
126
     *
127
     * @param string $name table name.
128
     *
129
     * @throws \Exception
130
     *
131
     * @return MysqlTableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
132
     */
133
    protected function loadTableSchema(string $name): ?MysqlTableSchema
134
    {
135
        $table = new MysqlTableSchema();
136
137
        $this->resolveTableNames($table, $name);
138
139
        if ($this->findColumns($table)) {
140
            $this->findConstraints($table);
141
142
            return $table;
143
        }
144
145
        return null;
146
    }
147
148
    /**
149
     * Loads a primary key for the given table.
150
     *
151
     * @param string $tableName table name.
152
     *
153
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
154
     */
155
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
156
    {
157
        return $this->loadTableConstraints($tableName, 'primaryKey');
158
    }
159
160
    /**
161
     * Loads all foreign keys for the given table.
162
     *
163
     * @param string $tableName table name.
164
     *
165
     * @return ForeignKeyConstraint[] foreign keys for the given table.
166
     */
167
    protected function loadTableForeignKeys(string $tableName): array
168
    {
169
        return $this->loadTableConstraints($tableName, 'foreignKeys');
170
    }
171
172
    /**
173
     * Loads all indexes for the given table.
174
     *
175
     * @param string $tableName table name.
176
     *
177
     * @return IndexConstraint[] indexes for the given table.
178
     */
179
    protected function loadTableIndexes(string $tableName): array
180
    {
181
        static $sql = <<<'SQL'
182
SELECT
183
    `s`.`INDEX_NAME` AS `name`,
184
    `s`.`COLUMN_NAME` AS `column_name`,
185
    `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
186
    `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
187
FROM `information_schema`.`STATISTICS` AS `s`
188
WHERE `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND `s`.`TABLE_NAME` = :tableName
189
ORDER BY `s`.`SEQ_IN_INDEX` ASC
190
SQL;
191
192
        $resolvedName = $this->resolveTableName($tableName);
193
194
        $indexes = $this->getDb()->createCommand($sql, [
195
            ':schemaName' => $resolvedName->getSchemaName(),
196
            ':tableName' => $resolvedName->getName(),
197
        ])->queryAll();
198
199
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
200
        $indexes = ArrayHelper::index($indexes, null, 'name');
201
        $result = [];
202
203
        foreach ($indexes as $name => $index) {
204
            $ic = new IndexConstraint();
205
206
            $ic->primary((bool) $index[0]['index_is_primary']);
207
            $ic->unique((bool) $index[0]['index_is_unique']);
208
            $ic->name($name !== 'PRIMARY' ? $name : null);
209
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
210
211
            $result[] = $ic;
212
        }
213
214
        return $result;
215
    }
216
217
    /**
218
     * Loads all unique constraints for the given table.
219
     *
220
     * @param string $tableName table name.
221
     *
222
     * @return Constraint[] unique constraints for the given table.
223
     */
224
    protected function loadTableUniques(string $tableName): array
225
    {
226
        return $this->loadTableConstraints($tableName, 'uniques');
227
    }
228
229
    /**
230
     * Loads all check constraints for the given table.
231
     *
232
     * @param string $tableName table name.
233
     *
234
     * @return CheckConstraint[] check constraints for the given table.
235
     */
236
    protected function loadTableChecks(string $tableName): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

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

236
    protected function loadTableChecks(/** @scrutinizer ignore-unused */ string $tableName): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
237
    {
238
        throw new NotSupportedException('MySQL does not support check constraints.');
239
    }
240
241
    /**
242
     * Loads all default value constraints for the given table.
243
     *
244
     * @param string $tableName table name.
245
     *
246
     * @return DefaultValueConstraint[] default value constraints for the given table.
247
     */
248
    protected function loadTableDefaultValues(string $tableName): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

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

248
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ string $tableName): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
249
    {
250
        throw new NotSupportedException('MySQL does not support default value constraints.');
251
    }
252
253
    /**
254
     * Creates a query builder for the MySQL database.
255
     *
256
     * @return MysqlQueryBuilder query builder instance
257
     */
258
    public function createQueryBuilder(): MysqlQueryBuilder
259
    {
260
        return new MysqlQueryBuilder($this->getDb());
0 ignored issues
show
Bug introduced by
It seems like $this->getDb() can also be of type null; however, parameter $db of Yiisoft\Db\Mysql\Query\M...yBuilder::__construct() does only seem to accept Yiisoft\Db\Connection\Connection, maybe add an additional type check? ( Ignorable by Annotation )

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

260
        return new MysqlQueryBuilder(/** @scrutinizer ignore-type */ $this->getDb());
Loading history...
261
    }
262
263
    /**
264
     * Resolves the table name and schema name (if any).
265
     *
266
     * @param MysqlTableSchema $table the table metadata object.
267
     * @param string $name the table name.
268
     */
269
    protected function resolveTableNames($table, $name): void
270
    {
271
        $parts = \explode('.', \str_replace('`', '', $name));
272
273
        if (isset($parts[1])) {
274
            $table->schemaName($parts[0]);
275
            $table->name($parts[1]);
276
            $table->fullName($table->getSchemaName() . '.' . $table->getName());
277
        } else {
278
            $table->name($parts[0]);
279
            $table->fullName($parts[0]);
280
        }
281
    }
282
283
    /**
284
     * Loads the column information into a {@see ColumnSchema} object.
285
     *
286
     * @param array $info column information.
287
     *
288
     * @return ColumnSchema the column schema object.
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Mysql\Schema\ColumnSchema 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...
289
     */
290
    protected function loadColumnSchema(array $info): MysqlColumnSchema
291
    {
292
        $column = $this->createColumnSchema();
293
294
        $column->name($info['field']);
295
        $column->allowNull($info['null'] === 'YES');
296
        $column->primaryKey(\strpos($info['key'], 'PRI') !== false);
297
        $column->autoIncrement(\stripos($info['extra'], 'auto_increment') !== false);
298
        $column->comment($info['comment']);
299
        $column->dbType($info['type']);
300
        $column->unsigned(\stripos($column->getDbType(), 'unsigned') !== false);
301
        $column->type(self::TYPE_STRING);
302
303
        if (\preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
304
            $type = \strtolower($matches[1]);
305
            if (isset($this->typeMap[$type])) {
306
                $column->type($this->typeMap[$type]);
307
            }
308
            if (!empty($matches[2])) {
309
                if ($type === 'enum') {
310
                    \preg_match_all("/'[^']*'/", $matches[2], $values);
311
                    foreach ($values[0] as $i => $value) {
312
                        $values[$i] = \trim($value, "'");
313
                    }
314
                    $column->enumValues($values);
315
                } else {
316
                    $values = \explode(',', $matches[2]);
317
                    $column->precision((int) $values[0]);
318
                    $column->size((int) $values[0]);
319
                    if (isset($values[1])) {
320
                        $column->scale((int) $values[1]);
321
                    }
322
                    if ($column->getSize() === 1 && $type === 'tinyint') {
323
                        $column->type('boolean');
324
                    } elseif ($type === 'bit') {
325
                        if ($column->getSize() > 32) {
326
                            $column->type('bigint');
327
                        } elseif ($column->getSize() === 32) {
328
                            $column->type('integer');
329
                        }
330
                    }
331
                }
332
            }
333
        }
334
335
        $column->phpType($this->getColumnPhpType($column));
336
337
        if (!$column->isPrimaryKey()) {
338
            /**
339
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
340
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
341
             *
342
             * See details here: https://mariadb.com/kb/en/library/now/#description
343
             */
344
            if (
345
                ($column->getType() === 'timestamp' || $column->getType() === 'datetime')
346
                && \preg_match('/^current_timestamp(?:\(([0-9]*)\))?$/i', (string) $info['default'], $matches)
347
            ) {
348
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
349
                    ? '(' . $matches[1] . ')' : '')));
350
            } elseif (isset($type) && $type === 'bit') {
351
                $column->defaultValue(\bindec(\trim((string) $info['default'], 'b\'')));
352
            } else {
353
                $column->defaultValue($column->phpTypecast($info['default']));
354
            }
355
        }
356
357
        return $column;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $column returns the type Yiisoft\Db\Mysql\Schema\MysqlColumnSchema which is incompatible with the documented return type Yiisoft\Db\Mysql\Schema\ColumnSchema.
Loading history...
358
    }
359
360
    /**
361
     * Collects the metadata of table columns.
362
     *
363
     * @param MysqlTableSchema $table the table metadata.
364
     *
365
     * @throws \Exception if DB query fails.
366
     *
367
     * @return bool whether the table exists in the database.
368
     */
369
    protected function findColumns($table): bool
370
    {
371
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName($table->getFullName());
0 ignored issues
show
Bug introduced by
It seems like $table->getFullName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteTableName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

371
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName(/** @scrutinizer ignore-type */ $table->getFullName());
Loading history...
372
373
        try {
374
            $columns = $this->getDb()->createCommand($sql)->queryAll();
375
        } catch (\Exception $e) {
376
            $previous = $e->getPrevious();
377
378
            if ($previous instanceof \PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
379
                /**
380
                 * table does not exist.
381
                 *
382
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
383
                 */
384
                return false;
385
            }
386
387
            throw $e;
388
        }
389
390
        foreach ($columns as $info) {
391
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_LOWER) {
392
                $info = \array_change_key_case($info, CASE_LOWER);
393
            }
394
395
            $column = $this->loadColumnSchema($info);
396
            $table->columns($column->getName(), $column);
397
398
            if ($column->isPrimaryKey()) {
399
                $table->primaryKey($column->getName());
400
                if ($column->isAutoIncrement()) {
401
                    $table->sequenceName('');
402
                }
403
            }
404
        }
405
406
        return true;
407
    }
408
409
    /**
410
     * Gets the CREATE TABLE sql string.
411
     *
412
     * @param MysqlTableSchema $table the table metadata.
413
     *
414
     * @throws Exception
415
     * @throws InvalidArgumentException
416
     * @throws InvalidConfigException
417
     *
418
     * @return string $sql the result of 'SHOW CREATE TABLE'.
419
     */
420
    protected function getCreateTableSql($table): string
421
    {
422
        $row = $this->getDb()->createCommand(
423
            'SHOW CREATE TABLE ' . $this->quoteTableName($table->getFullName())
0 ignored issues
show
Bug introduced by
It seems like $table->getFullName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteTableName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

423
            'SHOW CREATE TABLE ' . $this->quoteTableName(/** @scrutinizer ignore-type */ $table->getFullName())
Loading history...
424
        )->queryOne();
425
426
        if (isset($row['Create Table'])) {
427
            $sql = $row['Create Table'];
428
        } else {
429
            $row = \array_values($row);
0 ignored issues
show
Bug introduced by
$row of type false is incompatible with the type array expected by parameter $input 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

429
            $row = \array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
430
            $sql = $row[1];
431
        }
432
433
        return $sql;
434
    }
435
436
    /**
437
     * Collects the foreign key column details for the given table.
438
     *
439
     * @param MysqlTableSchema $table the table metadata.
440
     *
441
     * @throws \Exception
442
     */
443
    protected function findConstraints($table)
444
    {
445
        $sql = <<<'SQL'
446
SELECT
447
    `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
448
    `kcu`.`COLUMN_NAME` AS `column_name`,
449
    `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
450
    `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
451
FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
452
JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
453
    (
454
        `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
455
        (`kcu`.`CONSTRAINT_CATALOG` IS NULL AND `rc`.`CONSTRAINT_CATALOG` IS NULL)
456
    ) AND
457
    `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
458
    `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME`
459
WHERE `rc`.`CONSTRAINT_SCHEMA` = database() AND `kcu`.`TABLE_SCHEMA` = database()
460
AND `rc`.`TABLE_NAME` = :tableName AND `kcu`.`TABLE_NAME` = :tableName1
461
SQL;
462
463
        try {
464
            $rows = $this->getDb()->createCommand(
465
                $sql,
466
                [':tableName' => $table->getName(), ':tableName1' => $table->getName()]
467
            )->queryAll();
468
469
            $constraints = [];
470
471
            foreach ($rows as $row) {
472
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
473
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
474
            }
475
476
            $table->foreignKeys([]);
477
478
            foreach ($constraints as $name => $constraint) {
479
                $table->foreignKey($name, \array_merge(
480
                    [$constraint['referenced_table_name']],
481
                    $constraint['columns']
482
                ));
483
            }
484
        } catch (\Exception $e) {
485
            $previous = $e->getPrevious();
486
487
            if (!$previous instanceof \PDOException || \strpos($previous->getMessage(), 'SQLSTATE[42S02') === false) {
488
                throw $e;
489
            }
490
491
            // table does not exist, try to determine the foreign keys using the table creation sql
492
            $sql = $this->getCreateTableSql($table);
493
            $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi';
494
495
            if (\preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
496
                foreach ($matches as $match) {
497
                    $fks = array_map('trim', \explode(',', \str_replace('`', '', $match[1])));
498
                    $pks = array_map('trim', \explode(',', \str_replace('`', '', $match[3])));
499
                    $constraint = [\str_replace('`', '', $match[2])];
500
501
                    foreach ($fks as $k => $name) {
502
                        $constraint[$name] = $pks[$k];
503
                    }
504
505
                    $table->foreignKey(\md5(\serialize($constraint)), $constraint);
506
                }
507
                $table->foreignKeys(\array_values($table->getForeignKeys()));
508
            }
509
        }
510
    }
511
512
    /**
513
     * Returns all unique indexes for the given table.
514
     *
515
     * Each array element is of the following structure:
516
     *
517
     * ```php
518
     * [
519
     *     'IndexName1' => ['col1' [, ...]],
520
     *     'IndexName2' => ['col2' [, ...]],
521
     * ]
522
     * ```
523
     *
524
     * @param MysqlTableSchema $table the table metadata.
525
     *
526
     * @return array all unique indexes for the given table.
527
     */
528
    public function findUniqueIndexes($table): array
529
    {
530
        $sql = $this->getCreateTableSql($table);
531
532
        $uniqueIndexes = [];
533
534
        $regexp = '/UNIQUE KEY\s+\`(.+)\`\s*\((\`.+\`)+\)/mi';
535
536
        if (\preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
537
            foreach ($matches as $match) {
538
                $indexName = $match[1];
539
                $indexColumns = \array_map('trim', \explode('`,`', \trim($match[2], '`')));
540
                $uniqueIndexes[$indexName] = $indexColumns;
541
            }
542
        }
543
544
        return $uniqueIndexes;
545
    }
546
547
    public function createColumnSchemaBuilder(string $type, $length = null): MysqlColumnSchemaBuilder
548
    {
549
        return new MysqlColumnSchemaBuilder($type, $length, $this->getDb());
550
    }
551
552
    /**
553
     * @throws InvalidConfigException
554
     * @throws Exception
555
     *
556
     * @return bool whether the version of the MySQL being used is older than 5.1.
557
     */
558
    protected function isOldMysql(): bool
559
    {
560
        if ($this->oldMysql === null) {
561
            $version = $this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
562
563
            $this->oldMysql = \version_compare($version, '5.1', '<=');
564
        }
565
566
        return $this->oldMysql;
567
    }
568
569
    /**
570
     * Loads multiple types of constraints and returns the specified ones.
571
     *
572
     * @param string $tableName table name.
573
     * @param string $returnType return type:
574
     * - primaryKey
575
     * - foreignKeys
576
     * - uniques
577
     *
578
     * @throws Exception
579
     * @throws InvalidArgumentException
580
     * @throws InvalidConfigException
581
     *
582
     * @return mixed constraints.
583
     */
584
    private function loadTableConstraints(string $tableName, string $returnType)
585
    {
586
        static $sql = <<<'SQL'
587
SELECT
588
    `kcu`.`CONSTRAINT_NAME` AS `name`,
589
    `kcu`.`COLUMN_NAME` AS `column_name`,
590
    `tc`.`CONSTRAINT_TYPE` AS `type`,
591
    CASE
592
        WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
593
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
594
    END AS `foreign_table_schema`,
595
    `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
596
    `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
597
    `rc`.`UPDATE_RULE` AS `on_update`,
598
    `rc`.`DELETE_RULE` AS `on_delete`,
599
    `kcu`.`ORDINAL_POSITION` AS `position`
600
FROM
601
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
602
    `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`,
603
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
604
WHERE
605
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName
606
    AND `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`TABLE_NAME` = :tableName AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
607
    AND `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`TABLE_NAME` = :tableName AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY'
608
UNION
609
SELECT
610
    `kcu`.`CONSTRAINT_NAME` AS `name`,
611
    `kcu`.`COLUMN_NAME` AS `column_name`,
612
    `tc`.`CONSTRAINT_TYPE` AS `type`,
613
    NULL AS `foreign_table_schema`,
614
    NULL AS `foreign_table_name`,
615
    NULL AS `foreign_column_name`,
616
    NULL AS `on_update`,
617
    NULL AS `on_delete`,
618
    `kcu`.`ORDINAL_POSITION` AS `position`
619
FROM
620
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
621
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
622
WHERE
623
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`TABLE_NAME` = :tableName
624
    AND `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`TABLE_NAME` = :tableName AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE')
625
ORDER BY `position` ASC
626
SQL;
627
628
        $resolvedName = $this->resolveTableName($tableName);
629
630
        $constraints = $this->getDb()->createCommand(
631
            $sql,
632
            [
633
                ':schemaName' => $resolvedName->getSchemaName(),
634
                ':tableName' => $resolvedName->getName(),
635
            ]
636
        )->queryAll();
637
638
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
639
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
640
641
        $result = [
642
            'primaryKey' => null,
643
            'foreignKeys' => [],
644
            'uniques' => [],
645
        ];
646
647
        foreach ($constraints as $type => $names) {
648
            foreach ($names as $name => $constraint) {
649
                switch ($type) {
650
                    case 'PRIMARY KEY':
651
                        $ct = (new Constraint())
652
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
653
654
                        $result['primaryKey'] = $ct;
655
656
                        break;
657
                    case 'FOREIGN KEY':
658
                        $fk = (new ForeignKeyConstraint())
659
                            ->name($name)
660
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
661
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
662
                            ->foreignTableName($constraint[0]['foreign_table_name'])
663
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
664
                            ->onDelete($constraint[0]['on_delete'])
665
                            ->onUpdate($constraint[0]['on_update']);
666
667
                        $result['foreignKeys'][] = $fk;
668
669
                        break;
670
                    case 'UNIQUE':
671
                        $ct = (new Constraint())
672
                            ->name($name)
673
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
674
675
                        $result['uniques'][] = $ct;
676
677
                        break;
678
                }
679
            }
680
        }
681
682
        foreach ($result as $type => $data) {
683
            $this->setTableMetadata($tableName, $type, $data);
684
        }
685
686
        return $result[$returnType];
687
    }
688
689
    /**
690
     * Creates a column schema for the database.
691
     *
692
     * This method may be overridden by child classes to create a DBMS-specific column schema.
693
     *
694
     * @return MysqlColumnSchema column schema instance.
695
     */
696
    protected function createColumnSchema(): MysqlColumnSchema
697
    {
698
        return new MysqlColumnSchema();
699
    }
700
}
701