Passed
Pull Request — master (#19758)
by Sohel Ahmed
17:09 queued 03:29
created

Schema::defaultIsExpression()   C

Complexity

Conditions 14
Paths 13

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 89.4044

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 14
eloc 23
c 2
b 0
f 0
nc 13
nop 1
dl 0
loc 39
ccs 6
cts 22
cp 0.2727
crap 89.4044
rs 6.2666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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

Loading history...
225
    {
226 12
        throw new NotSupportedException('MySQL does not support check constraints.');
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     * @throws NotSupportedException if this method is called.
232
     */
233 12
    protected function loadTableDefaultValues($tableName)
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

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

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

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

Loading history...
234
    {
235 12
        throw new NotSupportedException('MySQL does not support default value constraints.');
236
    }
237
238
    /**
239
     * Creates a query builder for the MySQL database.
240
     * @return QueryBuilder query builder instance
241
     */
242 396
    public function createQueryBuilder()
243
    {
244 396
        return Yii::createObject(QueryBuilder::className(), [$this->db]);
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

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

244
        return Yii::createObject(/** @scrutinizer ignore-deprecated */ QueryBuilder::className(), [$this->db]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

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

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

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

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