Passed
Pull Request — master (#19881)
by Wilmer
16:57 queued 09:01
created

Schema::loadTableChecks()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 47
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 31.6277

Importance

Changes 0
Metric Value
cc 6
eloc 26
nc 6
nop 1
dl 0
loc 47
ccs 3
cts 28
cp 0.1071
crap 31.6277
rs 8.8817
c 0
b 0
f 0
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 409
    protected function loadTableSchema($name)
131
    {
132 409
        $table = new TableSchema();
133 409
        $this->resolveTableNames($table, $name);
134
135 409
        if ($this->findColumns($table)) {
136 404
            $this->findConstraints($table);
137 404
            return $table;
138
        }
139
140 15
        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
        // check version MySQL >= 8.0.16
209 12
        if (version_compare($this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION), '8.0.16', '<')) {
210 12
            throw new NotSupportedException('MySQL < 8.0.16 does not support check constraints.');
211
        }
212
213
        $checks = [];
214
215
        $sql = <<<SQL
216
        SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_NAME = :tableName
217
        SQL;
218
219
        $resolvedName = $this->resolveTableName($tableName);
220
        $tableRows = $this->db->createCommand($sql, [':tableName' => $resolvedName->name])->queryAll();
221
222
        if ($tableRows === []) {
223
            return $checks;
224
        }
225
226
        foreach ($tableRows as $tableRow) {
227
            $sql = <<<SQL
228
            SELECT * FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS WHERE CONSTRAINT_NAME = :constraintName
229
            SQL;
230
231
            $checkRows = $this->db->createCommand($sql, [':constraintName' => $tableRow['CONSTRAINT_NAME']])->queryAll();
232
233
            foreach ($checkRows as $checkRow) {
234
                $matches = [];
235
                $columnName = null;
236
237
                if (preg_match('/\(`?([a-zA-Z0-9_]+)`?\s*[><=]/', $checkRow['CHECK_CLAUSE'], $matches)) {
238
                    $columnName = $matches[1];
239
                }
240
241
                $check = new CheckConstraint(
242
                    [
243
                        'name' => $checkRow['CONSTRAINT_NAME'],
244
                        'columnNames' => $columnName,
245
                        'expression' => $checkRow['CHECK_CLAUSE'],
246
                    ]
247
                );
248
                $checks[] = $check;
249
            }
250
        }
251
252
        return $checks;
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     * @throws NotSupportedException if this method is called.
258
     */
259 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

259
    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...
260
    {
261 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
262
    }
263
264
    /**
265
     * Creates a query builder for the MySQL database.
266
     * @return QueryBuilder query builder instance
267
     */
268 403
    public function createQueryBuilder()
269
    {
270 403
        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

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

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

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

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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