Passed
Push — 2.2 ( c9917f...37714b )
by Alexander
02:01 queued 17s
created

Schema::findTableNames()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

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

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

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

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