Passed
Pull Request — master (#263)
by
unknown
04:48
created

Schema::findColumns()   B

Complexity

Conditions 7
Paths 20

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 7

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 14
c 3
b 0
f 0
dl 0
loc 27
ccs 15
cts 15
cp 1
rs 8.8333
cc 7
nc 20
nop 1
crap 7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite;
6
7
use Throwable;
8
use Yiisoft\Db\Constraint\CheckConstraint;
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\InvalidArgumentException;
15
use Yiisoft\Db\Exception\InvalidConfigException;
16
use Yiisoft\Db\Exception\NotSupportedException;
17
use Yiisoft\Db\Expression\Expression;
18
use Yiisoft\Db\Helper\DbArrayHelper;
19
use Yiisoft\Db\Schema\Builder\ColumnInterface;
20
use Yiisoft\Db\Schema\ColumnSchemaInterface;
21
use Yiisoft\Db\Schema\TableSchemaInterface;
22
23
use function array_merge;
24
use function count;
25
use function explode;
26
use function md5;
27
use function preg_match;
28
use function preg_replace;
29
use function serialize;
30
use function strncasecmp;
31
use function strtolower;
32
33
/**
34
 * Implements the SQLite Server specific schema, supporting SQLite 3.3.0 or higher.
35
 *
36
 * @psalm-type Column = array<array-key, array{seqno:string, cid:string, name:string}>
37
 * @psalm-type NormalizePragmaForeignKeyList = array<
38
 *   string,
39
 *   array<
40
 *     array-key,
41
 *     array{
42
 *       id:string,
43
 *       cid:string,
44
 *       seq:string,
45
 *       table:string,
46
 *       from:string,
47
 *       to:string,
48
 *       on_update:string,
49
 *       on_delete:string
50
 *     }
51
 *   >
52
 * >
53
 * @psalm-type PragmaForeignKeyList = array<
54
 *   string,
55
 *   array{
56
 *     id:string,
57
 *     cid:string,
58
 *     seq:string,
59
 *     table:string,
60
 *     from:string,
61
 *     to:string,
62
 *     on_update:string,
63
 *     on_delete:string
64
 *   }
65
 * >
66
 * @psalm-type PragmaIndexInfo = array<array-key, array{seqno:string, cid:string, name:string}>
67
 * @psalm-type PragmaIndexList = array<
68
 *   array-key,
69
 *   array{seq:string, name:string, unique:string, origin:string, partial:string}
70
 * >
71
 * @psalm-type PragmaTableInfo = array<
72
 *   array-key,
73
 *   array{cid:string, name:string, type:string, notnull:string, dflt_value:string|null, pk:string}
74
 * >
75
 */
76
final class Schema extends AbstractPdoSchema
77
{
78
    /**
79
     * @var array Mapping from physical column types (keys) to abstract column types (values).
80
     *
81
     * @psalm-var array<array-key, string> $typeMap
82
     */
83
    private array $typeMap = [
84
        'tinyint' => self::TYPE_TINYINT,
85
        'bit' => self::TYPE_SMALLINT,
86
        'boolean' => self::TYPE_BOOLEAN,
87
        'bool' => self::TYPE_BOOLEAN,
88
        'smallint' => self::TYPE_SMALLINT,
89
        'mediumint' => self::TYPE_INTEGER,
90
        'int' => self::TYPE_INTEGER,
91
        'integer' => self::TYPE_INTEGER,
92
        'bigint' => self::TYPE_BIGINT,
93
        'float' => self::TYPE_FLOAT,
94
        'double' => self::TYPE_DOUBLE,
95
        'real' => self::TYPE_FLOAT,
96
        'decimal' => self::TYPE_DECIMAL,
97
        'numeric' => self::TYPE_DECIMAL,
98
        'tinytext' => self::TYPE_TEXT,
99
        'mediumtext' => self::TYPE_TEXT,
100
        'longtext' => self::TYPE_TEXT,
101
        'text' => self::TYPE_TEXT,
102
        'varchar' => self::TYPE_STRING,
103
        'string' => self::TYPE_STRING,
104
        'char' => self::TYPE_CHAR,
105
        'blob' => self::TYPE_BINARY,
106
        'datetime' => self::TYPE_DATETIME,
107
        'year' => self::TYPE_DATE,
108
        'date' => self::TYPE_DATE,
109
        'time' => self::TYPE_TIME,
110
        'timestamp' => self::TYPE_TIMESTAMP,
111
        'enum' => self::TYPE_STRING,
112
        'json' => self::TYPE_JSON,
113
    ];
114
115 15
    public function createColumn(string $type, array|int|string $length = null): ColumnInterface
116
    {
117 15
        return new Column($type, $length);
118
    }
119
120
    /**
121
     * Returns all table names in the database.
122
     *
123
     * This method should be overridden by child classes to support this feature because the default implementation
124
     * simply throws an exception.
125
     *
126
     * @param string $schema The schema of the tables.
127
     * Defaults to empty string, meaning the current or default schema.
128
     *
129
     * @throws Exception
130
     * @throws InvalidConfigException
131
     * @throws Throwable
132
     *
133
     * @return array All tables name in the database. The names have NO schema name prefix.
134
     */
135 10
    protected function findTableNames(string $schema = ''): array
136
    {
137 10
        return $this->db
138 10
           ->createCommand(
139 10
               "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name"
140 10
           )
141 10
           ->queryColumn();
142
    }
143
144
    /**
145
     * Loads the metadata for the specified table.
146
     *
147
     * @param string $name The table name.
148
     *
149
     * @throws Exception
150
     * @throws InvalidArgumentException
151
     * @throws InvalidConfigException
152
     * @throws Throwable
153
     *
154
     * @return TableSchemaInterface|null DBMS-dependent table metadata, `null` if the table doesn't exist.
155
     */
156 159
    protected function loadTableSchema(string $name): TableSchemaInterface|null
157
    {
158 159
        $table = new TableSchema();
159
160 159
        $table->name($name);
161 159
        $table->fullName($name);
162
163 159
        if ($this->findColumns($table)) {
164 115
            $this->findConstraints($table);
165
166 115
            return $table;
167
        }
168
169 67
        return null;
170
    }
171
172
    /**
173
     * Loads a primary key for the given table.
174
     *
175
     * @param string $tableName The table name.
176
     *
177
     * @throws Exception
178
     * @throws InvalidArgumentException
179
     * @throws InvalidConfigException
180
     * @throws Throwable
181
     *
182
     * @return Constraint|null Primary key for the given table, `null` if the table has no primary key.
183
     */
184 49
    protected function loadTablePrimaryKey(string $tableName): Constraint|null
185
    {
186 49
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
187
188 49
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
189
    }
190
191
    /**
192
     * Loads all foreign keys for the given table.
193
     *
194
     * @param string $tableName The table name.
195
     *
196
     * @throws Exception
197
     * @throws InvalidConfigException
198
     * @throws Throwable
199
     *
200
     * @return ForeignKeyConstraint[] Foreign keys for the given table.
201
     */
202 9
    protected function loadTableForeignKeys(string $tableName): array
203
    {
204 9
        $result = [];
205
        /** @psalm-var PragmaForeignKeyList $foreignKeysList */
206 9
        $foreignKeysList = $this->getPragmaForeignKeyList($tableName);
207
        /** @psalm-var NormalizePragmaForeignKeyList $foreignKeysList */
208 9
        $foreignKeysList = $this->normalizeRowKeyCase($foreignKeysList, true);
209
        /** @psalm-var NormalizePragmaForeignKeyList $foreignKeysList */
210 9
        $foreignKeysList = DbArrayHelper::index($foreignKeysList, null, ['table']);
211 9
        DbArrayHelper::multisort($foreignKeysList, 'seq');
212
213
        /** @psalm-var NormalizePragmaForeignKeyList $foreignKeysList */
214 9
        foreach ($foreignKeysList as $table => $foreignKey) {
215 5
            $fk = (new ForeignKeyConstraint())
216 5
                ->columnNames(DbArrayHelper::getColumn($foreignKey, 'from'))
217 5
                ->foreignTableName($table)
218 5
                ->foreignColumnNames(DbArrayHelper::getColumn($foreignKey, 'to'))
219 5
                ->onDelete($foreignKey[0]['on_delete'] ?? null)
220 5
                ->onUpdate($foreignKey[0]['on_update'] ?? null);
221
222 5
            $result[] = $fk;
223
        }
224
225 9
        return $result;
226
    }
227
228
    /**
229
     * Loads all indexes for the given table.
230
     *
231
     * @param string $tableName The table name.
232
     *
233
     * @throws Exception
234
     * @throws InvalidArgumentException
235
     * @throws InvalidConfigException
236
     * @throws Throwable
237
     *
238
     * @return array Indexes for the given table.
239
     *
240
     * @psalm-return array|IndexConstraint[]
241
     */
242 14
    protected function loadTableIndexes(string $tableName): array
243
    {
244 14
        $tableIndexes = $this->loadTableConstraints($tableName, self::INDEXES);
245
246 14
        return is_array($tableIndexes) ? $tableIndexes : [];
247
    }
248
249
    /**
250
     * Loads all unique constraints for the given table.
251
     *
252
     * @param string $tableName The table name.
253
     *
254
     * @throws Exception
255
     * @throws InvalidArgumentException
256
     * @throws InvalidConfigException
257
     * @throws Throwable
258
     *
259
     * @return array Unique constraints for the given table.
260
     *
261
     * @psalm-return array|Constraint[]
262
     */
263 15
    protected function loadTableUniques(string $tableName): array
264
    {
265 15
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
266
267 15
        return is_array($tableUniques) ? $tableUniques : [];
268
    }
269
270
    /**
271
     * Loads all check constraints for the given table.
272
     *
273
     * @param string $tableName The table name.
274
     *
275
     * @throws Exception
276
     * @throws InvalidArgumentException
277
     * @throws InvalidConfigException
278
     * @throws Throwable
279
     *
280
     * @return CheckConstraint[] Check constraints for the given table.
281
     */
282 154
    protected function loadTableChecks(string $tableName): array
283
    {
284 154
        $sql = $this->db->createCommand(
285 154
            'SELECT `sql` FROM `sqlite_master` WHERE name = :tableName',
286 154
            [':tableName' => $tableName],
287 154
        )->queryScalar();
288
289 154
        $sql = ($sql === false || $sql === null) ? '' : (string) $sql;
290
291
        /** @psalm-var SqlToken[]|SqlToken[][]|SqlToken[][][] $code */
292 154
        $code = (new SqlTokenizer($sql))->tokenize();
293 154
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
294 154
        $result = [];
295
296 154
        if ($code[0] instanceof SqlToken && $code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
0 ignored issues
show
Bug introduced by
The method matches() does not exist on null. ( Ignorable by Annotation )

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

296
        if ($code[0] instanceof SqlToken && $code[0]->/** @scrutinizer ignore-call */ matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
297 113
            $offset = 0;
298 113
            $createTableToken = $code[0][(int) $lastMatchIndex - 1];
299 113
            $sqlTokenizerAnyCheck = new SqlTokenizer('any CHECK()');
300
301
            while (
302 113
                $createTableToken instanceof SqlToken &&
303 113
                $createTableToken->matches($sqlTokenizerAnyCheck->tokenize(), (int) $offset, $firstMatchIndex, $offset)
304
            ) {
305 23
                $name = null;
306 23
                $checkSql = (string) $createTableToken[(int) $offset - 1];
307 23
                $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
308
309
                if (
310 23
                    isset($createTableToken[(int) $firstMatchIndex - 2])
311 23
                    && $createTableToken->matches($pattern, (int) $firstMatchIndex - 2)
312
                ) {
313 1
                    $sqlToken = $createTableToken[(int) $firstMatchIndex - 1];
314 1
                    $name = $sqlToken?->getContent();
315
                }
316
317 23
                $result[] = (new CheckConstraint())->name($name)->expression($checkSql);
318
            }
319
        }
320
321 154
        return $result;
322
    }
323
324
    /**
325
     * Loads all default value constraints for the given table.
326
     *
327
     * @param string $tableName The table name.
328
     *
329
     * @throws NotSupportedException
330
     *
331
     * @return array Default value constraints for the given table.
332
     */
333 13
    protected function loadTableDefaultValues(string $tableName): array
334
    {
335 13
        throw new NotSupportedException('SQLite does not support default value constraints.');
336
    }
337
338
    /**
339
     * Collects the table column metadata.
340
     *
341
     * @param TableSchemaInterface $table The table metadata.
342
     *
343
     * @throws Exception
344
     * @throws InvalidConfigException
345
     * @throws Throwable
346
     *
347
     * @return bool Whether the table exists in the database.
348
     */
349 159
    protected function findColumns(TableSchemaInterface $table): bool
350
    {
351
        /** @psalm-var PragmaTableInfo $columns */
352 159
        $columns = $this->getPragmaTableInfo($table->getName());
353 159
        $jsonColumns = $this->getJsonColumns($table);
354
355 159
        foreach ($columns as $info) {
356 115
            if (in_array($info['name'], $jsonColumns, true)) {
357 18
                $info['type'] = self::TYPE_JSON;
358
            }
359
360 115
            $column = $this->loadColumnSchema($info);
361 115
            $table->column($column->getName(), $column);
362
363 115
            if ($column->isPrimaryKey()) {
364 76
                $table->primaryKey($column->getName());
365
            }
366
        }
367
368 159
        $column = count($table->getPrimaryKey()) === 1 ? $table->getColumn($table->getPrimaryKey()[0]) : null;
369
370 159
        if ($column !== null && !strncasecmp($column->getDbType() ?? '', 'int', 3)) {
371 71
            $table->sequenceName('');
372 71
            $column->autoIncrement(true);
373
        }
374
375 159
        return !empty($columns);
376
    }
377
378
    /**
379
     * Collects the foreign key column details for the given table.
380
     *
381
     * @param TableSchemaInterface $table The table metadata.
382
     *
383
     * @throws Exception
384
     * @throws InvalidConfigException
385
     * @throws Throwable
386
     */
387 115
    protected function findConstraints(TableSchemaInterface $table): void
388
    {
389
        /** @psalm-var PragmaForeignKeyList $foreignKeysList */
390 115
        $foreignKeysList = $this->getPragmaForeignKeyList($table->getName());
391
392 115
        foreach ($foreignKeysList as $foreignKey) {
393 5
            $id = (int) $foreignKey['id'];
394 5
            $fk = $table->getForeignKeys();
395
396 5
            if (!isset($fk[$id])) {
397 5
                $table->foreignKey($id, [$foreignKey['table'], $foreignKey['from'] => $foreignKey['to']]);
398
            } else {
399
                /** composite FK */
400 5
                $table->compositeForeignKey($id, $foreignKey['from'], $foreignKey['to']);
401
            }
402
        }
403
    }
404
405
    /**
406
     * Returns all unique indexes for the given table.
407
     *
408
     * Each array element is of the following structure:
409
     *
410
     * ```php
411
     * [
412
     *     'IndexName1' => ['col1' [, ...]],
413
     *     'IndexName2' => ['col2' [, ...]],
414
     * ]
415
     * ```
416
     *
417
     * @param TableSchemaInterface $table The table metadata.
418
     *
419
     * @throws Exception
420
     * @throws InvalidConfigException
421
     * @throws Throwable
422
     *
423
     * @return array All unique indexes for the given table.
424
     */
425 1
    public function findUniqueIndexes(TableSchemaInterface $table): array
426
    {
427
        /** @psalm-var PragmaIndexList $indexList */
428 1
        $indexList = $this->getPragmaIndexList($table->getName());
429 1
        $uniqueIndexes = [];
430
431 1
        foreach ($indexList as $index) {
432 1
            $indexName = $index['name'];
433
            /** @psalm-var PragmaIndexInfo $indexInfo */
434 1
            $indexInfo = $this->getPragmaIndexInfo($index['name']);
435
436 1
            if ($index['unique']) {
437 1
                $uniqueIndexes[$indexName] = [];
438 1
                foreach ($indexInfo as $row) {
439 1
                    $uniqueIndexes[$indexName][] = $row['name'];
440
                }
441
            }
442
        }
443
444 1
        return $uniqueIndexes;
445
    }
446
447
    /**
448
     * @throws NotSupportedException
449
     */
450 1
    public function getSchemaDefaultValues(string $schema = '', bool $refresh = false): array
451
    {
452 1
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
453
    }
454
455
    /**
456
     * Loads the column information into a {@see ColumnSchemaInterface} object.
457
     *
458
     * @param array $info The column information.
459
     *
460
     * @return ColumnSchemaInterface The column schema object.
461
     *
462
     * @psalm-param array{cid:string, name:string, type:string, notnull:string, dflt_value:string|null, pk:string} $info
463
     */
464 115
    protected function loadColumnSchema(array $info): ColumnSchemaInterface
465
    {
466 115
        $column = $this->createColumnSchema($info['name']);
467 115
        $column->allowNull(!$info['notnull']);
468 115
        $column->primaryKey($info['pk'] != '0');
469 115
        $column->dbType(strtolower($info['type']));
470 115
        $column->unsigned(str_contains($column->getDbType() ?? '', 'unsigned'));
471 115
        $column->type(self::TYPE_STRING);
472
473 115
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType() ?? '', $matches)) {
474 115
            $type = strtolower($matches[1]);
475
476 115
            if (isset($this->typeMap[$type])) {
477 115
                $column->type($this->typeMap[$type]);
478
            }
479
480 115
            if (!empty($matches[2])) {
481 100
                $values = explode(',', $matches[2]);
482 100
                $column->precision((int) $values[0]);
483 100
                $column->size((int) $values[0]);
484
485 100
                if (isset($values[1])) {
486 32
                    $column->scale((int) $values[1]);
487
                }
488
489 100
                if (($type === 'tinyint' || $type === 'bit') && $column->getSize() === 1) {
490 26
                    $column->type(self::TYPE_BOOLEAN);
491 100
                } elseif ($type === 'bit') {
492 4
                    if ($column->getSize() > 32) {
493 4
                        $column->type(self::TYPE_BIGINT);
494 4
                    } elseif ($column->getSize() === 32) {
495 4
                        $column->type(self::TYPE_INTEGER);
496
                    }
497
                }
498
            }
499
        }
500
501 115
        $column->phpType($this->getColumnPhpType($column));
502 115
        $column->defaultValue($this->normalizeDefaultValue($info['dflt_value'], $column));
503
504 115
        return $column;
505
    }
506
507
    /**
508
     * Converts column's default value according to {@see ColumnSchema::phpType} after retrieval from the database.
509
     *
510
     * @param string|null $defaultValue The default value retrieved from the database.
511
     * @param ColumnSchemaInterface $column The column schema object.
512
     *
513
     * @return mixed The normalized default value.
514
     */
515 115
    private function normalizeDefaultValue(string|null $defaultValue, ColumnSchemaInterface $column): mixed
516
    {
517 115
        if ($column->isPrimaryKey() || in_array($defaultValue, [null, '', 'null', 'NULL'], true)) {
518 111
            return null;
519
        }
520
521 77
        if (in_array($defaultValue, ['CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'], true)) {
522 26
            return 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

522
            return new Expression(/** @scrutinizer ignore-type */ $defaultValue);
Loading history...
523
        }
524
525 77
        $value = preg_replace('/^([\'"])(.*)\1$/s', '$2', $defaultValue);
526
527 77
        return $column->phpTypecast($value);
528
    }
529
530
    /**
531
     * Returns table columns info.
532
     *
533
     * @param string $tableName The table name.
534
     *
535
     * @throws Exception
536
     * @throws InvalidConfigException
537
     * @throws Throwable
538
     *
539
     * @return array The table columns info.
540
     */
541 54
    private function loadTableColumnsInfo(string $tableName): array
542
    {
543 54
        $tableColumns = $this->getPragmaTableInfo($tableName);
544
        /** @psalm-var PragmaTableInfo $tableColumns */
545 54
        $tableColumns = $this->normalizeRowKeyCase($tableColumns, true);
546
547 54
        return DbArrayHelper::index($tableColumns, 'cid');
548
    }
549
550
    /**
551
     * Loads multiple types of constraints and returns the specified ones.
552
     *
553
     * @param string $tableName The table name.
554
     * @param string $returnType Return type: (primaryKey, indexes, uniques).
555
     *
556
     * @throws Exception
557
     * @throws InvalidConfigException
558
     * @throws Throwable
559
     *
560
     * @psalm-return Constraint[]|IndexConstraint[]|Constraint|null
561
     */
562 78
    private function loadTableConstraints(string $tableName, string $returnType): Constraint|array|null
563
    {
564 78
        $indexList = $this->getPragmaIndexList($tableName);
565
        /** @psalm-var PragmaIndexList $indexes */
566 78
        $indexes = $this->normalizeRowKeyCase($indexList, true);
567 78
        $result = [
568 78
            self::PRIMARY_KEY => null,
569 78
            self::INDEXES => [],
570 78
            self::UNIQUES => [],
571 78
        ];
572
573 78
        foreach ($indexes as $index) {
574
            /** @psalm-var Column $columns */
575 59
            $columns = $this->getPragmaIndexInfo($index['name']);
576
577 59
            if ($index['origin'] === 'pk') {
578 24
                $result[self::PRIMARY_KEY] = (new Constraint())
579 24
                    ->columnNames(DbArrayHelper::getColumn($columns, 'name'));
580
            }
581
582 59
            if ($index['origin'] === 'u') {
583 54
                $result[self::UNIQUES][] = (new Constraint())
584 54
                    ->name($index['name'])
585 54
                    ->columnNames(DbArrayHelper::getColumn($columns, 'name'));
586
            }
587
588 59
            $result[self::INDEXES][] = (new IndexConstraint())
589 59
                ->primary($index['origin'] === 'pk')
590 59
                ->unique((bool) $index['unique'])
591 59
                ->name($index['name'])
592 59
                ->columnNames(DbArrayHelper::getColumn($columns, 'name'));
593
        }
594
595 78
        if (!isset($result[self::PRIMARY_KEY])) {
596
            /**
597
             * Extra check for PK in case of `INTEGER PRIMARY KEY` with ROWID.
598
             *
599
             * @link https://www.sqlite.org/lang_createtable.html#primkeyconst
600
             *
601
             * @psalm-var PragmaTableInfo $tableColumns
602
             */
603 54
            $tableColumns = $this->loadTableColumnsInfo($tableName);
604
605 54
            foreach ($tableColumns as $tableColumn) {
606 54
                if ($tableColumn['pk'] > 0) {
607 34
                    $result[self::PRIMARY_KEY] = (new Constraint())->columnNames([$tableColumn['name']]);
608 34
                    break;
609
                }
610
            }
611
        }
612
613 78
        foreach ($result as $type => $data) {
614 78
            $this->setTableMetadata($tableName, $type, $data);
615
        }
616
617 78
        return $result[$returnType];
618
    }
619
620
    /**
621
     * Creates a column schema for the database.
622
     *
623
     * This method may be overridden by child classes to create a DBMS-specific column schema.
624
     *
625
     * @param string $name Name of the column.
626
     */
627 115
    private function createColumnSchema(string $name): ColumnSchemaInterface
628
    {
629 115
        return new ColumnSchema($name);
630
    }
631
632
    /**
633
     * @throws Exception
634
     * @throws InvalidConfigException
635
     * @throws Throwable
636
     */
637 124
    private function getPragmaForeignKeyList(string $tableName): array
638
    {
639 124
        return $this->db->createCommand(
640 124
            'PRAGMA FOREIGN_KEY_LIST(' . $this->db->getQuoter()->quoteSimpleTableName($tableName) . ')'
641 124
        )->queryAll();
642
    }
643
644
    /**
645
     * @throws Exception
646
     * @throws InvalidConfigException
647
     * @throws Throwable
648
     */
649 60
    private function getPragmaIndexInfo(string $name): array
650
    {
651 60
        $column = $this->db
652 60
            ->createCommand('PRAGMA INDEX_INFO(' . (string) $this->db->getQuoter()->quoteValue($name) . ')')
653 60
            ->queryAll();
654
        /** @psalm-var Column $column */
655 60
        $column = $this->normalizeRowKeyCase($column, true);
656 60
        DbArrayHelper::multisort($column, 'seqno');
657
658 60
        return $column;
659
    }
660
661
    /**
662
     * @throws Exception
663
     * @throws InvalidConfigException
664
     * @throws Throwable
665
     */
666 79
    private function getPragmaIndexList(string $tableName): array
667
    {
668 79
        return $this->db
669 79
            ->createCommand('PRAGMA INDEX_LIST(' . (string) $this->db->getQuoter()->quoteValue($tableName) . ')')
670 79
            ->queryAll();
671
    }
672
673
    /**
674
     * @throws Exception
675
     * @throws InvalidConfigException
676
     * @throws Throwable
677
     */
678 168
    private function getPragmaTableInfo(string $tableName): array
679
    {
680 168
        return $this->db->createCommand(
681 168
            'PRAGMA TABLE_INFO(' . $this->db->getQuoter()->quoteSimpleTableName($tableName) . ')'
682 168
        )->queryAll();
683
    }
684
685
    /**
686
     * @throws Exception
687
     * @throws InvalidConfigException
688
     * @throws Throwable
689
     */
690 1
    protected function findViewNames(string $schema = ''): array
691
    {
692
        /** @psalm-var string[][] $views */
693 1
        $views = $this->db->createCommand(
694 1
            <<<SQL
695
            SELECT name as view FROM sqlite_master WHERE type = 'view' AND name NOT LIKE 'sqlite_%'
696 1
            SQL,
697 1
        )->queryAll();
698
699 1
        foreach ($views as $key => $view) {
700 1
            $views[$key] = $view['view'];
701
        }
702
703 1
        return $views;
704
    }
705
706
    /**
707
     * Returns the cache key for the specified table name.
708
     *
709
     * @param string $name the table name.
710
     *
711
     * @return array The cache key.
712
     */
713 226
    protected function getCacheKey(string $name): array
714
    {
715 226
        return array_merge([self::class], $this->generateCacheKey(), [$this->getRawTableName($name)]);
716
    }
717
718
    /**
719
     * Returns the cache tag name.
720
     *
721
     * This allows {@see refresh()} to invalidate all cached table schemas.
722
     *
723
     * @return string The cache tag name.
724
     */
725 210
    protected function getCacheTag(): string
726
    {
727 210
        return md5(serialize(array_merge([self::class], $this->generateCacheKey())));
728
    }
729
730
    /**
731
     * @throws Throwable
732
     */
733 159
    private function getJsonColumns(TableSchemaInterface $table): array
734
    {
735 159
        $result = [];
736
        /** @psalm-var CheckConstraint[] $checks */
737 159
        $checks = $this->getTableChecks((string) $table->getFullName());
738 159
        $regexp = '/\bjson_valid\(\s*["`\[]?(.+?)["`\]]?\s*\)/i';
739
740 159
        foreach ($checks as $check) {
741 19
            if (preg_match_all($regexp, $check->getExpression(), $matches, PREG_SET_ORDER)) {
742 18
                foreach ($matches as $match) {
743 18
                    $result[] = $match[1];
744
                }
745
            }
746
        }
747
748 159
        return $result;
749
    }
750
}
751