Passed
Pull Request — master (#263)
by Sergei
04:53 queued 51s
created

Schema::getJsonColumns()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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

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

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