Test Failed
Pull Request — master (#144)
by Def
24:27 queued 22:08
created

Schema::findConstraints()   B

Complexity

Conditions 5
Paths 10

Size

Total Lines 88
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5.0073

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 5
eloc 46
c 5
b 0
f 0
nc 10
nop 1
dl 0
loc 88
ccs 14
cts 15
cp 0.9333
crap 5.0073
rs 8.867

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\Pgsql;
6
7
use JsonException;
8
use Throwable;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Db\Constraint\CheckConstraint;
11
use Yiisoft\Db\Constraint\Constraint;
12
use Yiisoft\Db\Constraint\DefaultValueConstraint;
13
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
14
use Yiisoft\Db\Constraint\IndexConstraint;
15
use Yiisoft\Db\Exception\Exception;
16
use Yiisoft\Db\Exception\InvalidConfigException;
17
use Yiisoft\Db\Exception\NotSupportedException;
18
use Yiisoft\Db\Expression\Expression;
19
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
20
use Yiisoft\Db\Schema\ColumnSchemaInterface;
21
use Yiisoft\Db\Schema\Schema as AbstractSchema;
22
use Yiisoft\Db\Schema\TableSchemaInterface;
23
24
use function array_change_key_case;
25
use function array_merge;
26
use function array_unique;
27
use function array_values;
28
use function bindec;
29
use function explode;
30
use function preg_match;
31
use function preg_replace;
32
use function str_replace;
33
use function substr;
34
35
/**
36
 * The class Schema is the class for retrieving metadata from a PostgreSQL database
37
 * (version 9.6 and above).
38
 *
39
 * @psalm-type ColumnArray = array{
40
 *   table_schema: string,
41
 *   table_name: string,
42
 *   column_name: string,
43
 *   data_type: string,
44
 *   type_type: string|null,
45
 *   character_maximum_length: int,
46
 *   column_comment: string|null,
47
 *   modifier: int,
48
 *   is_nullable: bool,
49
 *   column_default: mixed,
50
 *   is_autoinc: bool,
51
 *   sequence_name: string|null,
52
 *   enum_values: array<array-key, float|int|string>|string|null,
53
 *   numeric_precision: int|null,
54
 *   numeric_scale: int|null,
55
 *   size: string|null,
56
 *   is_pkey: bool|null,
57
 *   dimension: int
58
 * }
59
 *
60
 * @psalm-type ConstraintArray = array<
61
 *   array-key,
62
 *   array {
63
 *     name: string,
64
 *     column_name: string,
65
 *     type: string,
66
 *     foreign_table_schema: string|null,
67
 *     foreign_table_name: string|null,
68
 *     foreign_column_name: string|null,
69
 *     on_update: string,
70
 *     on_delete: string,
71
 *     check_expr: string
72
 *   }
73
 * >
74
 *
75
 * @psalm-type FindConstraintArray = array{
76
 *   constraint_name: string,
77
 *   column_name: string,
78
 *   foreign_table_name: string,
79
 *   foreign_table_schema: string,
80
 *   foreign_column_name: string,
81
 * }
82
 */
83
final class Schema extends AbstractSchema
84
{
85
    public const TYPE_JSONB = 'jsonb';
86
87
    /**
88
     * @var array The mapping from physical column types (keys) to abstract column types (values).
89
     *
90
     * {@see http://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE}
91
     *
92
     * @psalm-var string[]
93
     */
94
    private array $typeMap = [
95
        'bit' => self::TYPE_INTEGER,
96
        'bit varying' => self::TYPE_INTEGER,
97
        'varbit' => self::TYPE_INTEGER,
98
        'bool' => self::TYPE_BOOLEAN,
99
        'boolean' => self::TYPE_BOOLEAN,
100
        'box' => self::TYPE_STRING,
101
        'circle' => self::TYPE_STRING,
102
        'point' => self::TYPE_STRING,
103
        'line' => self::TYPE_STRING,
104
        'lseg' => self::TYPE_STRING,
105
        'polygon' => self::TYPE_STRING,
106
        'path' => self::TYPE_STRING,
107
        'character' => self::TYPE_CHAR,
108
        'char' => self::TYPE_CHAR,
109
        'bpchar' => self::TYPE_CHAR,
110
        'character varying' => self::TYPE_STRING,
111
        'varchar' => self::TYPE_STRING,
112
        'text' => self::TYPE_TEXT,
113
        'bytea' => self::TYPE_BINARY,
114
        'cidr' => self::TYPE_STRING,
115
        'inet' => self::TYPE_STRING,
116
        'macaddr' => self::TYPE_STRING,
117
        'real' => self::TYPE_FLOAT,
118
        'float4' => self::TYPE_FLOAT,
119
        'double precision' => self::TYPE_DOUBLE,
120
        'float8' => self::TYPE_DOUBLE,
121
        'decimal' => self::TYPE_DECIMAL,
122
        'numeric' => self::TYPE_DECIMAL,
123
        'money' => self::TYPE_MONEY,
124
        'smallint' => self::TYPE_SMALLINT,
125
        'int2' => self::TYPE_SMALLINT,
126
        'int4' => self::TYPE_INTEGER,
127
        'int' => self::TYPE_INTEGER,
128
        'integer' => self::TYPE_INTEGER,
129
        'bigint' => self::TYPE_BIGINT,
130
        'int8' => self::TYPE_BIGINT,
131
        'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal!
132
        'smallserial' => self::TYPE_SMALLINT,
133
        'serial2' => self::TYPE_SMALLINT,
134
        'serial4' => self::TYPE_INTEGER,
135
        'serial' => self::TYPE_INTEGER,
136
        'bigserial' => self::TYPE_BIGINT,
137
        'serial8' => self::TYPE_BIGINT,
138
        'pg_lsn' => self::TYPE_BIGINT,
139
        'date' => self::TYPE_DATE,
140
        'interval' => self::TYPE_STRING,
141
        'time without time zone' => self::TYPE_TIME,
142
        'time' => self::TYPE_TIME,
143
        'time with time zone' => self::TYPE_TIME,
144
        'timetz' => self::TYPE_TIME,
145
        'timestamp without time zone' => self::TYPE_TIMESTAMP,
146
        'timestamp' => self::TYPE_TIMESTAMP,
147
        'timestamp with time zone' => self::TYPE_TIMESTAMP,
148
        'timestamptz' => self::TYPE_TIMESTAMP,
149
        'abstime' => self::TYPE_TIMESTAMP,
150
        'tsquery' => self::TYPE_STRING,
151
        'tsvector' => self::TYPE_STRING,
152
        'txid_snapshot' => self::TYPE_STRING,
153
        'unknown' => self::TYPE_STRING,
154
        'uuid' => self::TYPE_STRING,
155
        'json' => self::TYPE_JSON,
156
        'jsonb' => self::TYPE_JSON,
157
        'xml' => self::TYPE_STRING,
158
    ];
159
160
    /**
161
     * @var string|null the default schema used for the current session.
162 435
     */
163
    protected ?string $defaultSchema = 'public';
164 435
165
    /**
166
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
167
     * case starting and ending characters are different.
168
     */
169
    protected string|array $tableQuoteCharacter = '"';
170
171
    /**
172
     * Resolves the table name and schema name (if any).
173
     *
174
     * @param string $name the table name.
175
     *
176
     * @return TableSchemaInterface with resolved table, schema, etc. names.
177
     *
178
     * {@see TableSchemaInterface}
179
     */
180
    protected function resolveTableName(string $name): TableSchemaInterface
181
    {
182
        $resolvedName = new TableSchema();
183
184
        $parts = array_reverse(
185
            $this->db->getQuoter()->getTableNameParts($name)
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
186
        );
187 162
188
        $resolvedName->name($parts[0] ?? '');
189 162
        $resolvedName->schemaName($parts[1] ?? $this->defaultSchema);
190
191 162
        $resolvedName->fullName(
192 162
            $resolvedName->getSchemaName() !== $this->defaultSchema ?
193
            implode('.', array_reverse($parts)) : $resolvedName->getName()
194 162
        );
195 5
196 5
        return $resolvedName;
197
    }
198 162
199 162
    /**
200
     * Returns all schema names in the database, including the default one but not system schemas.
201
     *
202 162
     * This method should be overridden by child classes in order to support this feature because the default
203
     * implementation simply throws an exception.
204 162
     *
205
     * @throws Exception|InvalidConfigException|Throwable
206 162
     *
207 162
     * @return array All schema names in the database, except system schemas.
208
     */
209
    protected function findSchemaNames(): array
210 162
    {
211
        $sql = <<<SQL
212
        SELECT "ns"."nspname"
213
        FROM "pg_namespace" AS "ns"
214
        WHERE "ns"."nspname" != 'information_schema' AND "ns"."nspname" NOT LIKE 'pg_%'
215
        ORDER BY "ns"."nspname" ASC
216
        SQL;
217
218
        return $this->db->createCommand($sql)->queryColumn();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
219
    }
220
221
    /**
222
     * Returns all table names in the database.
223 2
     *
224
     * This method should be overridden by child classes in order to support this feature because the default
225 2
     * implementation simply throws an exception.
226
     *
227
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
228
     *
229
     * @throws Exception|InvalidConfigException|Throwable
230
     *
231
     * @return array All table names in the database. The names have NO schema name prefix.
232 2
     */
233
    protected function findTableNames(string $schema = ''): array
234
    {
235
        if ($schema === '') {
236
            $schema = $this->defaultSchema;
237
        }
238
239
        $sql = <<<SQL
240
        SELECT c.relname AS table_name
241
        FROM pg_class c
242
        INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
243
        WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f', 'p')
244
        ORDER BY c.relname
245
        SQL;
246
247 10
        return $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
248
    }
249 10
250 10
    /**
251
     * Loads the metadata for the specified table.
252
     *
253 10
     * @param string $name table name.
254
     *
255
     * @throws Exception|InvalidConfigException|Throwable
256
     *
257
     * @return TableSchemaInterface|null DBMS-dependent table metadata, `null` if the table does not exist.
258
     */
259
    protected function loadTableSchema(string $name): ?TableSchemaInterface
260
    {
261 10
        $table = $this->resolveTableName($name);
262
263
        if ($this->findColumns($table)) {
264
            $this->findConstraints($table);
265
            return $table;
266
        }
267
268
        return null;
269
    }
270
271
    /**
272
     * Loads a primary key for the given table.
273 109
     *
274
     * @param string $tableName table name.
275 109
     *
276
     * @throws Exception|InvalidConfigException|Throwable
277 109
     *
278 103
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
279 103
     */
280
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
281
    {
282 17
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
283
284
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
285
    }
286
287
    /**
288
     * Loads all foreign keys for the given table.
289
     *
290
     * @param string $tableName table name.
291
     *
292
     * @throws Exception|InvalidConfigException|Throwable
293
     *
294 32
     * @return array foreign keys for the given table.
295
     *
296 32
     * @psaml-return array|ForeignKeyConstraint[]
297
     */
298 32
    protected function loadTableForeignKeys(string $tableName): array
299
    {
300
        $tableForeignKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
301
302
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
303
    }
304
305
    /**
306
     * Loads all indexes for the given table.
307
     *
308
     * @param string $tableName table name.
309
     *
310
     * @throws Exception|InvalidConfigException|Throwable
311
     *
312 5
     * @return IndexConstraint[] indexes for the given table.
313
     */
314 5
    protected function loadTableIndexes(string $tableName): array
315
    {
316 5
        $sql = <<<SQL
317
        SELECT
318
            "ic"."relname" AS "name",
319
            "ia"."attname" AS "column_name",
320
            "i"."indisunique" AS "index_is_unique",
321
            "i"."indisprimary" AS "index_is_primary"
322
        FROM "pg_class" AS "tc"
323
        INNER JOIN "pg_namespace" AS "tcns"
324
            ON "tcns"."oid" = "tc"."relnamespace"
325
        INNER JOIN "pg_index" AS "i"
326
            ON "i"."indrelid" = "tc"."oid"
327
        INNER JOIN "pg_class" AS "ic"
328 29
            ON "ic"."oid" = "i"."indexrelid"
329
        INNER JOIN "pg_attribute" AS "ia"
330 29
            ON "ia"."attrelid" = "i"."indexrelid"
331
        WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
332
        ORDER BY "ia"."attnum" ASC
333
        SQL;
334
335
        $resolvedName = $this->resolveTableName($tableName);
336
337
        $indexes = $this->db->createCommand($sql, [
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
338
            ':schemaName' => $resolvedName->getSchemaName(),
339
            ':tableName' => $resolvedName->getName(),
340
        ])->queryAll();
341
342
        /** @var array[] @indexes */
343
        $indexes = $this->normalizeRowKeyCase($indexes, true);
344
        $indexes = ArrayHelper::index($indexes, null, 'name');
345
        $result = [];
346
347
        /**
348
         * @var object|string|null $name
349 29
         * @var array<
350
         *   array-key,
351 29
         *   array{
352 29
         *     name: string,
353 29
         *     column_name: string,
354 29
         *     index_is_unique: bool,
355
         *     index_is_primary: bool
356
         *   }
357 29
         * > $index
358 29
         */
359 29
        foreach ($indexes as $name => $index) {
360
            $ic = (new IndexConstraint())
361
                ->name($name)
362
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
363
                ->primary($index[0]['index_is_primary'])
364
                ->unique($index[0]['index_is_unique']);
365
366
            $result[] = $ic;
367
        }
368
369
        return $result;
370
    }
371
372
    /**
373 29
     * Loads all unique constraints for the given table.
374 26
     *
375 26
     * @param string $tableName table name.
376 26
     *
377 26
     * @throws Exception|InvalidConfigException|Throwable
378 26
     *
379
     * @return array unique constraints for the given table.
380 26
     *
381
     * @psalm-return array|Constraint[]
382
     */
383 29
    protected function loadTableUniques(string $tableName): array
384
    {
385
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
386
387
        return is_array($tableUniques) ? $tableUniques : [];
388
    }
389
390
    /**
391
     * Loads all check constraints for the given table.
392
     *
393
     * @param string $tableName table name.
394
     *
395
     * @throws Exception|InvalidConfigException|Throwable
396
     *
397 14
     * @return array check constraints for the given table.
398
     *
399 14
     * @psaml-return array|CheckConstraint[]
400
     */
401 14
    protected function loadTableChecks(string $tableName): array
402
    {
403
        $tableChecks = $this->loadTableConstraints($tableName, self::CHECKS);
404
405
        return is_array($tableChecks) ? $tableChecks : [];
406
    }
407
408
    /**
409
     * Loads all default value constraints for the given table.
410
     *
411
     * @param string $tableName table name.
412
     *
413
     * @throws NotSupportedException
414
     *
415 14
     * @return DefaultValueConstraint[] default value constraints for the given table.
416
     */
417 14
    protected function loadTableDefaultValues(string $tableName): array
418
    {
419 14
        throw new NotSupportedException('PostgreSQL does not support default value constraints.');
420
    }
421
422
    /**
423
     * @throws Exception|InvalidConfigException|Throwable
424
     */
425
    protected function findViewNames(string $schema = ''): array
426
    {
427
        if ($schema === '') {
428
            $schema = $this->defaultSchema;
429
        }
430
431 12
        $sql = <<<SQL
432
        SELECT c.relname AS table_name
433 12
        FROM pg_class c
434
        INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
435
        WHERE ns.nspname = :schemaName AND (c.relkind = 'v' OR c.relkind = 'm')
436
        ORDER BY c.relname
437
        SQL;
438
439 2
        return $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
440
    }
441 2
442
    /**
443
     * Collects the foreign key column details for the given table.
444
     *
445 2
     * @param TableSchemaInterface $table the table metadata
446
     *
447
     * @throws Exception|InvalidConfigException|Throwable
448
     */
449
    protected function findConstraints(TableSchemaInterface $table): void
450
    {
451
        /**
452
         * We need to extract the constraints de hard way since:
453 2
         * {@see http://www.postgresql.org/message-id/[email protected]}
454
         */
455
456
        $sql = <<<SQL
457
        SELECT
458
            ct.conname as constraint_name,
459
            a.attname as column_name,
460
            fc.relname as foreign_table_name,
461
            fns.nspname as foreign_table_schema,
462
            fa.attname as foreign_column_name
463 103
            FROM
464
            (SELECT ct.conname, ct.conrelid, ct.confrelid, ct.conkey, ct.contype, ct.confkey,
465 103
                generate_subscripts(ct.conkey, 1) AS s
466 103
                FROM pg_constraint ct
467
            ) AS ct
468
            inner join pg_class c on c.oid=ct.conrelid
469 103
            inner join pg_namespace ns on c.relnamespace=ns.oid
470
            inner join pg_attribute a on a.attrelid=ct.conrelid and a.attnum = ct.conkey[ct.s]
471 103
            left join pg_class fc on fc.oid=ct.confrelid
472
            left join pg_namespace fns on fc.relnamespace=fns.oid
473 103
            left join pg_attribute fa on fa.attrelid=ct.confrelid and fa.attnum = ct.confkey[ct.s]
474
        WHERE
475
            ct.contype='f'
476
            and c.relname=:tableName
477
            and ns.nspname=:schemaName
478
        ORDER BY
479
            fns.nspname, fc.relname, a.attnum
480
        SQL;
481 103
482
        /** @var array{array{tableName: string, columns: array}} $constraints */
483
        $constraints = [];
484
485
        /**
486
         * @psalm-var array<
487
         *   array{
488
         *     constraint_name: string,
489
         *     column_name: string,
490
         *     foreign_table_name: string,
491
         *     foreign_table_schema: string,
492
         *     foreign_column_name: string,
493
         *   }
494
         * > $rows
495
         */
496
        $rows = $this->db->createCommand($sql, [
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
497
            ':schemaName' => $table->getSchemaName(),
498
            ':tableName' => $table->getName(),
499
        ])->queryAll();
500
501
        foreach ($rows as $constraint) {
502
            /** @psalm-var array{
503
             *     constraint_name: string,
504
             *     column_name: string,
505
             *     foreign_table_name: string,
506
             *     foreign_table_schema: string,
507
             *     foreign_column_name: string,
508 103
             *   } $constraint */
509
            $constraint = $this->normalizeRowKeyCase($constraint, false);
510
511
            if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
512
                $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
513
            } else {
514
                $foreignTable = $constraint['foreign_table_name'];
515
            }
516
517
            $name = $constraint['constraint_name'];
518
519
            if (!isset($constraints[$name])) {
520
                $constraints[$name] = [
521 103
                    'tableName' => $foreignTable,
522
                    'columns' => [],
523 103
                ];
524
            }
525
526
            $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name'];
527
        }
528
529
        /**
530
         * @var int|string $foreingKeyName.
531 9
         * @var array{tableName: string, columns: array} $constraint
532
         */
533 9
        foreach ($constraints as $foreingKeyName => $constraint) {
534
            $table->foreignKey(
535
                (string) $foreingKeyName,
536 9
                array_merge([$constraint['tableName']], $constraint['columns'])
537
            );
538
        }
539 9
    }
540
541 9
    /**
542 9
     * Gets information about given table unique indexes.
543
     *
544
     * @param TableSchemaInterface $table the table metadata.
545
     *
546
     * @throws Exception|InvalidConfigException|Throwable
547
     *
548 9
     * @return array with index and column names.
549
     */
550
    protected function getUniqueIndexInformation(TableSchemaInterface $table): array
551
    {
552
        $sql = <<<'SQL'
553
        SELECT
554
            i.relname as indexname,
555 103
            pg_get_indexdef(idx.indexrelid, k + 1, TRUE) AS columnname
556 9
        FROM (
557 9
            SELECT *, generate_subscripts(indkey, 1) AS k
558 9
            FROM pg_index
559
        ) idx
560
        INNER JOIN pg_class i ON i.oid = idx.indexrelid
561
        INNER JOIN pg_class c ON c.oid = idx.indrelid
562
        INNER JOIN pg_namespace ns ON c.relnamespace = ns.oid
563
        WHERE idx.indisprimary = FALSE AND idx.indisunique = TRUE
564
        AND c.relname = :tableName AND ns.nspname = :schemaName
565
        ORDER BY i.relname, k
566
        SQL;
567
568
        return $this->db->createCommand($sql, [
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
569
            ':schemaName' => $table->getSchemaName(),
570
            ':tableName' => $table->getName(),
571
        ])->queryAll();
572 1
    }
573
574 1
    /**
575
     * Returns all unique indexes for the given table.
576
     *
577
     * Each array element is of the following structure:
578
     *
579
     * ```php
580
     * [
581
     *     'IndexName1' => ['col1' [, ...]],
582
     *     'IndexName2' => ['col2' [, ...]],
583
     * ]
584
     * ```
585
     *
586
     * @param TableSchemaInterface $table the table metadata
587
     *
588
     * @throws Exception|InvalidConfigException|Throwable
589
     *
590 1
     * @return array all unique indexes for the given table.
591 1
     */
592 1
    public function findUniqueIndexes(TableSchemaInterface $table): array
593 1
    {
594
        $uniqueIndexes = [];
595
596
        /** @var array{indexname: string, columnname: string} $row */
597
        foreach ($this->getUniqueIndexInformation($table) as $row) {
598
            /** @var array{indexname: string, columnname: string} $row */
599
            $row = $this->normalizeRowKeyCase($row, false);
600
601
            $column = $row['columnname'];
602
603
            if (!empty($column) && $column[0] === '"') {
604
                /**
605
                 * postgres will quote names that are not lowercase-only.
606
                 *
607
                 * {@see https://github.com/yiisoft/yii2/issues/10613}
608
                 */
609
                $column = substr($column, 1, -1);
610
            }
611
612
            $uniqueIndexes[$row['indexname']][] = $column;
613
        }
614 1
615
        return $uniqueIndexes;
616 1
    }
617
618
    /**
619 1
     * Collects the metadata of table columns.
620
     *
621 1
     * @param TableSchemaInterface $table the table metadata.
622
     *
623 1
     * @throws Exception|InvalidConfigException|JsonException|Throwable
624
     *
625 1
     * @return bool whether the table exists in the database.
626
     */
627
    protected function findColumns(TableSchemaInterface $table): bool
628
    {
629
        $orIdentity = '';
630
631 1
        if (version_compare($this->db->getServerVersion(), '12.0', '>=')) {
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
632
            $orIdentity = 'OR a.attidentity != \'\'';
633
        }
634 1
635
        $sql = <<<SQL
636
        SELECT
637 1
            d.nspname AS table_schema,
638
            c.relname AS table_name,
639
            a.attname AS column_name,
640
            COALESCE(td.typname, tb.typname, t.typname) AS data_type,
641
            COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
642
            a.attlen AS character_maximum_length,
643
            pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
644
            a.atttypmod AS modifier,
645
            a.attnotnull = false AS is_nullable,
646
            CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
647
            coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) $orIdentity AS is_autoinc,
648
            pg_get_serial_sequence(quote_ident(d.nspname) || '.' || quote_ident(c.relname), a.attname)
649 109
            AS sequence_name,
650
            CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
651 109
                THEN array_to_string(
652 109
                    (
653 109
                        SELECT array_agg(enumlabel)
654
                        FROM pg_enum
655
                        WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid)
656 109
                    )::varchar[],
657
                ',')
658 109
                ELSE NULL
659
            END AS enum_values,
660 109
            CASE atttypid
661
                WHEN 21 /*int2*/ THEN 16
662
                WHEN 23 /*int4*/ THEN 32
663 109
                WHEN 20 /*int8*/ THEN 64
664 109
                WHEN 1700 /*numeric*/ THEN
665
                    CASE WHEN atttypmod = -1
666
                        THEN null
667 109
                        ELSE ((atttypmod - 4) >> 16) & 65535
668
                        END
669
                WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/
670
                WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/
671
                    ELSE null
672
                    END   AS numeric_precision,
673
            CASE
674
                WHEN atttypid IN (21, 23, 20) THEN 0
675
                WHEN atttypid IN (1700) THEN
676
            CASE
677
                WHEN atttypmod = -1 THEN null
678
                    ELSE (atttypmod - 4) & 65535
679
                    END
680
                    ELSE null
681
                    END AS numeric_scale,
682
                    CAST(
683
                        information_schema._pg_char_max_length(
684
                        information_schema._pg_truetypid(a, t),
685
                        information_schema._pg_truetypmod(a, t)
686
                        ) AS numeric
687
                    ) AS size,
688
                    a.attnum = any (ct.conkey) as is_pkey,
689
                    COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
690
            FROM
691
                pg_class c
692
                LEFT JOIN pg_attribute a ON a.attrelid = c.oid
693
                LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
694
                LEFT JOIN pg_type t ON a.atttypid = t.oid
695
                LEFT JOIN pg_type tb ON (a.attndims > 0 OR t.typcategory='A') AND t.typelem > 0 AND t.typelem = tb.oid
696
                                            OR t.typbasetype > 0 AND t.typbasetype = tb.oid
697
                LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
698
                LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
699
                LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
700
            WHERE
701
                a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped
702
                AND c.relname = :tableName
703
                AND d.nspname = :schemaName
704
            ORDER BY
705
                a.attnum;
706
        SQL;
707
708
        $columns = $this->db->createCommand($sql, [
709
            ':schemaName' => $table->getSchemaName(),
710
            ':tableName' => $table->getName(),
711
        ])->queryAll();
712
713
        if (empty($columns)) {
714
            return false;
715
        }
716
717
        /** @var array $column */
718
        foreach ($columns as $column) {
719
            /** @psalm-var ColumnArray $column */
720
            $column = $this->normalizeRowKeyCase($column, false);
721
722
            /** @psalm-var ColumnSchema $loadColumnSchema */
723
            $loadColumnSchema = $this->loadColumnSchema($column);
724
725
            $table->columns($loadColumnSchema->getName(), $loadColumnSchema);
726
727
            /** @var mixed */
728
            $defaultValue = $loadColumnSchema->getDefaultValue();
729
730
            if ($loadColumnSchema->isPrimaryKey()) {
731
                $table->primaryKey($loadColumnSchema->getName());
732
733
                if ($table->getSequenceName() === null) {
734
                    $table->sequenceName($loadColumnSchema->getSequenceName());
735
                }
736
737
                $loadColumnSchema->defaultValue(null);
738
            } elseif ($defaultValue) {
739
                if (
740 109
                    is_string($defaultValue) &&
741
                    in_array(
742 109
                        $loadColumnSchema->getType(),
743 17
                        [self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME],
744
                        true
745
                    ) &&
746
                    in_array(
747 103
                        strtoupper($defaultValue),
748
                        ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'],
749 103
                        true
750
                    )
751
                ) {
752 103
                    $loadColumnSchema->defaultValue(new Expression($defaultValue));
753
                } elseif ($loadColumnSchema->getType() === 'boolean') {
754 103
                    $loadColumnSchema->defaultValue(($defaultValue  === 'true'));
755
                } elseif (is_string($defaultValue) && preg_match("/^B'(.*?)'::/", $defaultValue, $matches)) {
756
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
757 103
                } elseif (is_string($defaultValue) && preg_match("/^'(\d+)'::\"bit\"$/", $defaultValue, $matches)) {
758
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
759 103
                } elseif (is_string($defaultValue) && preg_match("/^'(.*?)'::/", $defaultValue, $matches)) {
760 70
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[1]));
761
                } elseif (
762 70
                    is_string($defaultValue) &&
763 70
                    preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $defaultValue, $matches)
764
                ) {
765
                    if ($matches[2] === 'NULL') {
766 70
                        $loadColumnSchema->defaultValue(null);
767 100
                    } else {
768
                        $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[2]));
769 59
                    }
770 59
                } else {
771 59
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($defaultValue));
772 59
                }
773
            }
774
        }
775 31
776 31
        return true;
777 31
    }
778
779
    /**
780
     * Loads the column information into a {@see ColumnSchemaInterface} object.
781 29
     *
782 59
     * @psalm-param array{
783 55
     *   table_schema: string,
784 34
     *   table_name: string,
785
     *   column_name: string,
786 34
     *   data_type: string,
787 29
     *   type_type: string|null,
788 34
     *   character_maximum_length: int,
789 31
     *   column_comment: string|null,
790
     *   modifier: int,
791 32
     *   is_nullable: bool,
792 32
     *   column_default: mixed,
793
     *   is_autoinc: bool,
794 32
     *   sequence_name: string|null,
795 5
     *   enum_values: array<array-key, float|int|string>|string|null,
796
     *   numeric_precision: int|null,
797 32
     *   numeric_scale: int|null,
798
     *   size: string|null,
799
     *   is_pkey: bool|null,
800
     *   dimension: int
801
     * } $info column information.
802
     *
803
     * @return ColumnSchemaInterface the column schema object.
804
     */
805 103
    protected function loadColumnSchema(array $info): ColumnSchemaInterface
806
    {
807
        $column = $this->createColumnSchema();
808
        $column->allowNull($info['is_nullable']);
809
        $column->autoIncrement($info['is_autoinc']);
810
        $column->comment($info['column_comment']);
811
        $column->dbType($info['data_type']);
812
        $column->defaultValue($info['column_default']);
813
        $column->enumValues(($info['enum_values'] !== null)
814
            ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) : null);
815
        $column->unsigned(false); // has no meaning in PG
816
        $column->primaryKey((bool) $info['is_pkey']);
817
        $column->name($info['column_name']);
818
        $column->precision($info['numeric_precision']);
819
        $column->scale($info['numeric_scale']);
820
        $column->size($info['size'] === null ? null : (int) $info['size']);
821
        $column->dimension($info['dimension']);
822
823
        /**
824
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
825
         * default value.
826
         *
827
         * @var mixed $defaultValue
828
         */
829
        $defaultValue = $column->getDefaultValue();
830
        $sequenceName = $info['sequence_name'] ?? null;
831
832
        if (
833
            isset($defaultValue) &&
834 103
            is_string($defaultValue) &&
835
            preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1
836 103
        ) {
837 103
            $column->sequenceName(preg_replace(
838 103
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
839 103
                '',
840 103
                $defaultValue
841 103
            ));
842 103
        } elseif ($sequenceName !== null) {
843 103
            $column->sequenceName($this->resolveTableName($sequenceName)->getFullName());
844 103
        }
845 103
846 103
        if (isset($this->typeMap[$column->getDbType()])) {
847 103
            $column->type($this->typeMap[$column->getDbType()]);
848 103
        } else {
849 103
            $column->type(self::TYPE_STRING);
850 103
        }
851
852
        $column->phpType($this->getColumnPhpType($column));
853
854
        return $column;
855
    }
856
857
    /**
858 103
     * Loads multiple types of constraints and returns the specified ones.
859 103
     *
860
     * @param string $tableName table name.
861
     * @param string $returnType return type:
862 103
     * - primaryKey
863 103
     * - foreignKeys
864 103
     * - uniques
865
     * - checks
866 65
     *
867 65
     * @throws Exception|InvalidConfigException|Throwable
868
     *
869
     * @return array|Constraint|null (CheckConstraint|Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
870
     */
871 101
    private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null
872 5
    {
873
        $sql = <<<SQL
874
        SELECT
875 103
            "c"."conname" AS "name",
876 103
            "a"."attname" AS "column_name",
877
            "c"."contype" AS "type",
878
            "ftcns"."nspname" AS "foreign_table_schema",
879
            "ftc"."relname" AS "foreign_table_name",
880
            "fa"."attname" AS "foreign_column_name",
881 103
            "c"."confupdtype" AS "on_update",
882
            "c"."confdeltype" AS "on_delete",
883 103
            pg_get_constraintdef("c"."oid") AS "check_expr"
884
        FROM "pg_class" AS "tc"
885
        INNER JOIN "pg_namespace" AS "tcns"
886
            ON "tcns"."oid" = "tc"."relnamespace"
887
        INNER JOIN "pg_constraint" AS "c"
888
            ON "c"."conrelid" = "tc"."oid"
889
        INNER JOIN "pg_attribute" AS "a"
890
            ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
891
        LEFT JOIN "pg_class" AS "ftc"
892
            ON "ftc"."oid" = "c"."confrelid"
893
        LEFT JOIN "pg_namespace" AS "ftcns"
894
            ON "ftcns"."oid" = "ftc"."relnamespace"
895
        LEFT JOIN "pg_attribute" "fa"
896
            ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
897
        WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
898
        ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
899
        SQL;
900 65
901
        /** @var array<array-key, string> $actionTypes */
902 65
        $actionTypes = [
903
            'a' => 'NO ACTION',
904
            'r' => 'RESTRICT',
905
            'c' => 'CASCADE',
906
            'n' => 'SET NULL',
907
            'd' => 'SET DEFAULT',
908
        ];
909
910
        $resolvedName = $this->resolveTableName($tableName);
911
912
        $constraints = $this->db->createCommand($sql, [
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
913
            ':schemaName' => $resolvedName->getSchemaName(),
914
            ':tableName' => $resolvedName->getName(),
915
        ])->queryAll();
916
917
        /** @var array<array-key, array> $constraints */
918
        $constraints = $this->normalizeRowKeyCase($constraints, true);
919
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
920
921
        $result = [
922
            self::PRIMARY_KEY => null,
923
            self::FOREIGN_KEYS => [],
924
            self::UNIQUES => [],
925
            self::CHECKS => [],
926
        ];
927
928
        /**
929
         * @var string $type
930
         * @var array $names
931 65
         */
932
        foreach ($constraints as $type => $names) {
933
            /**
934
             * @psalm-var object|string|null $name
935
             * @psalm-var ConstraintArray $constraint
936
             */
937
            foreach ($names as $name => $constraint) {
938
                switch ($type) {
939 65
                    case 'p':
940
                        $result[self::PRIMARY_KEY] = (new Constraint())
941 65
                            ->name($name)
942 65
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
943 65
                        break;
944 65
                    case 'f':
945
                        $onDelete = $actionTypes[$constraint[0]['on_delete']] ?? null;
946
                        $onUpdate = $actionTypes[$constraint[0]['on_update']] ?? null;
947 65
948 65
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
949
                            ->name($name)
950 65
                            ->columnNames(array_values(
951
                                array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
952 65
                            ))
953 65
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
954 65
                            ->foreignTableName($constraint[0]['foreign_table_name'])
955
                            ->foreignColumnNames(array_values(
956
                                array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
957
                            ))
958
                            ->onDelete($onDelete)
959
                            ->onUpdate($onUpdate);
960
                        break;
961 65
                    case 'u':
962
                        $result[self::UNIQUES][] = (new Constraint())
963
                            ->name($name)
964
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
965
                        break;
966 65
                    case 'c':
967 65
                        $result[self::CHECKS][] = (new CheckConstraint())
968 65
                            ->name($name)
969 50
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
970 50
                            ->expression($constraint[0]['check_expr']);
971 50
                        break;
972 50
                }
973 63
            }
974 17
        }
975 17
976
        foreach ($result as $type => $data) {
977 17
            $this->setTableMetadata($tableName, $type, $data);
978 17
        }
979 17
980 17
        return $result[$returnType];
981
    }
982 17
983 17
    /**
984 17
     * Creates a column schema for the database.
985 17
     *
986
     * This method may be overridden by child classes to create a DBMS-specific column schema.
987 17
     *
988 17
     * @return ColumnSchema column schema instance.
989 17
     */
990 51
    private function createColumnSchema(): ColumnSchema
991 50
    {
992 50
        return new ColumnSchema();
993 50
    }
994 50
995 14
    /**
996 14
     * Create a column schema builder instance giving the type and value precision.
997 14
     *
998 14
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
999 14
     *
1000 14
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
1001
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
1002
     *
1003
     * @return ColumnSchemaBuilder column schema builder instance
1004
     *
1005 65
     * @psalm-param int|string|string[]|null $length
1006 65
     */
1007
    public function createColumnSchemaBuilder(string $type, int|string|array|null $length = null): ColumnSchemaBuilder
1008
    {
1009 65
        return new ColumnSchemaBuilder($type, $length);
1010
    }
1011
1012
    /**
1013
     * Returns the cache key for the specified table name.
1014
     *
1015
     * @param string $name the table name.
1016
     *
1017
     * @return array the cache key.
1018
     */
1019 103
    protected function getCacheKey(string $name): array
1020
    {
1021 103
        return array_merge([__CLASS__], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
1022
    }
1023
1024
    /**
1025
     * Returns the cache tag name.
1026
     *
1027
     * This allows {@see refresh()} to invalidate all cached table schemas.
1028
     *
1029
     * @return string the cache tag name.
1030
     */
1031
    protected function getCacheTag(): string
1032
    {
1033
        return md5(serialize(array_merge([__CLASS__], $this->db->getCacheKey())));
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
1034
    }
1035
1036 4
    /**
1037
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
1038 4
     */
1039
    public function supportsSavepoint(): bool
1040
    {
1041
        return $this->db->isSavepointEnabled();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
1042
    }
1043
1044
    /**
1045
     * Changes row's array key case to lower.
1046
     *
1047
     * @param array $row row's array or an array of row's arrays.
1048
     * @param bool $multiple whether multiple rows or a single row passed.
1049
     *
1050
     * @return array normalized row or rows.
1051 174
     */
1052
    protected function normalizeRowKeyCase(array $row, bool $multiple): array
1053 174
    {
1054 23
        if ($multiple) {
1055
            return array_map(static function (array $row) {
1056 23
                return array_change_key_case($row, CASE_LOWER);
1057
            }, $row);
1058
        }
1059 174
1060
        return array_change_key_case($row, CASE_LOWER);
1061
    }
1062
1063
    /**
1064
     * @inheritDoc
1065
     */
1066
    public function getLastInsertID(?string $sequenceName = null): string
1067
    {
1068
        return $this->db->getLastInsertID($sequenceName);
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
1069 174
    }
1070
}
1071