Passed
Pull Request — dev (#96)
by Def
19:16 queued 15:49
created

Schema::loadColumnSchema()   C

Complexity

Conditions 17
Paths 92

Size

Total Lines 52
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 17.47

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 34
c 2
b 0
f 0
dl 0
loc 52
ccs 30
cts 34
cp 0.8824
rs 5.2166
cc 17
nc 92
nop 1
crap 17.47

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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