Passed
Pull Request — master (#50)
by Wilmer
12:16
created

Schema::resolveTableNames()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 8
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 11
ccs 2
cts 2
cp 1
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mysql;
6
7
use PDO;
8
use PDOException;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Db\Connection\ConnectionInterface;
11
use Yiisoft\Db\Constraint\Constraint;
12
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
13
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
14
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
15
use Yiisoft\Db\Constraint\IndexConstraint;
16
use Yiisoft\Db\Exception\Exception;
17
use Yiisoft\Db\Exception\InvalidArgumentException;
18
use Yiisoft\Db\Exception\InvalidConfigException;
19
use Yiisoft\Db\Exception\NotSupportedException;
20
use Yiisoft\Db\Expression\Expression;
21
use Yiisoft\Db\Schema\Schema as AbstractSchema;
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 preg_match;
30
use function preg_match_all;
31
use function str_replace;
32
use function stripos;
33
use function strpos;
34
use function strtolower;
35
use function trim;
36
use function version_compare;
37
38
final class Schema extends AbstractSchema implements ConstraintFinderInterface
39
{
40
    use ConstraintFinderTrait;
41
42
    private bool $oldMysql;
43
    private array $typeMap = [
44
        'tinyint' => self::TYPE_TINYINT,
45
        'bit' => self::TYPE_INTEGER,
46
        'smallint' => self::TYPE_SMALLINT,
47
        'mediumint' => self::TYPE_INTEGER,
48
        'int' => self::TYPE_INTEGER,
49
        'integer' => self::TYPE_INTEGER,
50
        'bigint' => self::TYPE_BIGINT,
51
        'float' => self::TYPE_FLOAT,
52
        'double' => self::TYPE_DOUBLE,
53
        'real' => self::TYPE_FLOAT,
54
        'decimal' => self::TYPE_DECIMAL,
55
        'numeric' => self::TYPE_DECIMAL,
56
        'tinytext' => self::TYPE_TEXT,
57
        'mediumtext' => self::TYPE_TEXT,
58
        'longtext' => self::TYPE_TEXT,
59
        'longblob' => self::TYPE_BINARY,
60
        'blob' => self::TYPE_BINARY,
61
        'text' => self::TYPE_TEXT,
62
        'varchar' => self::TYPE_STRING,
63
        'string' => self::TYPE_STRING,
64
        'char' => self::TYPE_CHAR,
65
        'datetime' => self::TYPE_DATETIME,
66
        'year' => self::TYPE_DATE,
67
        'date' => self::TYPE_DATE,
68
        'time' => self::TYPE_TIME,
69
        'timestamp' => self::TYPE_TIMESTAMP,
70
        'enum' => self::TYPE_STRING,
71
        'varbinary' => self::TYPE_BINARY,
72
        'json' => self::TYPE_JSON,
73
    ];
74
75
    /**
76
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
77
     * case starting and ending characters are different.
78
     */
79
    protected $tableQuoteCharacter = '`';
80
81
    /**
82
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
83
     * and ending characters are different.
84
     */
85
    protected $columnQuoteCharacter = '`';
86
87
    /** @var Connection $db */
88
    private ConnectionInterface $db;
89
90
    public function __construct(ConnectionInterface $db)
91
    {
92
        $this->db = $db;
0 ignored issues
show
Documentation Bug introduced by
$db is of type Yiisoft\Db\Connection\ConnectionInterface, but the property $db was declared to be of type Yiisoft\Db\Mysql\Connection. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
93
94
        parent::__construct($db);
95 58
    }
96
97 58
    /**
98
     * Resolves the table name and schema name (if any).
99 58
     *
100
     * @param string $name the table name.
101 58
     *
102
     * @return TableSchema
103
     *
104
     * {@see TableSchema}
105 58
     */
106 58
    protected function resolveTableName(string $name): TableSchema
107
    {
108
        $resolvedName = new TableSchema();
109 58
110 58
        $parts = explode('.', str_replace('`', '', $name));
111
112 58
        if (isset($parts[1])) {
113
            $resolvedName->schemaName($parts[0]);
114
            $resolvedName->name($parts[1]);
115
        } else {
116
            $resolvedName->schemaName($this->defaultSchema);
117
            $resolvedName->name($name);
118
        }
119
120
        $resolvedName->fullName(($resolvedName->getSchemaName() !== $this->defaultSchema ?
121
            $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName());
122
123
        return $resolvedName;
124
    }
125
126
    /**
127
     * Returns all table names in the database.
128
     *
129 3
     * This method should be overridden by child classes in order to support this feature because the default
130
     * implementation simply throws an exception.
131 3
     *
132
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
133 3
     *
134
     * @throws Exception
135
     * @throws InvalidConfigException
136
     * @throws InvalidArgumentException
137 3
     *
138
     * @return array all table names in the database. The names have NO schema name prefix.
139
     */
140
    protected function findTableNames(string $schema = ''): array
141
    {
142
        $sql = 'SHOW TABLES';
143
144
        if ($schema !== '') {
145
            $sql .= ' FROM ' . $this->quoteSimpleTableName($schema);
146
        }
147
148
        return $this->db->createCommand($sql)->queryColumn();
149 82
    }
150
151 82
    /**
152
     * Loads the metadata for the specified table.
153 82
     *
154
     * @param string $name table name.
155 82
     *
156 77
     * @throws Exception
157
     *
158 77
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
159
     */
160
    protected function loadTableSchema(string $name): ?TableSchema
161 13
    {
162
        $table = new TableSchema();
163
164
        $this->resolveTableNames($table, $name);
165
166
        if ($this->findColumns($table)) {
167
            $this->findConstraints($table);
168
169
            return $table;
170
        }
171
172
        return null;
173
    }
174
175 31
    /**
176
     * Loads a primary key for the given table.
177 31
     *
178
     * @param string $tableName table name.
179
     *
180
     * @throws Exception
181
     * @throws InvalidArgumentException
182
     * @throws InvalidConfigException
183
     *
184
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
185
     */
186
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
187
    {
188
        return $this->loadTableConstraints($tableName, 'primaryKey');
189
    }
190
191 4
    /**
192
     * Loads all foreign keys for the given table.
193 4
     *
194
     * @param string $tableName table name.
195
     *
196
     * @throws Exception
197
     * @throws InvalidArgumentException
198
     * @throws InvalidConfigException
199
     *
200
     * @return ForeignKeyConstraint[] foreign keys for the given table.
201
     */
202
    protected function loadTableForeignKeys(string $tableName): array
203
    {
204
        return $this->loadTableConstraints($tableName, 'foreignKeys');
205
    }
206
207 28
    /**
208
     * Loads all indexes for the given table.
209 28
     *
210
     * @param string $tableName table name.
211
     *
212
     * @throws Exception
213
     * @throws InvalidArgumentException
214
     * @throws InvalidConfigException
215
     *
216
     * @return IndexConstraint[] indexes for the given table.
217
     */
218
    protected function loadTableIndexes(string $tableName): array
219
    {
220 28
        static $sql = <<<'SQL'
221
SELECT
222 28
    `s`.`INDEX_NAME` AS `name`,
223 28
    `s`.`COLUMN_NAME` AS `column_name`,
224 28
    `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
225 28
    `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
226
FROM `information_schema`.`STATISTICS` AS `s`
227 28
WHERE `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND `s`.`TABLE_NAME` = :tableName
228 28
ORDER BY `s`.`SEQ_IN_INDEX` ASC
229 28
SQL;
230
231 28
        $resolvedName = $this->resolveTableName($tableName);
232 28
233
        $indexes = $this->db->createCommand($sql, [
234 28
            ':schemaName' => $resolvedName->getSchemaName(),
235 28
            ':tableName' => $resolvedName->getName(),
236 28
        ])->queryAll();
237 28
238
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
239 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
240
        $result = [];
241
242 28
        foreach ($indexes as $name => $index) {
243
            $ic = new IndexConstraint();
244
245
            $ic->primary((bool) $index[0]['index_is_primary']);
246
            $ic->unique((bool) $index[0]['index_is_unique']);
247
            $ic->name($name !== 'PRIMARY' ? $name : null);
248
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
249
250
            $result[] = $ic;
251
        }
252
253
        return $result;
254
    }
255
256 13
    /**
257
     * Loads all unique constraints for the given table.
258 13
     *
259
     * @param string $tableName table name.
260
     *
261
     * @throws Exception
262
     * @throws InvalidArgumentException
263
     * @throws InvalidConfigException
264
     *
265
     * @return Constraint[] unique constraints for the given table.
266
     */
267
    protected function loadTableUniques(string $tableName): array
268
    {
269
        return $this->loadTableConstraints($tableName, 'uniques');
270 12
    }
271
272 12
    /**
273
     * Loads all check constraints for the given table.
274
     *
275
     * @param string $tableName table name.
276
     *
277
     * @throws NotSupportedException
278
     *
279
     * @return array check constraints for the given table.
280
     */
281
    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

281
    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...
282
    {
283
        throw new NotSupportedException('MySQL does not support check constraints.');
284 12
    }
285
286 12
    /**
287
     * Loads all default value constraints for the given table.
288
     *
289
     * @param string $tableName table name.
290
     *
291
     * @throws NotSupportedException
292
     *
293
     * @return array default value constraints for the given table.
294 53
     */
295
    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

295
    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...
296 53
    {
297
        throw new NotSupportedException('MySQL does not support default value constraints.');
298
    }
299
300
    /**
301
     * Creates a query builder for the MySQL database.
302
     *
303
     * @return QueryBuilder query builder instance
304
     */
305 82
    public function createQueryBuilder(): QueryBuilder
306
    {
307 82
        return new QueryBuilder($this->db);
308
    }
309 82
310
    /**
311
     * Resolves the table name and schema name (if any).
312
     *
313
     * @param TableSchema $table the table metadata object.
314 82
     * @param string $name the table name.
315 82
     */
316
    protected function resolveTableNames($table, $name): void
317 82
    {
318
        $parts = explode('.', str_replace('`', '', $name));
319
320
        if (isset($parts[1])) {
321
            $table->schemaName($parts[0]);
322
            $table->name($parts[1]);
323
            $table->fullName($table->getSchemaName() . '.' . $table->getName());
324
        } else {
325
            $table->name($parts[0]);
326 78
            $table->fullName($parts[0]);
327
        }
328 78
    }
329
330 78
    /**
331 78
     * Loads the column information into a {@see ColumnSchema} object.
332 78
     *
333 78
     * @param array $info column information.
334 78
     *
335 78
     * @return ColumnSchema the column schema object.
336 78
     */
337 78
    protected function loadColumnSchema(array $info): ColumnSchema
338
    {
339 78
        $column = $this->createColumnSchema();
340 78
341
        $column->name($info['field']);
342 78
        $column->allowNull($info['null'] === 'YES');
343 78
        $column->primaryKey(strpos($info['key'], 'PRI') !== false);
344
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
345
        $column->comment($info['comment']);
346 78
        $column->dbType($info['type']);
347 77
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
348 20
        $column->type(self::TYPE_STRING);
349
350 20
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
351 20
            $type = strtolower($matches[1]);
352
353
            if (isset($this->typeMap[$type])) {
354 20
                $column->type($this->typeMap[$type]);
355
            }
356 77
357 77
            if (!empty($matches[2])) {
358 77
                if ($type === 'enum') {
359
                    preg_match_all("/'[^']*'/", $matches[2], $values);
360 77
361 24
                    foreach ($values[0] as $i => $value) {
362
                        $values[$i] = trim($value, "'");
363
                    }
364 77
365 21
                    $column->enumValues($values);
366 77
                } else {
367 20
                    $values = explode(',', $matches[2]);
368
                    $column->precision((int) $values[0]);
369 20
                    $column->size((int) $values[0]);
370
371
                    if (isset($values[1])) {
372
                        $column->scale((int) $values[1]);
373
                    }
374
375
                    if ($column->getSize() === 1 && $type === 'tinyint') {
376
                        $column->type('boolean');
377 78
                    } elseif ($type === 'bit') {
378
                        if ($column->getSize() > 32) {
379 78
                            $column->type('bigint');
380
                        } elseif ($column->getSize() === 32) {
381
                            $column->type('integer');
382
                        }
383
                    }
384
                }
385
            }
386
        }
387 75
388 75
        $column->phpType($this->getColumnPhpType($column));
389
390 24
        if (!$column->isPrimaryKey()) {
391 24
            /**
392 72
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
393 20
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
394
             *
395 72
             * See details here: https://mariadb.com/kb/en/library/now/#description
396
             */
397
            if (
398
                ($column->getType() === 'timestamp' || $column->getType() === 'datetime')
399 78
                && preg_match('/^current_timestamp(?:\(([0-9]*)\))?$/i', (string) $info['default'], $matches)
400
            ) {
401
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
402
                    ? '(' . $matches[1] . ')' : '')));
403
            } elseif (isset($type) && $type === 'bit') {
404
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
405
            } else {
406
                $column->defaultValue($column->phpTypecast($info['default']));
407
            }
408
        }
409
410
        return $column;
411 82
    }
412
413 82
    /**
414
     * Collects the metadata of table columns.
415
     *
416 82
     * @param TableSchema $table the table metadata.
417 13
     *
418 13
     * @throws Exception if DB query fails.
419
     *
420 13
     * @return bool whether the table exists in the database.
421
     */
422
    protected function findColumns($table): bool
423
    {
424
        $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

424
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName(/** @scrutinizer ignore-type */ $table->getFullName());
Loading history...
425
426 13
        try {
427
            $columns = $this->db->createCommand($sql)->queryAll();
428
        } catch (Exception $e) {
429
            $previous = $e->getPrevious();
430
431
            if ($previous instanceof PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
432 77
                /**
433 77
                 * table does not exist.
434 76
                 *
435
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
436
                 */
437 77
                return false;
438 77
            }
439
440 77
            throw $e;
441 53
        }
442 53
443 51
        foreach ($columns as $info) {
444
            if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_LOWER) {
445
                $info = array_change_key_case($info, CASE_LOWER);
446
            }
447
448 77
            $column = $this->loadColumnSchema($info);
449
            $table->columns($column->getName(), $column);
450
451
            if ($column->isPrimaryKey()) {
452
                $table->primaryKey($column->getName());
453
                if ($column->isAutoIncrement()) {
454
                    $table->sequenceName('');
455
                }
456
            }
457
        }
458
459
        return true;
460
    }
461
462
    /**
463
     * Gets the CREATE TABLE sql string.
464
     *
465
     * @param TableSchema $table the table metadata.
466
     *
467
     * @throws Exception
468
     * @throws InvalidArgumentException
469
     * @throws InvalidConfigException
470
     *
471
     * @return string $sql the result of 'SHOW CREATE TABLE'.
472
     */
473
    protected function getCreateTableSql($table): string
474
    {
475
        $row = $this->db->createCommand(
476
            '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

476
            'SHOW CREATE TABLE ' . $this->quoteTableName(/** @scrutinizer ignore-type */ $table->getFullName())
Loading history...
477
        )->queryOne();
478
479
        if (isset($row['Create Table'])) {
480
            $sql = $row['Create Table'];
481
        } else {
482
            $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

482
            $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
483
            $sql = $row[1];
484
        }
485 77
486
        return $sql;
487
    }
488 77
489
    /**
490
     * Collects the foreign key column details for the given table.
491
     *
492
     * @param TableSchema $table the table metadata.
493
     *
494
     * @throws Exception
495
     */
496
    protected function findConstraints($table)
497
    {
498
        $sql = <<<'SQL'
499
SELECT
500
    `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
501
    `kcu`.`COLUMN_NAME` AS `column_name`,
502
    `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
503
    `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
504
FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
505
JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
506 77
    (
507
        `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
508 77
        (`kcu`.`CONSTRAINT_CATALOG` IS NULL AND `rc`.`CONSTRAINT_CATALOG` IS NULL)
509 77
    ) AND
510
    `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
511 77
    `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME`
512
WHERE `rc`.`CONSTRAINT_SCHEMA` = database() AND `kcu`.`TABLE_SCHEMA` = database()
513 77
AND `rc`.`TABLE_NAME` = :tableName AND `kcu`.`TABLE_NAME` = :tableName1
514 24
SQL;
515 24
516
        try {
517
            $rows = $this->db->createCommand(
518 77
                $sql,
519
                [':tableName' => $table->getName(), ':tableName1' => $table->getName()]
520 77
            )->queryAll();
521 24
522 24
            $constraints = [];
523 24
524
            foreach ($rows as $row) {
525
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
526
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
527
            }
528
529
            $table->foreignKeys([]);
530
531
            foreach ($constraints as $name => $constraint) {
532
                $table->foreignKey($name, array_merge(
533
                    [$constraint['referenced_table_name']],
534
                    $constraint['columns']
535
                ));
536
            }
537
        } catch (Exception $e) {
538
            $previous = $e->getPrevious();
539
540
            if (!$previous instanceof PDOException || strpos($previous->getMessage(), 'SQLSTATE[42S02') === false) {
541
                throw $e;
542
            }
543
544
            // table does not exist, try to determine the foreign keys using the table creation sql
545
            $sql = $this->getCreateTableSql($table);
546
            $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi';
547
548
            if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
549
                foreach ($matches as $match) {
550
                    $fks = array_map('trim', explode(',', str_replace('`', '', $match[1])));
551
                    $pks = array_map('trim', explode(',', str_replace('`', '', $match[3])));
552 77
                    $constraint = [str_replace('`', '', $match[2])];
553
554
                    foreach ($fks as $k => $name) {
555
                        $constraint[$name] = $pks[$k];
556
                    }
557
558
                    $table->foreignKey(\md5(\serialize($constraint)), $constraint);
559
                }
560
                $table->foreignKeys(array_values($table->getForeignKeys()));
561
            }
562
        }
563
    }
564
565
    /**
566
     * Returns all unique indexes for the given table.
567
     *
568
     * Each array element is of the following structure:
569
     *
570
     * ```php
571
     * [
572
     *     'IndexName1' => ['col1' [, ...]],
573
     *     'IndexName2' => ['col2' [, ...]],
574
     * ]
575
     * ```
576
     *
577
     * @param TableSchema $table the table metadata.
578
     *
579
     * @throws Exception
580
     * @throws InvalidArgumentException
581
     * @throws InvalidConfigException
582
     *
583
     * @return array all unique indexes for the given table.
584
     */
585
    public function findUniqueIndexes($table): array
586
    {
587
        $sql = $this->getCreateTableSql($table);
588
589
        $uniqueIndexes = [];
590
591
        $regexp = '/UNIQUE KEY\s+\`(.+)\`\s*\((\`.+\`)+\)/mi';
592
593
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
594
            foreach ($matches as $match) {
595
                $indexName = $match[1];
596
                $indexColumns = array_map('trim', explode('`,`', trim($match[2], '`')));
597
                $uniqueIndexes[$indexName] = $indexColumns;
598
            }
599
        }
600
601
        return $uniqueIndexes;
602
    }
603 3
604
    /**
605 3
     * Create a column schema builder instance giving the type and value precision.
606
     *
607
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
608
     *
609
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
610
     * @param int|string|array $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
611
     *
612
     * @return ColumnSchemaBuilder column schema builder instance
613
     */
614
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
615
    {
616
        return new ColumnSchemaBuilder($type, $length, $this->db);
617
    }
618
619
    /**
620
     * @throws InvalidConfigException
621
     * @throws Exception
622
     *
623
     * @return bool whether the version of the MySQL being used is older than 5.1.
624
     */
625
    protected function isOldMysql(): bool
626
    {
627
        if ($this->oldMysql === null) {
628
            $version = $this->db->getSlavePdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
629
630
            $this->oldMysql = version_compare($version, '5.1', '<=');
631
        }
632
633
        return $this->oldMysql;
634
    }
635
636
    /**
637
     * Loads multiple types of constraints and returns the specified ones.
638
     *
639
     * @param string $tableName table name.
640 48
     * @param string $returnType return type:
641
     * - primaryKey
642 48
     * - foreignKeys
643
     * - uniques
644
     *
645
     * @throws Exception
646
     * @throws InvalidArgumentException
647
     * @throws InvalidConfigException
648
     *
649
     * @return mixed constraints.
650
     */
651
    private function loadTableConstraints(string $tableName, string $returnType)
652
    {
653
        static $sql = <<<'SQL'
654
SELECT
655
    `kcu`.`CONSTRAINT_NAME` AS `name`,
656
    `kcu`.`COLUMN_NAME` AS `column_name`,
657
    `tc`.`CONSTRAINT_TYPE` AS `type`,
658
    CASE
659
        WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
660
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
661
    END AS `foreign_table_schema`,
662
    `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
663
    `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
664
    `rc`.`UPDATE_RULE` AS `on_update`,
665
    `rc`.`DELETE_RULE` AS `on_delete`,
666
    `kcu`.`ORDINAL_POSITION` AS `position`
667
FROM
668
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
669
    `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`,
670
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
671
WHERE
672
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName
673
    AND `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`TABLE_NAME` = :tableName AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
674
    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'
675
UNION
676
SELECT
677
    `kcu`.`CONSTRAINT_NAME` AS `name`,
678
    `kcu`.`COLUMN_NAME` AS `column_name`,
679
    `tc`.`CONSTRAINT_TYPE` AS `type`,
680
    NULL AS `foreign_table_schema`,
681
    NULL AS `foreign_table_name`,
682
    NULL AS `foreign_column_name`,
683
    NULL AS `on_update`,
684 48
    NULL AS `on_delete`,
685
    `kcu`.`ORDINAL_POSITION` AS `position`
686 48
FROM
687
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
688
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
689 48
WHERE
690 48
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`TABLE_NAME` = :tableName
691
    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')
692 48
ORDER BY `position` ASC
693
SQL;
694 48
695 48
        $resolvedName = $this->resolveTableName($tableName);
696
697
        $constraints = $this->db->createCommand(
698 48
            $sql,
699
            [
700
                ':schemaName' => $resolvedName->getSchemaName(),
701
                ':tableName' => $resolvedName->getName(),
702
            ]
703 48
        )->queryAll();
704 48
705
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
706 48
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
707 37
708 37
        $result = [
709
            'primaryKey' => null,
710 37
            'foreignKeys' => [],
711
            'uniques' => [],
712 37
        ];
713 46
714 10
        foreach ($constraints as $type => $names) {
715 10
            foreach ($names as $name => $constraint) {
716 10
                switch ($type) {
717 10
                    case 'PRIMARY KEY':
718 10
                        $ct = (new Constraint())
719 10
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
720 10
721 10
                        $result['primaryKey'] = $ct;
722
723 10
                        break;
724
                    case 'FOREIGN KEY':
725 10
                        $fk = (new ForeignKeyConstraint())
726 37
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
727 37
                            ->foreignTableName($constraint[0]['foreign_table_name'])
728 37
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
729 37
                            ->onDelete($constraint[0]['on_delete'])
730
                            ->onUpdate($constraint[0]['on_update'])
731 37
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
732
                            ->name($name);
733 37
734
                        $result['foreignKeys'][] = $fk;
735
736
                        break;
737
                    case 'UNIQUE':
738 48
                        $ct = (new Constraint())
739 48
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
740
                            ->name($name);
741
742 48
                        $result['uniques'][] = $ct;
743
744
                        break;
745
                }
746
            }
747
        }
748
749
        foreach ($result as $type => $data) {
750
            $this->setTableMetadata($tableName, $type, $data);
751
        }
752 78
753
        return $result[$returnType];
754 78
    }
755
756
    /**
757
     * Creates a column schema for the database.
758
     *
759
     * This method may be overridden by child classes to create a DBMS-specific column schema.
760
     *
761
     * @return ColumnSchema column schema instance.
762
     */
763
    private function createColumnSchema(): ColumnSchema
764
    {
765
        return new ColumnSchema();
766
    }
767
}
768