Passed
Push — master ( b1a1af...f07e26 )
by Alexander
25:59 queued 19:47
created

Schema::setTransactionIsolationLevel()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0987

Importance

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

228
        $code = (new SqlTokenizer(/** @scrutinizer ignore-type */ $sql))->tokenize();
Loading history...
229
230 12
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
231
232 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...
233
            return [];
234
        }
235
236 12
        $createTableToken = $code[0][$lastMatchIndex - 1];
237 12
        $result = [];
238 12
        $offset = 0;
239
240 12
        while (true) {
241 12
            $pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
242
243 12
            if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
244 12
                break;
245
            }
246
247 3
            $checkSql = $createTableToken[$offset - 1]->getSql();
248 3
            $name = null;
249 3
            $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
250
251
            if (
252 3
                isset($createTableToken[$firstMatchIndex - 2])
253 3
                && $createTableToken->matches($pattern, $firstMatchIndex - 2)
254
            ) {
255
                $name = $createTableToken[$firstMatchIndex - 1]->getContent();
256
            }
257
258 3
            $ck = (new CheckConstraint())
259 3
                ->name($name)
260 3
                ->expression($checkSql);
261
262 3
            $result[] = $ck;
263
        }
264
265 12
        return $result;
266
    }
267
268
    /**
269
     * Loads all default value constraints for the given table.
270
     *
271
     * @param string $tableName table name.
272
     *
273
     * @throws NotSupportedException
274
     *
275
     * @return array default value constraints for the given table.
276
     */
277 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

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

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

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

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