Passed
Push — master ( c6145c...5ebc17 )
by Alexander
10:53
created

Schema::getJsonColumns()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.1406

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 2
nop 1
dl 0
loc 14
ccs 6
cts 8
cp 0.75
crap 3.1406
rs 10
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 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
        // 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 cc.CONSTRAINT_NAME as constraint_name, cc.CHECK_CLAUSE as check_clause
217
        FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
218
        JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc
219
        ON tc.CONSTRAINT_NAME = cc.CONSTRAINT_NAME
220
        WHERE tc.TABLE_NAME = :tableName AND tc.CONSTRAINT_TYPE = 'CHECK';
221
        SQL;
222
223
        $resolvedName = $this->resolveTableName($tableName);
224
        $tableRows = $this->db->createCommand($sql, [':tableName' => $resolvedName->name])->queryAll();
225
226
        if ($tableRows === []) {
227
            return $checks;
228
        }
229
230
        $tableRows = $this->normalizePdoRowKeyCase($tableRows, true);
231
232
        foreach ($tableRows as $tableRow) {
233
            $matches = [];
234
            $columnName = null;
235
236
            if (preg_match('/\(`?([a-zA-Z0-9_]+)`?\s*[><=]/', $tableRow['check_clause'], $matches)) {
237
                $columnName = $matches[1];
238
            }
239
240
            $check = new CheckConstraint(
241
                [
242
                    'name' => $tableRow['constraint_name'],
243
                    'columnNames' => [$columnName],
244
                    'expression' => $tableRow['check_clause'],
245
                ]
246
            );
247
            $checks[] = $check;
248
        }
249
250
        return $checks;
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     * @throws NotSupportedException if this method is called.
256
     */
257 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

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

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

388
            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...
389 406
                $info = array_change_key_case($info, CASE_LOWER);
390
            }
391
392 407
            if (\in_array($info['field'], $jsonColumns, true)) {
393
                $info['type'] = static::TYPE_JSON;
394
            }
395
396 407
            $column = $this->loadColumnSchema($info);
397 407
            $table->columns[$column->name] = $column;
398 407
            if ($column->isPrimaryKey) {
399 379
                $table->primaryKey[] = $column->name;
400 379
                if ($column->autoIncrement) {
401 251
                    $table->sequenceName = '';
402
                }
403
            }
404
        }
405
406 407
        return true;
407
    }
408
409
    /**
410
     * Gets the CREATE TABLE sql string.
411
     * @param TableSchema $table the table metadata
412
     * @return string $sql the result of 'SHOW CREATE TABLE'
413
     */
414 407
    protected function getCreateTableSql($table)
415
    {
416 407
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $this->quoteTableName($table->fullName))->queryOne();
417 407
        if (isset($row['Create Table'])) {
418 405
            $sql = $row['Create Table'];
419
        } else {
420 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

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

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