Schema   F
last analyzed

Complexity

Total Complexity 76

Size/Duplication

Total Lines 699
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 14
Bugs 1 Features 1
Metric Value
wmc 76
eloc 224
c 14
b 1
f 1
dl 0
loc 699
ccs 219
cts 219
cp 1
rs 2.32

26 Methods

Rating   Name   Duplication   Size   Complexity  
A findTableNames() 0 7 1
A findConstraints() 0 14 2
A loadTableUniques() 0 5 2
B loadTableConstraints() 0 53 8
B loadTableChecks() 0 39 9
A loadTableColumnsInfo() 0 8 1
B findColumns() 0 26 7
A findUniqueIndexes() 0 19 4
A loadTableDefaultValues() 0 3 1
A normalizeDefaultValue() 0 13 4
A getSchemaDefaultValues() 0 3 1
B loadColumnSchema() 0 41 11
A createColumnSchema() 0 3 1
A getPragmaIndexInfo() 0 10 1
B loadTableForeignKeys() 0 42 6
A getCacheTag() 0 3 1
A getPragmaForeignKeyList() 0 6 1
A loadTableIndexes() 0 4 1
A loadTableSchema() 0 14 2
A getCacheKey() 0 3 1
A getJsonColumns() 0 16 4
A createColumn() 0 3 1
A loadTablePrimaryKey() 0 5 2
A findViewNames() 0 14 2
A getPragmaIndexList() 0 6 1
A getPragmaTableInfo() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

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

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

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

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

738
        return array_merge([self::class], $this->generateCacheKey(), [/** @scrutinizer ignore-deprecated */ $this->getRawTableName($name)]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
739
    }
740
741
    /**
742
     * Returns the cache tag name.
743
     *
744
     * This allows {@see refresh()} to invalidate all cached table schemas.
745
     *
746
     * @return string The cache tag name.
747
     */
748 223
    protected function getCacheTag(): string
749
    {
750 223
        return md5(serialize(array_merge([self::class], $this->generateCacheKey())));
751
    }
752
753
    /**
754
     * @throws Throwable
755
     */
756 166
    private function getJsonColumns(TableSchemaInterface $table): array
757
    {
758 166
        $result = [];
759
        /** @psalm-var CheckConstraint[] $checks */
760 166
        $checks = $this->getTableChecks((string) $table->getFullName());
761 166
        $regexp = '/\bjson_valid\(\s*["`\[]?(.+?)["`\]]?\s*\)/i';
762
763 166
        foreach ($checks as $check) {
764 32
            if (preg_match_all($regexp, $check->getExpression(), $matches, PREG_SET_ORDER) > 0) {
765 31
                foreach ($matches as $match) {
766 31
                    $result[] = $match[1];
767
                }
768
            }
769
        }
770
771 166
        return $result;
772
    }
773
}
774