Test Failed
Pull Request — master (#286)
by Sergei
14:44
created

Schema::getPragmaForeignKeyList()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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

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

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

753
        if (preg_match('#^[^(]+?((?:\s*--[^\n]*|\s*/\*.*?\*/)+)\s*\(#', /** @scrutinizer ignore-type */ $sql, $matches) === 1) {
Loading history...
754 160
            $comment = $this->filterComment($matches[1]);
755 24
            $tableSchema->comment($comment);
756 23
        }
757 23
    }
758
759
    /**
760
     * @throws Exception
761
     * @throws InvalidConfigException
762 160
     * @throws Throwable
763
     */
764
    private function findComments(TableSchemaInterface $tableSchema): void
765
    {
766
        $sql = $tableSchema->getCreateSql();
767
768
        preg_match('#^(?:[^(]*--[^\n]*|[^(]*/\*.*?\*/)*[^(]*\((.*)\)[^)]*$#s', $sql, $matches);
0 ignored issues
show
Bug introduced by
It seems like $sql 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

768
        preg_match('#^(?:[^(]*--[^\n]*|[^(]*/\*.*?\*/)*[^(]*\((.*)\)[^)]*$#s', /** @scrutinizer ignore-type */ $sql, $matches);
Loading history...
769
770
        $columnsDefinition = $matches[1];
771
772
        $identifierPattern = '(?:([`"])([^`"]+)\1|\[([^\]]+)\]|([A-Za-z_]\w*))';
773
        $notCommaPattern = '(?:[^,]|\([^()]+\))*?';
774
        $commentPattern = '(?:\s*--[^\n]*|\s*/\*.*?\*/)';
775
776
        $pattern = "#$identifierPattern\s*$notCommaPattern,?($commentPattern+)#";
777
778
        if (preg_match_all($pattern, $columnsDefinition, $matches, PREG_SET_ORDER)) {
779
            foreach ($matches as $match) {
780
                $columnName = $match[2] ?: $match[3] ?: $match[4];
781
                $comment = $this->filterComment($match[5] ?: $match[6]);
782
783
                $tableSchema->getColumn($columnName)?->comment($comment);
784
            }
785
        }
786
    }
787
788
    private function filterComment(string $comment): string
789
    {
790
        preg_match_all('#--([^\n]*)|/\*(.*?)\*/#', $comment, $matches, PREG_SET_ORDER);
791
792
        $lines = [];
793
794
        foreach ($matches as $match) {
795
            $lines[] = trim($match[1] ?: $match[2]);
796
        }
797
798
        return implode("\n", $lines);
799
    }
800
801
    /**
802
     * Returns the `CREATE TABLE` SQL string.
803
     *
804
     * @param string $name The table name.
805
     *
806
     * @throws Exception
807
     * @throws InvalidConfigException
808
     * @throws Throwable
809
     * @return string The `CREATE TABLE` SQL string.
810
     */
811
    private function getCreateTableSql(string $name): string
812
    {
813
        $sql = $this->db->createCommand(
814
            'SELECT `sql` FROM `sqlite_master` WHERE `name` = :tableName',
815
            [':tableName' => $name],
816
        )->queryScalar();
817
818
        return (string)$sql;
819
    }
820
821
    /**
822
     * @throws Throwable
823
     */
824
    private function getJsonColumns(TableSchemaInterface $table): array
825
    {
826
        $result = [];
827
        /** @psalm-var CheckConstraint[] $checks */
828
        $checks = $this->getTableChecks((string) $table->getFullName());
829
        $regexp = '/\bjson_valid\(\s*["`\[]?(.+?)["`\]]?\s*\)/i';
830
831
        foreach ($checks as $check) {
832
            if (preg_match_all($regexp, $check->getExpression(), $matches, PREG_SET_ORDER)) {
833
                foreach ($matches as $match) {
834
                    $result[] = $match[1];
835
                }
836
            }
837
        }
838
839
        return $result;
840
    }
841
}
842