Passed
Push — dev ( 4758af...adb141 )
by Wilmer
02:26
created

SchemaPDOPgsql   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 1145
Duplicated Lines 0 %

Test Coverage

Coverage 95.25%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 56
eloc 717
c 2
b 0
f 0
dl 0
loc 1145
ccs 281
cts 295
cp 0.9525
rs 5.4029

27 Methods

Rating   Name   Duplication   Size   Complexity  
A resolveTableName() 0 23 3
A loadTablePrimaryKey() 0 5 2
A loadTableDefaultValues() 0 3 1
A findSchemaNames() 0 10 1
A findViewNames() 0 15 2
A __construct() 0 3 1
A resolveTableNames() 0 19 3
A loadTableUniques() 0 5 2
A loadTableChecks() 0 5 2
A loadTableForeignKeys() 0 5 2
A findTableNames() 0 15 2
A loadTableIndexes() 0 56 2
A loadTableSchema() 0 12 2
A createSavepoint() 0 3 1
C findConstraints() 0 555 9
A rollBackSavepoint() 0 3 1
A getViewNames() 0 7 4
A createColumnSchema() 0 3 1
A createColumnSchemaBuilder() 0 3 1
A getRawTableName() 0 9 2
A getCacheTag() 0 6 1
A getCacheKey() 0 7 1
A normalizePdoRowKeyCase() 0 13 3
A getLastInsertID() 0 11 4
A setTransactionIsolationLevel() 0 3 1
A releaseSavepoint() 0 3 1
A supportsSavepoint() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like SchemaPDOPgsql often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaPDOPgsql, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Pgsql\PDO;
6
7
use JsonException;
8
use PDO;
9
use Throwable;
10
use Yiisoft\Arrays\ArrayHelper;
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\DefaultValueConstraint;
16
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
17
use Yiisoft\Db\Constraint\IndexConstraint;
18
use Yiisoft\Db\Exception\Exception;
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\Pgsql\ColumnSchema;
24
use Yiisoft\Db\Pgsql\TableSchema;
25
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
26
use Yiisoft\Db\Schema\Schema;
27
use Yiisoft\Db\View\ViewInterface;
28
29
use function array_change_key_case;
30
use function array_merge;
31
use function array_unique;
32
use function array_values;
33
use function bindec;
34
use function explode;
35
use function preg_match;
36
use function preg_replace;
37
use function str_replace;
38
use function substr;
39
40
/**
41
 * The class Schema is the class for retrieving metadata from a PostgreSQL database
42
 * (version 9.6 and above).
43
 *
44
 * @psalm-type ColumnArray = array{
45
 *   table_schema: string,
46
 *   table_name: string,
47
 *   column_name: string,
48
 *   data_type: string,
49
 *   type_type: string|null,
50
 *   character_maximum_length: int,
51
 *   column_comment: string|null,
52
 *   modifier: int,
53
 *   is_nullable: bool,
54
 *   column_default: mixed,
55
 *   is_autoinc: bool,
56
 *   sequence_name: string|null,
57
 *   enum_values: array<array-key, float|int|string>|string|null,
58
 *   numeric_precision: int|null,
59
 *   numeric_scale: int|null,
60
 *   size: string|null,
61
 *   is_pkey: bool|null,
62
 *   dimension: int
63
 * }
64
 *
65
 * @psalm-type ConstraintArray = array<
66
 *   array-key,
67
 *   array {
68
 *     name: string,
69
 *     column_name: string,
70
 *     type: string,
71
 *     foreign_table_schema: string|null,
72
 *     foreign_table_name: string|null,
73
 *     foreign_column_name: string|null,
74
 *     on_update: string,
75
 *     on_delete: string,
76
 *     check_expr: string
77
 *   }
78
 * >
79
 *
80
 * @psalm-type FindConstraintArray = array{
81
 *   constraint_name: string,
82
 *   column_name: string,
83
 *   foreign_table_name: string,
84
 *   foreign_table_schema: string,
85
 *   foreign_column_name: string,
86
 * }
87
 */
88
final class SchemaPDOPgsql extends Schema implements ViewInterface
89
{
90
    public const TYPE_JSONB = 'jsonb';
91
92
    /**
93
     * @var array The mapping from physical column types (keys) to abstract column types (values).
94
     *
95
     * {@see http://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE}
96
     *
97
     * @psalm-var string[]
98
     */
99
    private array $typeMap = [
0 ignored issues
show
introduced by
The private property $typeMap is not used, and could be removed.
Loading history...
100
        'bit' => self::TYPE_INTEGER,
101
        'bit varying' => self::TYPE_INTEGER,
102
        'varbit' => self::TYPE_INTEGER,
103
        'bool' => self::TYPE_BOOLEAN,
104
        'boolean' => self::TYPE_BOOLEAN,
105
        'box' => self::TYPE_STRING,
106
        'circle' => self::TYPE_STRING,
107
        'point' => self::TYPE_STRING,
108
        'line' => self::TYPE_STRING,
109
        'lseg' => self::TYPE_STRING,
110
        'polygon' => self::TYPE_STRING,
111
        'path' => self::TYPE_STRING,
112
        'character' => self::TYPE_CHAR,
113
        'char' => self::TYPE_CHAR,
114
        'bpchar' => self::TYPE_CHAR,
115
        'character varying' => self::TYPE_STRING,
116
        'varchar' => self::TYPE_STRING,
117
        'text' => self::TYPE_TEXT,
118
        'bytea' => self::TYPE_BINARY,
119
        'cidr' => self::TYPE_STRING,
120
        'inet' => self::TYPE_STRING,
121
        'macaddr' => self::TYPE_STRING,
122
        'real' => self::TYPE_FLOAT,
123
        'float4' => self::TYPE_FLOAT,
124
        'double precision' => self::TYPE_DOUBLE,
125
        'float8' => self::TYPE_DOUBLE,
126
        'decimal' => self::TYPE_DECIMAL,
127
        'numeric' => self::TYPE_DECIMAL,
128
        'money' => self::TYPE_MONEY,
129
        'smallint' => self::TYPE_SMALLINT,
130
        'int2' => self::TYPE_SMALLINT,
131
        'int4' => self::TYPE_INTEGER,
132
        'int' => self::TYPE_INTEGER,
133
        'integer' => self::TYPE_INTEGER,
134
        'bigint' => self::TYPE_BIGINT,
135
        'int8' => self::TYPE_BIGINT,
136
        'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal!
137
        'smallserial' => self::TYPE_SMALLINT,
138
        'serial2' => self::TYPE_SMALLINT,
139
        'serial4' => self::TYPE_INTEGER,
140
        'serial' => self::TYPE_INTEGER,
141
        'bigserial' => self::TYPE_BIGINT,
142
        'serial8' => self::TYPE_BIGINT,
143
        'pg_lsn' => self::TYPE_BIGINT,
144
        'date' => self::TYPE_DATE,
145
        'interval' => self::TYPE_STRING,
146
        'time without time zone' => self::TYPE_TIME,
147
        'time' => self::TYPE_TIME,
148
        'time with time zone' => self::TYPE_TIME,
149
        'timetz' => self::TYPE_TIME,
150
        'timestamp without time zone' => self::TYPE_TIMESTAMP,
151
        'timestamp' => self::TYPE_TIMESTAMP,
152
        'timestamp with time zone' => self::TYPE_TIMESTAMP,
153
        'timestamptz' => self::TYPE_TIMESTAMP,
154
        'abstime' => self::TYPE_TIMESTAMP,
155
        'tsquery' => self::TYPE_STRING,
156
        'tsvector' => self::TYPE_STRING,
157
        'txid_snapshot' => self::TYPE_STRING,
158
        'unknown' => self::TYPE_STRING,
159
        'uuid' => self::TYPE_STRING,
160
        'json' => self::TYPE_JSON,
161
        'jsonb' => self::TYPE_JSON,
162
        'xml' => self::TYPE_STRING,
163
    ];
164
165
    private array $viewNames = [];
166
167 418
    public function __construct(private ConnectionPDOInterface $db, SchemaCache $schemaCache)
168
    {
169 418
        parent::__construct($schemaCache);
170
    }
171
172
    /**
173
     * @var string|null the default schema used for the current session.
174
     */
175
    protected ?string $defaultSchema = 'public';
176
177
    /**
178
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
179
     * case starting and ending characters are different.
180
     */
181
    protected string|array $tableQuoteCharacter = '"';
182
183
    /**
184
     * Resolves the table name and schema name (if any).
185
     *
186
     * @param string $name the table name.
187
     *
188
     * @return TableSchema with resolved table, schema, etc. names.
189
     *
190
     * {@see TableSchema}
191
     */
192 76
    protected function resolveTableName(string $name): TableSchema
193
    {
194 76
        $resolvedName = new TableSchema();
195
196 76
        $parts = explode('.', str_replace('"', '', $name));
197
198 76
        if (isset($parts[1])) {
199 5
            $resolvedName->schemaName($parts[0]);
200 5
            $resolvedName->name($parts[1]);
201
        } else {
202 71
            $resolvedName->schemaName($this->defaultSchema);
203 71
            $resolvedName->name($name);
204
        }
205
206 76
        $resolvedName->fullName(
207
            (
208 76
                $resolvedName->getSchemaName() !== $this->defaultSchema ?
209
                    (string) $resolvedName->getSchemaName() . '.' :
210 76
                    ''
211 76
            ) . $resolvedName->getName()
212
        );
213
214 76
        return $resolvedName;
215
    }
216
217
    /**
218
     * Returns all schema names in the database, including the default one but not system schemas.
219
     *
220
     * This method should be overridden by child classes in order to support this feature because the default
221
     * implementation simply throws an exception.
222
     *
223
     * @throws Exception|InvalidConfigException|Throwable
224
     *
225
     * @return array all schema names in the database, except system schemas.
226
     */
227 2
    protected function findSchemaNames(): array
228
    {
229 2
        $sql = <<<SQL
230
        SELECT "ns"."nspname"
231
        FROM "pg_namespace" AS "ns"
232
        WHERE "ns"."nspname" != 'information_schema' AND "ns"."nspname" NOT LIKE 'pg_%'
233
        ORDER BY "ns"."nspname" ASC
234
        SQL;
235
236 2
        return $this->db->createCommand($sql)->queryColumn();
237
    }
238
239
    /**
240
     * Returns all table names in the database.
241
     *
242
     * This method should be overridden by child classes in order to support this feature because the default
243
     * implementation simply throws an exception.
244
     *
245
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
246
     *
247
     * @throws Exception|InvalidConfigException|Throwable
248
     *
249
     * @return array all table names in the database. The names have NO schema name prefix.
250
     */
251 5
    protected function findTableNames(string $schema = ''): array
252
    {
253 5
        if ($schema === '') {
254 5
            $schema = $this->defaultSchema;
255
        }
256
257 5
        $sql = <<<SQL
258
        SELECT c.relname AS table_name
259
        FROM pg_class c
260
        INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
261
        WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f', 'p')
262
        ORDER BY c.relname
263
        SQL;
264
265 5
        return $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn();
266
    }
267
268
    /**
269
     * Loads the metadata for the specified table.
270
     *
271
     * @param string $name table name.
272
     *
273
     * @throws Exception|InvalidConfigException|Throwable
274
     *
275
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
276
     */
277 104
    protected function loadTableSchema(string $name): ?TableSchema
278
    {
279 104
        $table = new TableSchema();
280
281 104
        $this->resolveTableNames($table, $name);
282
283 104
        if ($this->findColumns($table)) {
0 ignored issues
show
Bug introduced by
The method findColumns() does not exist on Yiisoft\Db\Pgsql\PDO\SchemaPDOPgsql. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

283
        if ($this->/** @scrutinizer ignore-call */ findColumns($table)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
284 98
            $this->findConstraints($table);
285 98
            return $table;
286
        }
287
288 17
        return null;
289
    }
290
291
    /**
292
     * Loads a primary key for the given table.
293
     *
294
     * @param string $tableName table name.
295
     *
296
     * @throws Exception|InvalidConfigException|Throwable
297
     *
298
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
299
     */
300 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
301
    {
302 31
        $tablePrimaryKey = $this->loadTableConstraints($tableName, 'primaryKey');
0 ignored issues
show
Bug introduced by
The method loadTableConstraints() does not exist on Yiisoft\Db\Pgsql\PDO\SchemaPDOPgsql. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

302
        /** @scrutinizer ignore-call */ 
303
        $tablePrimaryKey = $this->loadTableConstraints($tableName, 'primaryKey');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
303
304 31
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
305
    }
306
307
    /**
308
     * Loads all foreign keys for the given table.
309
     *
310
     * @param string $tableName table name.
311
     *
312
     * @throws Exception|InvalidConfigException|Throwable
313
     *
314
     * @return array foreign keys for the given table.
315
     *
316
     * @psaml-return array|ForeignKeyConstraint[]
317
     */
318 4
    protected function loadTableForeignKeys(string $tableName): array
319
    {
320 4
        $tableForeignKeys = $this->loadTableConstraints($tableName, 'foreignKeys');
321
322 4
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
323
    }
324
325
    /**
326
     * Loads all indexes for the given table.
327
     *
328
     * @param string $tableName table name.
329
     *
330
     * @throws Exception|InvalidConfigException|Throwable
331
     *
332
     * @return IndexConstraint[] indexes for the given table.
333
     */
334 28
    protected function loadTableIndexes(string $tableName): array
335
    {
336 28
        $sql = <<<SQL
337
        SELECT
338
            "ic"."relname" AS "name",
339
            "ia"."attname" AS "column_name",
340
            "i"."indisunique" AS "index_is_unique",
341
            "i"."indisprimary" AS "index_is_primary"
342
        FROM "pg_class" AS "tc"
343
        INNER JOIN "pg_namespace" AS "tcns"
344
            ON "tcns"."oid" = "tc"."relnamespace"
345
        INNER JOIN "pg_index" AS "i"
346
            ON "i"."indrelid" = "tc"."oid"
347
        INNER JOIN "pg_class" AS "ic"
348
            ON "ic"."oid" = "i"."indexrelid"
349
        INNER JOIN "pg_attribute" AS "ia"
350
            ON "ia"."attrelid" = "i"."indexrelid"
351
        WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
352
        ORDER BY "ia"."attnum" ASC
353
        SQL;
354
355 28
        $resolvedName = $this->resolveTableName($tableName);
356
357 28
        $indexes = $this->db->createCommand($sql, [
358 28
            ':schemaName' => $resolvedName->getSchemaName(),
359 28
            ':tableName' => $resolvedName->getName(),
360 28
        ])->queryAll();
361
362
        /** @var array[] @indexes */
363 28
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
364 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
365 28
        $result = [];
366
367
        /**
368
         * @var object|string|null $name
369
         * @var array<
370
         *   array-key,
371
         *   array{
372
         *     name: string,
373
         *     column_name: string,
374
         *     index_is_unique: bool,
375
         *     index_is_primary: bool
376
         *   }
377
         * > $index
378
         */
379 28
        foreach ($indexes as $name => $index) {
380 25
            $ic = (new IndexConstraint())
381 25
                ->name($name)
382 25
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
383 25
                ->primary($index[0]['index_is_primary'])
384 25
                ->unique($index[0]['index_is_unique']);
385
386 25
            $result[] = $ic;
387
        }
388
389 28
        return $result;
390
    }
391
392
    /**
393
     * Loads all unique constraints for the given table.
394
     *
395
     * @param string $tableName table name.
396
     *
397
     * @throws Exception|InvalidConfigException|Throwable
398
     *
399
     * @return array unique constraints for the given table.
400
     *
401
     * @psalm-return array|Constraint[]
402
     */
403 13
    protected function loadTableUniques(string $tableName): array
404
    {
405 13
        $tableUniques = $this->loadTableConstraints($tableName, 'uniques');
406
407 13
        return is_array($tableUniques) ? $tableUniques : [];
408
    }
409
410
    /**
411
     * Loads all check constraints for the given table.
412
     *
413
     * @param string $tableName table name.
414
     *
415
     * @throws Exception|InvalidConfigException|Throwable
416
     *
417
     * @return array check constraints for the given table.
418
     *
419
     * @psaml-return array|CheckConstraint[]
420
     */
421 13
    protected function loadTableChecks(string $tableName): array
422
    {
423 13
        $tableChecks = $this->loadTableConstraints($tableName, 'checks');
424
425 13
        return is_array($tableChecks) ? $tableChecks : [];
426
    }
427
428
    /**
429
     * Loads all default value constraints for the given table.
430
     *
431
     * @param string $tableName table name.
432
     *
433
     * @throws NotSupportedException
434
     *
435
     * @return DefaultValueConstraint[] default value constraints for the given table.
436
     */
437 12
    protected function loadTableDefaultValues(string $tableName): array
438
    {
439 12
        throw new NotSupportedException('PostgreSQL does not support default value constraints.');
440
    }
441
442
    /**
443
     * Resolves the table name and schema name (if any).
444
     *
445
     * @param TableSchema $table the table metadata object.
446
     * @param string $name the table name
447
     */
448 104
    protected function resolveTableNames(TableSchema $table, string $name): void
449
    {
450 104
        $parts = explode('.', str_replace('"', '', $name));
451
452 104
        if (isset($parts[1])) {
453
            $table->schemaName($parts[0]);
454
            $table->name($parts[1]);
455
        } else {
456 104
            $table->schemaName($this->defaultSchema);
457 104
            $table->name($parts[0]);
458
        }
459
460 104
        if ($table->getSchemaName() !== $this->defaultSchema) {
461
            $name = (string) $table->getSchemaName() . '.' . $table->getName();
462
        } else {
463 104
            $name = $table->getName();
464
        }
465
466 104
        $table->fullName($name);
467
    }
468
469
    /**
470
     * @throws Exception|InvalidConfigException|Throwable
471
     */
472 1
    public function findViewNames(string $schema = ''): array
473
    {
474 1
        if ($schema === '') {
475
            $schema = $this->defaultSchema;
476
        }
477
478 1
        $sql = <<<SQL
479
        SELECT c.relname AS table_name
480
        FROM pg_class c
481
        INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
482
        WHERE ns.nspname = :schemaName AND (c.relkind = 'v' OR c.relkind = 'm')
483
        ORDER BY c.relname
484
        SQL;
485
486 1
        return $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn();
487
    }
488
489
    /**
490
     * Collects the foreign key column details for the given table.
491
     *
492
     * @param TableSchema $table the table metadata
493
     *
494
     * @throws Exception|InvalidConfigException|Throwable
495
     */
496 98
    protected function findConstraints(TableSchema $table): void
497
    {
498 98
        $tableName = $table->getName();
499 98
        $tableSchema = $table->getSchemaName();
500
501
        /** @var mixed */
502 98
        $tableName = $this->db->getQuoter()->quoteValue($tableName);
503
504 98
        if ($tableSchema !== null) {
505
            /** @var mixed */
506 98
            $tableSchema = $this->db->getQuoter()->quoteValue($tableSchema);
0 ignored issues
show
Unused Code introduced by
The assignment to $tableSchema is dead and can be removed.
Loading history...
507
        }
508
509
        /**
510
         * We need to extract the constraints de hard way since:
511
         * {@see http://www.postgresql.org/message-id/[email protected]}
512
         */
513
514 98
        $sql = <<<SQL
515
        SELECT
516
            ct.conname as constraint_name,
517
            a.attname as column_name,
518
            fc.relname as foreign_table_name,
519
            fns.nspname as foreign_table_schema,
520
            fa.attname as foreign_column_name
521
            FROM
522
            (SELECT ct.conname, ct.conrelid, ct.confrelid, ct.conkey, ct.contype, ct.confkey,
523
                generate_subscripts(ct.conkey, 1) AS s
524
                FROM pg_constraint ct
525
            ) AS ct
526
            inner join pg_class c on c.oid=ct.conrelid
527
            inner join pg_namespace ns on c.relnamespace=ns.oid
528
            inner join pg_attribute a on a.attrelid=ct.conrelid and a.attnum = ct.conkey[ct.s]
529
            left join pg_class fc on fc.oid=ct.confrelid
530
            left join pg_namespace fns on fc.relnamespace=fns.oid
531
            left join pg_attribute fa on fa.attrelid=ct.confrelid and fa.attnum = ct.confkey[ct.s]
532
        WHERE
533
            ct.contype='f'
534
            and c.relname=$tableName
535
            and ns.nspname=$tableSchema
536
        ORDER BY
537
            fns.nspname, fc.relname, a.attnum
538
        SQL;
539
540
        /** @var array{array{tableName: string, columns: array}} $constraints */
541 98
        $constraints = [];
542
543 98
        $slavePdo = $this->db->getSlavePdo();
544
545
        /**
546
         * @psalm-var array<
547
         *   array{
548
         *     constraint_name: string,
549
         *     column_name: string,
550
         *     foreign_table_name: string,
551
         *     foreign_table_schema: string,
552
         *     foreign_column_name: string,
553
         *   }
554
         * > $rows
555
         */
556 98
        $rows = $this->db->createCommand($sql)->queryAll();
557
558 98
        foreach ($rows as $constraint) {
559 9
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
560
                $constraint = array_change_key_case($constraint, CASE_LOWER);
561
            }
562
563 9
            if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
564
                $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
565
            } else {
566 9
                $foreignTable = $constraint['foreign_table_name'];
567
            }
568
569 9
            $name = $constraint['constraint_name'];
570
571 9
            if (!isset($constraints[$name])) {
572 9
                $constraints[$name] = [
573
                    'tableName' => $foreignTable,
574
                    'columns' => [],
575
                ];
576
            }
577
578 9
            $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name'];
579
        }
580
581
        /**
582
         * @var int|string $foreingKeyName.
583
         * @var array{tableName: string, columns: array} $constraint
584
         */
585 98
        foreach ($constraints as $foreingKeyName => $constraint) {
586 9
            $table->foreignKey(
587 9
                (string) $foreingKeyName,
588 9
                array_merge([$constraint['tableName']], $constraint['columns'])
589
            );
590
        }
591
    }
592
593
    /**
594
     * Gets information about given table unique indexes.
595
     *
596
     * @param TableSchema $table the table metadata.
597
     *
598
     * @throws Exception|InvalidConfigException|Throwable
599
     *
600
     * @return array with index and column names.
601
     */
602 1
    protected function getUniqueIndexInformation(TableSchema $table): array
603
    {
604 1
        $sql = <<<'SQL'
605
        SELECT
606
            i.relname as indexname,
607
            pg_get_indexdef(idx.indexrelid, k + 1, TRUE) AS columnname
608
        FROM (
609
            SELECT *, generate_subscripts(indkey, 1) AS k
610
            FROM pg_index
611
        ) idx
612
        INNER JOIN pg_class i ON i.oid = idx.indexrelid
613
        INNER JOIN pg_class c ON c.oid = idx.indrelid
614
        INNER JOIN pg_namespace ns ON c.relnamespace = ns.oid
615
        WHERE idx.indisprimary = FALSE AND idx.indisunique = TRUE
616
        AND c.relname = :tableName AND ns.nspname = :schemaName
617
        ORDER BY i.relname, k
618
        SQL;
619
620 1
        return $this->db->createCommand($sql, [
621 1
            ':schemaName' => $table->getSchemaName(),
622 1
            ':tableName' => $table->getName(),
623 1
        ])->queryAll();
624
    }
625
626
    /**
627
     * Returns all unique indexes for the given table.
628
     *
629
     * Each array element is of the following structure:
630
     *
631
     * ```php
632
     * [
633
     *     'IndexName1' => ['col1' [, ...]],
634
     *     'IndexName2' => ['col2' [, ...]],
635
     * ]
636
     * ```
637
     *
638
     * @param TableSchema $table the table metadata
639
     *
640
     * @throws Exception|InvalidConfigException|Throwable
641
     *
642
     * @return array all unique indexes for the given table.
643
     */
644 1
    public function findUniqueIndexes(TableSchema $table): array
645
    {
646 1
        $uniqueIndexes = [];
647 1
        $slavePdo = $this->db->getSlavePdo();
648
649
        /** @var array{indexname: string, columnname: string} $row */
650 1
        foreach ($this->getUniqueIndexInformation($table) as $row) {
651 1
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
652 1
                $row = array_change_key_case($row, CASE_LOWER);
653
            }
654
655 1
            $column = $row['columnname'];
656
657 1
            if (!empty($column) && $column[0] === '"') {
658
                /**
659
                 * postgres will quote names that are not lowercase-only.
660
                 *
661
                 * {@see https://github.com/yiisoft/yii2/issues/10613}
662
                 */
663 1
                $column = substr($column, 1, -1);
664
            }
665
666 1
            $uniqueIndexes[$row['indexname']][] = $column;
667
        }
668
669 1
        return $uniqueIndexes;
670
    }
671
672
    /**
673
     * Collects the metadata of table columns.
674
     *
675
     * @param TableSchema $table the table metadata.
676
     *
677
     * @throws Exception|InvalidConfigException|JsonException|Throwable
678
     *
679
     * @return bool whether the table exists in the database.
680
     */
681 104
    protected function findColumns(TableSchema $table): bool
682
    {
683 104
        $tableName = $table->getName();
684 104
        $schemaName = $table->getSchemaName();
685 104
        $orIdentity = '';
686
687
        /** @var mixed */
688 104
        $tableName = $this->db->getQuoter()->quoteValue($tableName);
689
690 104
        if ($schemaName !== null) {
691
            /** @var mixed */
692 104
            $schemaName = $this->db->getQuoter()->quoteValue($schemaName);
693
        }
694
695 104
        if (version_compare($this->db->getServerVersion(), '12.0', '>=')) {
696 104
            $orIdentity = 'OR a.attidentity != \'\'';
697
        }
698
699 104
        $sql = <<<SQL
700
        SELECT
701
            d.nspname AS table_schema,
702
            c.relname AS table_name,
703
            a.attname AS column_name,
704
            COALESCE(td.typname, tb.typname, t.typname) AS data_type,
705
            COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
706
            a.attlen AS character_maximum_length,
707
            pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
708
            a.atttypmod AS modifier,
709
            a.attnotnull = false AS is_nullable,
710
            CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
711
            coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) $orIdentity AS is_autoinc,
712
            pg_get_serial_sequence(quote_ident(d.nspname) || '.' || quote_ident(c.relname), a.attname)
713
            AS sequence_name,
714
            CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
715
                THEN array_to_string(
716
                    (
717
                        SELECT array_agg(enumlabel)
718
                        FROM pg_enum
719
                        WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid)
720
                    )::varchar[],
721
                ',')
722
                ELSE NULL
723
            END AS enum_values,
724
            CASE atttypid
725
                WHEN 21 /*int2*/ THEN 16
726
                WHEN 23 /*int4*/ THEN 32
727
                WHEN 20 /*int8*/ THEN 64
728
                WHEN 1700 /*numeric*/ THEN
729
                    CASE WHEN atttypmod = -1
730
                        THEN null
731
                        ELSE ((atttypmod - 4) >> 16) & 65535
732
                        END
733
                WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/
734
                WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/
735
                    ELSE null
736
                    END   AS numeric_precision,
737
            CASE
738
                WHEN atttypid IN (21, 23, 20) THEN 0
739
                WHEN atttypid IN (1700) THEN
740
            CASE
741
                WHEN atttypmod = -1 THEN null
742
                    ELSE (atttypmod - 4) & 65535
743
                    END
744
                    ELSE null
745
                    END AS numeric_scale,
746
                    CAST(
747
                        information_schema._pg_char_max_length(
748
                        information_schema._pg_truetypid(a, t),
749
                        information_schema._pg_truetypmod(a, t)
750
                        ) AS numeric
751
                    ) AS size,
752
                    a.attnum = any (ct.conkey) as is_pkey,
753
                    COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
754
            FROM
755
                pg_class c
756
                LEFT JOIN pg_attribute a ON a.attrelid = c.oid
757
                LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
758
                LEFT JOIN pg_type t ON a.atttypid = t.oid
759
                LEFT JOIN pg_type tb ON (a.attndims > 0 OR t.typcategory='A') AND t.typelem > 0 AND t.typelem = tb.oid
760
                                            OR t.typbasetype > 0 AND t.typbasetype = tb.oid
761
                LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
762
                LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
763
                LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
764
            WHERE
765
                a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped
766
                AND c.relname = $tableName
767
                AND d.nspname = $schemaName
768
            ORDER BY
769
                a.attnum;
770
        SQL;
771
772 104
        $columns = $this->db->createCommand($sql)->queryAll();
773 104
        $slavePdo = $this->db->getSlavePdo();
774
775 104
        if (empty($columns)) {
776 17
            return false;
777
        }
778
779
        /** @var array $column */
780 98
        foreach ($columns as $column) {
781 98
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
782 1
                $column = array_change_key_case($column, CASE_LOWER);
783
            }
784
785
            /** @psalm-var ColumnArray $column */
786 98
            $loadColumnSchema = $this->loadColumnSchema($column);
787 98
            $table->columns($loadColumnSchema->getName(), $loadColumnSchema);
788
789
            /** @var mixed */
790 98
            $defaultValue = $loadColumnSchema->getDefaultValue();
791
792 98
            if ($loadColumnSchema->isPrimaryKey()) {
793 66
                $table->primaryKey($loadColumnSchema->getName());
794
795 66
                if ($table->getSequenceName() === null) {
796 66
                    $table->sequenceName($loadColumnSchema->getSequenceName());
797
                }
798
799 66
                $loadColumnSchema->defaultValue(null);
800 95
            } elseif ($defaultValue) {
801
                if (
802 57
                    is_string($defaultValue) &&
803 57
                    in_array(
804 57
                        $loadColumnSchema->getType(),
805 57
                        [self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME],
806
                        true
807
                    ) &&
808 30
                    in_array(
809 30
                        strtoupper($defaultValue),
810 30
                        ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'],
811
                        true
812
                    )
813
                ) {
814 28
                    $loadColumnSchema->defaultValue(new Expression($defaultValue));
815 57
                } elseif ($loadColumnSchema->getType() === 'boolean') {
816 53
                    $loadColumnSchema->defaultValue(($defaultValue  === 'true'));
817 33
                } elseif (is_string($defaultValue) && preg_match("/^B'(.*?)'::/", $defaultValue, $matches)) {
818
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
819 33
                } elseif (is_string($defaultValue) && preg_match("/^'(\d+)'::\"bit\"$/", $defaultValue, $matches)) {
820 28
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
821 33
                } elseif (is_string($defaultValue) && preg_match("/^'(.*?)'::/", $defaultValue, $matches)) {
822 30
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[1]));
823
                } elseif (
824 31
                    is_string($defaultValue) &&
825 31
                    preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $defaultValue, $matches)
826
                ) {
827 31
                    if ($matches[2] === 'NULL') {
828 5
                        $loadColumnSchema->defaultValue(null);
829
                    } else {
830 31
                        $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[2]));
831
                    }
832
                } else {
833
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($defaultValue));
834
                }
835
            }
836
        }
837
838 98
        return true;
839
    }
840
841
    /**
842
     * Loads the column information into a {@see ColumnSchema} object.
843
     *
844
     * @psalm-param array{
845
     *   table_schema: string,
846
     *   table_name: string,
847
     *   column_name: string,
848
     *   data_type: string,
849
     *   type_type: string|null,
850
     *   character_maximum_length: int,
851
     *   column_comment: string|null,
852
     *   modifier: int,
853
     *   is_nullable: bool,
854
     *   column_default: mixed,
855
     *   is_autoinc: bool,
856
     *   sequence_name: string|null,
857
     *   enum_values: array<array-key, float|int|string>|string|null,
858
     *   numeric_precision: int|null,
859
     *   numeric_scale: int|null,
860
     *   size: string|null,
861
     *   is_pkey: bool|null,
862
     *   dimension: int
863
     * } $info column information.
864
     *
865
     * @return ColumnSchema the column schema object.
866
     */
867 98
    protected function loadColumnSchema(array $info): ColumnSchema
868
    {
869 98
        $column = $this->createColumnSchema();
870 98
        $column->allowNull($info['is_nullable']);
871 98
        $column->autoIncrement($info['is_autoinc']);
872 98
        $column->comment($info['column_comment']);
873 98
        $column->dbType($info['data_type']);
874 98
        $column->defaultValue($info['column_default']);
875 98
        $column->enumValues(($info['enum_values'] !== null)
876 98
            ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) : null);
877 98
        $column->unsigned(false); // has no meaning in PG
878 98
        $column->primaryKey((bool) $info['is_pkey']);
879 98
        $column->name($info['column_name']);
880 98
        $column->precision($info['numeric_precision']);
881 98
        $column->scale($info['numeric_scale']);
882 98
        $column->size($info['size'] === null ? null : (int) $info['size']);
883 98
        $column->dimension($info['dimension']);
884
885
        /**
886
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
887
         * default value.
888
         *
889
         * @var mixed $defaultValue
890
         */
891 98
        $defaultValue = $column->getDefaultValue();
892 98
        $sequenceName = $info['sequence_name'] ?? null;
893
894
        if (
895 98
            isset($defaultValue) &&
896 98
            is_string($defaultValue) &&
897 98
            preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1
898
        ) {
899 61
            $column->sequenceName(preg_replace(
900 61
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
901
                '',
902
                $defaultValue
903
            ));
904 96
        } elseif ($sequenceName !== null) {
905 5
            $column->sequenceName($this->resolveTableName($sequenceName)->getFullName());
906
        }
907
908 98
        if (isset($this->typeMap[$column->getDbType()])) {
909 98
            $column->type($this->typeMap[$column->getDbType()]);
910
        } else {
911
            $column->type(self::TYPE_STRING);
912
        }
913
914 98
        $column->phpType($this->getColumnPhpType($column));
915
916 98
        return $column;
917
    }
918
919
    /**
920
     * Loads multiple types of constraints and returns the specified ones.
921
     *
922
     * @param string $tableName table name.
923
     * @param string $returnType return type:
924
     * - primaryKey
925
     * - foreignKeys
926
     * - uniques
927
     * - checks
928
     *
929
     * @throws Exception|InvalidConfigException|Throwable
930
     *
931
     * @return array|Constraint|null (CheckConstraint|Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
932
     */
933 61
    private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null
934
    {
935 61
        $sql = <<<SQL
936
        SELECT
937
            "c"."conname" AS "name",
938
            "a"."attname" AS "column_name",
939
            "c"."contype" AS "type",
940
            "ftcns"."nspname" AS "foreign_table_schema",
941
            "ftc"."relname" AS "foreign_table_name",
942
            "fa"."attname" AS "foreign_column_name",
943
            "c"."confupdtype" AS "on_update",
944
            "c"."confdeltype" AS "on_delete",
945
            pg_get_constraintdef("c"."oid") AS "check_expr"
946
        FROM "pg_class" AS "tc"
947
        INNER JOIN "pg_namespace" AS "tcns"
948
            ON "tcns"."oid" = "tc"."relnamespace"
949
        INNER JOIN "pg_constraint" AS "c"
950
            ON "c"."conrelid" = "tc"."oid"
951
        INNER JOIN "pg_attribute" AS "a"
952
            ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
953
        LEFT JOIN "pg_class" AS "ftc"
954
            ON "ftc"."oid" = "c"."confrelid"
955
        LEFT JOIN "pg_namespace" AS "ftcns"
956
            ON "ftcns"."oid" = "ftc"."relnamespace"
957
        LEFT JOIN "pg_attribute" "fa"
958
            ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
959
        WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
960
        ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
961
        SQL;
962
963
        /** @var array<array-key, string> $actionTypes */
964 61
        $actionTypes = [
965
            'a' => 'NO ACTION',
966
            'r' => 'RESTRICT',
967
            'c' => 'CASCADE',
968
            'n' => 'SET NULL',
969
            'd' => 'SET DEFAULT',
970
        ];
971
972 61
        $resolvedName = $this->resolveTableName($tableName);
973
974 61
        $constraints = $this->db->createCommand($sql, [
975 61
            ':schemaName' => $resolvedName->getSchemaName(),
976 61
            ':tableName' => $resolvedName->getName(),
977 61
        ])->queryAll();
978
979
        /** @var array<array-key, array> $constraints */
980 61
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
981 61
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
982
983 61
        $result = [
984
            'primaryKey' => null,
985
            'foreignKeys' => [],
986
            'uniques' => [],
987
            'checks' => [],
988
        ];
989
990
        /**
991
         * @var string $type
992
         * @var array $names
993
         */
994 61
        foreach ($constraints as $type => $names) {
995
            /**
996
             * @psalm-var object|string|null $name
997
             * @psalm-var ConstraintArray $constraint
998
             */
999 61
            foreach ($names as $name => $constraint) {
1000 61
                switch ($type) {
1001 61
                    case 'p':
1002 46
                        $ct = (new Constraint())
1003 46
                            ->name($name)
1004 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1005
1006 46
                        $result['primaryKey'] = $ct;
1007 46
                        break;
1008 59
                    case 'f':
1009 13
                        $onDelete = $actionTypes[$constraint[0]['on_delete']] ?? null;
1010 13
                        $onUpdate = $actionTypes[$constraint[0]['on_update']] ?? null;
1011
1012 13
                        $fk = (new ForeignKeyConstraint())
1013 13
                            ->name($name)
1014 13
                            ->columnNames(array_values(
1015 13
                                array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
1016
                            ))
1017 13
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
1018 13
                            ->foreignTableName($constraint[0]['foreign_table_name'])
1019 13
                            ->foreignColumnNames(array_values(
1020 13
                                array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
1021
                            ))
1022 13
                            ->onDelete($onDelete)
1023 13
                            ->onUpdate($onUpdate);
1024
1025 13
                        $result['foreignKeys'][] = $fk;
1026 13
                        break;
1027 47
                    case 'u':
1028 46
                        $ct = (new Constraint())
1029 46
                            ->name($name)
1030 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1031
1032 46
                        $result['uniques'][] = $ct;
1033 46
                        break;
1034 10
                    case 'c':
1035 10
                        $ck = (new CheckConstraint())
1036 10
                            ->name($name)
1037 10
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
1038 10
                            ->expression($constraint[0]['check_expr']);
1039
1040 10
                        $result['checks'][] = $ck;
1041 10
                        break;
1042
                }
1043
            }
1044
        }
1045
1046 61
        foreach ($result as $type => $data) {
1047 61
            $this->setTableMetadata($tableName, $type, $data);
1048
        }
1049
1050 61
        return $result[$returnType];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $returnType seems to be never defined.
Loading history...
1051
    }
1052
1053
    /**
1054
     * Creates a column schema for the database.
1055
     *
1056
     * This method may be overridden by child classes to create a DBMS-specific column schema.
1057
     *
1058
     * @return ColumnSchema column schema instance.
1059
     */
1060 98
    private function createColumnSchema(): ColumnSchema
0 ignored issues
show
Unused Code introduced by
The method createColumnSchema() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
1061
    {
1062 98
        return new ColumnSchema();
1063
    }
1064
1065
    /**
1066
     * Create a column schema builder instance giving the type and value precision.
1067
     *
1068
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
1069
     *
1070
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
1071
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
1072
     *
1073
     * @return ColumnSchemaBuilder column schema builder instance
1074
     *
1075
     * @psalm-param int|string|string[]|null $length
1076
     */
1077 4
    public function createColumnSchemaBuilder(string $type, int|string|array|null $length = null): ColumnSchemaBuilder
1078
    {
1079 4
        return new ColumnSchemaBuilder($type, $length);
1080
    }
1081
1082 1
    public function rollBackSavepoint(string $name): void
1083
    {
1084 1
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
1085
    }
1086
1087 2
    public function setTransactionIsolationLevel(string $level): void
1088
    {
1089 2
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
1090
    }
1091
1092
    /**
1093
     * Returns the actual name of a given table name.
1094
     *
1095
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
1096
     * {@see ConnectionInterface::tablePrefix}.
1097
     *
1098
     * @param string $name the table name to be converted.
1099
     *
1100
     * @return string the real name of the given table name.
1101
     */
1102 164
    public function getRawTableName(string $name): string
1103
    {
1104 164
        if (str_contains($name, '{{')) {
1105 23
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
1106
1107 23
            return str_replace('%', $this->db->getTablePrefix(), $name);
1108
        }
1109
1110 164
        return $name;
1111
    }
1112
1113
    /**
1114
     * Returns the cache key for the specified table name.
1115
     *
1116
     * @param string $name the table name.
1117
     *
1118
     * @return array the cache key.
1119
     */
1120 164
    protected function getCacheKey(string $name): array
1121
    {
1122
        return [
1123 164
            __CLASS__,
1124 164
            $this->db->getDriver()->getDsn(),
1125 164
            $this->db->getDriver()->getUsername(),
1126 164
            $this->getRawTableName($name),
1127
        ];
1128
    }
1129
1130
    /**
1131
     * Returns the cache tag name.
1132
     *
1133
     * This allows {@see refresh()} to invalidate all cached table schemas.
1134
     *
1135
     * @return string the cache tag name.
1136
     */
1137 164
    protected function getCacheTag(): string
1138
    {
1139 164
        return md5(serialize([
1140
            __CLASS__,
1141 164
            $this->db->getDriver()->getDsn(),
1142 164
            $this->db->getDriver()->getUsername(),
1143
        ]));
1144
    }
1145
1146
    /**
1147
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
1148
     */
1149 2
    public function supportsSavepoint(): bool
1150
    {
1151 2
        return $this->db->isSavepointEnabled();
1152
    }
1153
1154
    /**
1155
     * Changes row's array key case to lower if PDO one is set to uppercase.
1156
     *
1157
     * @param array $row row's array or an array of row's arrays.
1158
     * @param bool $multiple whether multiple rows or a single row passed.
1159
     *
1160
     * @throws Exception
1161
     *
1162
     * @return array normalized row or rows.
1163
     */
1164 71
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
1165
    {
1166 71
        if ($this->db->getSlavePdo()?->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_UPPER) {
1167 55
            return $row;
1168
        }
1169
1170 16
        if ($multiple) {
1171 16
            return array_map(static function (array $row) {
1172 15
                return array_change_key_case($row, CASE_LOWER);
1173
            }, $row);
1174
        }
1175
1176
        return array_change_key_case($row, CASE_LOWER);
1177
    }
1178
1179
    /**
1180
     * Returns the ID of the last inserted row or sequence value.
1181
     *
1182
     * @param string $sequenceName name of the sequence object (required by some DBMS)
1183
     *
1184
     * @throws InvalidCallException if the DB connection is not active
1185
     *
1186
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
1187
     *
1188
     * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
1189
     */
1190 3
    public function getLastInsertID(string $sequenceName = ''): string
1191
    {
1192 3
        $pdo = $this->db->getPDO();
1193
1194 3
        if ($this->db->isActive() && $pdo instanceof PDO) {
1195 3
            return $pdo->lastInsertId(
1196 3
                $sequenceName === '' ? null : $this->db->getQuoter()->quoteTableName($sequenceName)
1197
            );
1198
        }
1199
1200
        throw new InvalidCallException('DB Connection is not active.');
1201
    }
1202
1203
    /**
1204
     * Creates a new savepoint.
1205
     *
1206
     * @param string $name the savepoint name
1207
     *
1208
     * @throws Exception|InvalidConfigException|Throwable
1209
     */
1210 1
    public function createSavepoint(string $name): void
1211
    {
1212 1
        $this->db->createCommand("SAVEPOINT $name")->execute();
1213
    }
1214
1215
    /**
1216
     * @throws Exception|InvalidConfigException|Throwable
1217
     */
1218
    public function releaseSavepoint(string $name): void
1219
    {
1220
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
1221
    }
1222
1223
    /**
1224
     * @throws Exception|InvalidConfigException|Throwable
1225
     */
1226 1
    public function getViewNames(string $schema = '', bool $refresh = false): array
1227
    {
1228 1
        if (!isset($this->viewNames[$schema]) || $refresh) {
1229 1
            $this->viewNames[$schema] = $this->findViewNames($schema);
1230
        }
1231
1232 1
        return is_array($this->viewNames[$schema]) ? $this->viewNames[$schema] : [];
1233
    }
1234
}
1235