Passed
Pull Request — 2.2 (#20357)
by Wilmer
09:39 queued 01:58
created

Schema::loadColumnSchema()   D

Complexity

Conditions 20
Paths 100

Size

Total Lines 68
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 420

Importance

Changes 0
Metric Value
cc 20
eloc 43
c 0
b 0
f 0
nc 100
nop 1
dl 0
loc 68
ccs 0
cts 41
cp 0
crap 420
rs 4.1666

How to fix   Long Method    Complexity   

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
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\db\mysql;
10
11
use Yii;
12
use yii\base\InvalidConfigException;
13
use yii\base\NotSupportedException;
14
use yii\db\CheckConstraint;
15
use yii\db\Constraint;
16
use yii\db\ConstraintFinderInterface;
17
use yii\db\ConstraintFinderTrait;
18
use yii\db\Exception;
19
use yii\db\Expression;
20
use yii\db\ForeignKeyConstraint;
21
use yii\db\IndexConstraint;
22
use yii\db\TableSchema;
23
use yii\helpers\ArrayHelper;
24
25
/**
26
 * Schema is the class for retrieving metadata from a MySQL database (version 4.1.x and 5.x).
27
 *
28
 * @author Qiang Xue <[email protected]>
29
 * @since 2.0
30
 */
31
class Schema extends \yii\db\Schema implements ConstraintFinderInterface
32
{
33
    use ConstraintFinderTrait;
34
35
    /**
36
     * {@inheritdoc}
37
     */
38
    public $columnSchemaClass = 'yii\db\mysql\ColumnSchema';
39
    /**
40
     * @var bool whether MySQL used is older than 5.1.
41
     */
42
    private $_oldMysql;
43
44
45
    /**
46
     * @var array mapping from physical column types (keys) to abstract column types (values)
47
     */
48
    public $typeMap = [
49
        'tinyint' => self::TYPE_TINYINT,
50
        'bool' => self::TYPE_TINYINT,
51
        'boolean' => self::TYPE_TINYINT,
52
        'bit' => self::TYPE_INTEGER,
53
        'smallint' => self::TYPE_SMALLINT,
54
        'mediumint' => self::TYPE_INTEGER,
55
        'int' => self::TYPE_INTEGER,
56
        'integer' => self::TYPE_INTEGER,
57
        'bigint' => self::TYPE_BIGINT,
58
        'float' => self::TYPE_FLOAT,
59
        'double' => self::TYPE_DOUBLE,
60
        'double precision' => self::TYPE_DOUBLE,
61
        'real' => self::TYPE_FLOAT,
62
        'decimal' => self::TYPE_DECIMAL,
63
        'numeric' => self::TYPE_DECIMAL,
64
        'dec' => self::TYPE_DECIMAL,
65
        'fixed' => self::TYPE_DECIMAL,
66
        'tinytext' => self::TYPE_TEXT,
67
        'mediumtext' => self::TYPE_TEXT,
68
        'longtext' => self::TYPE_TEXT,
69
        'longblob' => self::TYPE_BINARY,
70
        'blob' => self::TYPE_BINARY,
71
        'text' => self::TYPE_TEXT,
72
        'varchar' => self::TYPE_STRING,
73
        'string' => self::TYPE_STRING,
74
        'char' => self::TYPE_CHAR,
75
        'datetime' => self::TYPE_DATETIME,
76
        'year' => self::TYPE_DATE,
77
        'date' => self::TYPE_DATE,
78
        'time' => self::TYPE_TIME,
79
        'timestamp' => self::TYPE_TIMESTAMP,
80
        'enum' => self::TYPE_STRING,
81
        'set' => self::TYPE_STRING,
82
        'binary' => self::TYPE_BINARY,
83
        'varbinary' => self::TYPE_BINARY,
84
        'json' => self::TYPE_JSON,
85
    ];
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    protected $tableQuoteCharacter = '`';
91
    /**
92
     * {@inheritdoc}
93
     */
94
    protected $columnQuoteCharacter = '`';
95
96
    /**
97
     * {@inheritdoc}
98
     */
99
    protected function resolveTableName($name)
100
    {
101
        $resolvedName = new TableSchema();
102
        $parts = explode('.', str_replace('`', '', $name));
103
        if (isset($parts[1])) {
104
            $resolvedName->schemaName = $parts[0];
105
            $resolvedName->name = $parts[1];
106
        } else {
107
            $resolvedName->schemaName = $this->defaultSchema;
108
            $resolvedName->name = $name;
109
        }
110
        $resolvedName->fullName = ($resolvedName->schemaName !== $this->defaultSchema ? $resolvedName->schemaName . '.' : '') . $resolvedName->name;
111
        return $resolvedName;
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    protected function findTableNames($schema = '')
118
    {
119
        $sql = 'SHOW TABLES';
120
        if ($schema !== '') {
121
            $sql .= ' FROM ' . $this->quoteSimpleTableName($schema);
122
        }
123
124
        return $this->db->createCommand($sql)->queryColumn();
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130
    protected function loadTableSchema($name)
131
    {
132
        $table = new TableSchema();
133
        $this->resolveTableNames($table, $name);
134
135
        if ($this->findColumns($table)) {
136
            $this->findConstraints($table);
137
            return $table;
138
        }
139
140
        return null;
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    protected function loadTablePrimaryKey($tableName)
147
    {
148
        return $this->loadTableConstraints($tableName, 'primaryKey');
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154
    protected function loadTableForeignKeys($tableName)
155
    {
156
        return $this->loadTableConstraints($tableName, 'foreignKeys');
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    protected function loadTableIndexes($tableName)
163
    {
164
        static $sql = <<<'SQL'
165
SELECT
166
    `s`.`INDEX_NAME` AS `name`,
167
    `s`.`COLUMN_NAME` AS `column_name`,
168
    `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
169
    `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
170
FROM `information_schema`.`STATISTICS` AS `s`
171
WHERE `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND `s`.`TABLE_NAME` = :tableName
172
ORDER BY `s`.`SEQ_IN_INDEX` ASC
173
SQL;
174
175
        $resolvedName = $this->resolveTableName($tableName);
176
        $indexes = $this->db->createCommand($sql, [
177
            ':schemaName' => $resolvedName->schemaName,
178
            ':tableName' => $resolvedName->name,
179
        ])->queryAll();
180
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
181
        $indexes = ArrayHelper::index($indexes, null, 'name');
182
        $result = [];
183
        foreach ($indexes as $name => $index) {
184
            $result[] = new IndexConstraint([
185
                'isPrimary' => (bool) $index[0]['index_is_primary'],
186
                'isUnique' => (bool) $index[0]['index_is_unique'],
187
                'name' => $name !== 'PRIMARY' ? $name : null,
188
                'columnNames' => ArrayHelper::getColumn($index, 'column_name'),
189
            ]);
190
        }
191
192
        return $result;
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198
    protected function loadTableUniques($tableName)
199
    {
200
        return $this->loadTableConstraints($tableName, 'uniques');
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206
    protected function loadTableChecks($tableName)
207
    {
208
        // check version MySQL >= 8.0.16
209
        if (version_compare($this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION), '8.0.16', '<')) {
210
            throw new NotSupportedException('MySQL < 8.0.16 does not support check constraints.');
211
        }
212
213
        $checks = [];
214
215
        $sql = <<<SQL
216
        SELECT cc.CONSTRAINT_NAME as constraint_name, cc.CHECK_CLAUSE as check_clause
217
        FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
218
        JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc
219
        ON tc.CONSTRAINT_NAME = cc.CONSTRAINT_NAME
220
        WHERE tc.TABLE_NAME = :tableName AND tc.CONSTRAINT_TYPE = 'CHECK';
221
        SQL;
222
223
        $resolvedName = $this->resolveTableName($tableName);
224
        $tableRows = $this->db->createCommand($sql, [':tableName' => $resolvedName->name])->queryAll();
225
226
        if ($tableRows === []) {
227
            return $checks;
228
        }
229
230
        $tableRows = $this->normalizePdoRowKeyCase($tableRows, true);
231
232
        foreach ($tableRows as $tableRow) {
233
            $matches = [];
234
            $columnName = null;
235
236
            if (preg_match('/\(`?([a-zA-Z0-9_]+)`?\s*[><=]/', $tableRow['check_clause'], $matches)) {
237
                $columnName = $matches[1];
238
            }
239
240
            $check = new CheckConstraint(
241
                [
242
                    'name' => $tableRow['constraint_name'],
243
                    'columnNames' => [$columnName],
244
                    'expression' => $tableRow['check_clause'],
245
                ]
246
            );
247
            $checks[] = $check;
248
        }
249
250
        return $checks;
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     * @throws NotSupportedException if this method is called.
256
     */
257
    protected function loadTableDefaultValues($tableName)
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

257
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ $tableName)

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...
258
    {
259
        throw new NotSupportedException('MySQL does not support default value constraints.');
260
    }
261
262
    /**
263
     * Creates a query builder for the MySQL database.
264
     * @return QueryBuilder query builder instance
265
     */
266
    public function createQueryBuilder()
267
    {
268
        return Yii::createObject(QueryBuilder::class, [$this->db]);
269
    }
270
271
    /**
272
     * Resolves the table name and schema name (if any).
273
     * @param TableSchema $table the table metadata object
274
     * @param string $name the table name
275
     */
276
    protected function resolveTableNames($table, $name)
277
    {
278
        $parts = explode('.', str_replace('`', '', $name));
279
        if (isset($parts[1])) {
280
            $table->schemaName = $parts[0];
281
            $table->name = $parts[1];
282
            $table->fullName = $table->schemaName . '.' . $table->name;
283
        } else {
284
            $table->fullName = $table->name = $parts[0];
285
        }
286
    }
287
288
    /**
289
     * Loads the column information into a [[ColumnSchema]] object.
290
     * @param array $info column information
291
     * @return ColumnSchema the column schema object
292
     */
293
    protected function loadColumnSchema($info)
294
    {
295
        $column = $this->createColumnSchema();
296
297
        $column->name = $info['field'];
298
        $column->allowNull = $info['null'] === 'YES';
299
        $column->isPrimaryKey = strpos($info['key'], 'PRI') !== false;
300
        $column->autoIncrement = stripos($info['extra'], 'auto_increment') !== false;
301
        $column->comment = $info['comment'];
302
303
        $column->dbType = $info['type'];
304
        $column->unsigned = stripos($column->dbType, 'unsigned') !== false;
305
306
        $column->type = self::TYPE_STRING;
307
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
308
            $type = strtolower($matches[1]);
309
            if (isset($this->typeMap[$type])) {
310
                $column->type = $this->typeMap[$type];
311
            }
312
            if (!empty($matches[2])) {
313
                if ($type === 'enum') {
314
                    preg_match_all("/'[^']*'/", $matches[2], $values);
315
                    foreach ($values[0] as $i => $value) {
316
                        $values[$i] = trim($value, "'");
317
                    }
318
                    $column->enumValues = $values;
319
                } else {
320
                    $values = explode(',', $matches[2]);
321
                    $column->size = $column->precision = (int) $values[0];
322
                    if (isset($values[1])) {
323
                        $column->scale = (int) $values[1];
324
                    }
325
                    if ($column->size === 1 && $type === 'bit') {
326
                        $column->type = 'boolean';
327
                    } elseif ($type === 'bit') {
328
                        if ($column->size > 32) {
329
                            $column->type = 'bigint';
330
                        } elseif ($column->size === 32) {
331
                            $column->type = 'integer';
332
                        }
333
                    }
334
                }
335
            }
336
        }
337
338
        $column->phpType = $this->getColumnPhpType($column);
339
340
        if (!$column->isPrimaryKey) {
341
            /**
342
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
343
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
344
             *
345
             * See details here: https://mariadb.com/kb/en/library/now/#description
346
             */
347
            if (
348
                in_array($column->type, ['timestamp', 'datetime', 'date', 'time'])
349
                && isset($info['default'])
350
                && preg_match('/^current_timestamp(?:\(([0-9]*)\))?$/i', $info['default'], $matches)
351
            ) {
352
                $column->defaultValue = new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1]) ? '(' . $matches[1] . ')' : ''));
353
            } elseif (isset($type) && $type === 'bit') {
354
                $column->defaultValue = bindec(trim(isset($info['default']) ? $info['default'] : '', 'b\''));
355
            } else {
356
                $column->defaultValue = $column->phpTypecast($info['default']);
357
            }
358
        }
359
360
        return $column;
361
    }
362
363
    /**
364
     * Collects the metadata of table columns.
365
     * @param TableSchema $table the table metadata
366
     * @return bool whether the table exists in the database
367
     * @throws \Exception if DB query fails
368
     */
369
    protected function findColumns($table)
370
    {
371
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName($table->fullName);
372
        try {
373
            $columns = $this->db->createCommand($sql)->queryAll();
374
        } catch (\Exception $e) {
375
            $previous = $e->getPrevious();
376
            if ($previous instanceof \PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
377
                // table does not exist
378
                // https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
379
                return false;
380
            }
381
            throw $e;
382
        }
383
        foreach ($columns as $info) {
384
            if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_LOWER) {
0 ignored issues
show
Bug introduced by
The method getAttribute() does not exist on null. ( Ignorable by Annotation )

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

384
            if ($this->db->slavePdo->/** @scrutinizer ignore-call */ getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_LOWER) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
385
                $info = array_change_key_case($info, CASE_LOWER);
386
            }
387
            $column = $this->loadColumnSchema($info);
388
            $table->columns[$column->name] = $column;
389
            if ($column->isPrimaryKey) {
390
                $table->primaryKey[] = $column->name;
391
                if ($column->autoIncrement) {
392
                    $table->sequenceName = '';
393
                }
394
            }
395
        }
396
397
        return true;
398
    }
399
400
    /**
401
     * Gets the CREATE TABLE sql string.
402
     * @param TableSchema $table the table metadata
403
     * @return string $sql the result of 'SHOW CREATE TABLE'
404
     */
405
    protected function getCreateTableSql($table)
406
    {
407
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $this->quoteTableName($table->fullName))->queryOne();
408
        if (isset($row['Create Table'])) {
409
            $sql = $row['Create Table'];
410
        } else {
411
            $row = array_values($row);
0 ignored issues
show
Bug introduced by
$row of type false is incompatible with the type array expected by parameter $array 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

411
            $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
412
            $sql = $row[1];
413
        }
414
415
        return $sql;
416
    }
417
418
    /**
419
     * Collects the foreign key column details for the given table.
420
     * @param TableSchema $table the table metadata
421
     * @throws \Exception
422
     */
423
    protected function findConstraints($table)
424
    {
425
        $sql = <<<'SQL'
426
SELECT
427
    `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
428
    `kcu`.`COLUMN_NAME` AS `column_name`,
429
    `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
430
    `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
431
FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
432
JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
433
    (
434
        `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
435
        (`kcu`.`CONSTRAINT_CATALOG` IS NULL AND `rc`.`CONSTRAINT_CATALOG` IS NULL)
436
    ) AND
437
    `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
438
    `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME`
439
WHERE `rc`.`CONSTRAINT_SCHEMA` = database() AND `kcu`.`TABLE_SCHEMA` = database()
440
AND `rc`.`TABLE_NAME` = :tableName AND `kcu`.`TABLE_NAME` = :tableName1
441
SQL;
442
443
        try {
444
            $rows = $this->db->createCommand($sql, [':tableName' => $table->name, ':tableName1' => $table->name])->queryAll();
445
            $constraints = [];
446
447
            foreach ($rows as $row) {
448
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
449
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
450
            }
451
452
            $table->foreignKeys = [];
453
            foreach ($constraints as $name => $constraint) {
454
                $table->foreignKeys[$name] = array_merge(
455
                    [$constraint['referenced_table_name']],
456
                    $constraint['columns']
457
                );
458
            }
459
        } catch (\Exception $e) {
460
            $previous = $e->getPrevious();
461
            if (!$previous instanceof \PDOException || strpos($previous->getMessage(), 'SQLSTATE[42S02') === false) {
462
                throw $e;
463
            }
464
465
            // table does not exist, try to determine the foreign keys using the table creation sql
466
            $sql = $this->getCreateTableSql($table);
467
            $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi';
468
            if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
469
                foreach ($matches as $match) {
470
                    $fks = array_map('trim', explode(',', str_replace(['`', '"'], '', $match[1])));
471
                    $pks = array_map('trim', explode(',', str_replace(['`', '"'], '', $match[3])));
472
                    $constraint = [str_replace(['`', '"'], '', $match[2])];
473
                    foreach ($fks as $k => $name) {
474
                        $constraint[$name] = $pks[$k];
475
                    }
476
                    $table->foreignKeys[md5(serialize($constraint))] = $constraint;
477
                }
478
                $table->foreignKeys = array_values($table->foreignKeys);
479
            }
480
        }
481
    }
482
483
    /**
484
     * Returns all unique indexes for the given table.
485
     *
486
     * Each array element is of the following structure:
487
     *
488
     * ```php
489
     * [
490
     *     'IndexName1' => ['col1' [, ...]],
491
     *     'IndexName2' => ['col2' [, ...]],
492
     * ]
493
     * ```
494
     *
495
     * @param TableSchema $table the table metadata
496
     * @return array all unique indexes for the given table.
497
     */
498
    public function findUniqueIndexes($table)
499
    {
500
        $sql = $this->getCreateTableSql($table);
501
        $uniqueIndexes = [];
502
503
        $regexp = '/UNIQUE KEY\s+[`"](.+)[`"]\s*\(([`"].+[`"])+\)/mi';
504
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
505
            foreach ($matches as $match) {
506
                $indexName = $match[1];
507
                $indexColumns = array_map('trim', preg_split('/[`"],[`"]/', trim($match[2], '`"')));
508
                $uniqueIndexes[$indexName] = $indexColumns;
509
            }
510
        }
511
512
        return $uniqueIndexes;
513
    }
514
515
    /**
516
     * {@inheritdoc}
517
     */
518
    public function createColumnSchemaBuilder($type, $length = null)
519
    {
520
        return Yii::createObject(ColumnSchemaBuilder::class, [$type, $length, $this->db]);
521
    }
522
523
    /**
524
     * @return bool whether the version of the MySQL being used is older than 5.1.
525
     * @throws InvalidConfigException
526
     * @throws Exception
527
     * @since 2.0.13
528
     */
529
    protected function isOldMysql()
530
    {
531
        if ($this->_oldMysql === null) {
532
            $version = $this->db->getSlavePdo(true)->getAttribute(\PDO::ATTR_SERVER_VERSION);
533
            $this->_oldMysql = version_compare($version, '5.1', '<=');
0 ignored issues
show
Documentation Bug introduced by
It seems like version_compare($version, '5.1', '<=') can also be of type integer. However, the property $_oldMysql is declared as type boolean. Maybe add an additional type check?

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

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
534
        }
535
536
        return $this->_oldMysql;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_oldMysql also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
537
    }
538
539
    /**
540
     * Loads multiple types of constraints and returns the specified ones.
541
     * @param string $tableName table name.
542
     * @param string $returnType return type:
543
     * - primaryKey
544
     * - foreignKeys
545
     * - uniques
546
     * @return mixed constraints.
547
     */
548
    private function loadTableConstraints($tableName, $returnType)
549
    {
550
        static $sql = <<<'SQL'
551
SELECT
552
    `kcu`.`CONSTRAINT_NAME` AS `name`,
553
    `kcu`.`COLUMN_NAME` AS `column_name`,
554
    `tc`.`CONSTRAINT_TYPE` AS `type`,
555
    CASE
556
        WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
557
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
558
    END AS `foreign_table_schema`,
559
    `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
560
    `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
561
    `rc`.`UPDATE_RULE` AS `on_update`,
562
    `rc`.`DELETE_RULE` AS `on_delete`,
563
    `kcu`.`ORDINAL_POSITION` AS `position`
564
FROM
565
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
566
    `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`,
567
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
568
WHERE
569
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName1, DATABASE()) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName
570
    AND `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`TABLE_NAME` = :tableName1 AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
571
    AND `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`TABLE_NAME` = :tableName2 AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY'
572
UNION
573
SELECT
574
    `kcu`.`CONSTRAINT_NAME` AS `name`,
575
    `kcu`.`COLUMN_NAME` AS `column_name`,
576
    `tc`.`CONSTRAINT_TYPE` AS `type`,
577
    NULL AS `foreign_table_schema`,
578
    NULL AS `foreign_table_name`,
579
    NULL AS `foreign_column_name`,
580
    NULL AS `on_update`,
581
    NULL AS `on_delete`,
582
    `kcu`.`ORDINAL_POSITION` AS `position`
583
FROM
584
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
585
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
586
WHERE
587
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName2, DATABASE()) AND `kcu`.`TABLE_NAME` = :tableName3
588
    AND `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`TABLE_NAME` = :tableName4 AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE')
589
ORDER BY `position` ASC
590
SQL;
591
592
        $resolvedName = $this->resolveTableName($tableName);
593
        $constraints = $this->db->createCommand($sql, [
594
            ':schemaName' => $resolvedName->schemaName,
595
            ':schemaName1' => $resolvedName->schemaName,
596
            ':schemaName2' => $resolvedName->schemaName,
597
            ':tableName' => $resolvedName->name,
598
            ':tableName1' => $resolvedName->name,
599
            ':tableName2' => $resolvedName->name,
600
            ':tableName3' => $resolvedName->name,
601
            ':tableName4' => $resolvedName->name
602
        ])->queryAll();
603
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
604
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
605
        $result = [
606
            'primaryKey' => null,
607
            'foreignKeys' => [],
608
            'uniques' => [],
609
        ];
610
        foreach ($constraints as $type => $names) {
611
            foreach ($names as $name => $constraint) {
612
                switch ($type) {
613
                    case 'PRIMARY KEY':
614
                        $result['primaryKey'] = new Constraint([
615
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
616
                        ]);
617
                        break;
618
                    case 'FOREIGN KEY':
619
                        $result['foreignKeys'][] = new ForeignKeyConstraint([
620
                            'name' => $name,
621
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
622
                            'foreignSchemaName' => $constraint[0]['foreign_table_schema'],
623
                            'foreignTableName' => $constraint[0]['foreign_table_name'],
624
                            'foreignColumnNames' => ArrayHelper::getColumn($constraint, 'foreign_column_name'),
625
                            'onDelete' => $constraint[0]['on_delete'],
626
                            'onUpdate' => $constraint[0]['on_update'],
627
                        ]);
628
                        break;
629
                    case 'UNIQUE':
630
                        $result['uniques'][] = new Constraint([
631
                            'name' => $name,
632
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
633
                        ]);
634
                        break;
635
                }
636
            }
637
        }
638
        foreach ($result as $type => $data) {
639
            $this->setTableMetadata($tableName, $type, $data);
640
        }
641
642
        return $result[$returnType];
643
    }
644
}
645