Passed
Pull Request — master (#295)
by
unknown
06:09 queued 02:40
created

Schema::findColumns()   B

Complexity

Conditions 10
Paths 29

Size

Total Lines 67
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 13.3855

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 10
eloc 33
c 2
b 0
f 0
nc 29
nop 1
dl 0
loc 67
ccs 23
cts 34
cp 0.6765
crap 13.3855
rs 7.6666

How to fix   Long Method    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
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mysql;
6
7
use JsonException;
8
use Throwable;
9
use Yiisoft\Db\Constraint\Constraint;
10
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
11
use Yiisoft\Db\Constraint\IndexConstraint;
12
use Yiisoft\Db\Driver\Pdo\AbstractPdoSchema;
13
use Yiisoft\Db\Exception\Exception;
14
use Yiisoft\Db\Exception\InvalidConfigException;
15
use Yiisoft\Db\Exception\NotSupportedException;
16
use Yiisoft\Db\Expression\Expression;
17
use Yiisoft\Db\Helper\DbArrayHelper;
18
use Yiisoft\Db\Schema\Builder\ColumnInterface;
19
use Yiisoft\Db\Schema\ColumnSchemaInterface;
20
use Yiisoft\Db\Schema\TableSchemaInterface;
21
22
use function array_map;
23
use function array_merge;
24
use function array_values;
25
use function bindec;
26
use function explode;
27
use function in_array;
28
use function is_string;
29
use function ksort;
30
use function md5;
31
use function preg_match_all;
32
use function preg_match;
33
use function serialize;
34
use function stripos;
35
use function strtolower;
36
use function trim;
37
38
/**
39
 * Implements MySQL, MariaDB specific schema, supporting MySQL Server 5.7, MariaDB Server 10.4 and higher.
40
 *
41
 * @psalm-type ColumnArray = array{
42
 *   table_schema: string,
43
 *   table_name: string,
44
 *   column_name: string,
45
 *   data_type: string,
46
 *   type_type: string|null,
47
 *   character_maximum_length: int,
48
 *   column_comment: string|null,
49
 *   modifier: int,
50
 *   is_nullable: bool,
51
 *   column_default: mixed,
52
 *   is_autoinc: bool,
53
 *   sequence_name: string|null,
54
 *   enum_values: array<array-key, float|int|string>|string|null,
55
 *   numeric_precision: int|null,
56
 *   numeric_scale: int|null,
57
 *   size: string|null,
58
 *   is_pkey: bool|null,
59
 *   dimension: int
60
 * }
61
 * @psalm-type ColumnInfoArray = array{
62
 *   field: string,
63
 *   type: string,
64
 *   collation: string|null,
65
 *   null: string,
66
 *   key: string,
67
 *   default: string|null,
68
 *   extra: string,
69
 *   extra_default_value: string|null,
70
 *   privileges: string,
71
 *   comment: string
72
 * }
73
 * @psalm-type RowConstraint = array{
74
 *   constraint_name: string,
75
 *   column_name: string,
76
 *   referenced_table_name: string,
77
 *   referenced_column_name: string
78
 * }
79
 * @psalm-type ConstraintArray = array<
80
 *   array-key,
81
 *   array {
82
 *     name: string,
83
 *     column_name: string,
84
 *     type: string,
85
 *     foreign_table_schema: string|null,
86
 *     foreign_table_name: string|null,
87
 *     foreign_column_name: string|null,
88
 *     on_update: string,
89
 *     on_delete: string,
90
 *     check_expr: string
91
 *   }
92
 * >
93
 */
94
final class Schema extends AbstractPdoSchema
95
{
96
    /**
97
     * @var array Mapping from physical column types (keys) to abstract column types (values).
98
     *
99
     * @psalm-var string[]
100
     */
101
    private array $typeMap = [
102
        'tinyint' => self::TYPE_TINYINT,
103
        'bit' => self::TYPE_INTEGER,
104
        'smallint' => self::TYPE_SMALLINT,
105
        'mediumint' => self::TYPE_INTEGER,
106
        'int' => self::TYPE_INTEGER,
107
        'integer' => self::TYPE_INTEGER,
108
        'bigint' => self::TYPE_BIGINT,
109
        'float' => self::TYPE_FLOAT,
110
        'double' => self::TYPE_DOUBLE,
111
        'real' => self::TYPE_FLOAT,
112
        'decimal' => self::TYPE_DECIMAL,
113
        'numeric' => self::TYPE_DECIMAL,
114
        'tinytext' => self::TYPE_TEXT,
115
        'mediumtext' => self::TYPE_TEXT,
116
        'longtext' => self::TYPE_TEXT,
117
        'longblob' => self::TYPE_BINARY,
118
        'blob' => self::TYPE_BINARY,
119
        'text' => self::TYPE_TEXT,
120
        'varchar' => self::TYPE_STRING,
121
        'string' => self::TYPE_STRING,
122
        'char' => self::TYPE_CHAR,
123
        'datetime' => self::TYPE_DATETIME,
124
        'year' => self::TYPE_DATE,
125
        'date' => self::TYPE_DATE,
126
        'time' => self::TYPE_TIME,
127
        'timestamp' => self::TYPE_TIMESTAMP,
128
        'enum' => self::TYPE_STRING,
129
        'varbinary' => self::TYPE_BINARY,
130
        'json' => self::TYPE_JSON,
131
    ];
132
133 16
    public function createColumn(string $type, array|int|string $length = null): ColumnInterface
134
    {
135 16
        return new Column($type, $length);
136
    }
137
138
    /**
139
     * Returns all unique indexes for the given table.
140
     *
141
     * Each array element is of the following structure:
142
     *
143
     * ```php
144
     * [
145
     *     'IndexName1' => ['col1' [, ...]],
146
     *     'IndexName2' => ['col2' [, ...]],
147
     * ]
148
     * ```
149
     *
150
     * @param TableSchemaInterface $table The table metadata.
151
     *
152
     * @throws Exception
153
     * @throws InvalidConfigException
154
     * @throws Throwable
155
     *
156
     * @return array All unique indexes for the given table.
157
     */
158 1
    public function findUniqueIndexes(TableSchemaInterface $table): array
159
    {
160 1
        $sql = $this->getCreateTableSql($table);
161 1
        $uniqueIndexes = [];
162 1
        $regexp = '/UNIQUE KEY\s+[`"](.+)[`"]\s*\(([`"].+[`"])+\)/mi';
163
164 1
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
165 1
            foreach ($matches as $match) {
166 1
                $indexName = $match[1];
167 1
                $indexColumns = array_map('trim', preg_split('/[`"],[`"]/', trim($match[2], '`"')));
168 1
                $uniqueIndexes[$indexName] = $indexColumns;
169
            }
170
        }
171
172 1
        ksort($uniqueIndexes);
173
174 1
        return $uniqueIndexes;
175
    }
176
177
    /**
178
     * Collects the metadata of table columns.
179
     *
180
     * @param TableSchemaInterface $table The table metadata.
181
     *
182
     * @throws Exception
183
     * @throws Throwable If DB query fails.
184
     *
185
     * @return bool Whether the table exists in the database.
186
     */
187 161
    protected function findColumns(TableSchemaInterface $table): bool
188
    {
189 161
        $tableName = $table->getFullName() ?? '';
190 161
        $sql = 'SHOW FULL COLUMNS FROM ' . $this->db->getQuoter()->quoteTableName($tableName);
191
192
        try {
193 161
            $columns = $this->db->createCommand($sql)->queryAll();
194
            // Chapter 1: crutches for MariaDB. {@see https://github.com/yiisoft/yii2/issues/19747}
195 144
            $columnsExtra = [];
196 144
            if (str_contains($this->db->getServerVersion(), 'MariaDB')) {
197
                /** @psalm-var array[] $columnsExtra */
198
                $columnsExtra = $this->db->createCommand(
199
                    <<<SQL
200
                    SELECT `COLUMN_NAME` as name,`COLUMN_DEFAULT` as default_value
201
                    FROM INFORMATION_SCHEMA.COLUMNS
202
                    WHERE TABLE_SCHEMA = COALESCE(:schemaName, DATABASE()) AND TABLE_NAME = :tableName
203
                    SQL ,
204
                    [
205
                        ':schemaName' => $table->getSchemaName(),
206
                        ':tableName' => $table->getName(),
207
                    ]
208
                )->queryAll();
209
                /** @psalm-var string[] $cols */
210 144
                foreach ($columnsExtra as $cols) {
211
                    $columnsExtra[$cols['name']] = $cols['default_value'];
212
                }
213
            }
214 35
        } catch (Exception $e) {
215 35
            $previous = $e->getPrevious();
216
217 35
            if ($previous && str_contains($previous->getMessage(), 'SQLSTATE[42S02')) {
218
                /**
219
                 * The table doesn't exist.
220
                 *
221
                 * @link https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
222
                 */
223 35
                return false;
224
            }
225
226
            throw $e;
227
        }
228
229 144
        $jsonColumns = $this->getJsonColumns($table);
230
231
        /** @psalm-var ColumnInfoArray $info */
232 144
        foreach ($columns as $info) {
233 144
            $info = $this->normalizeRowKeyCase($info, false);
234
235 144
            $info['extra_default_value'] = $columnsExtra[(string) $info['field']] ?? '';
236
237 144
            if (in_array($info['field'], $jsonColumns, true)) {
238
                $info['type'] = self::TYPE_JSON;
239
            }
240
241
            /** @psalm-var ColumnInfoArray $info */
242 144
            $column = $this->loadColumnSchema($info);
243 144
            $table->column($column->getName(), $column);
244
245 144
            if ($column->isPrimaryKey()) {
246 91
                $table->primaryKey($column->getName());
247 91
                if ($column->isAutoIncrement()) {
248 73
                    $table->sequenceName('');
249
                }
250
            }
251
        }
252
253 144
        return true;
254
    }
255
256
    /**
257
     * Collects the foreign key column details for the given table.
258
     *
259
     * @param TableSchemaInterface $table The table metadata.
260
     *
261
     * @throws Exception
262
     * @throws InvalidConfigException
263
     * @throws Throwable
264
     */
265 144
    protected function findConstraints(TableSchemaInterface $table): void
266
    {
267 144
        $sql = <<<SQL
268
        SELECT
269
            `kcu`.`CONSTRAINT_NAME` AS `constraint_name`,
270
            `kcu`.`COLUMN_NAME` AS `column_name`,
271
            `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`,
272
            `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name`
273
        FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc`
274
        JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON
275
            (
276
                `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR
277
                (
278
                    `kcu`.`CONSTRAINT_CATALOG` IS NULL AND
279
                    `rc`.`CONSTRAINT_CATALOG` IS NULL
280
                )
281
            ) AND
282
            `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
283
            `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME` AND
284
            `kcu`.`TABLE_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND
285
            `kcu`.`TABLE_NAME` = `rc`.`TABLE_NAME`
286
        WHERE `rc`.`CONSTRAINT_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `rc`.`TABLE_NAME` = :tableName
287 144
        SQL;
288
289 144
        $constraints = [];
290 144
        $rows = $this->db->createCommand($sql, [
291 144
            ':schemaName' => $table->getSchemaName(),
292 144
            ':tableName' => $table->getName(),
293 144
        ])->queryAll();
294
295
        /**  @psalm-var RowConstraint $row */
296 144
        foreach ($rows as $row) {
297 35
            $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name'];
298 35
            $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name'];
299
        }
300
301 144
        $table->foreignKeys([]);
302
303
        /**
304
         * @psalm-var array{referenced_table_name: string, columns: array} $constraint
305
         */
306 144
        foreach ($constraints as $name => $constraint) {
307 35
            $table->foreignKey(
308 35
                $name,
309 35
                array_merge(
310 35
                    [$constraint['referenced_table_name']],
311 35
                    $constraint['columns']
312 35
                ),
313 35
            );
314
        }
315
    }
316
317
    /**
318
     * @throws Exception
319
     * @throws InvalidConfigException
320
     * @throws Throwable
321
     */
322 1
    protected function findSchemaNames(): array
323
    {
324 1
        $sql = <<<SQL
325
        SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
326 1
        SQL;
327
328 1
        return $this->db->createCommand($sql)->queryColumn();
329
    }
330
331
    /**
332
     * @throws Exception
333
     * @throws InvalidConfigException
334
     * @throws Throwable
335
     */
336 161
    protected function findTableComment(TableSchemaInterface $tableSchema): void
337
    {
338 161
        $sql = <<<SQL
339
        SELECT `TABLE_COMMENT`
340
        FROM `INFORMATION_SCHEMA`.`TABLES`
341
        WHERE
342
              `TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
343
              `TABLE_NAME` = :tableName;
344 161
        SQL;
345
346 161
        $comment = $this->db->createCommand($sql, [
347 161
            ':schemaName' => $tableSchema->getSchemaName(),
348 161
            ':tableName' => $tableSchema->getName(),
349 161
        ])->queryScalar();
350
351 161
        $tableSchema->comment(is_string($comment) ? $comment : null);
352
    }
353
354
    /**
355
     * Returns all table names in the database.
356
     *
357
     * This method should be overridden by child classes to support this feature because the default implementation
358
     * simply throws an exception.
359
     *
360
     * @param string $schema The schema of the tables.
361
     * Defaults to empty string, meaning the current or default schema.
362
     *
363
     * @throws Exception
364
     * @throws InvalidConfigException
365
     * @throws Throwable
366
     *
367
     * @return array All tables name in the database. The names have NO schema name prefix.
368
     */
369 12
    protected function findTableNames(string $schema = ''): array
370
    {
371 12
        $sql = 'SHOW TABLES';
372
373 12
        if ($schema !== '') {
374 1
            $sql .= ' FROM ' . $this->db->getQuoter()->quoteSimpleTableName($schema);
375
        }
376
377 12
        return $this->db->createCommand($sql)->queryColumn();
378
    }
379
380
    /**
381
     * @throws Exception
382
     * @throws InvalidConfigException
383
     * @throws Throwable
384
     */
385 1
    protected function findViewNames(string $schema = ''): array
386
    {
387 1
        $sql = match ($schema) {
388 1
            '' => <<<SQL
389
            SELECT table_name as view FROM information_schema.tables WHERE table_type LIKE 'VIEW' AND table_schema != 'sys' order by table_name
390 1
            SQL,
391 1
            default => <<<SQL
392 1
            SELECT table_name as view FROM information_schema.tables WHERE table_type LIKE 'VIEW' AND table_schema = '$schema' order by table_name
393 1
            SQL,
394 1
        };
395
396
        /** @psalm-var string[][] $views */
397 1
        $views = $this->db->createCommand($sql)->queryAll();
398
399 1
        foreach ($views as $key => $view) {
400 1
            $views[$key] = $view['view'];
401
        }
402
403 1
        return $views;
404
    }
405
406
    /**
407
     * Returns the cache key for the specified table name.
408
     *
409
     * @param string $name The table name.
410
     *
411
     * @return array The cache key.
412
     */
413 259
    protected function getCacheKey(string $name): array
414
    {
415 259
        return array_merge([self::class], $this->generateCacheKey(), [$this->getRawTableName($name)]);
416
    }
417
418
    /**
419
     * Returns the cache tag name.
420
     *
421
     * This allows {@see refresh()} to invalidate all cached table schemas.
422
     *
423
     * @return string The cache tag name.
424
     */
425 206
    protected function getCacheTag(): string
426
    {
427 206
        return md5(serialize(array_merge([self::class], $this->generateCacheKey())));
428
    }
429
430
    /**
431
     * Gets the `CREATE TABLE` SQL string.
432
     *
433
     * @param TableSchemaInterface $table The table metadata.
434
     *
435
     * @throws Exception
436
     * @throws InvalidConfigException
437
     * @throws Throwable
438
     *
439
     * @return string $sql The result of `SHOW CREATE TABLE`.
440
     */
441 161
    protected function getCreateTableSql(TableSchemaInterface $table): string
442
    {
443 161
        $tableName = $table->getFullName() ?? '';
444
445
        try {
446
            /** @psalm-var array<array-key, string> $row */
447 161
            $row = $this->db->createCommand(
448 161
                'SHOW CREATE TABLE ' . $this->db->getQuoter()->quoteTableName($tableName)
449 161
            )->queryOne();
450
451 144
            if (isset($row['Create Table'])) {
452 142
                $sql = $row['Create Table'];
453
            } else {
454 4
                $row = array_values($row);
0 ignored issues
show
Bug introduced by
$row of type null 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

454
                $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
455 144
                $sql = $row[1];
456
            }
457 35
        } catch (Exception) {
458 35
            $sql = '';
459
        }
460
461 161
        return $sql;
462
    }
463
464
    /**
465
     * Loads the column information into a {@see ColumnSchemaInterface} object.
466
     *
467
     * @param array $info The column information.
468
     *
469
     * @throws JsonException
470
     *
471
     * @return ColumnSchemaInterface The column schema object.
472
     *
473
     * @psalm-param ColumnInfoArray $info The column information.
474
     */
475 146
    protected function loadColumnSchema(array $info): ColumnSchemaInterface
476
    {
477 146
        $dbType = $info['type'];
478
479 146
        $column = $this->createColumnSchema($info['field']);
480
481
        /** @psalm-var ColumnInfoArray $info */
482 146
        $column->allowNull($info['null'] === 'YES');
483 146
        $column->primaryKey(str_contains($info['key'], 'PRI'));
484 146
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
485 146
        $column->comment($info['comment']);
486 146
        $column->dbType($dbType);
487 146
        $column->unsigned(stripos($dbType, 'unsigned') !== false);
488 146
        $column->type(self::TYPE_STRING);
489
490 146
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $dbType, $matches)) {
491 146
            $type = strtolower($matches[1]);
492
493 146
            if (isset($this->typeMap[$type])) {
494 141
                $column->type($this->typeMap[$type]);
495
            }
496
497 146
            if (!empty($matches[2])) {
498 110
                if ($type === 'enum') {
499 26
                    preg_match_all("/'[^']*'/", $matches[2], $values);
500
501 26
                    foreach ($values[0] as $i => $value) {
502 26
                        $values[$i] = trim($value, "'");
503
                    }
504
505 26
                    $column->enumValues($values);
506
                } else {
507 110
                    $values = explode(',', $matches[2]);
508 110
                    $column->precision((int) $values[0]);
509 110
                    $column->size((int) $values[0]);
510
511 110
                    if (isset($values[1])) {
512 41
                        $column->scale((int) $values[1]);
513
                    }
514
515 110
                    if ($type === 'bit') {
516 29
                        if ($column->getSize() === 1) {
517 29
                            $column->type(self::TYPE_BOOLEAN);
518 27
                        } elseif ($column->getSize() > 32) {
519 4
                            $column->type(self::TYPE_BIGINT);
520 27
                        } elseif ($column->getSize() === 32) {
521 4
                            $column->type(self::TYPE_INTEGER);
522
                        }
523
                    }
524
                }
525
            }
526
        }
527
528
        // Chapter 2: crutches for MariaDB {@see https://github.com/yiisoft/yii2/issues/19747}
529 146
        $extra = $info['extra'];
530
        if (
531 146
            empty($extra)
532 146
            && !empty($info['extra_default_value'])
533 146
            && !str_starts_with($info['extra_default_value'], '\'')
534 146
            && in_array($column->getType(), [
535 146
                self::TYPE_CHAR, self::TYPE_STRING, self::TYPE_TEXT,
536 146
                self::TYPE_DATETIME, self::TYPE_TIMESTAMP, self::TYPE_TIME, self::TYPE_DATE,
537 146
            ], true)
538
        ) {
539 1
            $extra = 'DEFAULT_GENERATED';
540
        }
541
542 146
        $column->extra($extra);
543 146
        $column->phpType($this->getColumnPhpType($column));
544 146
        $column->defaultValue($this->normalizeDefaultValue($info['default'], $column));
545
546 146
        if (str_starts_with($extra, 'DEFAULT_GENERATED')) {
547 33
            $column->extra(trim(strtoupper(substr($extra, 18))));
548
        }
549
550 146
        return $column;
551
    }
552
553
    /**
554
     * Converts column's default value according to {@see ColumnSchema::phpType} after retrieval from the database.
555
     *
556
     * @param string|null $defaultValue The default value retrieved from the database.
557
     * @param ColumnSchemaInterface $column The column schema object.
558
     *
559
     * @return mixed The normalized default value.
560
     */
561 146
    private function normalizeDefaultValue(?string $defaultValue, ColumnSchemaInterface $column): mixed
562
    {
563 146
        return match (true) {
564 146
            $defaultValue === null
565 146
                => null,
566 146
            $column->isPrimaryKey()
567 146
                => $column->phpTypecast($defaultValue),
568 146
            in_array($column->getType(), [
569 146
                self::TYPE_TIMESTAMP,
570 146
                self::TYPE_DATETIME,
571 146
                self::TYPE_DATE,
572 146
                self::TYPE_TIME,
573 146
            ], true)
574 146
                && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', $defaultValue, $matches) === 1
0 ignored issues
show
Bug introduced by
It seems like $defaultValue can also be of type null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

574
                && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', /** @scrutinizer ignore-type */ $defaultValue, $matches) === 1
Loading history...
575 146
                    => new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1]) ? '(' . $matches[1] . ')' : '')),
576 146
            !empty($column->getExtra())
577 146
                && !empty($defaultValue)
578 146
                    => new Expression($defaultValue),
0 ignored issues
show
Bug introduced by
It seems like $defaultValue can also be of type null; however, parameter $expression of Yiisoft\Db\Expression\Expression::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

578
                    => new Expression(/** @scrutinizer ignore-type */ $defaultValue),
Loading history...
579 146
            str_starts_with(strtolower((string) $column->getDbType()), 'bit')
580 146
                => $column->phpTypecast(bindec(trim($defaultValue, "b'"))),
0 ignored issues
show
Bug introduced by
It seems like $defaultValue can also be of type null; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

580
                => $column->phpTypecast(bindec(trim(/** @scrutinizer ignore-type */ $defaultValue, "b'"))),
Loading history...
581 146
            default
582 146
            => $column->phpTypecast($defaultValue),
583 146
        };
584
    }
585
586
    /**
587
     * Loads all check constraints for the given table.
588
     *
589
     * @param string $tableName The table name.
590
     *
591
     * @throws NotSupportedException
592
     *
593
     * @return array Check constraints for the given table.
594
     */
595 16
    protected function loadTableChecks(string $tableName): array
596
    {
597 16
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
598
    }
599
600
    /**
601
     * Loads multiple types of constraints and returns the specified ones.
602
     *
603
     * @param string $tableName table name.
604
     * @param string $returnType return type:
605
     * - primaryKey
606
     * - foreignKeys
607
     * - uniques
608
     *
609
     * @throws Exception
610
     * @throws InvalidConfigException
611
     * @throws Throwable
612
     *
613
     * @psalm-return Constraint[]|ForeignKeyConstraint[]|Constraint|null
614
     */
615 71
    private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null
616
    {
617 71
        $sql = <<<SQL
618
        SELECT
619
            `kcu`.`CONSTRAINT_NAME` AS `name`,
620
            `kcu`.`COLUMN_NAME` AS `column_name`,
621
            `tc`.`CONSTRAINT_TYPE` AS `type`,
622
        CASE
623
            WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
624
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
625
        END AS `foreign_table_schema`,
626
            `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
627
            `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
628
            `rc`.`UPDATE_RULE` AS `on_update`,
629
            `rc`.`DELETE_RULE` AS `on_delete`,
630
            `kcu`.`ORDINAL_POSITION` AS `position`
631
        FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
632
        JOIN `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc` ON
633
            `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
634
            `rc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
635
            `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
636
        JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON
637
            `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
638
            `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
639
            `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND
640
            `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY'
641
        WHERE
642
            `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
643
            `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
644
            `kcu`.`TABLE_NAME` = :tableName
645
        UNION
646
        SELECT
647
            `kcu`.`CONSTRAINT_NAME` AS `name`,
648
            `kcu`.`COLUMN_NAME` AS `column_name`,
649
            `tc`.`CONSTRAINT_TYPE` AS `type`,
650
        NULL AS `foreign_table_schema`,
651
        NULL AS `foreign_table_name`,
652
        NULL AS `foreign_column_name`,
653
        NULL AS `on_update`,
654
        NULL AS `on_delete`,
655
            `kcu`.`ORDINAL_POSITION` AS `position`
656
        FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
657
        JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON
658
            `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
659
            `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
660
            `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND
661
            `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE')
662
        WHERE
663
            `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
664
            `kcu`.`TABLE_NAME` = :tableName
665
        ORDER BY `position` ASC
666 71
        SQL;
667
668 71
        $resolvedName = $this->resolveTableName($tableName);
669 71
        $constraints = $this->db->createCommand($sql, [
670 71
            ':schemaName' => $resolvedName->getSchemaName(),
671 71
            ':tableName' => $resolvedName->getName(),
672 71
        ])->queryAll();
673
674
        /** @psalm-var array[][] $constraints */
675 71
        $constraints = $this->normalizeRowKeyCase($constraints, true);
676 71
        $constraints = DbArrayHelper::index($constraints, null, ['type', 'name']);
677
678 71
        $result = [
679 71
            self::PRIMARY_KEY => null,
680 71
            self::FOREIGN_KEYS => [],
681 71
            self::UNIQUES => [],
682 71
        ];
683
684
        /**
685
         * @psalm-var string $type
686
         * @psalm-var array $names
687
         */
688 71
        foreach ($constraints as $type => $names) {
689
            /**
690
             * @psalm-var object|string|null $name
691
             * @psalm-var ConstraintArray $constraint
692
             */
693 66
            foreach ($names as $name => $constraint) {
694
                switch ($type) {
695 66
                    case 'PRIMARY KEY':
696 47
                        $result[self::PRIMARY_KEY] = (new Constraint())
697 47
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'));
698 47
                        break;
699 58
                    case 'FOREIGN KEY':
700 16
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
701 16
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
702 16
                            ->foreignTableName($constraint[0]['foreign_table_name'])
703 16
                            ->foreignColumnNames(DbArrayHelper::getColumn($constraint, 'foreign_column_name'))
704 16
                            ->onDelete($constraint[0]['on_delete'])
705 16
                            ->onUpdate($constraint[0]['on_update'])
706 16
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'))
707 16
                            ->name($name);
708 16
                        break;
709 48
                    case 'UNIQUE':
710 48
                        $result[self::UNIQUES][] = (new Constraint())
711 48
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'))
712 48
                            ->name($name);
713 48
                        break;
714
                }
715
            }
716
        }
717
718 71
        foreach ($result as $type => $data) {
719 71
            $this->setTableMetadata($tableName, $type, $data);
720
        }
721
722 71
        return $result[$returnType];
723
    }
724
725
    /**
726
     * Loads all default value constraints for the given table.
727
     *
728
     * @param string $tableName The table name.
729
     *
730
     * @throws NotSupportedException
731
     *
732
     * @return array Default value constraints for the given table.
733
     */
734 15
    protected function loadTableDefaultValues(string $tableName): array
735
    {
736 15
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
737
    }
738
739
    /**
740
     * Loads all foreign keys for the given table.
741
     *
742
     * @param string $tableName The table name.
743
     *
744
     * @throws Exception
745
     * @throws InvalidConfigException
746
     * @throws Throwable
747
     *
748
     * @return array Foreign keys for the given table.
749
     */
750 9
    protected function loadTableForeignKeys(string $tableName): array
751
    {
752 9
        $tableForeignKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
753 9
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
754
    }
755
756
    /**
757
     * Loads all indexes for the given table.
758
     *
759
     * @param string $tableName The table name.
760
     *
761
     * @throws Exception
762
     * @throws InvalidConfigException
763
     * @throws Throwable
764
     *
765
     * @return IndexConstraint[] Indexes for the given table.
766
     */
767 38
    protected function loadTableIndexes(string $tableName): array
768
    {
769 38
        $sql = <<<SQL
770
        SELECT
771
            `s`.`INDEX_NAME` AS `name`,
772
            `s`.`COLUMN_NAME` AS `column_name`,
773
            `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
774
            `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
775
        FROM `information_schema`.`STATISTICS` AS `s`
776
        WHERE
777
            `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
778
            `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND
779
            `s`.`TABLE_NAME` = :tableName
780
        ORDER BY `s`.`SEQ_IN_INDEX` ASC
781 38
        SQL;
782
783 38
        $resolvedName = $this->resolveTableName($tableName);
784 38
        $indexes = $this->db->createCommand($sql, [
785 38
            ':schemaName' => $resolvedName->getSchemaName(),
786 38
            ':tableName' => $resolvedName->getName(),
787 38
        ])->queryAll();
788
789
        /** @psalm-var array[] $indexes */
790 38
        $indexes = $this->normalizeRowKeyCase($indexes, true);
791 38
        $indexes = DbArrayHelper::index($indexes, null, ['name']);
792 38
        $result = [];
793
794
        /**
795
         * @psalm-var object|string|null $name
796
         * @psalm-var array[] $index
797
         */
798 38
        foreach ($indexes as $name => $index) {
799 38
            $ic = new IndexConstraint();
800
801 38
            $ic->primary((bool) $index[0]['index_is_primary']);
802 38
            $ic->unique((bool) $index[0]['index_is_unique']);
803 38
            $ic->name($name !== 'PRIMARY' ? $name : null);
804 38
            $ic->columnNames(DbArrayHelper::getColumn($index, 'column_name'));
805
806 38
            $result[] = $ic;
807
        }
808
809 38
        return $result;
810
    }
811
812
    /**
813
     * Loads a primary key for the given table.
814
     *
815
     * @param string $tableName The table name.
816
     *
817
     * @throws Exception
818
     * @throws InvalidConfigException
819
     * @throws Throwable
820
     *
821
     * @return Constraint|null Primary key for the given table, `null` if the table has no primary key.*
822
     */
823 45
    protected function loadTablePrimaryKey(string $tableName): Constraint|null
824
    {
825 45
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
826 45
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
827
    }
828
829
    /**
830
     * Loads the metadata for the specified table.
831
     *
832
     * @param string $name The table name.
833
     *
834
     * @throws Exception
835
     * @throws Throwable
836
     *
837
     * @return TableSchemaInterface|null DBMS-dependent table metadata, `null` if the table doesn't exist.
838
     */
839 161
    protected function loadTableSchema(string $name): TableSchemaInterface|null
840
    {
841 161
        $table = $this->resolveTableName($name);
842 161
        $this->resolveTableCreateSql($table);
843 161
        $this->findTableComment($table);
844
845 161
        if ($this->findColumns($table)) {
846 144
            $this->findConstraints($table);
847
848 144
            return $table;
849
        }
850
851 35
        return null;
852
    }
853
854
    /**
855
     * Loads all unique constraints for the given table.
856
     *
857
     * @param string $tableName The table name.
858
     *
859
     * @throws Exception
860
     * @throws InvalidConfigException
861
     * @throws Throwable
862
     *
863
     * @return array Unique constraints for the given table.
864
     */
865 17
    protected function loadTableUniques(string $tableName): array
866
    {
867 17
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
868 17
        return is_array($tableUniques) ? $tableUniques : [];
869
    }
870
871
    /**
872
     * Resolves the table name and schema name (if any).
873
     *
874
     * @param string $name The table name.
875
     *
876
     * @see TableSchemaInterface
877
     */
878 205
    protected function resolveTableName(string $name): TableSchemaInterface
879
    {
880 205
        $resolvedName = new TableSchema();
881
882 205
        $parts = array_reverse($this->db->getQuoter()->getTableNameParts($name));
883 205
        $resolvedName->name($parts[0] ?? '');
884 205
        $resolvedName->schemaName($parts[1] ?? $this->defaultSchema);
885 205
        $resolvedName->fullName(
886 205
            $resolvedName->getSchemaName() !== $this->defaultSchema ?
887 205
            implode('.', array_reverse($parts)) : $resolvedName->getName()
888 205
        );
889
890 205
        return $resolvedName;
891
    }
892
893
    /**
894
     * @throws Exception
895
     * @throws InvalidConfigException
896
     * @throws Throwable
897
     */
898 161
    protected function resolveTableCreateSql(TableSchemaInterface $table): void
899
    {
900 161
        $sql = $this->getCreateTableSql($table);
901 161
        $table->createSql($sql);
902
    }
903
904
    /**
905
     * Creates a column schema for the database.
906
     *
907
     * This method may be overridden by child classes to create a DBMS-specific column schema.
908
     *
909
     * @param string $name Name of the column.
910
     */
911 146
    private function createColumnSchema(string $name): ColumnSchema
912
    {
913 146
        return new ColumnSchema($name);
914
    }
915
916
    /**
917
     * @throws Exception
918
     * @throws InvalidConfigException
919
     * @throws Throwable
920
     */
921 144
    private function getJsonColumns(TableSchemaInterface $table): array
922
    {
923 144
        $sql = $this->getCreateTableSql($table);
924 144
        $result = [];
925 144
        $regexp = '/json_valid\([\`"](.+)[\`"]\s*\)/mi';
926
927 144
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
928
            foreach ($matches as $match) {
929
                $result[] = $match[1];
930
            }
931
        }
932
933 144
        return $result;
934
    }
935
}
936