Test Failed
Pull Request — master (#83)
by Wilmer
16:31 queued 13:03
created

SchemaPDOSqlite::loadTableConstraints()   B

Complexity

Conditions 8
Paths 40

Size

Total Lines 52
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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

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