Passed
Push — master ( 1f0d9b...6a4b0f )
by Wilmer
12:11 queued 10:38
created

Schema   F

Complexity

Total Complexity 70

Size/Duplication

Total Lines 583
Duplicated Lines 0 %

Test Coverage

Coverage 85.91%

Importance

Changes 0
Metric Value
eloc 310
dl 0
loc 583
ccs 189
cts 220
cp 0.8591
rs 2.8
c 0
b 0
f 0
wmc 70

20 Methods

Rating   Name   Duplication   Size   Complexity  
A loadTableForeignKeys() 0 3 1
B loadTableConstraints() 0 100 7
A resolveTableName() 0 18 3
B findConstraints() 0 65 9
A isOldMysql() 0 9 2
A getCreateTableSql() 0 14 2
A loadTablePrimaryKey() 0 3 1
A loadTableDefaultValues() 0 3 1
D loadColumnSchema() 0 68 19
B findColumns() 0 37 8
A findTableNames() 0 9 2
A loadTableIndexes() 0 36 3
A resolveTableNames() 0 11 2
A loadTableChecks() 0 3 1
A findUniqueIndexes() 0 17 3
A loadTableUniques() 0 3 1
A loadTableSchema() 0 13 2
A createQueryBuilder() 0 3 1
A createColumnSchemaBuilder() 0 3 1
A createColumnSchema() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mysql\Schema;
6
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\Db\Constraint\Constraint;
9
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
10
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
11
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
12
use Yiisoft\Db\Constraint\IndexConstraint;
13
use Yiisoft\Db\Exception\InvalidConfigException;
14
use Yiisoft\Db\Exception\NotSupportedException;
15
use Yiisoft\Db\Expression\Expression;
16
use Yiisoft\Db\Mysql\Query\QueryBuilder;
17
use Yiisoft\Db\Schema\Schema as AbstractSchema;
18
19
/**
20
 * Schema is the class for retrieving metadata from a MySQL database.
21
 */
22
class Schema extends AbstractSchema implements ConstraintFinderInterface
23
{
24
    use ConstraintFinderTrait;
25
26
    protected string $columnSchemaClass = ColumnSchema::class;
27
    protected string $tableQuoteCharacter = '`';
28
    protected string $columnQuoteCharacter = '`';
29
    private bool $oldMysql;
30
    private array $typeMap = [
31
        'tinyint' => self::TYPE_TINYINT,
32
        'bit' => self::TYPE_INTEGER,
33
        'smallint' => self::TYPE_SMALLINT,
34
        'mediumint' => self::TYPE_INTEGER,
35
        'int' => self::TYPE_INTEGER,
36
        'integer' => self::TYPE_INTEGER,
37
        'bigint' => self::TYPE_BIGINT,
38
        'float' => self::TYPE_FLOAT,
39
        'double' => self::TYPE_DOUBLE,
40
        'real' => self::TYPE_FLOAT,
41
        'decimal' => self::TYPE_DECIMAL,
42
        'numeric' => self::TYPE_DECIMAL,
43
        'tinytext' => self::TYPE_TEXT,
44
        'mediumtext' => self::TYPE_TEXT,
45
        'longtext' => self::TYPE_TEXT,
46
        'longblob' => self::TYPE_BINARY,
47
        'blob' => self::TYPE_BINARY,
48
        'text' => self::TYPE_TEXT,
49
        'varchar' => self::TYPE_STRING,
50
        'string' => self::TYPE_STRING,
51
        'char' => self::TYPE_CHAR,
52
        'datetime' => self::TYPE_DATETIME,
53
        'year' => self::TYPE_DATE,
54
        'date' => self::TYPE_DATE,
55
        'time' => self::TYPE_TIME,
56
        'timestamp' => self::TYPE_TIMESTAMP,
57
        'enum' => self::TYPE_STRING,
58
        'varbinary' => self::TYPE_BINARY,
59
        'json' => self::TYPE_JSON,
60
    ];
61
62 58
    protected function resolveTableName($name)
63
    {
64 58
        $resolvedName = new TableSchema();
65
66 58
        $parts = explode('.', str_replace('`', '', $name));
67
68 58
        if (isset($parts[1])) {
69
            $resolvedName->schemaName($parts[0]);
70
            $resolvedName->name($parts[1]);
71
        } else {
72 58
            $resolvedName->schemaName($this->defaultSchema);
73 58
            $resolvedName->name($name);
74
        }
75
76 58
        $resolvedName->fullName(($resolvedName->getSchemaName() !== $this->defaultSchema ?
77 58
            $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName());
78
79 58
        return $resolvedName;
80
    }
81
82 5
    protected function findTableNames($schema = '')
83
    {
84 5
        $sql = 'SHOW TABLES';
85
86 5
        if ($schema !== '') {
87
            $sql .= ' FROM ' . $this->quoteSimpleTableName($schema);
88
        }
89
90 5
        return $this->getDb()->createCommand($sql)->queryColumn();
91
    }
92
93 90
    protected function loadTableSchema(string $name): ?TableSchema
94
    {
95 90
        $table = new TableSchema();
96
97 90
        $this->resolveTableNames($table, $name);
98
99 90
        if ($this->findColumns($table)) {
100 84
            $this->findConstraints($table);
101
102 84
            return $table;
103
        }
104
105 14
        return null;
106
    }
107
108 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
109
    {
110 31
        return $this->loadTableConstraints($tableName, 'primaryKey');
111
    }
112
113 4
    protected function loadTableForeignKeys(string $tableName): array
114
    {
115 4
        return $this->loadTableConstraints($tableName, 'foreignKeys');
116
    }
117
118 28
    protected function loadTableIndexes(string $tableName): array
119
    {
120 28
        static $sql = <<<'SQL'
121
SELECT
122
    `s`.`INDEX_NAME` AS `name`,
123
    `s`.`COLUMN_NAME` AS `column_name`,
124
    `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
125
    `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
126
FROM `information_schema`.`STATISTICS` AS `s`
127
WHERE `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND `s`.`TABLE_NAME` = :tableName
128
ORDER BY `s`.`SEQ_IN_INDEX` ASC
129
SQL;
130
131 28
        $resolvedName = $this->resolveTableName($tableName);
132
133 28
        $indexes = $this->getDb()->createCommand($sql, [
134 28
            ':schemaName' => $resolvedName->getSchemaName(),
135 28
            ':tableName' => $resolvedName->getName(),
136 28
        ])->queryAll();
137
138 28
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
139 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
140 28
        $result = [];
141
142 28
        foreach ($indexes as $name => $index) {
143 28
            $ic = new IndexConstraint();
144
145 28
            $ic->setIsPrimary((bool) $index[0]['index_is_primary']);
146 28
            $ic->setIsUnique((bool) $index[0]['index_is_unique']);
147 28
            $ic->setName($name !== 'PRIMARY' ? $name : null);
148 28
            $ic->setColumnNames(ArrayHelper::getColumn($index, 'column_name'));
149
150 28
            $result[] = $ic;
151
        }
152
153 28
        return $result;
154
    }
155
156 13
    protected function loadTableUniques(string $tableName): array
157
    {
158 13
        return $this->loadTableConstraints($tableName, 'uniques');
159
    }
160
161 12
    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

161
    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...
162
    {
163 12
        throw new NotSupportedException('MySQL does not support check constraints.');
164
    }
165
166 12
    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

166
    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...
167
    {
168 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
169
    }
170
171
    /**
172
     * Creates a query builder for the MySQL database.
173
     *
174
     * @return QueryBuilder query builder instance
175
     */
176 62
    public function createQueryBuilder()
177
    {
178 62
        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\Query\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

178
        return new QueryBuilder(/** @scrutinizer ignore-type */ $this->getDb());
Loading history...
179
    }
180
181
    /**
182
     * Resolves the table name and schema name (if any).
183
     *
184
     * @param TableSchema $table the table metadata object.
185
     * @param string $name the table name.
186
     */
187 90
    protected function resolveTableNames($table, $name)
188
    {
189 90
        $parts = explode('.', str_replace('`', '', $name));
190
191 90
        if (isset($parts[1])) {
192
            $table->schemaName($parts[0]);
193
            $table->name($parts[1]);
194
            $table->fullName($table->getSchemaName() . '.' . $table->getName());
195
        } else {
196 90
            $table->name($parts[0]);
197 90
            $table->fullName($parts[0]);
198
        }
199 90
    }
200
201
    /**
202
     * Loads the column information into a {@see ColumnSchema} object.
203
     *
204
     * @param array $info column information.
205
     *
206
     * @return ColumnSchema the column schema object.
207
     */
208 85
    protected function loadColumnSchema(array $info): ColumnSchema
209
    {
210 85
        $column = $this->createColumnSchema();
211
212 85
        $column->name($info['field']);
213 85
        $column->allowNull($info['null'] === 'YES');
214 85
        $column->isPrimaryKey(strpos($info['key'], 'PRI') !== false);
215 85
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
216 85
        $column->comment($info['comment']);
217 85
        $column->dbType($info['type']);
218 85
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
219 85
        $column->type(self::TYPE_STRING);
220
221 85
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
222 85
            $type = strtolower($matches[1]);
223 85
            if (isset($this->typeMap[$type])) {
224 85
                $column->type($this->typeMap[$type]);
225
            }
226 85
            if (!empty($matches[2])) {
227 84
                if ($type === 'enum') {
228 22
                    preg_match_all("/'[^']*'/", $matches[2], $values);
229 22
                    foreach ($values[0] as $i => $value) {
230 22
                        $values[$i] = trim($value, "'");
231
                    }
232 22
                    $column->enumValues($values);
233
                } else {
234 84
                    $values = explode(',', $matches[2]);
235 84
                    $column->precision((int) $values[0]);
236 84
                    $column->size((int) $values[0]);
237 84
                    if (isset($values[1])) {
238 25
                        $column->scale((int) $values[1]);
239
                    }
240 84
                    if ($column->getSize() === 1 && $type === 'bit') {
241 3
                        $column->type('boolean');
242 84
                    } elseif ($type === 'bit') {
243 22
                        if ($column->getSize() > 32) {
244
                            $column->type('bigint');
245 22
                        } elseif ($column->getSize() === 32) {
246
                            $column->type('integer');
247
                        }
248
                    }
249
                }
250
            }
251
        }
252
253 85
        $column->phpType($this->getColumnPhpType($column));
254
255 85
        if (!$column->getIsPrimaryKey()) {
256
            /**
257
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
258
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
259
             *
260
             * See details here: https://mariadb.com/kb/en/library/now/#description
261
             */
262
            if (
263 83
                ($column->getType() === 'timestamp' || $column->getType() === 'datetime')
264 83
                && preg_match('/^current_timestamp(?:\(([0-9]*)\))?$/i', (string) $info['default'], $matches)
265
            ) {
266 25
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
267 25
                    ? '(' . $matches[1] . ')' : '')));
268 80
            } elseif (isset($type) && $type === 'bit') {
269 22
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
270
            } else {
271 80
                $column->defaultValue($column->phpTypecast($info['default']));
272
            }
273
        }
274
275 85
        return $column;
276
    }
277
278
    /**
279
     * Collects the metadata of table columns.
280
     *
281
     * @param TableSchema $table the table metadata.
282
     *
283
     * @throws \Exception if DB query fails.
284
     *
285
     * @return bool whether the table exists in the database.
286
     */
287 90
    protected function findColumns($table)
288
    {
289 90
        $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

289
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName(/** @scrutinizer ignore-type */ $table->getFullName());
Loading history...
290
291
        try {
292 90
            $columns = $this->getDb()->createCommand($sql)->queryAll();
293 14
        } catch (\Exception $e) {
294 14
            $previous = $e->getPrevious();
295
296 14
            if ($previous instanceof \PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
297
                /**
298
                 * table does not exist
299
                 * https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
300
                 */
301 14
                return false;
302
            }
303
304
            throw $e;
305
        }
306
307 84
        foreach ($columns as $info) {
308 84
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_LOWER) {
309 83
                $info = array_change_key_case($info, CASE_LOWER);
310
            }
311
312 84
            $column = $this->loadColumnSchema($info);
313 84
            $table->columns($column->getName(), $column);
314
315 84
            if ($column->getIsPrimaryKey()) {
316 60
                $table->primaryKey($column->getName());
317 60
                if ($column->getAutoIncrement()) {
318 58
                    $table->sequenceName('');
319
                }
320
            }
321
        }
322
323 84
        return true;
324
    }
325
326
    /**
327
     * Gets the CREATE TABLE sql string.
328
     *
329
     * @param TableSchema $table the table metadata.
330
     *
331
     * @return string $sql the result of 'SHOW CREATE TABLE'.
332
     */
333 1
    protected function getCreateTableSql($table): string
334
    {
335 1
        $row = $this->getDb()->createCommand(
336 1
            '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

336
            'SHOW CREATE TABLE ' . $this->quoteTableName(/** @scrutinizer ignore-type */ $table->getFullName())
Loading history...
337 1
        )->queryOne();
338
339 1
        if (isset($row['Create Table'])) {
340 1
            $sql = $row['Create Table'];
341
        } else {
342
            $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

342
            $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
343
            $sql = $row[1];
344
        }
345
346 1
        return $sql;
347
    }
348
349
    /**
350
     * Collects the foreign key column details for the given table.
351
     *
352
     * @param TableSchema $table the table metadata.
353
     *
354
     * @throws \Exception
355
     */
356 84
    protected function findConstraints($table)
357
    {
358
        $sql = <<<'SQL'
359 84
SELECT
360
    kcu.constraint_name,
361
    kcu.column_name,
362
    kcu.referenced_table_name,
363
    kcu.referenced_column_name
364
FROM information_schema.referential_constraints AS rc
365
JOIN information_schema.key_column_usage AS kcu ON
366
    (
367
        kcu.constraint_catalog = rc.constraint_catalog OR
368
        (kcu.constraint_catalog IS NULL AND rc.constraint_catalog IS NULL)
369
    ) AND
370
    kcu.constraint_schema = rc.constraint_schema AND
371
    kcu.constraint_name = rc.constraint_name
372
WHERE rc.constraint_schema = database() AND kcu.table_schema = database()
373
AND rc.table_name = :tableName AND kcu.table_name = :tableName1
374
SQL;
375
376
        try {
377 84
            $rows = $this->getDb()->createCommand(
378 84
                $sql,
379 84
                [':tableName' => $table->getName(), ':tableName1' => $table->getName()]
380 84
            )->queryAll();
381
382 84
            $constraints = [];
383
384 84
            foreach ($rows as $row) {
385 27
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
386 27
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
387
            }
388
389 84
            $table->foreignKeys([]);
390
391 84
            foreach ($constraints as $name => $constraint) {
392 27
                $table->foreignKey($name, array_merge(
393 27
                    [$constraint['referenced_table_name']],
394 27
                    $constraint['columns']
395
                ));
396
            }
397
        } catch (\Exception $e) {
398
            $previous = $e->getPrevious();
399
400
            if (!$previous instanceof \PDOException || strpos($previous->getMessage(), 'SQLSTATE[42S02') === false) {
401
                throw $e;
402
            }
403
404
            // table does not exist, try to determine the foreign keys using the table creation sql
405
            $sql = $this->getCreateTableSql($table);
406
            $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi';
407
408
            if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
409
                foreach ($matches as $match) {
410
                    $fks = array_map('trim', explode(',', str_replace('`', '', $match[1])));
411
                    $pks = array_map('trim', explode(',', str_replace('`', '', $match[3])));
412
                    $constraint = [str_replace('`', '', $match[2])];
413
414
                    foreach ($fks as $k => $name) {
415
                        $constraint[$name] = $pks[$k];
416
                    }
417
418
                    $table->foreignKey(md5(serialize($constraint)), $constraint);
419
                }
420
                $table->foreignKeys(\array_values($table->getForeignKeys()));
421
            }
422
        }
423 84
    }
424
425
    /**
426
     * Returns all unique indexes for the given table.
427
     *
428
     * Each array element is of the following structure:
429
     *
430
     * ```php
431
     * [
432
     *     'IndexName1' => ['col1' [, ...]],
433
     *     'IndexName2' => ['col2' [, ...]],
434
     * ]
435
     * ```
436
     *
437
     * @param TableSchema $table the table metadata.
438
     *
439
     * @return array all unique indexes for the given table.
440
     */
441 1
    public function findUniqueIndexes($table)
442
    {
443 1
        $sql = $this->getCreateTableSql($table);
444
445 1
        $uniqueIndexes = [];
446
447 1
        $regexp = '/UNIQUE KEY\s+\`(.+)\`\s*\((\`.+\`)+\)/mi';
448
449 1
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
450 1
            foreach ($matches as $match) {
451 1
                $indexName = $match[1];
452 1
                $indexColumns = array_map('trim', explode('`,`', trim($match[2], '`')));
453 1
                $uniqueIndexes[$indexName] = $indexColumns;
454
            }
455
        }
456
457 1
        return $uniqueIndexes;
458
    }
459
460 1
    public function createColumnSchemaBuilder($type, $length = null)
461
    {
462 1
        return new ColumnSchemaBuilder($type, $length, $this->getDb());
463
    }
464
465
    /**
466
     * @throws InvalidConfigException
467
     * @throws Exception
468
     *
469
     * @return bool whether the version of the MySQL being used is older than 5.1.
470
     */
471
    protected function isOldMysql()
472
    {
473
        if ($this->oldMysql === null) {
474
            $version = $this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
475
476
            $this->oldMysql = version_compare($version, '5.1', '<=');
477
        }
478
479
        return $this->oldMysql;
480
    }
481
482
    /**
483
     * Loads multiple types of constraints and returns the specified ones.
484
     *
485
     * @param string $tableName table name.
486
     * @param string $returnType return type:
487
     * - primaryKey
488
     * - foreignKeys
489
     * - uniques
490
     *
491
     * @return mixed constraints.
492
     */
493 48
    private function loadTableConstraints(string $tableName, string $returnType)
494
    {
495 48
        static $sql = <<<'SQL'
496
SELECT
497
    `kcu`.`CONSTRAINT_NAME` AS `name`,
498
    `kcu`.`COLUMN_NAME` AS `column_name`,
499
    `tc`.`CONSTRAINT_TYPE` AS `type`,
500
    CASE
501
        WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
502
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
503
    END AS `foreign_table_schema`,
504
    `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
505
    `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
506
    `rc`.`UPDATE_RULE` AS `on_update`,
507
    `rc`.`DELETE_RULE` AS `on_delete`,
508
    `kcu`.`ORDINAL_POSITION` AS `position`
509
FROM
510
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
511
    `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`,
512
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
513
WHERE
514
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName
515
    AND `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`TABLE_NAME` = :tableName AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
516
    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'
517
UNION
518
SELECT
519
    `kcu`.`CONSTRAINT_NAME` AS `name`,
520
    `kcu`.`COLUMN_NAME` AS `column_name`,
521
    `tc`.`CONSTRAINT_TYPE` AS `type`,
522
    NULL AS `foreign_table_schema`,
523
    NULL AS `foreign_table_name`,
524
    NULL AS `foreign_column_name`,
525
    NULL AS `on_update`,
526
    NULL AS `on_delete`,
527
    `kcu`.`ORDINAL_POSITION` AS `position`
528
FROM
529
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
530
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
531
WHERE
532
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `kcu`.`TABLE_NAME` = :tableName
533
    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')
534
ORDER BY `position` ASC
535
SQL;
536
537 48
        $resolvedName = $this->resolveTableName($tableName);
538
539 48
        $constraints = $this->getDb()->createCommand(
540 48
            $sql,
541
            [
542 48
                ':schemaName' => $resolvedName->getSchemaName(),
543 48
                ':tableName' => $resolvedName->getName(),
544
            ]
545 48
        )->queryAll();
546
547 48
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
548 48
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
549
550
        $result = [
551 48
            'primaryKey' => null,
552
            'foreignKeys' => [],
553
            'uniques' => [],
554
        ];
555
556 48
        foreach ($constraints as $type => $names) {
557 48
            foreach ($names as $name => $constraint) {
558 48
                switch ($type) {
559 48
                    case 'PRIMARY KEY':
560 37
                        $ct = new Constraint();
561 37
                        $ct->setColumnNames(ArrayHelper::getColumn($constraint, 'column_name'));
562 37
                        $result['primaryKey'] = $ct;
563
564 37
                        break;
565 46
                    case 'FOREIGN KEY':
566 10
                        $fk = new ForeignKeyConstraint();
567 10
                        $fk->setName($name);
568 10
                        $fk->setColumnNames(ArrayHelper::getColumn($constraint, 'column_name'));
569 10
                        $fk->setForeignSchemaName($constraint[0]['foreign_table_schema']);
570 10
                        $fk->setForeignTableName($constraint[0]['foreign_table_name']);
571 10
                        $fk->setForeignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'));
572 10
                        $fk->setOnDelete($constraint[0]['on_delete']);
573 10
                        $fk->setOnUpdate($constraint[0]['on_update']);
574 10
                        $result['foreignKeys'][] = $fk;
575
576 10
                        break;
577 37
                    case 'UNIQUE':
578 37
                        $ct = new Constraint();
579 37
                        $ct->setName($name);
580 37
                        $ct->setColumnNames(ArrayHelper::getColumn($constraint, 'column_name'));
581 37
                        $result['uniques'][] = $ct;
582
583 37
                        break;
584
                }
585
            }
586
        }
587
588 48
        foreach ($result as $type => $data) {
589 48
            $this->setTableMetadata($tableName, $type, $data);
590
        }
591
592 48
        return $result[$returnType];
593
    }
594
595
    /**
596
     * Creates a column schema for the database.
597
     *
598
     * This method may be overridden by child classes to create a DBMS-specific column schema.
599
     *
600
     * @return ColumnSchema column schema instance.
601
     */
602 85
    protected function createColumnSchema(): ColumnSchema
603
    {
604 85
        return new ColumnSchema();
605
    }
606
}
607