Passed
Push — dev ( c872e6...d79752 )
by Def
24:43 queued 20:57
created

SchemaPDOSqlite::findTableNames()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

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