Passed
Pull Request — 2.2 (#20357)
by Wilmer
13:33 queued 05:55
created

Schema::loadTablePrimaryKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
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\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
        $version = $this->db->getServerVersion();
209
210
        // check version MySQL >= 8.0.16
211
        if (\stripos($version, 'MariaDb') === false && \version_compare($version, '8.0.16', '<')) {
212
            throw new NotSupportedException('MySQL < 8.0.16 does not support check constraints.');
213
        }
214
215
        $checks = [];
216
217
        $sql = <<<SQL
218
        SELECT cc.CONSTRAINT_NAME as constraint_name, cc.CHECK_CLAUSE as check_clause
219
        FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
220
        JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc
221
        ON tc.CONSTRAINT_NAME = cc.CONSTRAINT_NAME
222
        WHERE tc.TABLE_NAME = :tableName AND tc.CONSTRAINT_TYPE = 'CHECK';
223
        SQL;
224
225
        $resolvedName = $this->resolveTableName($tableName);
226
        $tableRows = $this->db->createCommand($sql, [':tableName' => $resolvedName->name])->queryAll();
227
228
        if ($tableRows === []) {
229
            return $checks;
230
        }
231
232
        $tableRows = $this->normalizePdoRowKeyCase($tableRows, true);
233
234
        foreach ($tableRows as $tableRow) {
235
            $check = new CheckConstraint(
236
                [
237
                    'name' => $tableRow['constraint_name'],
238
                    'expression' => $tableRow['check_clause'],
239
                ]
240
            );
241
            $checks[] = $check;
242
        }
243
244
        return $checks;
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     * @throws NotSupportedException if this method is called.
250
     */
251
    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

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

382
            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...
383
                $info = array_change_key_case($info, CASE_LOWER);
384
            }
385
386
            if (\in_array($info['field'], $jsonColumns, true)) {
387
                $info['type'] = static::TYPE_JSON;
388
            }
389
390
            $column = $this->loadColumnSchema($info);
391
            $table->columns[$column->name] = $column;
392
            if ($column->isPrimaryKey) {
393
                $table->primaryKey[] = $column->name;
394
                if ($column->autoIncrement) {
395
                    $table->sequenceName = '';
396
                }
397
            }
398
        }
399
400
        return true;
401
    }
402
403
    /**
404
     * Gets the CREATE TABLE sql string.
405
     * @param TableSchema $table the table metadata
406
     * @return string $sql the result of 'SHOW CREATE TABLE'
407
     */
408
    protected function getCreateTableSql($table)
409
    {
410
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $this->quoteTableName($table->fullName))->queryOne();
411
        if (isset($row['Create Table'])) {
412
            $sql = $row['Create Table'];
413
        } else {
414
            $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

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