Test Failed
Pull Request — master (#143)
by Def
07:51 queued 05:09
created

Schema::findConstraints()   B

Complexity

Conditions 6
Paths 20

Size

Total Lines 96
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6.0087

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 6
eloc 46
c 5
b 0
f 0
nc 20
nop 1
dl 0
loc 96
ccs 15
cts 16
cp 0.9375
crap 6.0087
rs 8.5559

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\Connection\ConnectionInterface;
11
use Yiisoft\Db\Constraint\CheckConstraint;
12
use Yiisoft\Db\Constraint\Constraint;
13
use Yiisoft\Db\Constraint\DefaultValueConstraint;
14
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
15
use Yiisoft\Db\Constraint\IndexConstraint;
16
use Yiisoft\Db\Exception\Exception;
17
use Yiisoft\Db\Exception\InvalidConfigException;
18
use Yiisoft\Db\Exception\NotSupportedException;
19
use Yiisoft\Db\Expression\Expression;
20
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
21
use Yiisoft\Db\Schema\ColumnSchemaInterface;
22
use Yiisoft\Db\Schema\Schema as AbstractSchema;
23
use Yiisoft\Db\Schema\TableNameInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Schema\TableNameInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

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

845
            $column->sequenceName($this->resolveTableName($this->/** @scrutinizer ignore-call */ asTableNameInterface($sequenceName))->getFullName());

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...
846 103
        }
847 103
848 103
        if (isset($this->typeMap[$column->getDbType()])) {
849 103
            $column->type($this->typeMap[$column->getDbType()]);
850 103
        } else {
851
            $column->type(self::TYPE_STRING);
852
        }
853
854
        $column->phpType($this->getColumnPhpType($column));
855
856
        return $column;
857
    }
858 103
859 103
    /**
860
     * Loads multiple types of constraints and returns the specified ones.
861
     *
862 103
     * @param TableNameInterface $tableName table name.
863 103
     * @param string $returnType return type:
864 103
     * - primaryKey
865
     * - foreignKeys
866 65
     * - uniques
867 65
     * - checks
868
     *
869
     * @throws Exception|InvalidConfigException|Throwable
870
     *
871 101
     * @return array|Constraint|null (CheckConstraint|Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
872 5
     */
873
    private function loadTableConstraints(TableNameInterface $tableName, string $returnType): array|Constraint|null
874
    {
875 103
        $sql = <<<SQL
876 103
        SELECT
877
            "c"."conname" AS "name",
878
            "a"."attname" AS "column_name",
879
            "c"."contype" AS "type",
880
            "ftcns"."nspname" AS "foreign_table_schema",
881 103
            "ftc"."relname" AS "foreign_table_name",
882
            "fa"."attname" AS "foreign_column_name",
883 103
            "c"."confupdtype" AS "on_update",
884
            "c"."confdeltype" AS "on_delete",
885
            pg_get_constraintdef("c"."oid") AS "check_expr"
886
        FROM "pg_class" AS "tc"
887
        INNER JOIN "pg_namespace" AS "tcns"
888
            ON "tcns"."oid" = "tc"."relnamespace"
889
        INNER JOIN "pg_constraint" AS "c"
890
            ON "c"."conrelid" = "tc"."oid"
891
        INNER JOIN "pg_attribute" AS "a"
892
            ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
893
        LEFT JOIN "pg_class" AS "ftc"
894
            ON "ftc"."oid" = "c"."confrelid"
895
        LEFT JOIN "pg_namespace" AS "ftcns"
896
            ON "ftcns"."oid" = "ftc"."relnamespace"
897
        LEFT JOIN "pg_attribute" "fa"
898
            ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
899
        WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
900 65
        ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
901
        SQL;
902 65
903
        /** @var array<array-key, string> $actionTypes */
904
        $actionTypes = [
905
            'a' => 'NO ACTION',
906
            'r' => 'RESTRICT',
907
            'c' => 'CASCADE',
908
            'n' => 'SET NULL',
909
            'd' => 'SET DEFAULT',
910
        ];
911
912
        $resolvedName = $this->resolveTableName($tableName);
913
914
        $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...
915
            ':schemaName' => $resolvedName->getSchemaName(),
916
            ':tableName' => $resolvedName->getName(),
917
        ])->queryAll();
918
919
        /** @var array<array-key, array> $constraints */
920
        $constraints = $this->normalizeRowKeyCase($constraints, true);
921
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
922
923
        $result = [
924
            self::PRIMARY_KEY => null,
925
            self::FOREIGN_KEYS => [],
926
            self::UNIQUES => [],
927
            self::CHECKS => [],
928
        ];
929
930
        /**
931 65
         * @var string $type
932
         * @var array $names
933
         */
934
        foreach ($constraints as $type => $names) {
935
            /**
936
             * @psalm-var object|string|null $name
937
             * @psalm-var ConstraintArray $constraint
938
             */
939 65
            foreach ($names as $name => $constraint) {
940
                switch ($type) {
941 65
                    case 'p':
942 65
                        $result[self::PRIMARY_KEY] = (new Constraint())
943 65
                            ->name($name)
944 65
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
945
                        break;
946
                    case 'f':
947 65
                        $onDelete = $actionTypes[$constraint[0]['on_delete']] ?? null;
948 65
                        $onUpdate = $actionTypes[$constraint[0]['on_update']] ?? null;
949
950 65
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
951
                            ->name($name)
952 65
                            ->columnNames(array_values(
953 65
                                array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
954 65
                            ))
955
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
956
                            ->foreignTableName($constraint[0]['foreign_table_name'])
957
                            ->foreignColumnNames(array_values(
958
                                array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
959
                            ))
960
                            ->onDelete($onDelete)
961 65
                            ->onUpdate($onUpdate);
962
                        break;
963
                    case 'u':
964
                        $result[self::UNIQUES][] = (new Constraint())
965
                            ->name($name)
966 65
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
967 65
                        break;
968 65
                    case 'c':
969 50
                        $result[self::CHECKS][] = (new CheckConstraint())
970 50
                            ->name($name)
971 50
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
972 50
                            ->expression($constraint[0]['check_expr']);
973 63
                        break;
974 17
                }
975 17
            }
976
        }
977 17
978 17
        foreach ($result as $type => $data) {
979 17
            $this->setTableMetadata($tableName, $type, $data);
980 17
        }
981
982 17
        return $result[$returnType];
983 17
    }
984 17
985 17
    /**
986
     * Creates a column schema for the database.
987 17
     *
988 17
     * This method may be overridden by child classes to create a DBMS-specific column schema.
989 17
     *
990 51
     * @return ColumnSchema column schema instance.
991 50
     */
992 50
    private function createColumnSchema(): ColumnSchema
993 50
    {
994 50
        return new ColumnSchema();
995 14
    }
996 14
997 14
    /**
998 14
     * Create a column schema builder instance giving the type and value precision.
999 14
     *
1000 14
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
1001
     *
1002
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
1003
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
1004
     *
1005 65
     * @return ColumnSchemaBuilder column schema builder instance
1006 65
     *
1007
     * @psalm-param int|string|string[]|null $length
1008
     */
1009 65
    public function createColumnSchemaBuilder(string $type, int|string|array|null $length = null): ColumnSchemaBuilder
1010
    {
1011
        return new ColumnSchemaBuilder($type, $length);
1012
    }
1013
1014
    /**
1015
     * Returns the actual name of a given table name.
1016
     *
1017
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
1018
     * {@see ConnectionInterface::tablePrefix}.
1019 103
     *
1020
     * @param string|TableNameInterface $name the table name to be converted.
1021 103
     *
1022
     * @return string the real name of the given table name.
1023
     */
1024
    public function getRawTableName(string|TableNameInterface $name): string
1025
    {
1026
        if ($name instanceof TableNameInterface) {
1027
            return (string)$name;
1028
        }
1029
1030
        if (!str_contains($name, '{{')) {
1031
            return $name;
1032
        }
1033
1034
        $name = preg_replace('/{{(.*?)}}/', '\1', $name);
1035
        return str_replace('%', $this->db->getTablePrefix(), $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...
1036 4
    }
1037
1038 4
    /**
1039
     * Returns the cache key for the specified table name.
1040
     *
1041
     * @param string $name the table name.
1042
     *
1043
     * @return array the cache key.
1044
     */
1045
    protected function getCacheKey(string $name): array
1046
    {
1047
        return array_merge([__CLASS__], $this->db->getCacheKey(), [$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...
1048
    }
1049
1050
    /**
1051 174
     * Returns the cache tag name.
1052
     *
1053 174
     * This allows {@see refresh()} to invalidate all cached table schemas.
1054 23
     *
1055
     * @return string the cache tag name.
1056 23
     */
1057
    protected function getCacheTag(): string
1058
    {
1059 174
        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...
1060
    }
1061
1062
    /**
1063
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
1064
     */
1065
    public function supportsSavepoint(): bool
1066
    {
1067
        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...
1068
    }
1069 174
1070
    /**
1071 174
     * Changes row's array key case to lower.
1072
     *
1073
     * @param array $row row's array or an array of row's arrays.
1074
     * @param bool $multiple whether multiple rows or a single row passed.
1075
     *
1076
     * @return array normalized row or rows.
1077
     */
1078
    protected function normalizeRowKeyCase(array $row, bool $multiple): array
1079
    {
1080
        if ($multiple) {
1081 174
            return array_map(static function (array $row) {
1082
                return array_change_key_case($row, CASE_LOWER);
1083 174
            }, $row);
1084
        }
1085
1086
        return array_change_key_case($row, CASE_LOWER);
1087
    }
1088
1089 5
    /**
1090
     * @inheritDoc
1091 5
     */
1092
    public function getLastInsertID(?string $sequenceName = null): string
1093
    {
1094
        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...
1095
    }
1096
}
1097