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

Schema::loadTablePrimaryKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 2
b 0
f 0
nc 1
nop 1
dl 0
loc 3
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\Constraint\Constraint;
11
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
12
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
13
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
14
use Yiisoft\Db\Constraint\IndexConstraint;
15
use Yiisoft\Db\Exception\Exception;
16
use Yiisoft\Db\Exception\InvalidArgumentException;
17
use Yiisoft\Db\Exception\InvalidConfigException;
18
use Yiisoft\Db\Exception\NotSupportedException;
19
use Yiisoft\Db\Expression\Expression;
20
use Yiisoft\Db\Mysql\QueryBuilder;
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
     */
96
    protected function resolveTableName(string $name): TableSchema
97
    {
98
        $resolvedName = new TableSchema();
99
100
        $parts = explode('.', str_replace('`', '', $name));
101
102
        if (isset($parts[1])) {
103
            $resolvedName->schemaName($parts[0]);
104
            $resolvedName->name($parts[1]);
105
        } else {
106
            $resolvedName->schemaName($this->defaultSchema);
107
            $resolvedName->name($name);
108
        }
109
110
        $resolvedName->fullName(($resolvedName->getSchemaName() !== $this->defaultSchema ?
111
            $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName());
112
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
     */
130
    protected function findTableNames(string $schema = ''): array
131
    {
132
        $sql = 'SHOW TABLES';
133
134
        if ($schema !== '') {
135
            $sql .= ' FROM ' . $this->quoteSimpleTableName($schema);
136
        }
137
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
     */
150
    protected function loadTableSchema(string $name): ?TableSchema
151
    {
152
        $table = new TableSchema();
153
154
        $this->resolveTableNames($table, $name);
155
156
        if ($this->findColumns($table)) {
157
            $this->findConstraints($table);
158
159
            return $table;
160
        }
161
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
     */
176
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
177
    {
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
     */
192
    protected function loadTableForeignKeys(string $tableName): array
193
    {
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
     */
208
    protected function loadTableIndexes(string $tableName): array
209
    {
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
221
        $resolvedName = $this->resolveTableName($tableName);
222
223
        $indexes = $this->getDb()->createCommand($sql, [
224
            ':schemaName' => $resolvedName->getSchemaName(),
225
            ':tableName' => $resolvedName->getName(),
226
        ])->queryAll();
227
228
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
229
        $indexes = ArrayHelper::index($indexes, null, 'name');
230
        $result = [];
231
232
        foreach ($indexes as $name => $index) {
233
            $ic = new IndexConstraint();
234
235
            $ic->primary((bool) $index[0]['index_is_primary']);
236
            $ic->unique((bool) $index[0]['index_is_unique']);
237
            $ic->name($name !== 'PRIMARY' ? $name : null);
238
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
239
240
            $result[] = $ic;
241
        }
242
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
     */
257
    protected function loadTableUniques(string $tableName): array
258
    {
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 CheckConstraint[] check constraints for the given table.
270
     */
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
    {
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 DefaultValueConstraint[] default value constraints for the given table.
284
     */
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
    {
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
     */
295
    public function createQueryBuilder(): QueryBuilder
296
    {
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\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

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
     */
306
    protected function resolveTableNames($table, $name): void
307
    {
308
        $parts = explode('.', str_replace('`', '', $name));
309
310
        if (isset($parts[1])) {
311
            $table->schemaName($parts[0]);
312
            $table->name($parts[1]);
313
            $table->fullName($table->getSchemaName() . '.' . $table->getName());
314
        } else {
315
            $table->name($parts[0]);
316
            $table->fullName($parts[0]);
317
        }
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
     */
327
    protected function loadColumnSchema(array $info): ColumnSchema
328
    {
329
        $column = $this->createColumnSchema();
330
331
        $column->name($info['field']);
332
        $column->allowNull($info['null'] === 'YES');
333
        $column->primaryKey(strpos($info['key'], 'PRI') !== false);
334
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
335
        $column->comment($info['comment']);
336
        $column->dbType($info['type']);
337
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
338
        $column->type(self::TYPE_STRING);
339
340
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
341
            $type = strtolower($matches[1]);
342
343
            if (isset($this->typeMap[$type])) {
344
                $column->type($this->typeMap[$type]);
345
            }
346
347
            if (!empty($matches[2])) {
348
                if ($type === 'enum') {
349
                    preg_match_all("/'[^']*'/", $matches[2], $values);
350
351
                    foreach ($values[0] as $i => $value) {
352
                        $values[$i] = trim($value, "'");
353
                    }
354
355
                    $column->enumValues($values);
356
                } else {
357
                    $values = explode(',', $matches[2]);
358
                    $column->precision((int) $values[0]);
359
                    $column->size((int) $values[0]);
360
361
                    if (isset($values[1])) {
362
                        $column->scale((int) $values[1]);
363
                    }
364
365
                    if ($column->getSize() === 1 && $type === 'tinyint') {
366
                        $column->type('boolean');
367
                    } elseif ($type === 'bit') {
368
                        if ($column->getSize() > 32) {
369
                            $column->type('bigint');
370
                        } elseif ($column->getSize() === 32) {
371
                            $column->type('integer');
372
                        }
373
                    }
374
                }
375
            }
376
        }
377
378
        $column->phpType($this->getColumnPhpType($column));
379
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
            if (
388
                ($column->getType() === 'timestamp' || $column->getType() === 'datetime')
389
                && preg_match('/^current_timestamp(?:\(([0-9]*)\))?$/i', (string) $info['default'], $matches)
390
            ) {
391
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
392
                    ? '(' . $matches[1] . ')' : '')));
393
            } elseif (isset($type) && $type === 'bit') {
394
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
395
            } else {
396
                $column->defaultValue($column->phpTypecast($info['default']));
397
            }
398
        }
399
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
     */
412
    protected function findColumns($table): bool
413
    {
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
        try {
417
            $columns = $this->getDb()->createCommand($sql)->queryAll();
418
        } catch (Exception $e) {
419
            $previous = $e->getPrevious();
420
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
                 */
427
                return false;
428
            }
429
430
            throw $e;
431
        }
432
433
        foreach ($columns as $info) {
434
            if ($this->getDb()->getSlavePdo()->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_LOWER) {
435
                $info = array_change_key_case($info, CASE_LOWER);
436
            }
437
438
            $column = $this->loadColumnSchema($info);
439
            $table->columns($column->getName(), $column);
440
441
            if ($column->isPrimaryKey()) {
442
                $table->primaryKey($column->getName());
443
                if ($column->isAutoIncrement()) {
444
                    $table->sequenceName('');
445
                }
446
            }
447
        }
448
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
     */
486
    protected function findConstraints($table)
487
    {
488
        $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
        try {
507
            $rows = $this->getDb()->createCommand(
508
                $sql,
509
                [':tableName' => $table->getName(), ':tableName1' => $table->getName()]
510
            )->queryAll();
511
512
            $constraints = [];
513
514
            foreach ($rows as $row) {
515
                $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
519
            $table->foreignKeys([]);
520
521
            foreach ($constraints as $name => $constraint) {
522
                $table->foreignKey($name, array_merge(
523
                    [$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
        }
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
     */
604
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
605
    {
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
     */
641
    private function loadTableConstraints(string $tableName, string $returnType)
642
    {
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
685
        $resolvedName = $this->resolveTableName($tableName);
686
687
        $constraints = $this->getDb()->createCommand(
688
            $sql,
689
            [
690
                ':schemaName' => $resolvedName->getSchemaName(),
691
                ':tableName' => $resolvedName->getName(),
692
            ]
693
        )->queryAll();
694
695
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
696
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
697
698
        $result = [
699
            'primaryKey' => null,
700
            'foreignKeys' => [],
701
            'uniques' => [],
702
        ];
703
704
        foreach ($constraints as $type => $names) {
705
            foreach ($names as $name => $constraint) {
706
                switch ($type) {
707
                    case 'PRIMARY KEY':
708
                        $ct = (new Constraint())
709
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
710
711
                        $result['primaryKey'] = $ct;
712
713
                        break;
714
                    case 'FOREIGN KEY':
715
                        $fk = (new ForeignKeyConstraint())
716
                            ->name($name)
717
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
718
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
719
                            ->foreignTableName($constraint[0]['foreign_table_name'])
720
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
721
                            ->onDelete($constraint[0]['on_delete'])
722
                            ->onUpdate($constraint[0]['on_update']);
723
724
                        $result['foreignKeys'][] = $fk;
725
726
                        break;
727
                    case 'UNIQUE':
728
                        $ct = (new Constraint())
729
                            ->name($name)
730
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
731
732
                        $result['uniques'][] = $ct;
733
734
                        break;
735
                }
736
            }
737
        }
738
739
        foreach ($result as $type => $data) {
740
            $this->setTableMetadata($tableName, $type, $data);
741
        }
742
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
     */
753
    private function createColumnSchema(): ColumnSchema
754
    {
755
        return new ColumnSchema();
756
    }
757
}
758