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

Schema::loadColumnSchema()   C

Complexity

Conditions 17
Paths 92

Size

Total Lines 50
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 17.47

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 17
eloc 34
c 2
b 0
f 0
nc 92
nop 1
dl 0
loc 50
ccs 30
cts 34
cp 0.8824
crap 17.47
rs 5.2166

How to fix   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