Passed
Push — master ( 707555...d3022f )
by Wilmer
11:56 queued 09:57
created

Schema::loadTableConstraints()   F

Complexity

Conditions 16
Paths 364

Size

Total Lines 106
Code Lines 61

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 47
CRAP Score 16.3713

Importance

Changes 0
Metric Value
cc 16
eloc 61
nc 364
nop 2
dl 0
loc 106
ccs 47
cts 53
cp 0.8868
crap 16.3713
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\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