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

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

578
                && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', /** @scrutinizer ignore-type */ $defaultValue, $matches) === 1
Loading history...
579 146
                    => new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1]) ? '(' . $matches[1] . ')' : '')),
580 146
            !empty($columnSchema->getExtra())
581 146
                && !empty($defaultValue)
582 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

582
                    => new Expression(/** @scrutinizer ignore-type */ $defaultValue),
Loading history...
583 146
            str_starts_with(strtolower((string) $columnSchema->getDbType()), 'bit')
584 146
                => $columnSchema->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

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