Passed
Pull Request — master (#19758)
by Alexander
37:15 queued 29:05
created

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

224
    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...
225
    {
226 12
        throw new NotSupportedException('MySQL does not support check constraints.');
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     * @throws NotSupportedException if this method is called.
232
     */
233 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

233
    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...
234
    {
235 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
236
    }
237
238
    /**
239
     * Creates a query builder for the MySQL database.
240
     * @return QueryBuilder query builder instance
241
     */
242 396
    public function createQueryBuilder()
243
    {
244 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

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

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

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

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