Passed
Pull Request — 2.2 (#20357)
by Wilmer
12:52 queued 05:12
created

Schema::findUniqueIndexes()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 2
nop 1
dl 0
loc 15
ccs 0
cts 10
cp 0
crap 12
rs 9.9666
c 0
b 0
f 0
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\Constraint;
15
use yii\db\ConstraintFinderInterface;
16
use yii\db\ConstraintFinderTrait;
17
use yii\db\Exception;
18
use yii\db\Expression;
19
use yii\db\ForeignKeyConstraint;
20
use yii\db\IndexConstraint;
21
use yii\db\TableSchema;
22
use yii\helpers\ArrayHelper;
23
24
/**
25
 * Schema is the class for retrieving metadata from a MySQL database (version 4.1.x and 5.x).
26
 *
27
 * @author Qiang Xue <[email protected]>
28
 * @since 2.0
29
 */
30
class Schema extends \yii\db\Schema implements ConstraintFinderInterface
31
{
32
    use ConstraintFinderTrait;
33
34
    /**
35
     * {@inheritdoc}
36
     */
37
    public $columnSchemaClass = 'yii\db\mysql\ColumnSchema';
38
    /**
39
     * @var bool whether MySQL used is older than 5.1.
40
     */
41
    private $_oldMysql;
42
43
44
    /**
45
     * @var array mapping from physical column types (keys) to abstract column types (values)
46
     */
47
    public $typeMap = [
48
        'tinyint' => self::TYPE_TINYINT,
49
        'bool' => self::TYPE_TINYINT,
50
        'boolean' => self::TYPE_TINYINT,
51
        'bit' => self::TYPE_INTEGER,
52
        'smallint' => self::TYPE_SMALLINT,
53
        'mediumint' => self::TYPE_INTEGER,
54
        'int' => self::TYPE_INTEGER,
55
        'integer' => self::TYPE_INTEGER,
56
        'bigint' => self::TYPE_BIGINT,
57
        'float' => self::TYPE_FLOAT,
58
        'double' => self::TYPE_DOUBLE,
59
        'double precision' => self::TYPE_DOUBLE,
60
        'real' => self::TYPE_FLOAT,
61
        'decimal' => self::TYPE_DECIMAL,
62
        'numeric' => self::TYPE_DECIMAL,
63
        'dec' => self::TYPE_DECIMAL,
64
        'fixed' => self::TYPE_DECIMAL,
65
        'tinytext' => self::TYPE_TEXT,
66
        'mediumtext' => self::TYPE_TEXT,
67
        'longtext' => self::TYPE_TEXT,
68
        'longblob' => self::TYPE_BINARY,
69
        'blob' => self::TYPE_BINARY,
70
        'text' => self::TYPE_TEXT,
71
        'varchar' => self::TYPE_STRING,
72
        'string' => self::TYPE_STRING,
73
        'char' => self::TYPE_CHAR,
74
        'datetime' => self::TYPE_DATETIME,
75
        'year' => self::TYPE_DATE,
76
        'date' => self::TYPE_DATE,
77
        'time' => self::TYPE_TIME,
78
        'timestamp' => self::TYPE_TIMESTAMP,
79
        'enum' => self::TYPE_STRING,
80
        'set' => self::TYPE_STRING,
81
        'binary' => self::TYPE_BINARY,
82
        'varbinary' => self::TYPE_BINARY,
83
        'json' => self::TYPE_JSON,
84
    ];
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    protected $tableQuoteCharacter = '`';
90
    /**
91
     * {@inheritdoc}
92
     */
93
    protected $columnQuoteCharacter = '`';
94
95
    /**
96
     * {@inheritdoc}
97
     */
98
    protected function resolveTableName($name)
99
    {
100
        $resolvedName = new TableSchema();
101
        $parts = explode('.', str_replace('`', '', $name));
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
        $resolvedName->fullName = ($resolvedName->schemaName !== $this->defaultSchema ? $resolvedName->schemaName . '.' : '') . $resolvedName->name;
110
        return $resolvedName;
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116
    protected function findTableNames($schema = '')
117
    {
118
        $sql = 'SHOW TABLES';
119
        if ($schema !== '') {
120
            $sql .= ' FROM ' . $this->quoteSimpleTableName($schema);
121
        }
122
123
        return $this->db->createCommand($sql)->queryColumn();
124
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129
    protected function loadTableSchema($name)
130
    {
131
        $table = new TableSchema();
132
        $this->resolveTableNames($table, $name);
133
134
        if ($this->findColumns($table)) {
135
            $this->findConstraints($table);
136
            return $table;
137
        }
138
139
        return null;
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    protected function loadTablePrimaryKey($tableName)
146
    {
147
        return $this->loadTableConstraints($tableName, 'primaryKey');
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153
    protected function loadTableForeignKeys($tableName)
154
    {
155
        return $this->loadTableConstraints($tableName, 'foreignKeys');
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    protected function loadTableIndexes($tableName)
162
    {
163
        static $sql = <<<'SQL'
164
SELECT
165
    `s`.`INDEX_NAME` AS `name`,
166
    `s`.`COLUMN_NAME` AS `column_name`,
167
    `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
168
    `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
169
FROM `information_schema`.`STATISTICS` AS `s`
170
WHERE `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND `s`.`TABLE_NAME` = :tableName
171
ORDER BY `s`.`SEQ_IN_INDEX` ASC
172
SQL;
173
174
        $resolvedName = $this->resolveTableName($tableName);
175
        $indexes = $this->db->createCommand($sql, [
176
            ':schemaName' => $resolvedName->schemaName,
177
            ':tableName' => $resolvedName->name,
178
        ])->queryAll();
179
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
180
        $indexes = ArrayHelper::index($indexes, null, 'name');
181
        $result = [];
182
        foreach ($indexes as $name => $index) {
183
            $result[] = new IndexConstraint([
184
                'isPrimary' => (bool) $index[0]['index_is_primary'],
185
                'isUnique' => (bool) $index[0]['index_is_unique'],
186
                'name' => $name !== 'PRIMARY' ? $name : null,
187
                'columnNames' => ArrayHelper::getColumn($index, 'column_name'),
188
            ]);
189
        }
190
191
        return $result;
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197
    protected function loadTableUniques($tableName)
198
    {
199
        return $this->loadTableConstraints($tableName, 'uniques');
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     * @throws NotSupportedException if this method is called.
205
     */
206
    protected function loadTableChecks($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

206
    protected function loadTableChecks(/** @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...
207
    {
208
        throw new NotSupportedException('MySQL does not support check constraints.');
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     * @throws NotSupportedException if this method is called.
214
     */
215
    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

215
    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...
216
    {
217
        throw new NotSupportedException('MySQL does not support default value constraints.');
218
    }
219
220
    /**
221
     * Creates a query builder for the MySQL database.
222
     * @return QueryBuilder query builder instance
223
     */
224
    public function createQueryBuilder()
225
    {
226
        return Yii::createObject(QueryBuilder::class, [$this->db]);
227
    }
228
229
    /**
230
     * Resolves the table name and schema name (if any).
231
     * @param TableSchema $table the table metadata object
232
     * @param string $name the table name
233
     */
234
    protected function resolveTableNames($table, $name)
235
    {
236
        $parts = explode('.', str_replace('`', '', $name));
237
        if (isset($parts[1])) {
238
            $table->schemaName = $parts[0];
239
            $table->name = $parts[1];
240
            $table->fullName = $table->schemaName . '.' . $table->name;
241
        } else {
242
            $table->fullName = $table->name = $parts[0];
243
        }
244
    }
245
246
    /**
247
     * Loads the column information into a [[ColumnSchema]] object.
248
     * @param array $info column information
249
     * @return ColumnSchema the column schema object
250
     */
251
    protected function loadColumnSchema($info)
252
    {
253
        $column = $this->createColumnSchema();
254
255
        $column->name = $info['field'];
256
        $column->allowNull = $info['null'] === 'YES';
257
        $column->isPrimaryKey = strpos($info['key'], 'PRI') !== false;
258
        $column->autoIncrement = stripos($info['extra'], 'auto_increment') !== false;
259
        $column->comment = $info['comment'];
260
261
        $column->dbType = $info['type'];
262
        $column->unsigned = stripos($column->dbType, 'unsigned') !== false;
263
264
        $column->type = self::TYPE_STRING;
265
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
266
            $type = strtolower($matches[1]);
267
            if (isset($this->typeMap[$type])) {
268
                $column->type = $this->typeMap[$type];
269
            }
270
            if (!empty($matches[2])) {
271
                if ($type === 'enum') {
272
                    preg_match_all("/'[^']*'/", $matches[2], $values);
273
                    foreach ($values[0] as $i => $value) {
274
                        $values[$i] = trim($value, "'");
275
                    }
276
                    $column->enumValues = $values;
277
                } else {
278
                    $values = explode(',', $matches[2]);
279
                    $column->size = $column->precision = (int) $values[0];
280
                    if (isset($values[1])) {
281
                        $column->scale = (int) $values[1];
282
                    }
283
                    if ($column->size === 1 && $type === 'bit') {
284
                        $column->type = 'boolean';
285
                    } elseif ($type === 'bit') {
286
                        if ($column->size > 32) {
287
                            $column->type = 'bigint';
288
                        } elseif ($column->size === 32) {
289
                            $column->type = 'integer';
290
                        }
291
                    }
292
                }
293
            }
294
        }
295
296
        $column->phpType = $this->getColumnPhpType($column);
297
298
        if (!$column->isPrimaryKey) {
299
            /**
300
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
301
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
302
             *
303
             * See details here: https://mariadb.com/kb/en/library/now/#description
304
             */
305
            if (
306
                in_array($column->type, ['timestamp', 'datetime', 'date', 'time'])
307
                && isset($info['default'])
308
                && preg_match('/^current_timestamp(?:\(([0-9]*)\))?$/i', $info['default'], $matches)
309
            ) {
310
                $column->defaultValue = new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1]) ? '(' . $matches[1] . ')' : ''));
311
            } elseif (isset($type) && $type === 'bit') {
312
                $column->defaultValue = bindec(trim(isset($info['default']) ? $info['default'] : '', 'b\''));
313
            } else {
314
                $column->defaultValue = $column->phpTypecast($info['default']);
315
            }
316
        }
317
318
        return $column;
319
    }
320
321
    /**
322
     * Collects the metadata of table columns.
323
     * @param TableSchema $table the table metadata
324
     * @return bool whether the table exists in the database
325
     * @throws \Exception if DB query fails
326
     */
327
    protected function findColumns($table)
328
    {
329
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName($table->fullName);
330
        try {
331
            $columns = $this->db->createCommand($sql)->queryAll();
332
        } catch (\Exception $e) {
333
            $previous = $e->getPrevious();
334
            if ($previous instanceof \PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
335
                // table does not exist
336
                // https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
337
                return false;
338
            }
339
            throw $e;
340
        }
341
        foreach ($columns as $info) {
342
            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

342
            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...
343
                $info = array_change_key_case($info, CASE_LOWER);
344
            }
345
            $column = $this->loadColumnSchema($info);
346
            $table->columns[$column->name] = $column;
347
            if ($column->isPrimaryKey) {
348
                $table->primaryKey[] = $column->name;
349
                if ($column->autoIncrement) {
350
                    $table->sequenceName = '';
351
                }
352
            }
353
        }
354
355
        return true;
356
    }
357
358
    /**
359
     * Gets the CREATE TABLE sql string.
360
     * @param TableSchema $table the table metadata
361
     * @return string $sql the result of 'SHOW CREATE TABLE'
362
     */
363
    protected function getCreateTableSql($table)
364
    {
365
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $this->quoteTableName($table->fullName))->queryOne();
366
        if (isset($row['Create Table'])) {
367
            $sql = $row['Create Table'];
368
        } else {
369
            $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

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