Passed
Pull Request — master (#33)
by Wilmer
24:01 queued 09:03
created

Schema::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 5
ccs 0
cts 0
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite;
6
7
use Throwable;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Arrays\ArraySorter;
10
use Yiisoft\Db\Connection\ConnectionInterface;
11
use Yiisoft\Db\Constraint\CheckConstraint;
12
use Yiisoft\Db\Constraint\Constraint;
13
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
14
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
15
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
16
use Yiisoft\Db\Constraint\IndexConstraint;
17
use Yiisoft\Db\Exception\Exception;
18
use Yiisoft\Db\Exception\InvalidArgumentException;
19
use Yiisoft\Db\Exception\InvalidConfigException;
20
use Yiisoft\Db\Exception\NotSupportedException;
21
use Yiisoft\Db\Expression\Expression;
22
use Yiisoft\Db\Schema\ColumnSchema;
23
use Yiisoft\Db\Schema\Schema as AbstractSchema;
24
use Yiisoft\Db\Transaction\Transaction;
25
26
use function count;
27
use function explode;
28
use function get_class;
29
use function preg_match;
30
use function strncasecmp;
31
use function strncmp;
32
use function strpos;
33
use function strtolower;
34
use function trim;
35
36
/**
37
 * Schema is the class for retrieving metadata from a SQLite (2/3) database.
38
 *
39
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction. This can be
40
 * either {@see Transaction::READ_UNCOMMITTED} or {@see Transaction::SERIALIZABLE}.
41
 */
42
final class Schema extends AbstractSchema implements ConstraintFinderInterface
43
{
44
    use ConstraintFinderTrait;
45
46
    /**
47
     * @var array mapping from physical column types (keys) to abstract column types (values)
48
     */
49
    private array $typeMap = [
50
        'tinyint' => self::TYPE_TINYINT,
51
        'bit' => self::TYPE_SMALLINT,
52
        'boolean' => self::TYPE_BOOLEAN,
53
        'bool' => self::TYPE_BOOLEAN,
54
        'smallint' => self::TYPE_SMALLINT,
55
        'mediumint' => self::TYPE_INTEGER,
56
        'int' => self::TYPE_INTEGER,
57
        'integer' => self::TYPE_INTEGER,
58
        'bigint' => self::TYPE_BIGINT,
59
        'float' => self::TYPE_FLOAT,
60
        'double' => self::TYPE_DOUBLE,
61
        'real' => self::TYPE_FLOAT,
62
        'decimal' => self::TYPE_DECIMAL,
63
        'numeric' => self::TYPE_DECIMAL,
64
        'tinytext' => self::TYPE_TEXT,
65
        'mediumtext' => self::TYPE_TEXT,
66
        'longtext' => self::TYPE_TEXT,
67
        'text' => self::TYPE_TEXT,
68
        'varchar' => self::TYPE_STRING,
69
        'string' => self::TYPE_STRING,
70
        'char' => self::TYPE_CHAR,
71
        'blob' => self::TYPE_BINARY,
72
        'datetime' => self::TYPE_DATETIME,
73
        'year' => self::TYPE_DATE,
74
        'date' => self::TYPE_DATE,
75
        'time' => self::TYPE_TIME,
76
        'timestamp' => self::TYPE_TIMESTAMP,
77
        'enum' => self::TYPE_STRING,
78
    ];
79
80
    /**
81
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
82
     * case starting and ending characters are different.
83
     */
84
    protected $tableQuoteCharacter = '`';
85
86
    /**
87
     * @var string|string[] character used to quote column names. An array of 2 characters can be used in case starting
88
     * and ending characters are different.
89
     */
90
    protected $columnQuoteCharacter = '`';
91
92
    /** @psalm-var Connection $db */
93
    private ConnectionInterface $db;
94
95
    public function __construct(ConnectionInterface $db)
96
    {
97
        $this->db = $db;
98
99
        parent::__construct($db);
100
    }
101
102 5
    /**
103
     * Returns all table names in the database.
104 5
     *
105
     * This method should be overridden by child classes in order to support this feature because the default
106 5
     * implementation simply throws an exception.
107
     *
108
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
109
     *
110
     * @throws Exception|InvalidConfigException|Throwable
111
     *
112
     * @return array all table names in the database. The names have NO schema name prefix.
113
     */
114
    protected function findTableNames(string $schema = ''): array
115
    {
116
        $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name";
117
118
        return $this->db->createCommand($sql)->queryColumn();
119
    }
120 80
121
    /**
122 80
     * Loads the metadata for the specified table.
123
     *
124 80
     * @param string $name table name.
125 80
     *
126
     * @throws Exception|InvalidArgumentException|InvalidConfigException
127 80
     *
128 74
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
129
     */
130 74
    protected function loadTableSchema(string $name): ?TableSchema
131
    {
132
        $table = new TableSchema();
133 14
134
        $table->name($name);
135
        $table->fullName($name);
136
137
        if ($this->findColumns($table)) {
138
            $this->findConstraints($table);
139
140
            return $table;
141
        }
142
143
        return null;
144
    }
145
146
    /**
147 30
     * Loads a primary key for the given table.
148
     *
149 30
     * @param string $tableName table name.
150
     *
151
     * @throws Exception|InvalidArgumentException|InvalidConfigException
152
     *
153
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
154
     */
155
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
156
    {
157
        return $this->loadTableConstraints($tableName, 'primaryKey');
158
    }
159
160
    /**
161
     * Loads all foreign keys for the given table.
162
     *
163 4
     * @param string $tableName table name.
164
     *
165 4
     * @throws Exception|InvalidConfigException|Throwable
166 4
     *
167 4
     * @return ForeignKeyConstraint[] foreign keys for the given table.
168
     */
169 4
    protected function loadTableForeignKeys(string $tableName): array
170
    {
171 4
        $foreignKeys = $this->db->createCommand(
172
            'PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')'
173 4
        )->queryAll();
174
175 4
        $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true);
176
177 4
        $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table');
178 4
179 4
        ArraySorter::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC);
180 4
181 4
        $result = [];
182 4
183 4
        foreach ($foreignKeys as $table => $foreignKey) {
184
            $fk = (new ForeignKeyConstraint())
185 4
                ->columnNames(ArrayHelper::getColumn($foreignKey, 'from'))
186
                ->foreignTableName($table)
187
                ->foreignColumnNames(ArrayHelper::getColumn($foreignKey, 'to'))
188 4
                ->onDelete($foreignKey[0]['on_delete'] ?? null)
189
                ->onUpdate($foreignKey[0]['on_update'] ?? null);
190
191
            $result[] = $fk;
192
        }
193
194
        return $result;
195
    }
196
197
    /**
198
     * Loads all indexes for the given table.
199
     *
200
     * @param string $tableName table name.
201
     *
202 10
     * @throws Exception|InvalidArgumentException|InvalidConfigException
203
     *
204 10
     * @return IndexConstraint[] indexes for the given table.
205
     */
206
    protected function loadTableIndexes(string $tableName): array
207
    {
208
        return $this->loadTableConstraints($tableName, 'indexes');
209
    }
210
211
    /**
212
     * Loads all unique constraints for the given table.
213
     *
214
     * @param string $tableName table name.
215
     *
216
     * @throws Exception|InvalidArgumentException|InvalidConfigException
217
     *
218 13
     * @return Constraint[] unique constraints for the given table.
219
     */
220 13
    protected function loadTableUniques(string $tableName): array
221
    {
222
        return $this->loadTableConstraints($tableName, 'uniques');
223
    }
224
225
    /**
226
     * Loads all check constraints for the given table.
227
     *
228
     * @param string $tableName table name.
229
     *
230
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
231
     *
232
     * @return CheckConstraint[] check constraints for the given table.
233
     */
234 12
    protected function loadTableChecks(string $tableName): array
235
    {
236 12
        $sql = $this->db->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [
237 12
            ':tableName' => $tableName,
238 12
        ])->queryScalar();
239
240
        /** @var SqlToken[]|SqlToken[][]|SqlToken[][][] $code */
241 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\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 12
243
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
244 12
245
        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...
246
            return [];
247
        }
248 12
249 12
        $createTableToken = $code[0][$lastMatchIndex - 1];
250 12
        $result = [];
251
        $offset = 0;
252 12
253 12
        while (true) {
254
            $pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
255 12
256 12
            if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
257
                break;
258
            }
259 3
260 3
            $checkSql = $createTableToken[$offset - 1]->getSql();
261 3
            $name = null;
262
            $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
263
264 3
            if (
265 3
                isset($createTableToken[$firstMatchIndex - 2])
266
                && $createTableToken->matches($pattern, $firstMatchIndex - 2)
267
            ) {
268
                $name = $createTableToken[$firstMatchIndex - 1]->getContent();
269
            }
270 3
271 3
            $ck = (new CheckConstraint())
272 3
                ->name($name)
273
                ->expression($checkSql);
274 3
275
            $result[] = $ck;
276
        }
277 12
278
        return $result;
279
    }
280
281
    /**
282
     * Loads all default value constraints for the given table.
283
     *
284
     * @param string $tableName table name.
285
     *
286
     * @throws NotSupportedException
287
     *
288
     * @return array default value constraints for the given table.
289 12
     */
290
    protected function loadTableDefaultValues(string $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

290
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ string $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...
291 12
    {
292
        throw new NotSupportedException('SQLite does not support default value constraints.');
293
    }
294
295
    /**
296
     * Creates a query builder for the MySQL database.
297
     *
298
     * This method may be overridden by child classes to create a DBMS-specific query builder.
299
     *
300
     * @return QueryBuilder query builder instance.
301 58
     */
302
    public function createQueryBuilder(): QueryBuilder
303 58
    {
304
        return new QueryBuilder($this->db);
305
    }
306
307
    /**
308
     * Create a column schema builder instance giving the type and value precision.
309
     *
310
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
311
     *
312
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
313
     * @param int|string|array|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
314
     *
315
     * @return ColumnSchemaBuilder column schema builder instance.
316 3
     */
317
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
318 3
    {
319
        return new ColumnSchemaBuilder($type, $length);
320
    }
321
322
    /**
323
     * Collects the table column metadata.
324
     *
325
     * @param TableSchema $table the table metadata.
326
     *
327
     * @throws Exception|InvalidConfigException|Throwable
328
     *
329
     * @return bool whether the table exists in the database.
330
     */
331
    protected function findColumns(TableSchema $table): bool
332 80
    {
333
        $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

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

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

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