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

Schema::findConstraints()   B

Complexity

Conditions 9
Paths 43

Size

Total Lines 65
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 21.4523

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 46
c 1
b 0
f 0
nc 43
nop 1
dl 0
loc 65
ccs 13
cts 28
cp 0.4643
crap 21.4523
rs 7.6226

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

271
    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...
272 12
    {
273
        throw new NotSupportedException('MySQL does not support check constraints.');
274
    }
275
276
    /**
277
     * Loads all default value constraints for the given table.
278
     *
279
     * @param string $tableName table name.
280
     *
281
     * @throws NotSupportedException
282
     *
283
     * @return array default value constraints for the given table.
284 12
     */
285
    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

285
    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...
286 12
    {
287
        throw new NotSupportedException('MySQL does not support default value constraints.');
288
    }
289
290
    /**
291
     * Creates a query builder for the MySQL database.
292
     *
293
     * @return QueryBuilder query builder instance
294 53
     */
295
    public function createQueryBuilder(): QueryBuilder
296 53
    {
297
        return new QueryBuilder($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\QueryBuilder::__construct() does only seem to accept Yiisoft\Db\Connection\ConnectionInterface, 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

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

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

466
            'SHOW CREATE TABLE ' . $this->quoteTableName(/** @scrutinizer ignore-type */ $table->getFullName())
Loading history...
467
        )->queryOne();
468
469
        if (isset($row['Create Table'])) {
470
            $sql = $row['Create Table'];
471
        } else {
472
            $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

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