Passed
Push — master ( 3cf8ec...4591f3 )
by Wilmer
16:32 queued 14:58
created

Schema::loadTableConstraints()   F

Complexity

Conditions 16
Paths 364

Size

Total Lines 95
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 16.5

Importance

Changes 0
Metric Value
cc 16
eloc 50
nc 364
nop 2
dl 0
loc 95
ccs 42
cts 48
cp 0.875
crap 16.5
rs 2.6833
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

272
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ $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...
273
    {
274 12
        throw new NotSupportedException('SQLite does not support default value constraints.');
275
    }
276
277
    /**
278
     * Creates a query builder for the MySQL database.
279
     *
280
     * This method may be overridden by child classes to create a DBMS-specific query builder.
281
     *
282
     * @return QueryBuilder query builder instance.
283
     */
284 50
    public function createQueryBuilder(): QueryBuilder
285
    {
286 50
        return new QueryBuilder($this->getDb());
0 ignored issues
show
Bug introduced by
It seems like $this->getDb() can also be of type null; however, parameter $db of Yiisoft\Db\Sqlite\Query\...yBuilder::__construct() does only seem to accept Yiisoft\Db\Connection\Connection, 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

286
        return new QueryBuilder(/** @scrutinizer ignore-type */ $this->getDb());
Loading history...
287
    }
288
289
    /**
290
     * Create a column schema builder instance giving the type and value precision.
291
     *
292
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
293
     *
294
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
295
     * @param int|string|array $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
296
     *
297
     * @return ColumnSchemaBuilder column schema builder instance.
298
     */
299 3
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
300
    {
301 3
        return new ColumnSchemaBuilder($type, $length);
302
    }
303
304
    /**
305
     * Collects the table column metadata.
306
     *
307
     * @param TableSchema $table the table metadata.
308
     *
309
     * @throws Exception
310
     * @throws InvalidArgumentException
311
     * @throws InvalidConfigException
312
     *
313
     * @return bool whether the table exists in the database.
314
     */
315 77
    protected function findColumns($table): bool
316
    {
317 77
        $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

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

352
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName(/** @scrutinizer ignore-type */ $table->getName()) . ')';
Loading history...
353 75
        $keys = $this->getDb()->createCommand($sql)->queryAll();
354
355 75
        foreach ($keys as $key) {
356 5
            $id = (int) $key['id'];
357 5
            $fk = $table->getForeignKeys();
358 5
            if (!isset($fk[$id])) {
359 5
                $table->foreignKey($id, ([$key['table'], $key['from'] => $key['to']]));
360
            } else {
361
                /** composite FK */
362 5
                $table->compositeFK($id, $key['from'], $key['to']);
363
            }
364
        }
365 75
    }
366
367
    /**
368
     * Returns all unique indexes for the given table.
369
     *
370
     * Each array element is of the following structure:
371
     *
372
     * ```php
373
     * [
374
     *     'IndexName1' => ['col1' [, ...]],
375
     *     'IndexName2' => ['col2' [, ...]],
376
     * ]
377
     * ```
378
     *
379
     * @param TableSchema $table the table metadata.
380
     *
381
     * @throws Exception
382
     * @throws InvalidArgumentException
383
     * @throws InvalidConfigException
384
     *
385
     * @return array all unique indexes for the given table.
386
     */
387 1
    public function findUniqueIndexes($table): array
388
    {
389 1
        $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 1
        $indexes = $this->getDb()->createCommand($sql)->queryAll();
391 1
        $uniqueIndexes = [];
392
393 1
        foreach ($indexes as $index) {
394 1
            $indexName = $index['name'];
395 1
            $indexInfo = $this->getDb()->createCommand(
396 1
                'PRAGMA index_info(' . $this->quoteValue($index['name']) . ')'
397 1
            )->queryAll();
398
399 1
            if ($index['unique']) {
400 1
                $uniqueIndexes[$indexName] = [];
401 1
                foreach ($indexInfo as $row) {
402 1
                    $uniqueIndexes[$indexName][] = $row['name'];
403
                }
404
            }
405
        }
406
407 1
        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 75
    protected function loadColumnSchema($info): ColumnSchema
418
    {
419 75
        $column = $this->createColumnSchema();
420 75
        $column->name($info['name']);
421 75
        $column->allowNull(!$info['notnull']);
422 75
        $column->primaryKey($info['pk'] != 0);
423 75
        $column->dbType(\strtolower($info['type']));
424 75
        $column->unsigned(\strpos($column->getDbType(), 'unsigned') !== false);
425 75
        $column->type(self::TYPE_STRING);
426
427 75
        if (\preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
428 75
            $type = \strtolower($matches[1]);
429
430 75
            if (isset($this->typeMap[$type])) {
431 75
                $column->type($this->typeMap[$type]);
432
            }
433
434 75
            if (!empty($matches[2])) {
435 72
                $values = \explode(',', $matches[2]);
436 72
                $column->precision((int) $values[0]);
437 72
                $column->size((int) $values[0]);
438 72
                if (isset($values[1])) {
439 25
                    $column->scale((int) $values[1]);
440
                }
441 72
                if ($column->getSize() === 1 && ($type === 'tinyint' || $type === 'bit')) {
442 21
                    $column->type('boolean');
443 72
                } 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 75
        $column->phpType($this->getColumnPhpType($column));
454
455 75
        if (!$column->isPrimaryKey()) {
456 74
            if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
457 73
                $column->defaultValue(null);
458 60
            } elseif ($column->getType() === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
459 21
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP'));
460
            } else {
461 60
                $value = \trim($info['dflt_value'], "'\"");
462 60
                $column->defaultValue($column->phpTypecast($value));
463
            }
464
        }
465
466 75
        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
476
     * @throws InvalidConfigException
477
     * @throws NotSupportedException when unsupported isolation levels are used. SQLite only supports SERIALIZABLE and
478
     * READ UNCOMMITTED.
479
     *
480
     * {@see http://www.sqlite.org/pragma.html#pragma_read_uncommitted}
481
     */
482 3
    public function setTransactionIsolationLevel($level): void
483
    {
484
        switch ($level) {
485 3
            case Transaction::SERIALIZABLE:
486 1
                $this->getDb()->createCommand('PRAGMA read_uncommitted = False;')->execute();
487 1
                break;
488 3
            case Transaction::READ_UNCOMMITTED:
489 3
                $this->getDb()->createCommand('PRAGMA read_uncommitted = True;')->execute();
490 3
                break;
491
            default:
492
                throw new NotSupportedException(
493
                    \get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.'
494
                );
495
        }
496 3
    }
497
498
    /**
499
     * Returns table columns info.
500
     *
501
     * @param string $tableName table name.
502
     *
503
     * @throws Exception
504
     * @throws InvalidArgumentException
505
     * @throws InvalidConfigException
506
     *
507
     * @return array
508
     */
509 28
    private function loadTableColumnsInfo($tableName): array
510
    {
511 28
        $tableColumns = $this->getDb()->createCommand(
512 28
            'PRAGMA TABLE_INFO (' . $this->quoteValue($tableName) . ')'
513 28
        )->queryAll();
514
515 28
        $tableColumns = $this->normalizePdoRowKeyCase($tableColumns, true);
516
517 28
        return ArrayHelper::index($tableColumns, 'cid');
518
    }
519
520
    /**
521
     * Loads multiple types of constraints and returns the specified ones.
522
     *
523
     * @param string $tableName table name.
524
     * @param string $returnType return type: (primaryKey, indexes, uniques).
525
     *
526
     * @throws Exception
527
     * @throws InvalidArgumentException
528
     * @throws InvalidConfigException
529
     *
530
     * @return mixed constraints.
531
     */
532 52
    private function loadTableConstraints($tableName, $returnType)
533
    {
534 52
        $indexes = $this->getDb()->createCommand(
535 52
            'PRAGMA INDEX_LIST (' . $this->quoteValue($tableName) . ')'
536 52
        )->queryAll();
537
538 52
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
539 52
        $tableColumns = null;
540
541 52
        if (!empty($indexes) && !isset($indexes[0]['origin'])) {
542
            /**
543
             * SQLite may not have an "origin" column in INDEX_LIST.
544
             *
545
             * {See https://www.sqlite.org/src/info/2743846cdba572f6}
546
             */
547
            $tableColumns = $this->loadTableColumnsInfo($tableName);
548
        }
549
550
        $result = [
551 52
            'primaryKey' => null,
552
            'indexes' => [],
553
            'uniques' => [],
554
        ];
555
556 52
        foreach ($indexes as $index) {
557 42
            $columns = $this->getDb()->createCommand(
558 42
                'PRAGMA INDEX_INFO (' . $this->quoteValue($index['name']) . ')'
559 42
            )->queryAll();
560
561 42
            $columns = $this->normalizePdoRowKeyCase($columns, true);
562
563 42
            ArrayHelper::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC);
564
565 42
            if ($tableColumns !== null) {
566
                // SQLite may not have an "origin" column in INDEX_LIST
567
                $index['origin'] = 'c';
568
                if (!empty($columns) && $tableColumns[$columns[0]['cid']]['pk'] > 0) {
569
                    $index['origin'] = 'pk';
570
                } elseif ($index['unique'] && $this->isSystemIdentifier($index['name'])) {
571
                    $index['origin'] = 'u';
572
                }
573
            }
574
575 42
            $ic = new IndexConstraint();
576
577 42
            $ic->setIsPrimary($index['origin'] === 'pk');
578 42
            $ic->setIsUnique((bool) $index['unique']);
579 42
            $ic->setName($index['name']);
580 42
            $ic->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
581
582 42
            $result['indexes'][] = $ic;
583
584 42
            if ($index['origin'] === 'u') {
585 41
                $ct = new Constraint();
586
587 41
                $ct->setName($index['name']);
588 41
                $ct->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
589
590 41
                $result['uniques'][] = $ct;
591 25
            } elseif ($index['origin'] === 'pk') {
592 24
                $ct = new Constraint();
593
594 24
                $ct->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
595
596 24
                $result['primaryKey'] = $ct;
597
            }
598
        }
599
600 52
        if ($result['primaryKey'] === null) {
601
            /**
602
             * Additional check for PK in case of INTEGER PRIMARY KEY with ROWID.
603
             *
604
             * {@See https://www.sqlite.org/lang_createtable.html#primkeyconst}
605
             */
606
607 28
            if ($tableColumns === null) {
608 28
                $tableColumns = $this->loadTableColumnsInfo($tableName);
609
            }
610
611 28
            foreach ($tableColumns as $tableColumn) {
612 28
                if ($tableColumn['pk'] > 0) {
613 18
                    $ct = new Constraint();
614 18
                    $ct->setColumnNames([$tableColumn['name']]);
615
616 18
                    $result['primaryKey'] = $ct;
617 18
                    break;
618
                }
619
            }
620
        }
621
622 52
        foreach ($result as $type => $data) {
623 52
            $this->setTableMetadata($tableName, $type, $data);
624
        }
625
626 52
        return $result[$returnType];
627
    }
628
629
    /**
630
     * Return whether the specified identifier is a SQLite system identifier.
631
     *
632
     * @param string $identifier
633
     *
634
     * @return bool
635
     *
636
     * {@see https://www.sqlite.org/src/artifact/74108007d286232f}
637
     */
638
    private function isSystemIdentifier($identifier): bool
639
    {
640
        return \strncmp($identifier, 'sqlite_', 7) === 0;
641
    }
642
}
643