Issues (910)

framework/db/mysql/Schema.php (3 issues)

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\CheckConstraint;
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 81
    protected function resolveTableName($name)
99
    {
100 81
        $resolvedName = new TableSchema();
101 81
        $parts = explode('.', str_replace('`', '', $name));
102 81
        if (isset($parts[1])) {
103
            $resolvedName->schemaName = $parts[0];
104
            $resolvedName->name = $parts[1];
105
        } else {
106 81
            $resolvedName->schemaName = $this->defaultSchema;
107 81
            $resolvedName->name = $name;
108
        }
109 81
        $resolvedName->fullName = ($resolvedName->schemaName !== $this->defaultSchema ? $resolvedName->schemaName . '.' : '') . $resolvedName->name;
110 81
        return $resolvedName;
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116 7
    protected function findTableNames($schema = '')
117
    {
118 7
        $sql = 'SHOW TABLES';
119 7
        if ($schema !== '') {
120
            $sql .= ' FROM ' . $this->quoteSimpleTableName($schema);
121
        }
122
123 7
        return $this->db->createCommand($sql)->queryColumn();
124
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129 412
    protected function loadTableSchema($name)
130
    {
131 412
        $table = new TableSchema();
132 412
        $this->resolveTableNames($table, $name);
133
134 412
        if ($this->findColumns($table)) {
135 407
            $this->findConstraints($table);
136 407
            return $table;
137
        }
138
139 16
        return null;
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 54
    protected function loadTablePrimaryKey($tableName)
146
    {
147 54
        return $this->loadTableConstraints($tableName, 'primaryKey');
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153 4
    protected function loadTableForeignKeys($tableName)
154
    {
155 4
        return $this->loadTableConstraints($tableName, 'foreignKeys');
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161 51
    protected function loadTableIndexes($tableName)
162
    {
163 51
        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 51
SQL;
173
174 51
        $resolvedName = $this->resolveTableName($tableName);
175 51
        $indexes = $this->db->createCommand($sql, [
176 51
            ':schemaName' => $resolvedName->schemaName,
177 51
            ':tableName' => $resolvedName->name,
178 51
        ])->queryAll();
179 51
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
180 51
        $indexes = ArrayHelper::index($indexes, null, 'name');
181 51
        $result = [];
182 51
        foreach ($indexes as $name => $index) {
183 51
            $result[] = new IndexConstraint([
184 51
                'isPrimary' => (bool) $index[0]['index_is_primary'],
185 51
                'isUnique' => (bool) $index[0]['index_is_unique'],
186 51
                'name' => $name !== 'PRIMARY' ? $name : null,
187 51
                'columnNames' => ArrayHelper::getColumn($index, 'column_name'),
188 51
            ]);
189
        }
190
191 51
        return $result;
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197 13
    protected function loadTableUniques($tableName)
198
    {
199 13
        return $this->loadTableConstraints($tableName, 'uniques');
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205 12
    protected function loadTableChecks($tableName)
206
    {
207 12
        $version = $this->db->getServerVersion();
208
209
        // check version MySQL >= 8.0.16
210 12
        if (\stripos($version, 'MariaDb') === false && \version_compare($version, '8.0.16', '<')) {
211 12
            throw new NotSupportedException('MySQL < 8.0.16 does not support check constraints.');
212
        }
213
214
        $checks = [];
215
216
        $sql = <<<SQL
217
        SELECT cc.CONSTRAINT_NAME as constraint_name, cc.CHECK_CLAUSE as check_clause
218
        FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
219
        JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc
220
        ON tc.CONSTRAINT_NAME = cc.CONSTRAINT_NAME
221
        WHERE tc.TABLE_NAME = :tableName AND tc.CONSTRAINT_TYPE = 'CHECK';
222
        SQL;
223
224
        $resolvedName = $this->resolveTableName($tableName);
225
        $tableRows = $this->db->createCommand($sql, [':tableName' => $resolvedName->name])->queryAll();
226
227
        if ($tableRows === []) {
228
            return $checks;
229
        }
230
231
        $tableRows = $this->normalizePdoRowKeyCase($tableRows, true);
232
233
        foreach ($tableRows as $tableRow) {
234
            $check = new CheckConstraint(
235
                [
236
                    'name' => $tableRow['constraint_name'],
237
                    'expression' => $tableRow['check_clause'],
238
                ]
239
            );
240
            $checks[] = $check;
241
        }
242
243
        return $checks;
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     * @throws NotSupportedException if this method is called.
249
     */
250 12
    protected function loadTableDefaultValues($tableName)
0 ignored issues
show
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

250
    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...
251
    {
252 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
253
    }
254
255
    /**
256
     * Creates a query builder for the MySQL database.
257
     * @return QueryBuilder query builder instance
258
     */
259 406
    public function createQueryBuilder()
260
    {
261 406
        return Yii::createObject(QueryBuilder::className(), [$this->db]);
262
    }
263
264
    /**
265
     * Resolves the table name and schema name (if any).
266
     * @param TableSchema $table the table metadata object
267
     * @param string $name the table name
268
     */
269 412
    protected function resolveTableNames($table, $name)
270
    {
271 412
        $parts = explode('.', str_replace('`', '', $name));
272 412
        if (isset($parts[1])) {
273
            $table->schemaName = $parts[0];
274
            $table->name = $parts[1];
275
            $table->fullName = $table->schemaName . '.' . $table->name;
276
        } else {
277 412
            $table->fullName = $table->name = $parts[0];
278
        }
279
    }
280
281
    /**
282
     * Loads the column information into a [[ColumnSchema]] object.
283
     * @param array $info column information
284
     * @return ColumnSchema the column schema object
285
     */
286 409
    protected function loadColumnSchema($info)
287
    {
288 409
        $column = $this->createColumnSchema();
289
290 409
        $column->name = $info['field'];
291 409
        $column->allowNull = $info['null'] === 'YES';
292 409
        $column->isPrimaryKey = strpos($info['key'], 'PRI') !== false;
293 409
        $column->autoIncrement = stripos($info['extra'], 'auto_increment') !== false;
294 409
        $column->comment = $info['comment'];
295
296 409
        $column->dbType = $info['type'];
297 409
        $column->unsigned = stripos($column->dbType, 'unsigned') !== false;
298
299 409
        $column->type = self::TYPE_STRING;
300 409
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
301 409
            $type = strtolower($matches[1]);
302 409
            if (isset($this->typeMap[$type])) {
303 409
                $column->type = $this->typeMap[$type];
304
            }
305 409
            if (!empty($matches[2])) {
306 407
                if ($type === 'enum') {
307 28
                    preg_match_all("/'[^']*'/", $matches[2], $values);
308 28
                    foreach ($values[0] as $i => $value) {
309 28
                        $values[$i] = trim($value, "'");
310
                    }
311 28
                    $column->enumValues = $values;
312
                } else {
313 407
                    $values = explode(',', $matches[2]);
314 407
                    $column->size = $column->precision = (int) $values[0];
315 407
                    if (isset($values[1])) {
316 106
                        $column->scale = (int) $values[1];
317
                    }
318 407
                    if ($column->size === 1 && $type === 'bit') {
319 6
                        $column->type = 'boolean';
320 407
                    } elseif ($type === 'bit') {
321 28
                        if ($column->size > 32) {
322
                            $column->type = 'bigint';
323 28
                        } elseif ($column->size === 32) {
324
                            $column->type = 'integer';
325
                        }
326
                    }
327
                }
328
            }
329
        }
330
331 409
        $column->phpType = $this->getColumnPhpType($column);
332
333 409
        if (!$column->isPrimaryKey) {
334
            /**
335
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
336
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
337
             *
338
             * See details here: https://mariadb.com/kb/en/library/now/#description
339
             */
340
            if (
341 404
                in_array($column->type, ['timestamp', 'datetime', 'date', 'time'])
342 404
                && isset($info['default'])
343 404
                && preg_match('/^current_timestamp(?:\(([0-9]*)\))?$/i', $info['default'], $matches)
344
            ) {
345 31
                $column->defaultValue = new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1]) ? '(' . $matches[1] . ')' : ''));
346 401
            } elseif (isset($type) && $type === 'bit') {
347 29
                $column->defaultValue = bindec(trim(isset($info['default']) ? $info['default'] : '', 'b\''));
348
            } else {
349 400
                $column->defaultValue = $column->phpTypecast($info['default']);
350
            }
351
        }
352
353 409
        return $column;
354
    }
355
356
    /**
357
     * Collects the metadata of table columns.
358
     * @param TableSchema $table the table metadata
359
     * @return bool whether the table exists in the database
360
     * @throws \Exception if DB query fails
361
     */
362 412
    protected function findColumns($table)
363
    {
364 412
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName($table->fullName);
365
        try {
366 412
            $columns = $this->db->createCommand($sql)->queryAll();
367 16
        } catch (\Exception $e) {
368 16
            $previous = $e->getPrevious();
369 16
            if ($previous instanceof \PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
370
                // table does not exist
371
                // https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
372 16
                return false;
373
            }
374
            throw $e;
375
        }
376
377
378 407
        $jsonColumns = $this->getJsonColumns($table);
379
380 407
        foreach ($columns as $info) {
381 407
            if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_LOWER) {
382 406
                $info = array_change_key_case($info, CASE_LOWER);
383
            }
384
385 407
            if (\in_array($info['field'], $jsonColumns, true)) {
386
                $info['type'] = static::TYPE_JSON;
387
            }
388
389 407
            $column = $this->loadColumnSchema($info);
390 407
            $table->columns[$column->name] = $column;
391 407
            if ($column->isPrimaryKey) {
392 379
                $table->primaryKey[] = $column->name;
393 379
                if ($column->autoIncrement) {
394 251
                    $table->sequenceName = '';
395
                }
396
            }
397
        }
398
399 407
        return true;
400
    }
401
402
    /**
403
     * Gets the CREATE TABLE sql string.
404
     * @param TableSchema $table the table metadata
405
     * @return string $sql the result of 'SHOW CREATE TABLE'
406
     */
407 407
    protected function getCreateTableSql($table)
408
    {
409 407
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $this->quoteTableName($table->fullName))->queryOne();
410 407
        if (isset($row['Create Table'])) {
411 405
            $sql = $row['Create Table'];
412
        } else {
413 6
            $row = array_values($row);
414 6
            $sql = $row[1];
415
        }
416
417 407
        return $sql;
418
    }
419
420
    /**
421
     * Collects the foreign key column details for the given table.
422
     * @param TableSchema $table the table metadata
423
     * @throws \Exception
424
     */
425 407
    protected function findConstraints($table)
426
    {
427 407
        $sql = <<<'SQL'
428
SELECT
429
    `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
430
    `kcu`.`COLUMN_NAME` AS `column_name`,
431
    `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
432
    `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
433
FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
434
JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
435
    (
436
        `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
437
        (`kcu`.`CONSTRAINT_CATALOG` IS NULL AND `rc`.`CONSTRAINT_CATALOG` IS NULL)
438
    ) AND
439
    `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
440
    `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME`
441
WHERE `rc`.`CONSTRAINT_SCHEMA` = database() AND `kcu`.`TABLE_SCHEMA` = database()
442
AND `rc`.`TABLE_NAME` = :tableName AND `kcu`.`TABLE_NAME` = :tableName1
443 407
SQL;
444
445
        try {
446 407
            $rows = $this->db->createCommand($sql, [':tableName' => $table->name, ':tableName1' => $table->name])->queryAll();
447 407
            $constraints = [];
448
449 407
            foreach ($rows as $row) {
450 273
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
451 273
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
452
            }
453
454 407
            $table->foreignKeys = [];
455 407
            foreach ($constraints as $name => $constraint) {
456 273
                $table->foreignKeys[$name] = array_merge(
457 273
                    [$constraint['referenced_table_name']],
458 273
                    $constraint['columns']
459 273
                );
460
            }
461
        } catch (\Exception $e) {
462
            $previous = $e->getPrevious();
463
            if (!$previous instanceof \PDOException || strpos($previous->getMessage(), 'SQLSTATE[42S02') === false) {
464
                throw $e;
465
            }
466
467
            // table does not exist, try to determine the foreign keys using the table creation sql
468
            $sql = $this->getCreateTableSql($table);
469
            $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi';
470
            if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
471
                foreach ($matches as $match) {
472
                    $fks = array_map('trim', explode(',', str_replace(['`', '"'], '', $match[1])));
473
                    $pks = array_map('trim', explode(',', str_replace(['`', '"'], '', $match[3])));
474
                    $constraint = [str_replace(['`', '"'], '', $match[2])];
475
                    foreach ($fks as $k => $name) {
476
                        $constraint[$name] = $pks[$k];
477
                    }
478
                    $table->foreignKeys[md5(serialize($constraint))] = $constraint;
479
                }
480
                $table->foreignKeys = array_values($table->foreignKeys);
481
            }
482
        }
483
    }
484
485
    /**
486
     * Returns all unique indexes for the given table.
487
     *
488
     * Each array element is of the following structure:
489
     *
490
     * ```php
491
     * [
492
     *     'IndexName1' => ['col1' [, ...]],
493
     *     'IndexName2' => ['col2' [, ...]],
494
     * ]
495
     * ```
496
     *
497
     * @param TableSchema $table the table metadata
498
     * @return array all unique indexes for the given table.
499
     */
500 1
    public function findUniqueIndexes($table)
501
    {
502 1
        $sql = $this->getCreateTableSql($table);
503 1
        $uniqueIndexes = [];
504
505 1
        $regexp = '/UNIQUE KEY\s+[`"](.+)[`"]\s*\(([`"].+[`"])+\)/mi';
506 1
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
507 1
            foreach ($matches as $match) {
508 1
                $indexName = $match[1];
509 1
                $indexColumns = array_map('trim', preg_split('/[`"],[`"]/', trim($match[2], '`"')));
510 1
                $uniqueIndexes[$indexName] = $indexColumns;
511
            }
512
        }
513
514 1
        return $uniqueIndexes;
515
    }
516
517
    /**
518
     * {@inheritdoc}
519
     */
520 17
    public function createColumnSchemaBuilder($type, $length = null)
521
    {
522 17
        return Yii::createObject(ColumnSchemaBuilder::className(), [$type, $length, $this->db]);
523
    }
524
525
    /**
526
     * @return bool whether the version of the MySQL being used is older than 5.1.
527
     * @throws InvalidConfigException
528
     * @throws Exception
529
     * @since 2.0.13
530
     */
531
    protected function isOldMysql()
532
    {
533
        if ($this->_oldMysql === null) {
534
            $version = $this->db->getSlavePdo(true)->getAttribute(\PDO::ATTR_SERVER_VERSION);
535
            $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...
536
        }
537
538
        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...
539
    }
540
541
    /**
542
     * Loads multiple types of constraints and returns the specified ones.
543
     * @param string $tableName table name.
544
     * @param string $returnType return type:
545
     * - primaryKey
546
     * - foreignKeys
547
     * - uniques
548
     * @return mixed constraints.
549
     */
550 71
    private function loadTableConstraints($tableName, $returnType)
551
    {
552 71
        static $sql = <<<'SQL'
553
SELECT
554
    `kcu`.`CONSTRAINT_NAME` AS `name`,
555
    `kcu`.`COLUMN_NAME` AS `column_name`,
556
    `tc`.`CONSTRAINT_TYPE` AS `type`,
557
    CASE
558
        WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
559
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
560
    END AS `foreign_table_schema`,
561
    `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
562
    `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
563
    `rc`.`UPDATE_RULE` AS `on_update`,
564
    `rc`.`DELETE_RULE` AS `on_delete`,
565
    `kcu`.`ORDINAL_POSITION` AS `position`
566
FROM
567
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
568
    `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`,
569
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
570
WHERE
571
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName1, DATABASE()) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName
572
    AND `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`TABLE_NAME` = :tableName1 AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
573
    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'
574
UNION
575
SELECT
576
    `kcu`.`CONSTRAINT_NAME` AS `name`,
577
    `kcu`.`COLUMN_NAME` AS `column_name`,
578
    `tc`.`CONSTRAINT_TYPE` AS `type`,
579
    NULL AS `foreign_table_schema`,
580
    NULL AS `foreign_table_name`,
581
    NULL AS `foreign_column_name`,
582
    NULL AS `on_update`,
583
    NULL AS `on_delete`,
584
    `kcu`.`ORDINAL_POSITION` AS `position`
585
FROM
586
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
587
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
588
WHERE
589
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName2, DATABASE()) AND `kcu`.`TABLE_NAME` = :tableName3
590
    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')
591
ORDER BY `position` ASC
592 71
SQL;
593
594 71
        $resolvedName = $this->resolveTableName($tableName);
595 71
        $constraints = $this->db->createCommand($sql, [
596 71
            ':schemaName' => $resolvedName->schemaName,
597 71
            ':schemaName1' => $resolvedName->schemaName,
598 71
            ':schemaName2' => $resolvedName->schemaName,
599 71
            ':tableName' => $resolvedName->name,
600 71
            ':tableName1' => $resolvedName->name,
601 71
            ':tableName2' => $resolvedName->name,
602 71
            ':tableName3' => $resolvedName->name,
603 71
            ':tableName4' => $resolvedName->name
604 71
        ])->queryAll();
605 71
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
606 71
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
607 71
        $result = [
608 71
            'primaryKey' => null,
609 71
            'foreignKeys' => [],
610 71
            'uniques' => [],
611 71
        ];
612 71
        foreach ($constraints as $type => $names) {
613 71
            foreach ($names as $name => $constraint) {
614
                switch ($type) {
615 71
                    case 'PRIMARY KEY':
616 60
                        $result['primaryKey'] = new Constraint([
617 60
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
618 60
                        ]);
619 60
                        break;
620 46
                    case 'FOREIGN KEY':
621 10
                        $result['foreignKeys'][] = new ForeignKeyConstraint([
622 10
                            'name' => $name,
623 10
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
624 10
                            'foreignSchemaName' => $constraint[0]['foreign_table_schema'],
625 10
                            'foreignTableName' => $constraint[0]['foreign_table_name'],
626 10
                            'foreignColumnNames' => ArrayHelper::getColumn($constraint, 'foreign_column_name'),
627 10
                            'onDelete' => $constraint[0]['on_delete'],
628 10
                            'onUpdate' => $constraint[0]['on_update'],
629 10
                        ]);
630 10
                        break;
631 37
                    case 'UNIQUE':
632 37
                        $result['uniques'][] = new Constraint([
633 37
                            'name' => $name,
634 37
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
635 37
                        ]);
636 37
                        break;
637
                }
638
            }
639
        }
640 71
        foreach ($result as $type => $data) {
641 71
            $this->setTableMetadata($tableName, $type, $data);
642
        }
643
644 71
        return $result[$returnType];
645
    }
646
647 407
    private function getJsonColumns(TableSchema $table): array
648
    {
649 407
        $sql = $this->getCreateTableSql($table);
650 407
        $result = [];
651
652 407
        $regexp = '/json_valid\([\`"](.+)[\`"]\s*\)/mi';
653
654 407
        if (\preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
655
            foreach ($matches as $match) {
656
                $result[] = $match[1];
657
            }
658
        }
659
660 407
        return $result;
661
    }
662
}
663