Passed
Push — master ( 21771a...749afb )
by Wilmer
18:31 queued 03:34
created

Schema::loadColumnSchema()   C

Complexity

Conditions 17
Paths 92

Size

Total Lines 50
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 34
dl 0
loc 50
rs 5.2166
c 0
b 0
f 0
cc 17
nc 92
nop 1

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

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

289
    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...
290
    {
291
        throw new NotSupportedException('SQLite does not support default value constraints.');
292
    }
293
294
    /**
295
     * Creates a query builder for the MySQL database.
296
     *
297
     * This method may be overridden by child classes to create a DBMS-specific query builder.
298
     *
299
     * @return QueryBuilder query builder instance.
300
     */
301
    public function createQueryBuilder(): QueryBuilder
302
    {
303
        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\QueryBuilder::__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

303
        return new QueryBuilder(/** @scrutinizer ignore-type */ $this->getDb());
Loading history...
304
    }
305
306
    /**
307
     * Create a column schema builder instance giving the type and value precision.
308
     *
309
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
310
     *
311
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
312
     * @param int|string|array $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
313
     *
314
     * @return ColumnSchemaBuilder column schema builder instance.
315
     */
316
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
317
    {
318
        return new ColumnSchemaBuilder($type, $length);
319
    }
320
321
    /**
322
     * Collects the table column metadata.
323
     *
324
     * @param TableSchema $table the table metadata.
325
     *
326
     * @throws Exception
327
     * @throws InvalidArgumentException
328
     * @throws InvalidConfigException
329
     *
330
     * @return bool whether the table exists in the database.
331
     */
332
    protected function findColumns($table): bool
333
    {
334
        $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

334
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName(/** @scrutinizer ignore-type */ $table->getName()) . ')';
Loading history...
335
        $columns = $this->getDb()->createCommand($sql)->queryAll();
336
337
        if (empty($columns)) {
338
            return false;
339
        }
340
341
        foreach ($columns as $info) {
342
            $column = $this->loadColumnSchema($info);
343
            $table->columns($column->getName(), $column);
344
            if ($column->isPrimaryKey()) {
345
                $table->primaryKey($column->getName());
346
            }
347
        }
348
349
        $pk = $table->getPrimaryKey();
350
        if (count($pk) === 1 && !strncasecmp($table->getColumn($pk[0])->getDbType(), 'int', 3)) {
351
            $table->sequenceName('');
352
            $table->getColumn($pk[0])->autoIncrement(true);
353
        }
354
355
        return true;
356
    }
357
358
    /**
359
     * Collects the foreign key column details for the given table.
360
     *
361
     * @param TableSchema $table the table metadata.
362
     *
363
     * @throws Exception
364
     * @throws InvalidArgumentException
365
     * @throws InvalidConfigException
366
     */
367
    protected function findConstraints(TableSchema $table): void
368
    {
369
        $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

369
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName(/** @scrutinizer ignore-type */ $table->getName()) . ')';
Loading history...
370
        $keys = $this->getDb()->createCommand($sql)->queryAll();
371
372
        foreach ($keys as $key) {
373
            $id = (int) $key['id'];
374
            $fk = $table->getForeignKeys();
375
            if (!isset($fk[$id])) {
376
                $table->foreignKey($id, ([$key['table'], $key['from'] => $key['to']]));
377
            } else {
378
                /** composite FK */
379
                $table->compositeFK($id, $key['from'], $key['to']);
380
            }
381
        }
382
    }
383
384
    /**
385
     * Returns all unique indexes for the given table.
386
     *
387
     * Each array element is of the following structure:
388
     *
389
     * ```php
390
     * [
391
     *     'IndexName1' => ['col1' [, ...]],
392
     *     'IndexName2' => ['col2' [, ...]],
393
     * ]
394
     * ```
395
     *
396
     * @param TableSchema $table the table metadata.
397
     *
398
     * @throws Exception
399
     * @throws InvalidArgumentException
400
     * @throws InvalidConfigException
401
     *
402
     * @return array all unique indexes for the given table.
403
     */
404
    public function findUniqueIndexes($table): array
405
    {
406
        $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

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