Passed
Branch dev (ea35c5)
by Wilmer
17:29 queued 12:43
created

SchemaPDOSqlite::loadTableUniques()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
c 1
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite\PDO;
6
7
use PDO;
8
use Throwable;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Arrays\ArraySorter;
11
use Yiisoft\Db\Cache\SchemaCache;
12
use Yiisoft\Db\Connection\ConnectionPDOInterface;
13
use Yiisoft\Db\Constraint\CheckConstraint;
14
use Yiisoft\Db\Constraint\Constraint;
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\InvalidCallException;
20
use Yiisoft\Db\Exception\InvalidConfigException;
21
use Yiisoft\Db\Exception\NotSupportedException;
22
use Yiisoft\Db\Expression\Expression;
23
use Yiisoft\Db\Schema\ColumnSchema;
24
use Yiisoft\Db\Schema\Schema;
25
use Yiisoft\Db\Sqlite\ColumnSchemaBuilder;
26
use Yiisoft\Db\Sqlite\SqlToken;
27
use Yiisoft\Db\Sqlite\SqlTokenizer;
28
use Yiisoft\Db\Sqlite\TableSchema;
29
30
use function count;
31
use function explode;
32
use function preg_match;
33
use function strncasecmp;
34
use function strtolower;
35
use function trim;
36
37
/**
38
 * Schema is the class for retrieving metadata from a SQLite (2/3) database.
39
 *
40
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction. This can be
41
 * either {@see TransactionPDOSqlite::READ_UNCOMMITTED} or {@see TransactionPDOSqlite::SERIALIZABLE}.
42
 *
43
 * @psalm-type Column = array<array-key, array{seqno:string, cid:string, name:string}>
44
 *
45
 * @psalm-type NormalizePragmaForeignKeyList = array<
46
 *   string,
47
 *   array<
48
 *     array-key,
49
 *     array{
50
 *       id:string,
51
 *       cid:string,
52
 *       seq:string,
53
 *       table:string,
54
 *       from:string,
55
 *       to:string,
56
 *       on_update:string,
57
 *       on_delete:string
58
 *     }
59
 *   >
60
 * >
61
 *
62
 * @psalm-type PragmaForeignKeyList = array<
63
 *   string,
64
 *   array{
65
 *     id:string,
66
 *     cid:string,
67
 *     seq:string,
68
 *     table:string,
69
 *     from:string,
70
 *     to:string,
71
 *     on_update:string,
72
 *     on_delete:string
73
 *   }
74
 * >
75
 *
76
 * @psalm-type PragmaIndexInfo = array<array-key, array{seqno:string, cid:string, name:string}>
77
 *
78
 * @psalm-type PragmaIndexList = array<
79
 *   array-key,
80
 *   array{seq:string, name:string, unique:string, origin:string, partial:string}
81
 * >
82
 *
83
 * @psalm-type PragmaTableInfo = array<
84
 *   array-key,
85
 *   array{cid:string, name:string, type:string, notnull:string, dflt_value:string|null, pk:string}
86
 * >
87
 */
88
final class SchemaPDOSqlite extends Schema
89
{
90
    /**
91
     * @var array mapping from physical column types (keys) to abstract column types (values)
92
     *
93
     * @psalm-var array<array-key, string> $typeMap
94
     */
95
    private array $typeMap = [
96
        'tinyint' => self::TYPE_TINYINT,
97
        'bit' => self::TYPE_SMALLINT,
98
        'boolean' => self::TYPE_BOOLEAN,
99
        'bool' => self::TYPE_BOOLEAN,
100
        'smallint' => self::TYPE_SMALLINT,
101
        'mediumint' => self::TYPE_INTEGER,
102
        'int' => self::TYPE_INTEGER,
103
        'integer' => self::TYPE_INTEGER,
104
        'bigint' => self::TYPE_BIGINT,
105
        'float' => self::TYPE_FLOAT,
106
        'double' => self::TYPE_DOUBLE,
107
        'real' => self::TYPE_FLOAT,
108
        'decimal' => self::TYPE_DECIMAL,
109
        'numeric' => self::TYPE_DECIMAL,
110
        'tinytext' => self::TYPE_TEXT,
111
        'mediumtext' => self::TYPE_TEXT,
112
        'longtext' => self::TYPE_TEXT,
113
        'text' => self::TYPE_TEXT,
114
        'varchar' => self::TYPE_STRING,
115
        'string' => self::TYPE_STRING,
116
        'char' => self::TYPE_CHAR,
117
        'blob' => self::TYPE_BINARY,
118
        'datetime' => self::TYPE_DATETIME,
119
        'year' => self::TYPE_DATE,
120
        'date' => self::TYPE_DATE,
121
        'time' => self::TYPE_TIME,
122
        'timestamp' => self::TYPE_TIMESTAMP,
123
        'enum' => self::TYPE_STRING,
124
    ];
125
126 342
    public function __construct(private ConnectionPDOInterface $db, SchemaCache $schemaCache)
127
    {
128 342
        parent::__construct($schemaCache);
129
    }
130
131
    /**
132
     * Returns all table names in the database.
133
     *
134
     * This method should be overridden by child classes in order to support this feature because the default
135
     * implementation simply throws an exception.
136
     *
137
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
138
     *
139
     * @throws Exception|InvalidConfigException|Throwable
140
     *
141
     * @return array all table names in the database. The names have NO schema name prefix.
142
     */
143 5
    protected function findTableNames(string $schema = ''): array
144
    {
145 5
        return $this->db->createCommand(
146
            "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name"
147 5
        )->queryColumn();
148
    }
149
150
    /**
151
     * @throws Exception|InvalidCallException|InvalidConfigException|Throwable
152
     */
153
    public function insert(string $table, array $columns): bool|array
154
    {
155
        $command = $this->db->createCommand()->insert($table, $columns);
156
        $tablePrimaryKey = [];
157
158
        if (!$command->execute()) {
159
            return false;
160
        }
161
162
        $tableSchema = $this->getTableSchema($table);
163
        $result = [];
164
165
        if ($tableSchema !== null) {
166
            $tablePrimaryKey = $tableSchema->getPrimaryKey();
167
        }
168
169
        foreach ($tablePrimaryKey as $name) {
170
            if ($tableSchema?->getColumn($name)?->isAutoIncrement()) {
171
                $result[$name] = $this->getLastInsertID((string) $tableSchema?->getSequenceName());
172
                break;
173
            }
174
175
            /** @var mixed */
176
            $result[$name] = $columns[$name] ?? $tableSchema?->getColumn($name)?->getDefaultValue();
177
        }
178
179
        return $result;
180
    }
181
182
    /**
183
     * Loads the metadata for the specified table.
184
     *
185
     * @param string $name table name.
186
     *
187
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
188
     *
189
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
190
     */
191 80
    protected function loadTableSchema(string $name): ?TableSchema
192
    {
193 80
        $table = new TableSchema();
194
195 80
        $table->name($name);
196 80
        $table->fullName($name);
197
198 80
        if ($this->findColumns($table)) {
199 76
            $this->findConstraints($table);
200
201 76
            return $table;
202
        }
203
204 12
        return null;
205
    }
206
207
    /**
208
     * Loads a primary key for the given table.
209
     *
210
     * @param string $tableName table name.
211
     *
212
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
213
     *
214
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
215
     */
216 30
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
217
    {
218 30
        $tablePrimaryKey = $this->loadTableConstraints($tableName, 'primaryKey');
219
220 30
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
221
    }
222
223
    /**
224
     * Loads all foreign keys for the given table.
225
     *
226
     * @param string $tableName table name.
227
     *
228
     * @throws Exception|InvalidConfigException|Throwable
229
     *
230
     * @return ForeignKeyConstraint[] foreign keys for the given table.
231
     */
232 4
    protected function loadTableForeignKeys(string $tableName): array
233
    {
234 4
        $result = [];
235
        /** @psalm-var PragmaForeignKeyList */
236 4
        $foreignKeysList = $this->getPragmaForeignKeyList($tableName);
237
        /** @psalm-var NormalizePragmaForeignKeyList */
238 4
        $foreignKeysList = $this->normalizePdoRowKeyCase($foreignKeysList, true);
239
        /** @psalm-var NormalizePragmaForeignKeyList */
240 4
        $foreignKeysList = ArrayHelper::index($foreignKeysList, null, 'table');
241 4
        ArraySorter::multisort($foreignKeysList, 'seq', SORT_ASC, SORT_NUMERIC);
242
243
        /** @psalm-var NormalizePragmaForeignKeyList $foreignKeysList */
244 4
        foreach ($foreignKeysList as $table => $foreignKey) {
245 4
            $fk = (new ForeignKeyConstraint())
246 4
                ->columnNames(ArrayHelper::getColumn($foreignKey, 'from'))
247 4
                ->foreignTableName($table)
248 4
                ->foreignColumnNames(ArrayHelper::getColumn($foreignKey, 'to'))
249 4
                ->onDelete($foreignKey[0]['on_delete'] ?? null)
250 4
                ->onUpdate($foreignKey[0]['on_update'] ?? null);
251
252 4
            $result[] = $fk;
253
        }
254
255 4
        return $result;
256
    }
257
258
    /**
259
     * Loads all indexes for the given table.
260
     *
261
     * @param string $tableName table name.
262
     *
263
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
264
     *
265
     * @return array indexes for the given table.
266
     *
267
     * @psalm-return array|IndexConstraint[]
268
     */
269 10
    protected function loadTableIndexes(string $tableName): array
270
    {
271 10
        $tableIndexes = $this->loadTableConstraints($tableName, 'indexes');
272
273 10
        return is_array($tableIndexes) ? $tableIndexes : [];
274
    }
275
276
    /**
277
     * Loads all unique constraints for the given table.
278
     *
279
     * @param string $tableName table name.
280
     *
281
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
282
     *
283
     * @return array unique constraints for the given table.
284
     *
285
     * @psalm-return array|Constraint[]
286
     */
287 12
    protected function loadTableUniques(string $tableName): array
288
    {
289 12
        $tableUniques = $this->loadTableConstraints($tableName, 'uniques');
290
291 12
        return is_array($tableUniques) ? $tableUniques : [];
292
    }
293
294
    /**
295
     * Loads all check constraints for the given table.
296
     *
297
     * @param string $tableName table name.
298
     *
299
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
300
     *
301
     * @return CheckConstraint[] check constraints for the given table.
302
     */
303 12
    protected function loadTableChecks(string $tableName): array
304
    {
305 12
        $sql = $this->db->createCommand(
306
            'SELECT `sql` FROM `sqlite_master` WHERE name = :tableName',
307
            [':tableName' => $tableName],
308 12
        )->queryScalar();
309
310 12
        $sql = ($sql === false || $sql === null) ? '' : (string) $sql;
311
312
        /** @var SqlToken[]|SqlToken[][]|SqlToken[][][] $code */
313 12
        $code = (new SqlTokenizer($sql))->tokenize();
314 12
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
315 12
        $result = [];
316
317 12
        if ($code[0] instanceof SqlToken && $code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $firstMatchIndex seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $lastMatchIndex seems to be never defined.
Loading history...
318 12
            $offset = 0;
319 12
            $createTableToken = $code[0][(int) $lastMatchIndex - 1];
320 12
            $sqlTokenizerAnyCheck = new SqlTokenizer('any CHECK()');
321
322
            while (
323 12
                $createTableToken instanceof SqlToken &&
324 12
                $createTableToken->matches($sqlTokenizerAnyCheck->tokenize(), (int) $offset, $firstMatchIndex, $offset)
325
            ) {
326 3
                $name = null;
327 3
                $checkSql = (string) $createTableToken[(int) $offset - 1];
328 3
                $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
329
330
                if (
331 3
                    isset($createTableToken[(int) $firstMatchIndex - 2])
332 3
                    && $createTableToken->matches($pattern, (int) $firstMatchIndex - 2)
333
                ) {
334
                    $sqlToken = $createTableToken[(int) $firstMatchIndex - 1];
335
                    $name = $sqlToken?->getContent();
336
                }
337
338 3
                $result[] = (new CheckConstraint())->name($name)->expression($checkSql);
339
            }
340
        }
341
342 12
        return $result;
343
    }
344
345
    /**
346
     * Loads all default value constraints for the given table.
347
     *
348
     * @param string $tableName table name.
349
     *
350
     * @throws NotSupportedException
351
     *
352
     * @return array default value constraints for the given table.
353
     */
354 12
    protected function loadTableDefaultValues(string $tableName): array
355
    {
356 12
        throw new NotSupportedException('SQLite does not support default value constraints.');
357
    }
358
359
    /**
360
     * Create a column schema builder instance giving the type and value precision.
361
     *
362
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
363
     *
364
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
365
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
366
     *
367
     * @return ColumnSchemaBuilder column schema builder instance.
368
     */
369 3
    public function createColumnSchemaBuilder(string $type, array|int|string $length = null): ColumnSchemaBuilder
370
    {
371 3
        return new ColumnSchemaBuilder($type, $length);
372
    }
373
374
    /**
375
     * Collects the table column metadata.
376
     *
377
     * @param TableSchema $table the table metadata.
378
     *
379
     * @throws Exception|InvalidConfigException|Throwable
380
     *
381
     * @return bool whether the table exists in the database.
382
     */
383 80
    protected function findColumns(TableSchema $table): bool
384
    {
385
        /** @psalm-var PragmaTableInfo */
386 80
        $columns = $this->getPragmaTableInfo($table->getName());
387
388 80
        foreach ($columns as $info) {
389 76
            $column = $this->loadColumnSchema($info);
390 76
            $table->columns($column->getName(), $column);
391
392 76
            if ($column->isPrimaryKey()) {
393 52
                $table->primaryKey($column->getName());
394
            }
395
        }
396
397 80
        $column = count($table->getPrimaryKey()) === 1 ? $table->getColumn($table->getPrimaryKey()[0]) : null;
398
399 80
        if ($column !== null && !strncasecmp($column->getDbType(), 'int', 3)) {
400 52
            $table->sequenceName('');
401 52
            $column->autoIncrement(true);
402
        }
403
404 80
        return !empty($columns);
405
    }
406
407
    /**
408
     * Collects the foreign key column details for the given table.
409
     *
410
     * @param TableSchema $table the table metadata.
411
     *
412
     * @throws Exception|InvalidConfigException|Throwable
413
     */
414 76
    protected function findConstraints(TableSchema $table): void
415
    {
416
        /** @psalm-var PragmaForeignKeyList */
417 76
        $foreignKeysList = $this->getPragmaForeignKeyList($table->getName());
418
419 76
        foreach ($foreignKeysList as $foreignKey) {
420 5
            $id = (int) $foreignKey['id'];
421 5
            $fk = $table->getForeignKeys();
422
423 5
            if (!isset($fk[$id])) {
424 5
                $table->foreignKey($id, ([$foreignKey['table'], $foreignKey['from'] => $foreignKey['to']]));
425
            } else {
426
                /** composite FK */
427 5
                $table->compositeFK($id, $foreignKey['from'], $foreignKey['to']);
428
            }
429
        }
430
    }
431
432
    /**
433
     * Returns all unique indexes for the given table.
434
     *
435
     * Each array element is of the following structure:
436
     *
437
     * ```php
438
     * [
439
     *     'IndexName1' => ['col1' [, ...]],
440
     *     'IndexName2' => ['col2' [, ...]],
441
     * ]
442
     * ```
443
     *
444
     * @param TableSchema $table the table metadata.
445
     *
446
     * @throws Exception|InvalidConfigException|Throwable
447
     *
448
     * @return array all unique indexes for the given table.
449
     */
450 1
    public function findUniqueIndexes(TableSchema $table): array
451
    {
452
        /** @psalm-var PragmaIndexList */
453 1
        $indexList = $this->getPragmaIndexList($table->getName());
454 1
        $uniqueIndexes = [];
455
456 1
        foreach ($indexList as $index) {
457 1
            $indexName = $index['name'];
458
            /** @psalm-var PragmaIndexInfo */
459 1
            $indexInfo = $this->getPragmaIndexInfo($index['name']);
460
461 1
            if ($index['unique']) {
462 1
                $uniqueIndexes[$indexName] = [];
463 1
                foreach ($indexInfo as $row) {
464 1
                    $uniqueIndexes[$indexName][] = $row['name'];
465
                }
466
            }
467
        }
468
469 1
        return $uniqueIndexes;
470
    }
471
472
    /**
473
     * Loads the column information into a {@see ColumnSchema} object.
474
     *
475
     * @param array $info column information.
476
     *
477
     * @return ColumnSchema the column schema object.
478
     *
479
     * @psalm-param array{cid:string, name:string, type:string, notnull:string, dflt_value:string|null, pk:string} $info
480
     */
481 76
    protected function loadColumnSchema(array $info): ColumnSchema
482
    {
483 76
        $column = $this->createColumnSchema();
484 76
        $column->name($info['name']);
485 76
        $column->allowNull(!$info['notnull']);
486 76
        $column->primaryKey($info['pk'] !== '0');
487 76
        $column->dbType(strtolower($info['type']));
488 76
        $column->unsigned(str_contains($column->getDbType(), 'unsigned'));
489 76
        $column->type(self::TYPE_STRING);
490
491 76
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
492 76
            $type = strtolower($matches[1]);
493
494 76
            if (isset($this->typeMap[$type])) {
495 76
                $column->type($this->typeMap[$type]);
496
            }
497
498 76
            if (!empty($matches[2])) {
499 72
                $values = explode(',', $matches[2]);
500 72
                $column->precision((int) $values[0]);
501 72
                $column->size((int) $values[0]);
502
503 72
                if (isset($values[1])) {
504 26
                    $column->scale((int) $values[1]);
505
                }
506
507 72
                if ($column->getSize() === 1 && ($type === 'tinyint' || $type === 'bit')) {
508 21
                    $column->type('boolean');
509 72
                } elseif ($type === 'bit') {
510
                    if ($column->getSize() > 32) {
511
                        $column->type('bigint');
512
                    } elseif ($column->getSize() === 32) {
513
                        $column->type('integer');
514
                    }
515
                }
516
            }
517
        }
518
519 76
        $column->phpType($this->getColumnPhpType($column));
520
521 76
        if (!$column->isPrimaryKey()) {
522 74
            if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
523 72
                $column->defaultValue(null);
524 61
            } elseif ($column->getType() === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
525 21
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP'));
526
            } else {
527 61
                $value = trim($info['dflt_value'], "'\"");
528 61
                $column->defaultValue($column->phpTypecast($value));
529
            }
530
        }
531
532 76
        return $column;
533
    }
534
535
    /**
536
     * Sets the isolation level of the current transaction.
537
     *
538
     * @param string $level The transaction isolation level to use for this transaction. This can be either
539
     * {@see TransactionPDOSqlite::READ_UNCOMMITTED} or {@see TransactionPDOSqlite::SERIALIZABLE}.
540
     *
541
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable when unsupported isolation levels are
542
     * used. SQLite only supports SERIALIZABLE and READ UNCOMMITTED.
543
     *
544
     * {@see http://www.sqlite.org/pragma.html#pragma_read_uncommitted}
545
     */
546 2
    public function setTransactionIsolationLevel(string $level): void
547
    {
548
        switch ($level) {
549 2
            case TransactionPDOSqlite::SERIALIZABLE:
550 1
                $this->db->createCommand('PRAGMA read_uncommitted = False;')->execute();
551 1
                break;
552 2
            case TransactionPDOSqlite::READ_UNCOMMITTED:
553 2
                $this->db->createCommand('PRAGMA read_uncommitted = True;')->execute();
554 2
                break;
555
            default:
556
                throw new NotSupportedException(
557
                    self::class . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.'
558
                );
559
        }
560
    }
561
562
    /**
563
     * Returns table columns info.
564
     *
565
     * @param string $tableName table name.
566
     *
567
     * @throws Exception|InvalidConfigException|Throwable
568
     *
569
     * @return array
570
     */
571 28
    private function loadTableColumnsInfo(string $tableName): array
572
    {
573 28
        $tableColumns = $this->getPragmaTableInfo($tableName);
574
        /** @psalm-var PragmaTableInfo */
575 28
        $tableColumns = $this->normalizePdoRowKeyCase($tableColumns, true);
576
577 28
        return ArrayHelper::index($tableColumns, 'cid');
578
    }
579
580
    /**
581
     * Loads multiple types of constraints and returns the specified ones.
582
     *
583
     * @param string $tableName table name.
584
     * @param string $returnType return type: (primaryKey, indexes, uniques).
585
     *
586
     * @throws Exception|InvalidConfigException|Throwable
587
     *
588
     * @return array|Constraint|null
589
     *
590
     * @psalm-return (Constraint|IndexConstraint)[]|Constraint|null
591
     */
592 52
    private function loadTableConstraints(string $tableName, string $returnType): Constraint|array|null
593
    {
594 52
        $indexList = $this->getPragmaIndexList($tableName);
595
        /** @psalm-var PragmaIndexList $indexes */
596 52
        $indexes = $this->normalizePdoRowKeyCase($indexList, true);
597 52
        $result = ['primaryKey' => null, 'indexes' => [], 'uniques' => []];
598
599 52
        foreach ($indexes as $index) {
600
            /** @psalm-var Column $columns */
601 42
            $columns = $this->getPragmaIndexInfo($index['name']);
602
603 42
            $result['indexes'][] = (new IndexConstraint())
604 42
                ->primary($index['origin'] === 'pk')
605 42
                ->unique((bool) $index['unique'])
606 42
                ->name($index['name'])
607 42
                ->columnNames(ArrayHelper::getColumn($columns, 'name'));
608
609 42
            if ($index['origin'] === 'u') {
610 41
                $result['uniques'][] = (new Constraint())
611 41
                    ->name($index['name'])
612 41
                    ->columnNames(ArrayHelper::getColumn($columns, 'name'));
613
            }
614
615 42
            if ($index['origin'] === 'pk') {
616 24
                $result['primaryKey'] = (new Constraint())
617 24
                    ->columnNames(ArrayHelper::getColumn($columns, 'name'));
618
            }
619
        }
620
621 52
        if (!isset($result['primaryKey'])) {
622
            /**
623
             * Additional check for PK in case of INTEGER PRIMARY KEY with ROWID.
624
             *
625
             * {@See https://www.sqlite.org/lang_createtable.html#primkeyconst}
626
             *
627
             * @psalm-var PragmaTableInfo
628
             */
629 28
            $tableColumns = $this->loadTableColumnsInfo($tableName);
630
631 28
            foreach ($tableColumns as $tableColumn) {
632 28
                if ($tableColumn['pk'] > 0) {
633 18
                    $result['primaryKey'] = (new Constraint())->columnNames([$tableColumn['name']]);
634 18
                    break;
635
                }
636
            }
637
        }
638
639 52
        foreach ($result as $type => $data) {
640 52
            $this->setTableMetadata($tableName, $type, $data);
641
        }
642
643 52
        return $result[$returnType];
644
    }
645
646
    /**
647
     * Creates a column schema for the database.
648
     *
649
     * This method may be overridden by child classes to create a DBMS-specific column schema.
650
     *
651
     * @return ColumnSchema column schema instance.
652
     */
653 76
    private function createColumnSchema(): ColumnSchema
654
    {
655 76
        return new ColumnSchema();
656
    }
657
658
    /**
659
     * @throws Exception|InvalidConfigException|Throwable
660
     */
661 80
    private function getPragmaForeignKeyList(string $tableName): array
662
    {
663 80
        return $this->db->createCommand(
664 80
            'PRAGMA FOREIGN_KEY_LIST(' . $this->db->getQuoter()->quoteSimpleTableName(($tableName)) . ')'
665 80
        )->queryAll();
666
    }
667
668
    /**
669
     * @throws Exception|InvalidConfigException|Throwable
670
     */
671 43
    private function getPragmaIndexInfo(string $name): array
672
    {
673 43
        $column = $this->db
674 43
            ->createCommand('PRAGMA INDEX_INFO(' . (string) $this->db->getQuoter()->quoteValue($name) . ')')
675 43
            ->queryAll();
676
        /** @psalm-var Column */
677 43
        $column = $this->normalizePdoRowKeyCase($column, true);
678 43
        ArraySorter::multisort($column, 'seqno', SORT_ASC, SORT_NUMERIC);
679
680 43
        return $column;
681
    }
682
683
    /**
684
     * @throws Exception|InvalidConfigException|Throwable
685
     */
686 53
    private function getPragmaIndexList(string $tableName): array
687
    {
688 53
        return $this->db
689 53
            ->createCommand('PRAGMA INDEX_LIST(' . (string) $this->db->getQuoter()->quoteValue($tableName) . ')')
690 53
            ->queryAll();
691
    }
692
693
    /**
694
     * @throws Exception|InvalidConfigException|Throwable
695
     */
696 89
    private function getPragmaTableInfo(string $tableName): array
697
    {
698 89
        return $this->db->createCommand(
699 89
            'PRAGMA TABLE_INFO(' . $this->db->getQuoter()->quoteSimpleTableName($tableName) . ')'
700 89
        )->queryAll();
701
    }
702
703 1
    public function rollBackSavepoint(string $name): void
704
    {
705 1
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
706
    }
707
708
    /**
709
     * Returns the actual name of a given table name.
710
     *
711
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
712
     * {@see ConnectionInterface::tablePrefix}.
713
     *
714
     * @param string $name the table name to be converted.
715
     *
716
     * @return string the real name of the given table name.
717
     */
718 140
    public function getRawTableName(string $name): string
719
    {
720 140
        if (str_contains($name, '{{')) {
721 23
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
722
723 23
            return str_replace('%', $this->db->getTablePrefix(), $name);
724
        }
725
726 140
        return $name;
727
    }
728
729
    /**
730
     * Returns the cache key for the specified table name.
731
     *
732
     * @param string $name the table name.
733
     *
734
     * @return array the cache key.
735
     */
736 140
    protected function getCacheKey(string $name): array
737
    {
738
        return [
739
            __CLASS__,
740 140
            $this->db->getDriver()->getDsn(),
741 140
            $this->db->getDriver()->getUsername(),
742 140
            $this->getRawTableName($name),
743
        ];
744
    }
745
746
    /**
747
     * Returns the cache tag name.
748
     *
749
     * This allows {@see refresh()} to invalidate all cached table schemas.
750
     *
751
     * @return string the cache tag name.
752
     */
753 140
    protected function getCacheTag(): string
754
    {
755 140
        return md5(serialize([
756
            __CLASS__,
757 140
            $this->db->getDriver()->getDsn(),
758 140
            $this->db->getDriver()->getUsername(),
759
        ]));
760
    }
761
762
    /**
763
     * Changes row's array key case to lower if PDO one is set to uppercase.
764
     *
765
     * @param array $row row's array or an array of row's arrays.
766
     * @param bool $multiple whether multiple rows or a single row passed.
767
     *
768
     * @throws Exception
769
     *
770
     * @return array normalized row or rows.
771
     */
772 57
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
773
    {
774 57
        if ($this->db->getSlavePdo()?->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_UPPER) {
775 45
            return $row;
776
        }
777
778 12
        if ($multiple) {
779 12
            return array_map(static function (array $row) {
780 12
                return array_change_key_case($row, CASE_LOWER);
781
            }, $row);
782
        }
783
784
        return array_change_key_case($row, CASE_LOWER);
785
    }
786
787
    /**
788
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
789
     */
790 2
    public function supportsSavepoint(): bool
791
    {
792 2
        return $this->db->isSavepointEnabled();
793
    }
794
795
    /**
796
     * Creates a new savepoint.
797
     *
798
     * @param string $name the savepoint name
799
     *
800
     * @throws Exception|InvalidConfigException|Throwable
801
     */
802 1
    public function createSavepoint(string $name): void
803
    {
804 1
        $this->db->createCommand("SAVEPOINT $name")->execute();
805
    }
806
807
    /**
808
     * Returns the ID of the last inserted row or sequence value.
809
     *
810
     * @param string $sequenceName name of the sequence object (required by some DBMS)
811
     *
812
     * @throws InvalidCallException if the DB connection is not active
813
     *
814
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
815
     *
816
     * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
817
     */
818 3
    public function getLastInsertID(string $sequenceName = ''): string
819
    {
820 3
        $pdo = $this->db->getPDO();
821
822 3
        if ($pdo !== null && $this->db->isActive()) {
823 3
            return $pdo->lastInsertId(
824 3
                $sequenceName === '' ? null : $this->db->getQuoter()->quoteTableName($sequenceName)
825
            );
826
        }
827
828
        throw new InvalidCallException('DB Connection is not active.');
829
    }
830
831
    /**
832
     * Releases an existing savepoint.
833
     *
834
     * @param string $name the savepoint name
835
     *
836
     * @throws Exception|InvalidConfigException|Throwable
837
     */
838
    public function releaseSavepoint(string $name): void
839
    {
840
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
841
    }
842
}
843