Passed
Pull Request — master (#20174)
by Wilmer
10:39
created

Schema   F

Complexity

Total Complexity 78

Size/Duplication

Total Lines 631
Duplicated Lines 0 %

Test Coverage

Coverage 80.4%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 340
dl 0
loc 631
ccs 201
cts 250
cp 0.804
rs 2.16
c 1
b 0
f 0
wmc 78

20 Methods

Rating   Name   Duplication   Size   Complexity  
A loadTableUniques() 0 3 1
A loadTableIndexes() 0 31 3
A loadTablePrimaryKey() 0 3 1
A resolveTableName() 0 13 3
A loadTableForeignKeys() 0 3 1
A loadTableSchema() 0 11 2
A findTableNames() 0 8 2
A loadTableChecks() 0 39 5
B findColumns() 0 38 9
A createColumnSchemaBuilder() 0 3 1
A getJsonColumns() 0 14 3
A isOldMysql() 0 8 2
A resolveTableNames() 0 9 2
B loadTableConstraints() 0 95 7
A createQueryBuilder() 0 3 1
D loadColumnSchema() 0 68 20
A loadTableDefaultValues() 0 3 1
B findConstraints() 0 56 9
A findUniqueIndexes() 0 15 3
A getCreateTableSql() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

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

251
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ $tableName)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
252
    {
253 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
254
    }
255
256
    /**
257
     * Creates a query builder for the MySQL database.
258
     * @return QueryBuilder query builder instance
259
     */
260 406
    public function createQueryBuilder()
261
    {
262 406
        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

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

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

382
            if ($this->db->slavePdo->/** @scrutinizer ignore-call */ getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_LOWER) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
383 406
                $info = array_change_key_case($info, CASE_LOWER);
384
            }
385
386 407
            if (\in_array($info['field'], $jsonColumns, true)) {
387
                $info['type'] = static::TYPE_JSON;
388
            }
389
390 407
            $column = $this->loadColumnSchema($info);
391 407
            $table->columns[$column->name] = $column;
392 407
            if ($column->isPrimaryKey) {
393 379
                $table->primaryKey[] = $column->name;
394 379
                if ($column->autoIncrement) {
395 251
                    $table->sequenceName = '';
396
                }
397
            }
398
        }
399
400 407
        return true;
401
    }
402
403
    /**
404
     * Gets the CREATE TABLE sql string.
405
     * @param TableSchema $table the table metadata
406
     * @return string $sql the result of 'SHOW CREATE TABLE'
407
     */
408 407
    protected function getCreateTableSql($table)
409
    {
410 407
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $this->quoteTableName($table->fullName))->queryOne();
411 407
        if (isset($row['Create Table'])) {
412 405
            $sql = $row['Create Table'];
413
        } else {
414 6
            $row = array_values($row);
0 ignored issues
show
Bug introduced by
$row of type false is incompatible with the type array expected by parameter $array of array_values(). ( Ignorable by Annotation )

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

414
            $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
415 6
            $sql = $row[1];
416
        }
417
418 407
        return $sql;
419
    }
420
421
    /**
422
     * Collects the foreign key column details for the given table.
423
     * @param TableSchema $table the table metadata
424
     * @throws \Exception
425
     */
426 407
    protected function findConstraints($table)
427
    {
428 407
        $sql = <<<'SQL'
429
SELECT
430
    `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
431
    `kcu`.`COLUMN_NAME` AS `column_name`,
432
    `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
433
    `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
434
FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
435
JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
436
    (
437
        `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
438
        (`kcu`.`CONSTRAINT_CATALOG` IS NULL AND `rc`.`CONSTRAINT_CATALOG` IS NULL)
439
    ) AND
440
    `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
441
    `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME`
442
WHERE `rc`.`CONSTRAINT_SCHEMA` = database() AND `kcu`.`TABLE_SCHEMA` = database()
443
AND `rc`.`TABLE_NAME` = :tableName AND `kcu`.`TABLE_NAME` = :tableName1
444 407
SQL;
445
446
        try {
447 407
            $rows = $this->db->createCommand($sql, [':tableName' => $table->name, ':tableName1' => $table->name])->queryAll();
448 407
            $constraints = [];
449
450 407
            foreach ($rows as $row) {
451 273
                $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
452 273
                $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
453
            }
454
455 407
            $table->foreignKeys = [];
456 407
            foreach ($constraints as $name => $constraint) {
457 273
                $table->foreignKeys[$name] = array_merge(
458 273
                    [$constraint['referenced_table_name']],
459 273
                    $constraint['columns']
460 273
                );
461
            }
462
        } catch (\Exception $e) {
463
            $previous = $e->getPrevious();
464
            if (!$previous instanceof \PDOException || strpos($previous->getMessage(), 'SQLSTATE[42S02') === false) {
465
                throw $e;
466
            }
467
468
            // table does not exist, try to determine the foreign keys using the table creation sql
469
            $sql = $this->getCreateTableSql($table);
470
            $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi';
471
            if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
472
                foreach ($matches as $match) {
473
                    $fks = array_map('trim', explode(',', str_replace(['`', '"'], '', $match[1])));
474
                    $pks = array_map('trim', explode(',', str_replace(['`', '"'], '', $match[3])));
475
                    $constraint = [str_replace(['`', '"'], '', $match[2])];
476
                    foreach ($fks as $k => $name) {
477
                        $constraint[$name] = $pks[$k];
478
                    }
479
                    $table->foreignKeys[md5(serialize($constraint))] = $constraint;
480
                }
481
                $table->foreignKeys = array_values($table->foreignKeys);
482
            }
483
        }
484
    }
485
486
    /**
487
     * Returns all unique indexes for the given table.
488
     *
489
     * Each array element is of the following structure:
490
     *
491
     * ```php
492
     * [
493
     *     'IndexName1' => ['col1' [, ...]],
494
     *     'IndexName2' => ['col2' [, ...]],
495
     * ]
496
     * ```
497
     *
498
     * @param TableSchema $table the table metadata
499
     * @return array all unique indexes for the given table.
500
     */
501 1
    public function findUniqueIndexes($table)
502
    {
503 1
        $sql = $this->getCreateTableSql($table);
504 1
        $uniqueIndexes = [];
505
506 1
        $regexp = '/UNIQUE KEY\s+[`"](.+)[`"]\s*\(([`"].+[`"])+\)/mi';
507 1
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
508 1
            foreach ($matches as $match) {
509 1
                $indexName = $match[1];
510 1
                $indexColumns = array_map('trim', preg_split('/[`"],[`"]/', trim($match[2], '`"')));
511 1
                $uniqueIndexes[$indexName] = $indexColumns;
512
            }
513
        }
514
515 1
        return $uniqueIndexes;
516
    }
517
518
    /**
519
     * {@inheritdoc}
520
     */
521 17
    public function createColumnSchemaBuilder($type, $length = null)
522
    {
523 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

523
        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...
524
    }
525
526
    /**
527
     * @return bool whether the version of the MySQL being used is older than 5.1.
528
     * @throws InvalidConfigException
529
     * @throws Exception
530
     * @since 2.0.13
531
     */
532
    protected function isOldMysql()
533
    {
534
        if ($this->_oldMysql === null) {
535
            $version = $this->db->getSlavePdo(true)->getAttribute(\PDO::ATTR_SERVER_VERSION);
536
            $this->_oldMysql = version_compare($version, '5.1', '<=');
0 ignored issues
show
Documentation Bug introduced by
It seems like version_compare($version, '5.1', '<=') can also be of type integer. However, the property $_oldMysql is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
537
        }
538
539
        return $this->_oldMysql;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_oldMysql also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
540
    }
541
542
    /**
543
     * Loads multiple types of constraints and returns the specified ones.
544
     * @param string $tableName table name.
545
     * @param string $returnType return type:
546
     * - primaryKey
547
     * - foreignKeys
548
     * - uniques
549
     * @return mixed constraints.
550
     */
551 71
    private function loadTableConstraints($tableName, $returnType)
552
    {
553 71
        static $sql = <<<'SQL'
554
SELECT
555
    `kcu`.`CONSTRAINT_NAME` AS `name`,
556
    `kcu`.`COLUMN_NAME` AS `column_name`,
557
    `tc`.`CONSTRAINT_TYPE` AS `type`,
558
    CASE
559
        WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
560
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
561
    END AS `foreign_table_schema`,
562
    `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
563
    `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
564
    `rc`.`UPDATE_RULE` AS `on_update`,
565
    `rc`.`DELETE_RULE` AS `on_delete`,
566
    `kcu`.`ORDINAL_POSITION` AS `position`
567
FROM
568
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
569
    `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`,
570
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
571
WHERE
572
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName1, DATABASE()) AND `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `kcu`.`TABLE_NAME` = :tableName
573
    AND `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `rc`.`TABLE_NAME` = :tableName1 AND `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
574
    AND `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`TABLE_NAME` = :tableName2 AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY'
575
UNION
576
SELECT
577
    `kcu`.`CONSTRAINT_NAME` AS `name`,
578
    `kcu`.`COLUMN_NAME` AS `column_name`,
579
    `tc`.`CONSTRAINT_TYPE` AS `type`,
580
    NULL AS `foreign_table_schema`,
581
    NULL AS `foreign_table_name`,
582
    NULL AS `foreign_column_name`,
583
    NULL AS `on_update`,
584
    NULL AS `on_delete`,
585
    `kcu`.`ORDINAL_POSITION` AS `position`
586
FROM
587
    `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`,
588
    `information_schema`.`TABLE_CONSTRAINTS` AS `tc`
589
WHERE
590
    `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName2, DATABASE()) AND `kcu`.`TABLE_NAME` = :tableName3
591
    AND `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND `tc`.`TABLE_NAME` = :tableName4 AND `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE')
592
ORDER BY `position` ASC
593 71
SQL;
594
595 71
        $resolvedName = $this->resolveTableName($tableName);
596 71
        $constraints = $this->db->createCommand($sql, [
597 71
            ':schemaName' => $resolvedName->schemaName,
598 71
            ':schemaName1' => $resolvedName->schemaName,
599 71
            ':schemaName2' => $resolvedName->schemaName,
600 71
            ':tableName' => $resolvedName->name,
601 71
            ':tableName1' => $resolvedName->name,
602 71
            ':tableName2' => $resolvedName->name,
603 71
            ':tableName3' => $resolvedName->name,
604 71
            ':tableName4' => $resolvedName->name
605 71
        ])->queryAll();
606 71
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
607 71
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
608 71
        $result = [
609 71
            'primaryKey' => null,
610 71
            'foreignKeys' => [],
611 71
            'uniques' => [],
612 71
        ];
613 71
        foreach ($constraints as $type => $names) {
614 71
            foreach ($names as $name => $constraint) {
615
                switch ($type) {
616 71
                    case 'PRIMARY KEY':
617 60
                        $result['primaryKey'] = new Constraint([
618 60
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
619 60
                        ]);
620 60
                        break;
621 46
                    case 'FOREIGN KEY':
622 10
                        $result['foreignKeys'][] = new ForeignKeyConstraint([
623 10
                            'name' => $name,
624 10
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
625 10
                            'foreignSchemaName' => $constraint[0]['foreign_table_schema'],
626 10
                            'foreignTableName' => $constraint[0]['foreign_table_name'],
627 10
                            'foreignColumnNames' => ArrayHelper::getColumn($constraint, 'foreign_column_name'),
628 10
                            'onDelete' => $constraint[0]['on_delete'],
629 10
                            'onUpdate' => $constraint[0]['on_update'],
630 10
                        ]);
631 10
                        break;
632 37
                    case 'UNIQUE':
633 37
                        $result['uniques'][] = new Constraint([
634 37
                            'name' => $name,
635 37
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
636 37
                        ]);
637 37
                        break;
638
                }
639
            }
640
        }
641 71
        foreach ($result as $type => $data) {
642 71
            $this->setTableMetadata($tableName, $type, $data);
643
        }
644
645 71
        return $result[$returnType];
646
    }
647
648 407
    private function getJsonColumns(TableSchema $table): array
649
    {
650 407
        $sql = $this->getCreateTableSql($table);
651 407
        $result = [];
652
653 407
        $regexp = '/json_valid\([\`"](.+)[\`"]\s*\)/mi';
654
655 407
        if (\preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
656
            foreach ($matches as $match) {
657
                $result[] = $match[1];
658
            }
659
        }
660
661 407
        return $result;
662
    }
663
}
664