Passed
Pull Request — dev (#95)
by Def
18:06 queued 14:42
created

SchemaPDOSqlite::loadTableConstraints()   B

Complexity

Conditions 8
Paths 40

Size

Total Lines 52
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 8

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 26
dl 0
loc 52
ccs 27
cts 27
cp 1
rs 8.4444
c 1
b 0
f 0
cc 8
nc 40
nop 2
crap 8

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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