Passed
Push — master ( 707555...d3022f )
by Wilmer
11:56 queued 09:57
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\Arrays\ArraySorter;
9
use Yiisoft\Db\Constraint\CheckConstraint;
10
use Yiisoft\Db\Constraint\Constraint;
11
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
12
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
13
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
14
use Yiisoft\Db\Constraint\IndexConstraint;
15
use Yiisoft\Db\Exception\Exception;
16
use Yiisoft\Db\Exception\InvalidArgumentException;
17
use Yiisoft\Db\Exception\InvalidConfigException;
18
use Yiisoft\Db\Exception\NotSupportedException;
19
use Yiisoft\Db\Expression\Expression;
20
use Yiisoft\Db\Schema\ColumnSchema;
21
use Yiisoft\Db\Schema\Schema as AbstractSchema;
22
use Yiisoft\Db\Sqlite\Query\QueryBuilder;
23
use Yiisoft\Db\Sqlite\Token\SqlToken;
24
use Yiisoft\Db\Sqlite\Token\SqlTokenizer;
25
use Yiisoft\Db\Transaction\Transaction;
26
27
/**
28
 * Schema is the class for retrieving metadata from a SQLite (2/3) database.
29
 *
30
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction. This can be
31
 * either {@see Transaction::READ_UNCOMMITTED} or {@see Transaction::SERIALIZABLE}.
32
 */
33
class Schema extends AbstractSchema implements ConstraintFinderInterface
34
{
35
    use ConstraintFinderTrait;
36
37
    protected string $tableQuoteCharacter = '`';
38
    protected string $columnQuoteCharacter = '`';
39
40
    /**
41
     * @var array mapping from physical column types (keys) to abstract column types (values)
42
     */
43
    private array $typeMap = [
44
        'tinyint' => self::TYPE_TINYINT,
45
        'bit' => self::TYPE_SMALLINT,
46
        'boolean' => self::TYPE_BOOLEAN,
47
        'bool' => self::TYPE_BOOLEAN,
48
        'smallint' => self::TYPE_SMALLINT,
49
        'mediumint' => self::TYPE_INTEGER,
50
        'int' => self::TYPE_INTEGER,
51
        'integer' => self::TYPE_INTEGER,
52
        'bigint' => self::TYPE_BIGINT,
53
        'float' => self::TYPE_FLOAT,
54
        'double' => self::TYPE_DOUBLE,
55
        'real' => self::TYPE_FLOAT,
56
        'decimal' => self::TYPE_DECIMAL,
57
        'numeric' => self::TYPE_DECIMAL,
58
        'tinytext' => self::TYPE_TEXT,
59
        'mediumtext' => self::TYPE_TEXT,
60
        'longtext' => self::TYPE_TEXT,
61
        'text' => self::TYPE_TEXT,
62
        'varchar' => self::TYPE_STRING,
63
        'string' => self::TYPE_STRING,
64
        'char' => self::TYPE_CHAR,
65
        'blob' => self::TYPE_BINARY,
66
        'datetime' => self::TYPE_DATETIME,
67
        'year' => self::TYPE_DATE,
68
        'date' => self::TYPE_DATE,
69
        'time' => self::TYPE_TIME,
70
        'timestamp' => self::TYPE_TIMESTAMP,
71
        'enum' => self::TYPE_STRING,
72
    ];
73
74
    /**
75
     * Returns all table names in the database.
76
     *
77
     * This method should be overridden by child classes in order to support this feature because the default
78
     * implementation simply throws an exception.
79
     *
80
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
81
     *
82
     * @throws Exception
83
     * @throws InvalidArgumentException
84
     * @throws InvalidConfigException
85
     *
86
     * @return array all table names in the database. The names have NO schema name prefix.
87
     */
88 5
    protected function findTableNames(string $schema = ''): array
89
    {
90 5
        $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name";
91
92 5
        return $this->getDb()->createCommand($sql)->queryColumn();
93
    }
94
95
    /**
96
     * Loads the metadata for the specified table.
97
     *
98
     * @param string $name table name.
99
     *
100
     * @throws Exception
101
     * @throws InvalidArgumentException
102
     * @throws InvalidConfigException
103
     *
104
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
105
     */
106 79
    protected function loadTableSchema(string $name): ?TableSchema
107
    {
108 79
        $table = new TableSchema();
109
110 79
        $table->name($name);
111 79
        $table->fullName($name);
112
113 79
        if ($this->findColumns($table)) {
114 75
            $this->findConstraints($table);
115
116 75
            return $table;
117
        }
118
119 12
        return null;
120
    }
121
122
    /**
123
     * Loads a primary key for the given table.
124
     *
125
     * @param string $tableName table name.
126
     *
127
     * @throws Exception
128
     * @throws InvalidArgumentException
129
     * @throws InvalidConfigException
130
     *
131
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
132
     */
133 30
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
134
    {
135 30
        return $this->loadTableConstraints($tableName, 'primaryKey');
136
    }
137
138
    /**
139
     * Loads all foreign keys for the given table.
140
     *
141
     * @param string $tableName table name.
142
     *
143
     * @throws Exception
144
     * @throws InvalidArgumentException
145
     * @throws InvalidConfigException
146
     *
147
     * @return ForeignKeyConstraint[] foreign keys for the given table.
148
     */
149 4
    protected function loadTableForeignKeys(string $tableName): array
150
    {
151 4
        $foreignKeys = $this->getDb()->createCommand(
152 4
            'PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')'
153 4
        )->queryAll();
154
155 4
        $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true);
156
157 4
        $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table');
158
159 4
        ArraySorter::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC);
160
161 4
        $result = [];
162
163 4
        foreach ($foreignKeys as $table => $foreignKey) {
164 4
            $fk = (new ForeignKeyConstraint())
165 4
                ->columnNames(ArrayHelper::getColumn($foreignKey, 'from'))
166 4
                ->foreignTableName($table)
167 4
                ->foreignColumnNames(ArrayHelper::getColumn($foreignKey, 'to'))
168 4
                ->onDelete($foreignKey[0]['on_delete'] ?? null)
169 4
                ->onUpdate($foreignKey[0]['on_update'] ?? null);
170
171 4
            $result[] = $fk;
172
        }
173
174 4
        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 13
    protected function loadTableUniques(string $tableName): array
205
    {
206 13
        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
            if (
250 3
                isset($createTableToken[$firstMatchIndex - 2])
251 3
                && $createTableToken->matches($pattern, $firstMatchIndex - 2)
252
            ) {
253
                $name = $createTableToken[$firstMatchIndex - 1]->getContent();
254
            }
255
256 3
            $ck = (new CheckConstraint())
257 3
                ->name($name)
258 3
                ->expression($checkSql);
259
260 3
            $result[] = $ck;
261
        }
262
263 12
        return $result;
264
    }
265
266
    /**
267
     * Loads all default value constraints for the given table.
268
     *
269
     * @param string $tableName table name.
270
     *
271
     * @throws NotSupportedException
272
     *
273
     * @return array default value constraints for the given table.
274
     */
275 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

275
    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...
276
    {
277 12
        throw new NotSupportedException('SQLite does not support default value constraints.');
278
    }
279
280
    /**
281
     * Creates a query builder for the MySQL database.
282
     *
283
     * This method may be overridden by child classes to create a DBMS-specific query builder.
284
     *
285
     * @return QueryBuilder query builder instance.
286
     */
287 52
    public function createQueryBuilder(): QueryBuilder
288
    {
289 52
        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

289
        return new QueryBuilder(/** @scrutinizer ignore-type */ $this->getDb());
Loading history...
290
    }
291
292
    /**
293
     * Create a column schema builder instance giving the type and value precision.
294
     *
295
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
296
     *
297
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
298
     * @param int|string|array $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
299
     *
300
     * @return ColumnSchemaBuilder column schema builder instance.
301
     */
302 3
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
303
    {
304 3
        return new ColumnSchemaBuilder($type, $length);
305
    }
306
307
    /**
308
     * Collects the table column metadata.
309
     *
310
     * @param TableSchema $table the table metadata.
311
     *
312
     * @throws Exception
313
     * @throws InvalidArgumentException
314
     * @throws InvalidConfigException
315
     *
316
     * @return bool whether the table exists in the database.
317
     */
318 79
    protected function findColumns($table): bool
319
    {
320 79
        $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 79
        $columns = $this->getDb()->createCommand($sql)->queryAll();
322
323 79
        if (empty($columns)) {
324 12
            return false;
325
        }
326
327 75
        foreach ($columns as $info) {
328 75
            $column = $this->loadColumnSchema($info);
329 75
            $table->columns($column->getName(), $column);
330 75
            if ($column->isPrimaryKey()) {
331 52
                $table->primaryKey($column->getName());
332
            }
333
        }
334
335 75
        $pk = $table->getPrimaryKey();
336 75
        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 75
        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
350
     * @throws InvalidArgumentException
351
     * @throws InvalidConfigException
352
     */
353 75
    protected function findConstraints(TableSchema $table): void
354
    {
355 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

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

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