Test Failed
Pull Request — master (#259)
by Def
05:37 queued 02:55
created

Schema::findColumns()   B

Complexity

Conditions 10
Paths 29

Size

Total Lines 66
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 13.7025

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 33
c 1
b 0
f 0
nc 29
nop 1
dl 0
loc 66
ccs 22
cts 33
cp 0.6667
crap 13.7025
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\Exception\Exception;
13
use Yiisoft\Db\Exception\InvalidConfigException;
14
use Yiisoft\Db\Exception\NotSupportedException;
15
use Yiisoft\Db\Expression\Expression;
16
use Yiisoft\Db\Helper\ArrayHelper;
17
use Yiisoft\Db\Schema\AbstractSchema;
18
use Yiisoft\Db\Schema\Builder\AbstractColumn;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Schema\Builder\AbstractColumn was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use Yiisoft\Db\Schema\ColumnSchemaInterface;
20
use Yiisoft\Db\Schema\Builder\ColumnInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Schema\Builder\ColumnInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

458
                $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
459
                $sql = $row[1];
460 4
            }
461 136
        } catch (Exception) {
462
            $sql = '';
463 35
        }
464 35
465
        return $sql;
466
    }
467 154
468
    /**
469
     * Loads the column information into a {@see ColumnSchemaInterface} object.
470
     *
471
     * @param array $info The column information.
472
     *
473
     * @throws JsonException
474
     *
475
     * @return ColumnSchemaInterface The column schema object.
476
     */
477
    protected function loadColumnSchema(array $info): ColumnSchemaInterface
478
    {
479 138
        $column = $this->createColumnSchema();
480
481 138
        /** @psalm-var ColumnInfoArray $info */
482
        $column->name($info['field']);
483
        $column->allowNull($info['null'] === 'YES');
484 138
        $column->primaryKey(str_contains($info['key'], 'PRI'));
485 138
        $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false);
486 138
        $column->comment($info['comment']);
487 138
        $column->dbType($info['type']);
488 138
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
489 138
        $column->type(self::TYPE_STRING);
490 138
491 138
        $extra = $info['extra'];
492
493 138
        if (str_starts_with($extra, 'DEFAULT_GENERATED')) {
494
            $extra = strtoupper(substr($extra, 18));
495 138
        }
496 28
        $column->extra(trim($extra));
497
498 138
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
499
            $type = strtolower($matches[1]);
500 138
501 138
            if (isset($this->typeMap[$type])) {
502
                $column->type($this->typeMap[$type]);
503 138
            }
504 138
505
            if (!empty($matches[2])) {
506
                if ($type === 'enum') {
507 138
                    preg_match_all("/'[^']*'/", $matches[2], $values);
508 103
509 26
                    foreach ($values[0] as $i => $value) {
510
                        $values[$i] = trim($value, "'");
511 26
                    }
512 26
513
                    $column->enumValues($values);
514
                } else {
515 26
                    $values = explode(',', $matches[2]);
516
                    $column->precision((int) $values[0]);
517 103
                    $column->size((int) $values[0]);
518 103
519 103
                    if (isset($values[1])) {
520
                        $column->scale((int) $values[1]);
521 103
                    }
522 41
523
                    if ($column->getSize() === 1 && $type === 'bit') {
524
                        $column->type(self::TYPE_BOOLEAN);
525 103
                    } elseif ($type === 'bit') {
526 29
                        if ($column->getSize() > 32) {
527 103
                            $column->type(self::TYPE_BIGINT);
528 27
                        } elseif ($column->getSize() === 32) {
529 4
                            $column->type(self::TYPE_INTEGER);
530 27
                        }
531 4
                    }
532
                }
533
            }
534
        }
535
536
        $column->phpType($this->getColumnPhpType($column));
537
538 138
        if (!$column->isPrimaryKey()) {
539
            // Chapter 2: cruthes for MariaDB {@see https://github.com/yiisoft/yii2/issues/19747}
540 138
            /** @psalm-var string $columnCategory */
541
            $columnCategory = $this->createColumn(
542
                $column->getType(),
543 136
                $column->getSize()
544 136
            )->getCategoryMap()[$column->getType()] ?? '';
545 136
            $defaultValue = $info['extra_default_value'] ?? '';
546 136
547 136
            if (
548
                empty($info['extra']) &&
549
                !empty($defaultValue) &&
550 136
                in_array($columnCategory, [
551 136
                    AbstractColumn::CATEGORY_STRING,
552 136
                    AbstractColumn::CATEGORY_TIME,
553 136
                ], true)
554 136
                && !str_starts_with($defaultValue, '\'')
555 136
            ) {
556 136
                $info['extra'] = 'DEFAULT_GENERATED';
557
            }
558
559
            /**
560
             * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed
561
             * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3.
562
             *
563
             * See details here: https://mariadb.com/kb/en/library/now/#description
564
             */
565
            if (
566
                in_array($column->getType(), [self::TYPE_TIMESTAMP, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME], true)
567
                && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', (string) $info['default'], $matches)
568 136
            ) {
569 136
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1])
570
                    ? '(' . $matches[1] . ')' : '')));
571 29
            } elseif (!empty($info['extra']) && !empty($info['default'])) {
572 29
                $column->defaultValue(new Expression($info['default']));
573 134
            } elseif (isset($type) && $type === 'bit' && $column->getType() !== self::TYPE_BOOLEAN) {
574 4
                $column->defaultValue(bindec(trim((string) $info['default'], 'b\'')));
575 134
            } else {
576 27
                $column->defaultValue($column->phpTypecast($info['default']));
577
            }
578 136
        } elseif ($info['default'] !== null) {
579
            $column->defaultValue($column->phpTypecast($info['default']));
580 87
        }
581 3
582
        return $column;
583
    }
584 138
585
    /**
586
     * Loads all check constraints for the given table.
587
     *
588
     * @param string $tableName The table name.
589
     *
590
     * @throws NotSupportedException
591
     *
592
     * @return array Check constraints for the given table.
593
     */
594
    protected function loadTableChecks(string $tableName): array
595
    {
596 16
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
597
    }
598 16
599
    /**
600
     * Loads multiple types of constraints and returns the specified ones.
601
     *
602
     * @param string $tableName table name.
603
     * @param string $returnType return type:
604
     * - primaryKey
605
     * - foreignKeys
606
     * - uniques
607
     *
608
     * @throws Exception
609
     * @throws InvalidConfigException
610
     * @throws Throwable
611
     *
612
     * @psalm-return Constraint[]|ForeignKeyConstraint[]|Constraint|null
613
     */
614
    private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null
615
    {
616 71
        $sql = <<<SQL
617
        SELECT
618 71
            `kcu`.`CONSTRAINT_NAME` AS `name`,
619
            `kcu`.`COLUMN_NAME` AS `column_name`,
620
            `tc`.`CONSTRAINT_TYPE` AS `type`,
621
        CASE
622
            WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL
623
        ELSE `kcu`.`REFERENCED_TABLE_SCHEMA`
624
        END AS `foreign_table_schema`,
625
            `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`,
626
            `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`,
627
            `rc`.`UPDATE_RULE` AS `on_update`,
628
            `rc`.`DELETE_RULE` AS `on_delete`,
629
            `kcu`.`ORDINAL_POSITION` AS `position`
630
        FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
631
        JOIN `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc` ON
632
            `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
633
            `rc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
634
            `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME`
635
        JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON
636
            `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
637
            `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
638
            `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND
639
            `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY'
640
        WHERE
641
            `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
642
            `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
643
            `kcu`.`TABLE_NAME` = :tableName
644
        UNION
645
        SELECT
646
            `kcu`.`CONSTRAINT_NAME` AS `name`,
647
            `kcu`.`COLUMN_NAME` AS `column_name`,
648
            `tc`.`CONSTRAINT_TYPE` AS `type`,
649
        NULL AS `foreign_table_schema`,
650
        NULL AS `foreign_table_name`,
651
        NULL AS `foreign_column_name`,
652
        NULL AS `on_update`,
653
        NULL AS `on_delete`,
654
            `kcu`.`ORDINAL_POSITION` AS `position`
655
        FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu`
656
        JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON
657
            `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND
658
            `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND
659
            `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND
660
            `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE')
661
        WHERE
662
            `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
663
            `kcu`.`TABLE_NAME` = :tableName
664
        ORDER BY `position` ASC
665
        SQL;
666
667 71
        $resolvedName = $this->resolveTableName($tableName);
668
        $constraints = $this->db->createCommand($sql, [
669 71
            ':schemaName' => $resolvedName->getSchemaName(),
670 71
            ':tableName' => $resolvedName->getName(),
671 71
        ])->queryAll();
672 71
673 71
        /** @psalm-var array[][] $constraints */
674
        $constraints = $this->normalizeRowKeyCase($constraints, true);
675
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
676 71
677 71
        $result = [
678
            self::PRIMARY_KEY => null,
679 71
            self::FOREIGN_KEYS => [],
680 71
            self::UNIQUES => [],
681 71
        ];
682 71
683 71
        /**
684
         * @psalm-var string $type
685
         * @psalm-var array $names
686
         */
687
        foreach ($constraints as $type => $names) {
688
            /**
689 71
             * @psalm-var object|string|null $name
690
             * @psalm-var ConstraintArray $constraint
691
             */
692
            foreach ($names as $name => $constraint) {
693
                switch ($type) {
694 66
                    case 'PRIMARY KEY':
695
                        $result[self::PRIMARY_KEY] = (new Constraint())
696 66
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
697 47
                        break;
698 47
                    case 'FOREIGN KEY':
699 47
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
700 58
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
701 16
                            ->foreignTableName($constraint[0]['foreign_table_name'])
702 16
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
703 16
                            ->onDelete($constraint[0]['on_delete'])
704 16
                            ->onUpdate($constraint[0]['on_update'])
705 16
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
706 16
                            ->name($name);
707 16
                        break;
708 16
                    case 'UNIQUE':
709 16
                        $result[self::UNIQUES][] = (new Constraint())
710 48
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
711 48
                            ->name($name);
712 48
                        break;
713 48
                }
714 48
            }
715
        }
716
717
        foreach ($result as $type => $data) {
718
            $this->setTableMetadata($tableName, $type, $data);
719 71
        }
720 71
721
        return $result[$returnType];
722
    }
723 71
724
    /**
725
     * Loads all default value constraints for the given table.
726
     *
727
     * @param string $tableName The table name.
728
     *
729
     * @throws NotSupportedException
730
     *
731
     * @return array Default value constraints for the given table.
732
     */
733
    protected function loadTableDefaultValues(string $tableName): array
734
    {
735 15
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
736
    }
737 15
738
    /**
739
     * Loads all foreign keys for the given table.
740
     *
741
     * @param string $tableName The table name.
742
     *
743
     * @throws Exception
744
     * @throws InvalidConfigException
745
     * @throws Throwable
746
     *
747
     * @return array Foreign keys for the given table.
748
     */
749
    protected function loadTableForeignKeys(string $tableName): array
750
    {
751 9
        $tableForeignKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
752
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
753 9
    }
754 9
755
    /**
756
     * Loads all indexes for the given table.
757
     *
758
     * @param string $tableName The table name.
759
     *
760
     * @throws Exception
761
     * @throws InvalidConfigException
762
     * @throws Throwable
763
     *
764
     * @return IndexConstraint[] Indexes for the given table.
765
     */
766
    protected function loadTableIndexes(string $tableName): array
767
    {
768 38
        $sql = <<<SQL
769
        SELECT
770 38
            `s`.`INDEX_NAME` AS `name`,
771
            `s`.`COLUMN_NAME` AS `column_name`,
772
            `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`,
773
            `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary`
774
        FROM `information_schema`.`STATISTICS` AS `s`
775
        WHERE
776
            `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND
777
            `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND
778
            `s`.`TABLE_NAME` = :tableName
779
        ORDER BY `s`.`SEQ_IN_INDEX` ASC
780
        SQL;
781
782 38
        $resolvedName = $this->resolveTableName($tableName);
783
        $indexes = $this->db->createCommand($sql, [
784 38
            ':schemaName' => $resolvedName->getSchemaName(),
785 38
            ':tableName' => $resolvedName->getName(),
786 38
        ])->queryAll();
787 38
788 38
        /** @psalm-var array[] $indexes */
789
        $indexes = $this->normalizeRowKeyCase($indexes, true);
790
        $indexes = ArrayHelper::index($indexes, null, ['name']);
791 38
        $result = [];
792 38
793 38
        /**
794
         * @psalm-var object|string|null $name
795
         * @psalm-var array[] $index
796
         */
797
        foreach ($indexes as $name => $index) {
798
            $ic = new IndexConstraint();
799 38
800 38
            $ic->primary((bool) $index[0]['index_is_primary']);
801
            $ic->unique((bool) $index[0]['index_is_unique']);
802 38
            $ic->name($name !== 'PRIMARY' ? $name : null);
803 38
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
804 38
805 38
            $result[] = $ic;
806
        }
807 38
808
        return $result;
809
    }
810 38
811
    /**
812
     * Loads a primary key for the given table.
813
     *
814
     * @param string $tableName The table name.
815
     *
816
     * @throws Exception
817
     * @throws InvalidConfigException
818
     * @throws Throwable
819
     *
820
     * @return Constraint|null Primary key for the given table, `null` if the table has no primary key.*
821
     */
822
    protected function loadTablePrimaryKey(string $tableName): Constraint|null
823
    {
824 45
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
825
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
826 45
    }
827 45
828
    /**
829
     * Loads the metadata for the specified table.
830
     *
831
     * @param string $name The table name.
832
     *
833
     * @throws Exception
834
     * @throws Throwable
835
     *
836
     * @return TableSchemaInterface|null DBMS-dependent table metadata, `null` if the table doesn't exist.
837
     */
838
    protected function loadTableSchema(string $name): TableSchemaInterface|null
839
    {
840 154
        $table = $this->resolveTableName($name);
841
        $this->resolveTableCreateSql($table);
842 154
        $this->findTableComment($table);
843 154
844 154
        if ($this->findColumns($table)) {
845
            $this->findConstraints($table);
846 154
847 136
            return $table;
848
        }
849 136
850
        return null;
851
    }
852 35
853
    /**
854
     * Loads all unique constraints for the given table.
855
     *
856
     * @param string $tableName The table name.
857
     *
858
     * @throws Exception
859
     * @throws InvalidConfigException
860
     * @throws Throwable
861
     *
862
     * @return array Unique constraints for the given table.
863
     */
864
    protected function loadTableUniques(string $tableName): array
865
    {
866 17
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
867
        return is_array($tableUniques) ? $tableUniques : [];
868 17
    }
869 17
870
    /**
871
     * Resolves the table name and schema name (if any).
872
     *
873
     * @param string $name The table name.
874
     *
875
     * @see TableSchemaInterface
876
     */
877
    protected function resolveTableName(string $name): TableSchemaInterface
878
    {
879 198
        $resolvedName = new TableSchema();
880
881 198
        $parts = array_reverse($this->db->getQuoter()->getTableNameParts($name));
882
        $resolvedName->name($parts[0] ?? '');
883 198
        $resolvedName->schemaName($parts[1] ?? $this->defaultSchema);
884 198
        $resolvedName->fullName(
885 198
            $resolvedName->getSchemaName() !== $this->defaultSchema ?
886 198
            implode('.', array_reverse($parts)) : $resolvedName->getName()
887 198
        );
888 198
889 198
        return $resolvedName;
890
    }
891 198
892
    /**
893
     * @throws Exception
894
     * @throws InvalidConfigException
895
     * @throws Throwable
896
     */
897
    protected function resolveTableCreateSql(TableSchemaInterface $table): void
898
    {
899 154
        $sql = $this->getCreateTableSql($table);
900
        $table->createSql($sql);
901 154
    }
902 154
903
    /**
904
     * Creates a column schema for the database.
905
     *
906
     * This method may be overridden by child classes to create a DBMS-specific column schema.
907
     */
908
    private function createColumnSchema(): ColumnSchema
909
    {
910 138
        return new ColumnSchema();
911
    }
912 138
913
    /**
914
     * @throws Exception
915
     * @throws InvalidConfigException
916
     * @throws Throwable
917
     */
918
    private function getJsonColumns(TableSchemaInterface $table): array
919
    {
920 136
        $sql = $this->getCreateTableSql($table);
921
        $result = [];
922 136
        $regexp = '/json_valid\([\`"](.+)[\`"]\s*\)/mi';
923 136
924 136
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
925
            foreach ($matches as $match) {
926 136
                $result[] = $match[1];
927
            }
928
        }
929
930
        return $result;
931
    }
932
}
933