Passed
Pull Request — master (#75)
by Wilmer
04:57
created

Schema::findUniqueIndexes()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 13
c 2
b 0
f 0
dl 0
loc 21
ccs 0
cts 14
cp 0
rs 9.8333
cc 4
nc 4
nop 1
crap 20
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite;
6
7
use Throwable;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Arrays\ArraySorter;
10
use Yiisoft\Db\Constraint\CheckConstraint;
11
use Yiisoft\Db\Constraint\Constraint;
12
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
13
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
14
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
15
use Yiisoft\Db\Constraint\IndexConstraint;
16
use Yiisoft\Db\Exception\Exception;
17
use Yiisoft\Db\Exception\InvalidArgumentException;
18
use Yiisoft\Db\Exception\InvalidConfigException;
19
use Yiisoft\Db\Exception\NotSupportedException;
20
use Yiisoft\Db\Expression\Expression;
21
use Yiisoft\Db\Schema\ColumnSchema;
22
use Yiisoft\Db\Schema\Schema as AbstractSchema;
23
use Yiisoft\Db\Transaction\Transaction;
24
25
use function count;
26
use function explode;
27
use function preg_match;
28
use function strncasecmp;
29
use function strncmp;
30
use function strpos;
31
use function strtolower;
32
use function trim;
33
34
/**
35
 * Schema is the class for retrieving metadata from a SQLite (2/3) database.
36
 *
37
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction. This can be
38
 * either {@see Transaction::READ_UNCOMMITTED} or {@see Transaction::SERIALIZABLE}.
39
 */
40
final class Schema extends AbstractSchema implements ConstraintFinderInterface
41
{
42
    use ConstraintFinderTrait;
43
44
    /**
45
     * @var array mapping from physical column types (keys) to abstract column types (values)
46
     */
47
    private array $typeMap = [
48
        'tinyint' => self::TYPE_TINYINT,
49
        'bit' => self::TYPE_SMALLINT,
50
        'boolean' => self::TYPE_BOOLEAN,
51
        'bool' => self::TYPE_BOOLEAN,
52
        'smallint' => self::TYPE_SMALLINT,
53
        'mediumint' => self::TYPE_INTEGER,
54
        'int' => self::TYPE_INTEGER,
55
        'integer' => self::TYPE_INTEGER,
56
        'bigint' => self::TYPE_BIGINT,
57
        'float' => self::TYPE_FLOAT,
58
        'double' => self::TYPE_DOUBLE,
59
        'real' => self::TYPE_FLOAT,
60
        'decimal' => self::TYPE_DECIMAL,
61
        'numeric' => self::TYPE_DECIMAL,
62
        'tinytext' => self::TYPE_TEXT,
63
        'mediumtext' => self::TYPE_TEXT,
64
        'longtext' => self::TYPE_TEXT,
65
        'text' => self::TYPE_TEXT,
66
        'varchar' => self::TYPE_STRING,
67
        'string' => self::TYPE_STRING,
68
        'char' => self::TYPE_CHAR,
69
        'blob' => self::TYPE_BINARY,
70
        'datetime' => self::TYPE_DATETIME,
71
        'year' => self::TYPE_DATE,
72
        'date' => self::TYPE_DATE,
73
        'time' => self::TYPE_TIME,
74
        'timestamp' => self::TYPE_TIMESTAMP,
75
        'enum' => self::TYPE_STRING,
76
    ];
77
78
    /**
79
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
80
     * case starting and ending characters are different.
81
     */
82
    protected $tableQuoteCharacter = '`';
83
84
    /**
85
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
86
     * and ending characters are different.
87
     */
88
    protected $columnQuoteCharacter = '`';
89
90
    /**
91
     * Returns all table names in the database.
92
     *
93
     * This method should be overridden by child classes in order to support this feature because the default
94
     * implementation simply throws an exception.
95
     *
96
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
97
     *
98
     * @throws Exception|InvalidConfigException|Throwable
99
     *
100
     * @return array all table names in the database. The names have NO schema name prefix.
101
     */
102 5
    protected function findTableNames(string $schema = ''): array
103
    {
104 5
        $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name";
105
106 5
        return $this->getDb()->createCommand($sql)->queryColumn();
107
    }
108
109
    /**
110
     * Loads the metadata for the specified table.
111
     *
112
     * @param string $name table name.
113
     *
114
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
115
     *
116
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
117
     */
118 82
    protected function loadTableSchema(string $name): ?TableSchema
119
    {
120 82
        $table = new TableSchema();
121
122 82
        $table->name($name);
123 82
        $table->fullName($name);
124
125 82
        if ($this->findColumns($table)) {
126 76
            $this->findConstraints($table);
127
128 76
            return $table;
129
        }
130
131 14
        return null;
132
    }
133
134
    /**
135
     * Loads a primary key for the given table.
136
     *
137
     * @param string $tableName table name.
138
     *
139
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
140
     *
141
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
142
     */
143 30
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
144
    {
145 30
        return $this->loadTableConstraints($tableName, 'primaryKey');
146
    }
147
148
    /**
149
     * Loads all foreign keys for the given table.
150
     *
151
     * @param string $tableName table name.
152
     *
153
     * @throws Exception|InvalidConfigException|Throwable
154
     *
155
     * @return ForeignKeyConstraint[] foreign keys for the given table.
156
     */
157 4
    protected function loadTableForeignKeys(string $tableName): array
158
    {
159 4
        $foreignKeys = $this->getDb()->createCommand(
160 4
            'PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')'
161 4
        )->queryAll();
162
163 4
        $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true);
164
165 4
        $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table');
166
167 4
        ArraySorter::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC);
168
169 4
        $result = [];
170
171 4
        foreach ($foreignKeys as $table => $foreignKey) {
172 4
            $fk = (new ForeignKeyConstraint())
173 4
                ->columnNames(ArrayHelper::getColumn($foreignKey, 'from'))
174 4
                ->foreignTableName($table)
175 4
                ->foreignColumnNames(ArrayHelper::getColumn($foreignKey, 'to'))
176 4
                ->onDelete($foreignKey[0]['on_delete'] ?? null)
177 4
                ->onUpdate($foreignKey[0]['on_update'] ?? null);
178
179 4
            $result[] = $fk;
180
        }
181
182 4
        return $result;
183
    }
184
185
    /**
186
     * Loads all indexes for the given table.
187
     *
188
     * @param string $tableName table name.
189
     *
190
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
191
     *
192
     * @return IndexConstraint[] indexes for the given table.
193
     */
194 11
    protected function loadTableIndexes(string $tableName): array
195
    {
196 11
        return $this->loadTableConstraints($tableName, 'indexes');
197
    }
198
199
    /**
200
     * Loads all unique constraints for the given table.
201
     *
202
     * @param string $tableName table name.
203
     *
204
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
205
     *
206
     * @return Constraint[] unique constraints for the given table.
207
     */
208 14
    protected function loadTableUniques(string $tableName): array
209
    {
210 14
        return $this->loadTableConstraints($tableName, 'uniques');
211
    }
212
213
    /**
214
     * Loads all check constraints for the given table.
215
     *
216
     * @param string $tableName table name.
217
     *
218
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
219
     *
220
     * @return CheckConstraint[] check constraints for the given table.
221
     */
222 12
    protected function loadTableChecks(string $tableName): array
223
    {
224 12
        $sql = $this->getDb()->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [
225 12
            ':tableName' => $tableName,
226 12
        ])->queryScalar();
227
228
        /** @var SqlToken[]|SqlToken[][]|SqlToken[][][] $code */
229 12
        $code = (new SqlTokenizer($sql))->tokenize();
0 ignored issues
show
Bug introduced by
It seems like $sql can also be of type false and null; however, parameter $sql of Yiisoft\Db\Sqlite\SqlTokenizer::__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

229
        $code = (new SqlTokenizer(/** @scrutinizer ignore-type */ $sql))->tokenize();
Loading history...
230
231 12
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
232
233 12
        if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $firstMatchIndex seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $lastMatchIndex seems to be never defined.
Loading history...
234
            return [];
235
        }
236
237 12
        $createTableToken = $code[0][$lastMatchIndex - 1];
238 12
        $result = [];
239 12
        $offset = 0;
240
241 12
        while (true) {
242 12
            $pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
243
244 12
            if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
245 12
                break;
246
            }
247
248 3
            $checkSql = $createTableToken[$offset - 1]->getSql();
249 3
            $name = null;
250 3
            $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
251
252
            if (
253 3
                isset($createTableToken[$firstMatchIndex - 2])
254 3
                && $createTableToken->matches($pattern, $firstMatchIndex - 2)
255
            ) {
256
                $name = $createTableToken[$firstMatchIndex - 1]->getContent();
257
            }
258
259 3
            $ck = (new CheckConstraint())
260 3
                ->name($name)
261 3
                ->expression($checkSql);
262
263 3
            $result[] = $ck;
264
        }
265
266 12
        return $result;
267
    }
268
269
    /**
270
     * Loads all default value constraints for the given table.
271
     *
272
     * @param string $tableName table name.
273
     *
274
     * @throws NotSupportedException
275
     *
276
     * @return array default value constraints for the given table.
277
     */
278 12
    protected function loadTableDefaultValues(string $tableName): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

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

278
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ string $tableName): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
279
    {
280 12
        throw new NotSupportedException('SQLite does not support default value constraints.');
281
    }
282
283
    /**
284
     * Creates a query builder for the MySQL database.
285
     *
286
     * This method may be overridden by child classes to create a DBMS-specific query builder.
287
     *
288
     * @return QueryBuilder query builder instance.
289
     */
290 59
    public function createQueryBuilder(): QueryBuilder
291
    {
292 59
        return new QueryBuilder($this->getDb());
293
    }
294
295
    /**
296
     * Create a column schema builder instance giving the type and value precision.
297
     *
298
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
299
     *
300
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
301
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
302
     *
303
     * @return ColumnSchemaBuilder column schema builder instance.
304
     */
305 3
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
306
    {
307 3
        return new ColumnSchemaBuilder($type, $length);
308
    }
309
310
    /**
311
     * Collects the table column metadata.
312
     *
313
     * @param TableSchema $table the table metadata.
314
     *
315
     * @throws Exception|InvalidConfigException|Throwable
316
     *
317
     * @return bool whether the table exists in the database.
318
     */
319 82
    protected function findColumns(TableSchema $table): bool
320
    {
321 82
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName($table->getName()) . ')';
0 ignored issues
show
Bug introduced by
It seems like $table->getName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteSimpleTableName() 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

321
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName(/** @scrutinizer ignore-type */ $table->getName()) . ')';
Loading history...
322 82
        $columns = $this->getDb()->createCommand($sql)->queryAll();
323
324 82
        if (empty($columns)) {
325 14
            return false;
326
        }
327
328 76
        foreach ($columns as $info) {
329 76
            $column = $this->loadColumnSchema($info);
330 76
            $table->columns($column->getName(), $column);
331 76
            if ($column->isPrimaryKey()) {
332 52
                $table->primaryKey($column->getName());
333
            }
334
        }
335
336 76
        $pk = $table->getPrimaryKey();
337 76
        if (count($pk) === 1 && !strncasecmp($table->getColumn($pk[0])->getDbType(), 'int', 3)) {
338 52
            $table->sequenceName('');
339 52
            $table->getColumn($pk[0])->autoIncrement(true);
340
        }
341
342 76
        return true;
343
    }
344
345
    /**
346
     * Collects the foreign key column details for the given table.
347
     *
348
     * @param TableSchema $table the table metadata.
349
     *
350
     * @throws Exception|InvalidConfigException|Throwable
351
     */
352 76
    protected function findConstraints(TableSchema $table): void
353
    {
354 76
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName($table->getName()) . ')';
0 ignored issues
show
Bug introduced by
It seems like $table->getName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteSimpleTableName() 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

354
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName(/** @scrutinizer ignore-type */ $table->getName()) . ')';
Loading history...
355 76
        $keys = $this->getDb()->createCommand($sql)->queryAll();
356
357 76
        foreach ($keys as $key) {
358 5
            $id = (int) $key['id'];
359 5
            $fk = $table->getForeignKeys();
360 5
            if (!isset($fk[$id])) {
361 5
                $table->foreignKey($id, ([$key['table'], $key['from'] => $key['to']]));
362
            } else {
363
                /** composite FK */
364 5
                $table->compositeFK($id, $key['from'], $key['to']);
365
            }
366
        }
367 76
    }
368
369
    /**
370
     * Returns all unique indexes for the given table.
371
     *
372
     * Each array element is of the following structure:
373
     *
374
     * ```php
375
     * [
376
     *     'IndexName1' => ['col1' [, ...]],
377
     *     'IndexName2' => ['col2' [, ...]],
378
     * ]
379
     * ```
380
     *
381
     * @param TableSchema $table the table metadata.
382
     *
383
     * @throws Exception|InvalidConfigException|Throwable
384
     *
385
     * @return array all unique indexes for the given table.
386
     */
387
    public function findUniqueIndexes(TableSchema $table): array
388
    {
389
        $sql = 'PRAGMA index_list(' . $this->quoteSimpleTableName($table->getName()) . ')';
0 ignored issues
show
Bug introduced by
It seems like $table->getName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteSimpleTableName() 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

389
        $sql = 'PRAGMA index_list(' . $this->quoteSimpleTableName(/** @scrutinizer ignore-type */ $table->getName()) . ')';
Loading history...
390
        $indexes = $this->getDb()->createCommand($sql)->queryAll();
391
        $uniqueIndexes = [];
392
393
        foreach ($indexes as $index) {
394
            $indexName = $index['name'];
395
            $indexInfo = $this->getDb()->createCommand(
396
                'PRAGMA index_info(' . $this->quoteValue($index['name']) . ')'
397
            )->queryAll();
398
399
            if ($index['unique']) {
400
                $uniqueIndexes[$indexName] = [];
401
                foreach ($indexInfo as $row) {
402
                    $uniqueIndexes[$indexName][] = $row['name'];
403
                }
404
            }
405
        }
406
407
        return $uniqueIndexes;
408
    }
409
410
    /**
411
     * Loads the column information into a {@see ColumnSchema} object.
412
     *
413
     * @param array $info column information.
414
     *
415
     * @return ColumnSchema the column schema object.
416
     */
417 76
    protected function loadColumnSchema(array $info): ColumnSchema
418
    {
419 76
        $column = $this->createColumnSchema();
420 76
        $column->name($info['name']);
421 76
        $column->allowNull(!$info['notnull']);
422 76
        $column->primaryKey($info['pk'] != 0);
423 76
        $column->dbType(strtolower($info['type']));
424 76
        $column->unsigned(strpos($column->getDbType(), 'unsigned') !== false);
425 76
        $column->type(self::TYPE_STRING);
426
427 76
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
428 76
            $type = strtolower($matches[1]);
429
430 76
            if (isset($this->typeMap[$type])) {
431 76
                $column->type($this->typeMap[$type]);
432
            }
433
434 76
            if (!empty($matches[2])) {
435 71
                $values = explode(',', $matches[2]);
436 71
                $column->precision((int) $values[0]);
437 71
                $column->size((int) $values[0]);
438 71
                if (isset($values[1])) {
439 26
                    $column->scale((int) $values[1]);
440
                }
441 71
                if ($column->getSize() === 1 && ($type === 'tinyint' || $type === 'bit')) {
442 21
                    $column->type('boolean');
443 71
                } elseif ($type === 'bit') {
444
                    if ($column->getSize() > 32) {
445
                        $column->type('bigint');
446
                    } elseif ($column->getSize() === 32) {
447
                        $column->type('integer');
448
                    }
449
                }
450
            }
451
        }
452
453 76
        $column->phpType($this->getColumnPhpType($column));
454
455 76
        if (!$column->isPrimaryKey()) {
456 74
            if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
457 72
                $column->defaultValue(null);
458 61
            } elseif ($column->getType() === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
459 21
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP'));
460
            } else {
461 61
                $value = trim($info['dflt_value'], "'\"");
462 61
                $column->defaultValue($column->phpTypecast($value));
463
            }
464
        }
465
466 76
        return $column;
467
    }
468
469
    /**
470
     * Sets the isolation level of the current transaction.
471
     *
472
     * @param string $level The transaction isolation level to use for this transaction. This can be either
473
     * {@see Transaction::READ_UNCOMMITTED} or {@see Transaction::SERIALIZABLE}.
474
     *
475
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable when unsupported isolation levels are
476
     * used. SQLite only supports SERIALIZABLE and READ UNCOMMITTED.
477
     *
478
     * {@see http://www.sqlite.org/pragma.html#pragma_read_uncommitted}
479
     */
480 2
    public function setTransactionIsolationLevel(string $level): void
481
    {
482
        switch ($level) {
483 2
            case Transaction::SERIALIZABLE:
484 1
                $this->getDb()->createCommand('PRAGMA read_uncommitted = False;')->execute();
485 1
                break;
486 2
            case Transaction::READ_UNCOMMITTED:
487 2
                $this->getDb()->createCommand('PRAGMA read_uncommitted = True;')->execute();
488 2
                break;
489
            default:
490
                throw new NotSupportedException(
491
                    self::class . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.'
492
                );
493
        }
494 2
    }
495
496
    /**
497
     * Returns table columns info.
498
     *
499
     * @param string $tableName table name.
500
     *
501
     * @throws Exception|InvalidConfigException|Throwable
502
     *
503
     * @return array
504
     */
505 30
    private function loadTableColumnsInfo(string $tableName): array
506
    {
507 30
        $tableColumns = $this->getDb()->createCommand(
508 30
            'PRAGMA TABLE_INFO (' . $this->quoteValue($tableName) . ')'
509 30
        )->queryAll();
510
511 30
        $tableColumns = $this->normalizePdoRowKeyCase($tableColumns, true);
512
513 30
        return ArrayHelper::index($tableColumns, 'cid');
514
    }
515
516
    /**
517
     * Loads multiple types of constraints and returns the specified ones.
518
     *
519
     * @param string $tableName table name.
520
     * @param string $returnType return type: (primaryKey, indexes, uniques).
521
     *
522
     * @throws Exception|InvalidConfigException|Throwable
523
     *
524
     * @return mixed constraints.
525
     */
526 54
    private function loadTableConstraints(string $tableName, string $returnType)
527
    {
528 54
        $tableColumns = null;
529 54
        $indexList = $this->getDb()->createCommand(
530 54
            'PRAGMA INDEX_LIST (' . $this->quoteValue($tableName) . ')'
531 54
        )->queryAll();
532 54
        $indexes = $this->normalizePdoRowKeyCase($indexList, true);
533
534 54
        if (!empty($indexes) && !isset($indexes[0]['origin'])) {
535
            /**
536
             * SQLite may not have an "origin" column in INDEX_LIST.
537
             *
538
             * {See https://www.sqlite.org/src/info/2743846cdba572f6}
539
             */
540
            $tableColumns = $this->loadTableColumnsInfo($tableName);
541
        }
542
543 54
        $result = [
544
            'primaryKey' => null,
545
            'indexes' => [],
546
            'uniques' => [],
547
        ];
548
549 54
        foreach ($indexes as $index) {
550 44
            $columns = $this->getPragmaIndexInfo($index['name']);
551
552 44
            if ($tableColumns !== null) {
553
                /** SQLite may not have an "origin" column in INDEX_LIST */
554
                $index['origin'] = 'c';
555
556
                if (!empty($columns) && $tableColumns[$columns[0]['cid']]['pk'] > 0) {
557
                    $index['origin'] = 'pk';
558
                } elseif ($index['unique'] && $this->isSystemIdentifier($index['name'])) {
559
                    $index['origin'] = 'u';
560
                }
561
            }
562
563 44
            $ic = (new IndexConstraint())
564 44
                ->primary($index['origin'] === 'pk')
565 44
                ->unique((bool) $index['unique'])
566 44
                ->name($index['name'])
567 44
                ->columnNames(ArrayHelper::getColumn($columns, 'name'));
568
569 44
            $result['indexes'][] = $ic;
570
571 44
            if (($index['origin'] === 'u') || ($index['origin'] === 'c' && $index['unique'])) {
572 44
                $ct = (new Constraint())
573 44
                    ->name($index['name'])
574 44
                    ->columnNames(ArrayHelper::getColumn($columns, 'name'));
575
576 44
                $result['uniques'][] = $ct;
577 25
            } elseif ($index['origin'] === 'pk') {
578 24
                $ct = (new Constraint())
579 24
                    ->columnNames(ArrayHelper::getColumn($columns, 'name'));
580
581 24
                $result['primaryKey'] = $ct;
582
            }
583
        }
584
585 54
        if ($result['primaryKey'] === null) {
586
            /**
587
             * Additional check for PK in case of INTEGER PRIMARY KEY with ROWID.
588
             *
589
             * {@See https://www.sqlite.org/lang_createtable.html#primkeyconst}
590
             */
591
592 30
            if ($tableColumns === null) {
593 30
                $tableColumns = $this->loadTableColumnsInfo($tableName);
594
            }
595
596 30
            foreach ($tableColumns as $tableColumn) {
597 30
                if ($tableColumn['pk'] > 0) {
598 18
                    $ct = (new Constraint())
599 18
                        ->columnNames([$tableColumn['name']]);
600
601 18
                    $result['primaryKey'] = $ct;
602 18
                    break;
603
                }
604
            }
605
        }
606
607 54
        foreach ($result as $type => $data) {
608 54
            $this->setTableMetadata($tableName, $type, $data);
609
        }
610
611 54
        return $result[$returnType];
612
    }
613
614
    /**
615
     * Return whether the specified identifier is a SQLite system identifier.
616
     *
617
     * @param string $identifier
618
     *
619
     * @return bool
620
     *
621
     * {@see https://www.sqlite.org/src/artifact/74108007d286232f}
622
     */
623
    private function isSystemIdentifier(string $identifier): bool
624
    {
625
        return strncmp($identifier, 'sqlite_', 7) === 0;
626
    }
627
628
    /**
629
     * Creates a column schema for the database.
630
     *
631
     * This method may be overridden by child classes to create a DBMS-specific column schema.
632
     *
633
     * @return ColumnSchema column schema instance.
634
     */
635 76
    private function createColumnSchema(): ColumnSchema
636
    {
637 76
        return new ColumnSchema();
638
    }
639
640
    /**
641
     * @throws Exception|InvalidConfigException|Throwable
642
     */
643 44
    private function getPragmaIndexInfo(string $name): array
644
    {
645 44
        $column = $this->getDb()->createCommand('PRAGMA INDEX_INFO (' . $this->quoteValue($name) . ')')->queryAll();
646 44
        $columns = $this->normalizePdoRowKeyCase($column, true);
647 44
        ArraySorter::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC);
648
649 44
        return $columns;
650
    }
651
}
652