Schema::loadTableSchema()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 11
ccs 0
cts 7
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\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
    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
     */
205
    protected function loadTableChecks($tableName)
206
    {
207
        $version = $this->db->getServerVersion();
208
209
        // check version MySQL >= 8.0.16
210
        if (\stripos($version, 'MariaDb') === false && \version_compare($version, '8.0.16', '<')) {
211
            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
    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

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
        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
    public function createQueryBuilder()
260
    {
261
        return Yii::createObject(QueryBuilder::class, [$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
    protected function resolveTableNames($table, $name)
270
    {
271
        $parts = explode('.', str_replace('`', '', $name));
272
        if (isset($parts[1])) {
273
            $table->schemaName = $parts[0];
274
            $table->name = $parts[1];
275
            $table->fullName = $table->schemaName . '.' . $table->name;
276
        } else {
277
            $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
    protected function loadColumnSchema($info)
287
    {
288
        $column = $this->createColumnSchema();
289
290
        $column->name = $info['field'];
291
        $column->allowNull = $info['null'] === 'YES';
292
        $column->isPrimaryKey = strpos($info['key'], 'PRI') !== false;
293
        $column->autoIncrement = stripos($info['extra'], 'auto_increment') !== false;
294
        $column->comment = $info['comment'];
295
296
        $column->dbType = $info['type'];
297
        $column->unsigned = stripos($column->dbType, 'unsigned') !== false;
298
299
        $column->type = self::TYPE_STRING;
300
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
301
            $type = strtolower($matches[1]);
302
            if (isset($this->typeMap[$type])) {
303
                $column->type = $this->typeMap[$type];
304
            }
305
            if (!empty($matches[2])) {
306
                if ($type === 'enum') {
307
                    preg_match_all("/'[^']*'/", $matches[2], $values);
308
                    foreach ($values[0] as $i => $value) {
309
                        $values[$i] = trim($value, "'");
310
                    }
311
                    $column->enumValues = $values;
312
                } else {
313
                    $values = explode(',', $matches[2]);
314
                    $column->size = $column->precision = (int) $values[0];
315
                    if (isset($values[1])) {
316
                        $column->scale = (int) $values[1];
317
                    }
318
                    if ($column->size === 1 && $type === 'bit') {
319
                        $column->type = 'boolean';
320
                    } elseif ($type === 'bit') {
321
                        if ($column->size > 32) {
322
                            $column->type = 'bigint';
323
                        } elseif ($column->size === 32) {
324
                            $column->type = 'integer';
325
                        }
326
                    }
327
                }
328
            }
329
        }
330
331
        $column->phpType = $this->getColumnPhpType($column);
332
333
        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
                in_array($column->type, ['timestamp', 'datetime', 'date', 'time'])
342
                && isset($info['default'])
343
                && preg_match('/^current_timestamp(?:\(([0-9]*)\))?$/i', $info['default'], $matches)
344
            ) {
345
                $column->defaultValue = new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1]) ? '(' . $matches[1] . ')' : ''));
346
            } elseif (isset($type) && $type === 'bit') {
347
                $column->defaultValue = bindec(trim(isset($info['default']) ? $info['default'] : '', 'b\''));
348
            } else {
349
                $column->defaultValue = $column->phpTypecast($info['default']);
350
            }
351
        }
352
353
        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
    protected function findColumns($table)
363
    {
364
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName($table->fullName);
365
        try {
366
            $columns = $this->db->createCommand($sql)->queryAll();
367
        } catch (\Exception $e) {
368
            $previous = $e->getPrevious();
369
            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
                return false;
373
            }
374
            throw $e;
375
        }
376
377
378
        $jsonColumns = $this->getJsonColumns($table);
379
380
        foreach ($columns as $info) {
381
            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

381
            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...
382
                $info = array_change_key_case($info, CASE_LOWER);
383
            }
384
385
            if (\in_array($info['field'], $jsonColumns, true)) {
386
                $info['type'] = static::TYPE_JSON;
387
            }
388
389
            $column = $this->loadColumnSchema($info);
390
            $table->columns[$column->name] = $column;
391
            if ($column->isPrimaryKey) {
392
                $table->primaryKey[] = $column->name;
393
                if ($column->autoIncrement) {
394
                    $table->sequenceName = '';
395
                }
396
            }
397
        }
398
399
        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
    protected function getCreateTableSql($table)
408
    {
409
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $this->quoteTableName($table->fullName))->queryOne();
410
        if (isset($row['Create Table'])) {
411
            $sql = $row['Create Table'];
412
        } else {
413
            $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

413
            $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
414
            $sql = $row[1];
415
        }
416
417
        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
    protected function findConstraints($table)
426
    {
427
        $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
SQL;
444
445
        try {
446
            $rows = $this->db->createCommand($sql, [':tableName' => $table->name, ':tableName1' => $table->name])->queryAll();
447
            $constraints = [];
448
449
            foreach ($rows as $row) {
450
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
451
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
452
            }
453
454
            $table->foreignKeys = [];
455
            foreach ($constraints as $name => $constraint) {
456
                $table->foreignKeys[$name] = array_merge(
457
                    [$constraint['referenced_table_name']],
458
                    $constraint['columns']
459
                );
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
    public function findUniqueIndexes($table)
501
    {
502
        $sql = $this->getCreateTableSql($table);
503
        $uniqueIndexes = [];
504
505
        $regexp = '/UNIQUE KEY\s+[`"](.+)[`"]\s*\(([`"].+[`"])+\)/mi';
506
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
507
            foreach ($matches as $match) {
508
                $indexName = $match[1];
509
                $indexColumns = array_map('trim', preg_split('/[`"],[`"]/', trim($match[2], '`"')));
510
                $uniqueIndexes[$indexName] = $indexColumns;
511
            }
512
        }
513
514
        return $uniqueIndexes;
515
    }
516
517
    /**
518
     * {@inheritdoc}
519
     */
520
    public function createColumnSchemaBuilder($type, $length = null)
521
    {
522
        return Yii::createObject(ColumnSchemaBuilder::class, [$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
    private function loadTableConstraints($tableName, $returnType)
551
    {
552
        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
SQL;
593
594
        $resolvedName = $this->resolveTableName($tableName);
595
        $constraints = $this->db->createCommand($sql, [
596
            ':schemaName' => $resolvedName->schemaName,
597
            ':schemaName1' => $resolvedName->schemaName,
598
            ':schemaName2' => $resolvedName->schemaName,
599
            ':tableName' => $resolvedName->name,
600
            ':tableName1' => $resolvedName->name,
601
            ':tableName2' => $resolvedName->name,
602
            ':tableName3' => $resolvedName->name,
603
            ':tableName4' => $resolvedName->name
604
        ])->queryAll();
605
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
606
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
607
        $result = [
608
            'primaryKey' => null,
609
            'foreignKeys' => [],
610
            'uniques' => [],
611
        ];
612
        foreach ($constraints as $type => $names) {
613
            foreach ($names as $name => $constraint) {
614
                switch ($type) {
615
                    case 'PRIMARY KEY':
616
                        $result['primaryKey'] = new Constraint([
617
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
618
                        ]);
619
                        break;
620
                    case 'FOREIGN KEY':
621
                        $result['foreignKeys'][] = new ForeignKeyConstraint([
622
                            'name' => $name,
623
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
624
                            'foreignSchemaName' => $constraint[0]['foreign_table_schema'],
625
                            'foreignTableName' => $constraint[0]['foreign_table_name'],
626
                            'foreignColumnNames' => ArrayHelper::getColumn($constraint, 'foreign_column_name'),
627
                            'onDelete' => $constraint[0]['on_delete'],
628
                            'onUpdate' => $constraint[0]['on_update'],
629
                        ]);
630
                        break;
631
                    case 'UNIQUE':
632
                        $result['uniques'][] = new Constraint([
633
                            'name' => $name,
634
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
635
                        ]);
636
                        break;
637
                }
638
            }
639
        }
640
        foreach ($result as $type => $data) {
641
            $this->setTableMetadata($tableName, $type, $data);
642
        }
643
644
        return $result[$returnType];
645
    }
646
647
    private function getJsonColumns(TableSchema $table): array
648
    {
649
        $sql = $this->getCreateTableSql($table);
650
        $result = [];
651
652
        $regexp = '/json_valid\([\`"](.+)[\`"]\s*\)/mi';
653
654
        if (\preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
655
            foreach ($matches as $match) {
656
                $result[] = $match[1];
657
            }
658
        }
659
660
        return $result;
661
    }
662
}
663