Passed
Pull Request — master (#19758)
by Sohel Ahmed
12:35
created

Schema::isMariaDb()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

215
    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...
216
    {
217 12
        throw new NotSupportedException('MySQL does not support check constraints.');
218
    }
219
220
    /**
221
     * {@inheritdoc}
222
     * @throws NotSupportedException if this method is called.
223
     */
224 12
    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

224
    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...
225
    {
226 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
227
    }
228
229
    /**
230
     * Creates a query builder for the MySQL database.
231
     * @return QueryBuilder query builder instance
232
     */
233 396
    public function createQueryBuilder()
234
    {
235 396
        return Yii::createObject(QueryBuilder::className(), [$this->db]);
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

235
        return Yii::createObject(/** @scrutinizer ignore-deprecated */ QueryBuilder::className(), [$this->db]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
236
    }
237
238
    /**
239
     * Resolves the table name and schema name (if any).
240
     * @param TableSchema $table the table metadata object
241
     * @param string $name the table name
242
     */
243 403
    protected function resolveTableNames($table, $name)
244
    {
245 403
        $parts = explode('.', str_replace('`', '', $name));
246 403
        if (isset($parts[1])) {
247
            $table->schemaName = $parts[0];
248
            $table->name = $parts[1];
249
            $table->fullName = $table->schemaName . '.' . $table->name;
250
        } else {
251 403
            $table->fullName = $table->name = $parts[0];
252
        }
253 403
    }
254
255
    /**
256
     * Loads the column information into a [[ColumnSchema]] object.
257
     * @param array $info column information
258
     * @return ColumnSchema the column schema object
259
     */
260 400
    protected function loadColumnSchema($info)
261
    {
262 400
        $column = $this->createColumnSchema();
263
264 400
        $column->name = $info['field'];
265 400
        $column->allowNull = $info['null'] === 'YES';
266 400
        $column->isPrimaryKey = strpos($info['key'], 'PRI') !== false;
267 400
        $column->autoIncrement = stripos($info['extra'], 'auto_increment') !== false;
268 400
        $column->comment = $info['comment'];
269
270 400
        $column->dbType = $info['type'];
271 400
        $column->unsigned = stripos($column->dbType, 'unsigned') !== false;
272
273 400
        $column->type = self::TYPE_STRING;
274 400
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
275 400
            $type = strtolower($matches[1]);
276 400
            if (isset($this->typeMap[$type])) {
277 400
                $column->type = $this->typeMap[$type];
278
            }
279 400
            if (!empty($matches[2])) {
280 398
                if ($type === 'enum') {
281 28
                    preg_match_all("/'[^']*'/", $matches[2], $values);
282 28
                    foreach ($values[0] as $i => $value) {
283 28
                        $values[$i] = trim($value, "'");
284
                    }
285 28
                    $column->enumValues = $values;
286
                } else {
287 398
                    $values = explode(',', $matches[2]);
288 398
                    $column->size = $column->precision = (int) $values[0];
289 398
                    if (isset($values[1])) {
290 105
                        $column->scale = (int) $values[1];
291
                    }
292 398
                    if ($column->size === 1 && $type === 'bit') {
293 6
                        $column->type = 'boolean';
294 398
                    } elseif ($type === 'bit') {
295 28
                        if ($column->size > 32) {
296
                            $column->type = 'bigint';
297 28
                        } elseif ($column->size === 32) {
298
                            $column->type = 'integer';
299
                        }
300
                    }
301
                }
302
            }
303
        }
304
305 400
        $column->phpType = $this->getColumnPhpType($column);
306
307 400
        if (!$column->isPrimaryKey) {
308 395
            if (isset($info['default'])) {
309 168
                if ($this->defaultIsExpression($info)) {
310 30
                    $column->defaultValue = new Expression($info['default']);
311
                } else {
312 166
                    $column->defaultValue = $column->phpTypecast($info['default']);
313 166
                    if (isset($type) && $type === 'bit') {
314 28
                        $column->defaultValue = bindec(trim(isset($info['default']) ? $info['default'] : '', 'b\''));
315
                    }
316
                }
317
            }
318
        }
319
320 400
        return $column;
321
    }
322
323
    /**
324
     * Collects the metadata of table columns.
325
     * @param TableSchema $table the table metadata
326
     * @return bool whether the table exists in the database
327
     * @throws \Exception if DB query fails
328
     */
329 403
    protected function findColumns($table)
330
    {
331 403
        $this->_tableName = $table->fullName;
332 403
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->quoteTableName($table->fullName);
333
        try {
334 403
            $columns = $this->db->createCommand($sql)->queryAll();
335 15
        } catch (\Exception $e) {
336 15
            $previous = $e->getPrevious();
337 15
            if ($previous instanceof \PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
338
                // table does not exist
339
                // https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
340 15
                return false;
341
            }
342
            throw $e;
343
        }
344 398
        foreach ($columns as $info) {
345 398
            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

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

372
            $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
373
            $sql = $row[1];
374
        }
375
376 1
        return $sql;
377
    }
378
379
    /**
380
     * Collects the foreign key column details for the given table.
381
     * @param TableSchema $table the table metadata
382
     * @throws \Exception
383
     */
384 398
    protected function findConstraints($table)
385
    {
386
        $sql = <<<'SQL'
387 398
SELECT
388
    `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
389
    `kcu`.`COLUMN_NAME` AS `column_name`,
390
    `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
391
    `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
392
FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
393
JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
394
    (
395
        `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
396
        (`kcu`.`CONSTRAINT_CATALOG` IS NULL AND `rc`.`CONSTRAINT_CATALOG` IS NULL)
397
    ) AND
398
    `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
399
    `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME`
400
WHERE `rc`.`CONSTRAINT_SCHEMA` = database() AND `kcu`.`TABLE_SCHEMA` = database()
401
AND `rc`.`TABLE_NAME` = :tableName AND `kcu`.`TABLE_NAME` = :tableName1
402
SQL;
403
404
        try {
405 398
            $rows = $this->db->createCommand($sql, [':tableName' => $table->name, ':tableName1' => $table->name])->queryAll();
406 398
            $constraints = [];
407
408 398
            foreach ($rows as $row) {
409 271
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
410 271
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
411
            }
412
413 398
            $table->foreignKeys = [];
414 398
            foreach ($constraints as $name => $constraint) {
415 271
                $table->foreignKeys[$name] = array_merge(
416 271
                    [$constraint['referenced_table_name']],
417 271
                    $constraint['columns']
418
                );
419
            }
420
        } catch (\Exception $e) {
421
            $previous = $e->getPrevious();
422
            if (!$previous instanceof \PDOException || strpos($previous->getMessage(), 'SQLSTATE[42S02') === false) {
423
                throw $e;
424
            }
425
426
            // table does not exist, try to determine the foreign keys using the table creation sql
427
            $sql = $this->getCreateTableSql($table);
428
            $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi';
429
            if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
430
                foreach ($matches as $match) {
431
                    $fks = array_map('trim', explode(',', str_replace(['`', '"'], '', $match[1])));
432
                    $pks = array_map('trim', explode(',', str_replace(['`', '"'], '', $match[3])));
433
                    $constraint = [str_replace(['`', '"'], '', $match[2])];
434
                    foreach ($fks as $k => $name) {
435
                        $constraint[$name] = $pks[$k];
436
                    }
437
                    $table->foreignKeys[md5(serialize($constraint))] = $constraint;
438
                }
439
                $table->foreignKeys = array_values($table->foreignKeys);
440
            }
441
        }
442 398
    }
443
444
    /**
445
     * Returns all unique indexes for the given table.
446
     *
447
     * Each array element is of the following structure:
448
     *
449
     * ```php
450
     * [
451
     *     'IndexName1' => ['col1' [, ...]],
452
     *     'IndexName2' => ['col2' [, ...]],
453
     * ]
454
     * ```
455
     *
456
     * @param TableSchema $table the table metadata
457
     * @return array all unique indexes for the given table.
458
     */
459 1
    public function findUniqueIndexes($table)
460
    {
461 1
        $sql = $this->getCreateTableSql($table);
462 1
        $uniqueIndexes = [];
463
464 1
        $regexp = '/UNIQUE KEY\s+[`"](.+)[`"]\s*\(([`"].+[`"])+\)/mi';
465 1
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
466 1
            foreach ($matches as $match) {
467 1
                $indexName = $match[1];
468 1
                $indexColumns = array_map('trim', preg_split('/[`"],[`"]/', trim($match[2], '`"')));
469 1
                $uniqueIndexes[$indexName] = $indexColumns;
470
            }
471
        }
472
473 1
        return $uniqueIndexes;
474
    }
475
476
    /**
477
     * {@inheritdoc}
478
     */
479 17
    public function createColumnSchemaBuilder($type, $length = null)
480
    {
481 17
        return Yii::createObject(ColumnSchemaBuilder::className(), [$type, $length, $this->db]);
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

481
        return Yii::createObject(/** @scrutinizer ignore-deprecated */ ColumnSchemaBuilder::className(), [$type, $length, $this->db]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
482
    }
483
484
    /**
485
     * @return bool whether the version of the MySQL being used is older than 5.1.
486
     * @throws InvalidConfigException
487
     * @throws Exception
488
     * @since 2.0.13
489
     */
490
    protected function isOldMysql()
491
    {
492
        if ($this->_oldMysql === null) {
493
            $version = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
494
            $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...
495
        }
496
497
        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...
498
    }
499
500
    /**
501
     * Loads multiple types of constraints and returns the specified ones.
502
     * @param string $tableName table name.
503
     * @param string $returnType return type:
504
     * - primaryKey
505
     * - foreignKeys
506
     * - uniques
507
     * @return mixed constraints.
508
     */
509 71
    private function loadTableConstraints($tableName, $returnType)
510
    {
511 71
        static $sql = <<<'SQL'
512
SELECT
513
    `kcu`.`CONSTRAINT_NAME` AS `name`,
514
    `kcu`.`COLUMN_NAME` AS `column_name`,
515
    `tc`.`CONSTRAINT_TYPE` AS `type`,
516
    CASE
517
        WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
518
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
519
    END AS `foreign_table_schema`,
520
    `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
521
    `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
522
    `rc`.`UPDATE_RULE` AS `on_update`,
523
    `rc`.`DELETE_RULE` AS `on_delete`,
524
    `kcu`.`ORDINAL_POSITION` AS `position`
525
FROM
526
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
527
    `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`,
528
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
529
WHERE
530
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName1, DATABASE()) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName
531
    AND `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`TABLE_NAME` = :tableName1 AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
532
    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'
533
UNION
534
SELECT
535
    `kcu`.`CONSTRAINT_NAME` AS `name`,
536
    `kcu`.`COLUMN_NAME` AS `column_name`,
537
    `tc`.`CONSTRAINT_TYPE` AS `type`,
538
    NULL AS `foreign_table_schema`,
539
    NULL AS `foreign_table_name`,
540
    NULL AS `foreign_column_name`,
541
    NULL AS `on_update`,
542
    NULL AS `on_delete`,
543
    `kcu`.`ORDINAL_POSITION` AS `position`
544
FROM
545
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
546
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
547
WHERE
548
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName2, DATABASE()) AND `kcu`.`TABLE_NAME` = :tableName3
549
    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')
550
ORDER BY `position` ASC
551
SQL;
552
553 71
        $resolvedName = $this->resolveTableName($tableName);
554 71
        $constraints = $this->db->createCommand($sql, [
555 71
            ':schemaName' => $resolvedName->schemaName,
556 71
            ':schemaName1' => $resolvedName->schemaName,
557 71
            ':schemaName2' => $resolvedName->schemaName,
558 71
            ':tableName' => $resolvedName->name,
559 71
            ':tableName1' => $resolvedName->name,
560 71
            ':tableName2' => $resolvedName->name,
561 71
            ':tableName3' => $resolvedName->name,
562 71
            ':tableName4' => $resolvedName->name
563 71
        ])->queryAll();
564 71
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
565 71
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
566
        $result = [
567 71
            'primaryKey' => null,
568
            'foreignKeys' => [],
569
            'uniques' => [],
570
        ];
571 71
        foreach ($constraints as $type => $names) {
572 71
            foreach ($names as $name => $constraint) {
573 71
                switch ($type) {
574 71
                    case 'PRIMARY KEY':
575 60
                        $result['primaryKey'] = new Constraint([
576 60
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
577
                        ]);
578 60
                        break;
579 46
                    case 'FOREIGN KEY':
580 10
                        $result['foreignKeys'][] = new ForeignKeyConstraint([
581 10
                            'name' => $name,
582 10
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
583 10
                            'foreignSchemaName' => $constraint[0]['foreign_table_schema'],
584 10
                            'foreignTableName' => $constraint[0]['foreign_table_name'],
585 10
                            'foreignColumnNames' => ArrayHelper::getColumn($constraint, 'foreign_column_name'),
586 10
                            'onDelete' => $constraint[0]['on_delete'],
587 10
                            'onUpdate' => $constraint[0]['on_update'],
588
                        ]);
589 10
                        break;
590 37
                    case 'UNIQUE':
591 37
                        $result['uniques'][] = new Constraint([
592 37
                            'name' => $name,
593 37
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
594
                        ]);
595 37
                        break;
596
                }
597
            }
598
        }
599 71
        foreach ($result as $type => $data) {
600 71
            $this->setTableMetadata($tableName, $type, $data);
601
        }
602
603 71
        return $result[$returnType];
604
    }
605
606
    /**
607
     * Detect if a column has a default value in form of expression
608
     * @param $extra string 'Extra' detail obtained from "SHOW FULL COLUMNS ...". Example: 'DEFAULT_GENERATED'
609
     * @return bool true if the column has default value in form of expression instead of constant
610
     * @see https://github.com/yiisoft/yii2/issues/19747
611
     * @see https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html
612
     * @since 2.0.48
613
     */
614 168
    public function defaultIsExpression($info)
615
    {
616 168
        if ($this->isMysql()) {
617
            // https://dev.mysql.com/doc/refman/5.7/en/information-schema-columns-table.html and
618
            // https://dev.mysql.com/doc/refman/8.0/en/information-schema-columns-table.html
619
            return (
620
                // for MySQL >= 8
621 168
                (strpos($info['extra'], static::DEFAULT_EXPRESSION_IDENTIFIER) !== false) ||
622 168
                (strpos($info['extra'], static::CURRENT_TIMESTAMP_DEFAULT_EXPRESSION_IDENTIFIER) !== false) ||
623
                // for MySQL < 8
624 168
                (strpos($info['default'], static::CURRENT_TIMESTAMP_DEFAULT_EXPRESSION_IDENTIFIER) !== false &&
625 168
                    ((strpos($info['type'], 'datetime') !== false) || (strpos($info['type'], 'timestamp') !== false))
626
                )
627
            );
628
        } else { // MariaDB
629
            // There is no strong way in MariaDB to detect default value is constant or expression. This is implemented on the basis pattern observed from data in Information_schema.Extra
630
            $moreInfo = $this->moreColumnInfo($info['field']);
631
            $default = $moreInfo['COLUMN_DEFAULT'];
632
            $isNullable = $moreInfo['IS_NULLABLE'];
633
634
            if (empty($default)) {
635
                return false;
636
            }
637
638
            if ($isNullable === 'YES' && $default === 'NULL') {
639
                return false;
640
            }
641
642
            if (is_numeric($default)) {
643
                return false;
644
            } elseif(is_string($default) &&
645
                     $default[0] === "'" &&
646
                     $default[strlen($default) - 1] === "'"
647
            ) {
648
                return false;
649
            } elseif (is_string($default)) { // if the default value is string and not quoted and not 'null', it is expression
650
                return true;
651
            }
652
            return false;
653
        }
654
    }
655
656
    /**
657
     * @return bool
658
     * @since 2.0.48
659
     * Adopted from https://github.com/cebe/yii2-openapi
660
     */
661 168
    public function isMysql()
662
    {
663 168
        return ($this->db->schema instanceof static && !$this->isMariaDb());
664
    }
665
666
    /**
667
     * @return bool
668
     * @since 2.0.48
669
     * Adopted from https://github.com/cebe/yii2-openapi
670
     */
671 168
    public function isMariaDb()
672
    {
673 168
        return strpos($this->db->schema->getServerVersion(), 'MariaDB') !== false;
674
    }
675
676
    /**
677
     * SQL to get more info of a table column
678
     * @param string $tableName
679
     * @param string $columnName
680
     * @return string the SQL
681
     */
682
    public static function moreColumnInfoSql($tableName, $columnName)
683
    {
684
        return <<<SQL
685
            SELECT `COLUMN_DEFAULT`, `IS_NULLABLE`
686
              FROM `information_schema`.`COLUMNS`
687
              WHERE `table_name` = "$tableName"
688
              AND `column_name` = "$columnName"
689
SQL;
690
    }
691
692
    /**
693
     * @param string $columnName
694
     * @return array. Example:
0 ignored issues
show
Documentation Bug introduced by
The doc comment array. at position 0 could not be parsed: Unknown type name 'array.' at position 0 in array..
Loading history...
695
     * ```
696
     * [
697
     *      'COLUMN_DEFAULT' => CURRENT_TIMESTAMP
698
     *      'IS_NULLABLE' => Yes
699
     * ]
700
     * ```
701
     */
702
    public function moreColumnInfo($columnName)
703
    {
704
        return $this->db->createCommand(static::moreColumnInfoSql($this->_tableName, $columnName))->queryOne();
705
    }
706
}
707