Passed
Pull Request — master (#19758)
by Sohel Ahmed
13:37
created

Schema::moreColumnInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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

212
    protected function loadTableChecks(/** @scrutinizer ignore-unused */ $tableName)

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

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

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

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

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

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

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

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

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