Passed
Pull Request — master (#241)
by Def
05:44 queued 02:25
created

Schema::findConstraints()   C

Complexity

Conditions 8
Paths 14

Size

Total Lines 559
Code Lines 477

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 192
CRAP Score 8

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 8
eloc 477
c 5
b 0
f 0
nc 14
nop 1
dl 0
loc 559
ccs 192
cts 194
cp 0.9897
crap 8
rs 6.7555

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

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

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

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

Loading history...
289 143
            $this->findConstraints($table);
290 143
            return $table;
291
        }
292
293 41
        return null;
294
    }
295
296
    /**
297
     * Loads a primary key for the given table.
298
     *
299
     * @param string $tableName table name.
300
     *
301
     * @throws Exception
302
     * @throws InvalidConfigException
303
     * @throws Throwable
304
     *
305
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
306
     */
307 40
    protected function loadTablePrimaryKey(string $tableName): Constraint|null
308
    {
309 40
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
0 ignored issues
show
Bug introduced by
The method loadTableConstraints() 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

309
        /** @scrutinizer ignore-call */ 
310
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);

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...
310
311 40
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
312
    }
313
314
    /**
315
     * Loads all foreign keys for the given table.
316
     *
317
     * @param string $tableName table name.
318
     *
319
     * @throws Exception
320
     * @throws InvalidConfigException
321
     * @throws Throwable
322
     *
323
     * @return array foreign keys for the given table.
324
     *
325
     * @psaml-return array|ForeignKeyConstraint[]
326
     */
327 8
    protected function loadTableForeignKeys(string $tableName): array
328
    {
329 8
        $tableForeignKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
330
331 8
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
332
    }
333
334
    /**
335
     * Loads all indexes for the given table.
336
     *
337
     * @param string $tableName table name.
338
     *
339
     * @throws Exception
340
     * @throws InvalidConfigException
341
     * @throws Throwable
342
     *
343
     * @return IndexConstraint[] indexes for the given table.
344
     */
345 38
    protected function loadTableIndexes(string $tableName): array
346
    {
347 38
        $sql = <<<SQL
348
        SELECT
349
            "ic"."relname" AS "name",
350
            "ia"."attname" AS "column_name",
351
            "i"."indisunique" AS "index_is_unique",
352
            "i"."indisprimary" AS "index_is_primary"
353
        FROM "pg_class" AS "tc"
354
        INNER JOIN "pg_namespace" AS "tcns"
355
            ON "tcns"."oid" = "tc"."relnamespace"
356
        INNER JOIN "pg_index" AS "i"
357
            ON "i"."indrelid" = "tc"."oid"
358
        INNER JOIN "pg_class" AS "ic"
359
            ON "ic"."oid" = "i"."indexrelid"
360
        INNER JOIN "pg_attribute" AS "ia"
361
            ON "ia"."attrelid" = "i"."indexrelid"
362
        WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
363
        ORDER BY "ia"."attnum" ASC
364 38
        SQL;
365
366 38
        $resolvedName = $this->resolveTableName($tableName);
367
368 38
        $indexes = $this->db->createCommand($sql, [
369 38
            ':schemaName' => $resolvedName->getSchemaName(),
370 38
            ':tableName' => $resolvedName->getName(),
371 38
        ])->queryAll();
372
373
        /** @var array[] $indexes */
374 38
        $indexes = $this->normalizeRowKeyCase($indexes, true);
375 38
        $indexes = ArrayHelper::index($indexes, null, ['name']);
376 38
        $result = [];
377
378
        /**
379
         * @psalm-var object|string|null $name
380
         * @psalm-var array<
381
         *   array-key,
382
         *   array{
383
         *     name: string,
384
         *     column_name: string,
385
         *     index_is_unique: bool,
386
         *     index_is_primary: bool
387
         *   }
388
         * > $index
389
         */
390 38
        foreach ($indexes as $name => $index) {
391 35
            $ic = (new IndexConstraint())
392 35
                ->name($name)
393 35
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
394 35
                ->primary($index[0]['index_is_primary'])
395 35
                ->unique($index[0]['index_is_unique']);
396
397 35
            $result[] = $ic;
398
        }
399
400 38
        return $result;
401
    }
402
403
    /**
404
     * Loads all unique constraints for the given table.
405
     *
406
     * @param string $tableName table name.
407
     *
408
     * @throws Exception
409
     * @throws InvalidConfigException
410
     * @throws Throwable
411
     *
412
     * @return array unique constraints for the given table.
413
     *
414
     * @psalm-return array|Constraint[]
415
     */
416 17
    protected function loadTableUniques(string $tableName): array
417
    {
418 17
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
419
420 17
        return is_array($tableUniques) ? $tableUniques : [];
421
    }
422
423
    /**
424
     * Loads all check constraints for the given table.
425
     *
426
     * @param string $tableName table name.
427
     *
428
     * @throws Exception
429
     * @throws InvalidConfigException
430
     * @throws Throwable
431
     *
432
     * @return array check constraints for the given table.
433
     *
434
     * @psaml-return array|CheckConstraint[]
435
     */
436 17
    protected function loadTableChecks(string $tableName): array
437
    {
438 17
        $tableChecks = $this->loadTableConstraints($tableName, self::CHECKS);
439
440 17
        return is_array($tableChecks) ? $tableChecks : [];
441
    }
442
443
    /**
444
     * Loads all default value constraints for the given table.
445
     *
446
     * @param string $tableName table name.
447
     *
448
     * @throws NotSupportedException
449
     *
450
     * @return DefaultValueConstraint[] default value constraints for the given table.
451
     */
452 13
    protected function loadTableDefaultValues(string $tableName): array
453
    {
454 13
        throw new NotSupportedException(__METHOD__ . ' is not supported by PostgreSQL.');
455
    }
456
457
    /**
458
     * @throws Exception
459
     * @throws InvalidConfigException
460
     * @throws Throwable
461
     */
462 3
    protected function findViewNames(string $schema = ''): array
463
    {
464 3
        if ($schema === '') {
465 1
            $schema = $this->defaultSchema;
466
        }
467
468 3
        $sql = <<<SQL
469
        SELECT c.relname AS table_name
470
        FROM pg_class c
471
        INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
472
        WHERE ns.nspname = :schemaName AND (c.relkind = 'v' OR c.relkind = 'm')
473
        ORDER BY c.relname
474 3
        SQL;
475
476 3
        return $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn();
477
    }
478
479
    /**
480
     * Collects the foreign key column details for the given table.
481
     *
482
     * @param TableSchemaInterface $table the table metadata
483
     *
484
     * @throws Exception
485
     * @throws InvalidConfigException
486
     * @throws Throwable
487
     */
488 143
    protected function findConstraints(TableSchemaInterface $table): void
0 ignored issues
show
Unused Code introduced by
The parameter $table is not used and could be removed. ( Ignorable by Annotation )

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

488
    protected function findConstraints(/** @scrutinizer ignore-unused */ TableSchemaInterface $table): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
489
    {
490
        /**
491
         * We need to extract the constraints de hard way since:
492
         * {@see http://www.postgresql.org/message-id/[email protected]}
493
         */
494
495 143
        $sql = <<<SQL
496
        SELECT
497
            ct.conname as constraint_name,
498
            a.attname as column_name,
499
            fc.relname as foreign_table_name,
500
            fns.nspname as foreign_table_schema,
501
            fa.attname as foreign_column_name
502
            FROM
503
            (SELECT ct.conname, ct.conrelid, ct.confrelid, ct.conkey, ct.contype, ct.confkey,
504
                generate_subscripts(ct.conkey, 1) AS s
505
                FROM pg_constraint ct
506
            ) AS ct
507
            inner join pg_class c on c.oid=ct.conrelid
508
            inner join pg_namespace ns on c.relnamespace=ns.oid
509
            inner join pg_attribute a on a.attrelid=ct.conrelid and a.attnum = ct.conkey[ct.s]
510
            left join pg_class fc on fc.oid=ct.confrelid
511
            left join pg_namespace fns on fc.relnamespace=fns.oid
512
            left join pg_attribute fa on fa.attrelid=ct.confrelid and fa.attnum = ct.confkey[ct.s]
513
        WHERE
514
            ct.contype='f'
515
            and c.relname=:tableName
516
            and ns.nspname=:schemaName
517
        ORDER BY
518
            fns.nspname, fc.relname, a.attnum
519 143
        SQL;
520
521
        /** @var array{array{tableName: string, columns: array}} $constraints */
522 143
        $constraints = [];
523
524
        /**
525
         * @psalm-var array<
526
         *   array{
527
         *     constraint_name: string,
528
         *     column_name: string,
529
         *     foreign_table_name: string,
530
         *     foreign_table_schema: string,
531
         *     foreign_column_name: string,
532
         *   }
533
         * > $rows
534
         */
535 143
        $rows = $this->db->createCommand($sql, [
536 143
            ':schemaName' => $table->getSchemaName(),
537 143
            ':tableName' => $table->getName(),
538 143
        ])->queryAll();
539
540 143
        foreach ($rows as $constraint) {
541
            /** @psalm-var array{
542
             *     constraint_name: string,
543
             *     column_name: string,
544
             *     foreign_table_name: string,
545
             *     foreign_table_schema: string,
546
             *     foreign_column_name: string,
547
             *   } $constraint */
548 16
            $constraint = $this->normalizeRowKeyCase($constraint, false);
549
550 16
            if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
551 3
                $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
552
            } else {
553 16
                $foreignTable = $constraint['foreign_table_name'];
554
            }
555
556 16
            $name = $constraint['constraint_name'];
557
558 16
            if (!isset($constraints[$name])) {
559 16
                $constraints[$name] = [
560 16
                    'tableName' => $foreignTable,
561 16
                    'columns' => [],
562 16
                ];
563
            }
564
565 16
            $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name'];
566
        }
567
568
        /**
569
         * @var int|string $foreingKeyName.
570
         * @var array{tableName: string, columns: array} $constraint
571
         */
572 143
        foreach ($constraints as $foreingKeyName => $constraint) {
573 16
            $table->foreignKey(
574 16
                (string) $foreingKeyName,
575 16
                array_merge([$constraint['tableName']], $constraint['columns'])
576 16
            );
577
        }
578
    }
579
580
    /**
581
     * Gets information about given table unique indexes.
582
     *
583
     * @param TableSchemaInterface $table the table metadata.
584
     *
585
     * @throws Exception
586
     * @throws InvalidConfigException
587
     * @throws Throwable
588
     *
589
     * @return array with index and column names.
590
     */
591 1
    protected function getUniqueIndexInformation(TableSchemaInterface $table): array
592
    {
593 1
        $sql = <<<'SQL'
594
        SELECT
595
            i.relname as indexname,
596
            pg_get_indexdef(idx.indexrelid, k + 1, TRUE) AS columnname
597
        FROM (
598
            SELECT *, generate_subscripts(indkey, 1) AS k
599
            FROM pg_index
600
        ) idx
601
        INNER JOIN pg_class i ON i.oid = idx.indexrelid
602
        INNER JOIN pg_class c ON c.oid = idx.indrelid
603
        INNER JOIN pg_namespace ns ON c.relnamespace = ns.oid
604
        WHERE idx.indisprimary = FALSE AND idx.indisunique = TRUE
605
        AND c.relname = :tableName AND ns.nspname = :schemaName
606
        ORDER BY i.relname, k
607 1
        SQL;
608
609 1
        return $this->db->createCommand($sql, [
610 1
            ':schemaName' => $table->getSchemaName(),
611 1
            ':tableName' => $table->getName(),
612 1
        ])->queryAll();
613
    }
614
615
    /**
616
     * Returns all unique indexes for the given table.
617
     *
618
     * Each array element is of the following structure:
619
     *
620
     * ```php
621
     * [
622
     *     'IndexName1' => ['col1' [, ...]],
623
     *     'IndexName2' => ['col2' [, ...]],
624
     * ]
625
     * ```
626
     *
627
     * @param TableSchemaInterface $table the table metadata
628
     *
629
     * @throws Exception
630
     * @throws InvalidConfigException
631
     * @throws Throwable
632
     *
633
     * @return array all unique indexes for the given table.
634
     */
635 1
    public function findUniqueIndexes(TableSchemaInterface $table): array
636
    {
637 1
        $uniqueIndexes = [];
638
639
        /** @var array{indexname: string, columnname: string} $row */
640 1
        foreach ($this->getUniqueIndexInformation($table) as $row) {
641
            /** @var array{indexname: string, columnname: string} $row */
642 1
            $row = $this->normalizeRowKeyCase($row, false);
643
644 1
            $column = $row['columnname'];
645
646 1
            if (str_starts_with($column, '"') && str_ends_with($column, '"')) {
647
                /**
648
                 * postgres will quote names that are not lowercase-only.
649
                 *
650
                 * {@see https://github.com/yiisoft/yii2/issues/10613}
651
                 */
652 1
                $column = substr($column, 1, -1);
653
            }
654
655 1
            $uniqueIndexes[$row['indexname']][] = $column;
656
        }
657
658 1
        return $uniqueIndexes;
659
    }
660
661
    /**
662
     * Collects the metadata of table columns.
663
     *
664
     * @param TableSchemaInterface $table the table metadata.
665
     *
666
     * @throws Exception
667
     * @throws InvalidConfigException
668
     * @throws JsonException
669
     * @throws Throwable
670
     *
671
     * @return bool whether the table exists in the database.
672
     */
673 165
    protected function findColumns(TableSchemaInterface $table): bool
674
    {
675 165
        $orIdentity = '';
676
677 165
        if (version_compare($this->db->getServerVersion(), '12.0', '>=')) {
678 158
            $orIdentity = 'OR a.attidentity != \'\'';
679
        }
680
681 165
        $sql = <<<SQL
682 165
        SELECT
683
            d.nspname AS table_schema,
684
            c.relname AS table_name,
685
            a.attname AS column_name,
686
            COALESCE(td.typname, tb.typname, t.typname) AS data_type,
687
            COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
688
            (SELECT nspname FROM pg_namespace WHERE oid = COALESCE(td.typnamespace, tb.typnamespace, t.typnamespace)) AS type_scheme,
689
            a.attlen AS character_maximum_length,
690
            pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
691
            a.atttypmod AS modifier,
692
            a.attnotnull = false AS is_nullable,
693
            CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
694 165
            coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) $orIdentity AS is_autoinc,
695
            pg_get_serial_sequence(quote_ident(d.nspname) || '.' || quote_ident(c.relname), a.attname)
696
            AS sequence_name,
697
            CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
698
                THEN array_to_string(
699
                    (
700
                        SELECT array_agg(enumlabel)
701
                        FROM pg_enum
702
                        WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid)
703
                    )::varchar[],
704
                ',')
705
                ELSE NULL
706
            END AS enum_values,
707
            CASE atttypid
708
                WHEN 21 /*int2*/ THEN 16
709
                WHEN 23 /*int4*/ THEN 32
710
                WHEN 20 /*int8*/ THEN 64
711
                WHEN 1700 /*numeric*/ THEN
712
                    CASE WHEN atttypmod = -1
713
                        THEN null
714
                        ELSE ((atttypmod - 4) >> 16) & 65535
715
                        END
716
                WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/
717
                WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/
718
                    ELSE null
719
                    END   AS numeric_precision,
720
            CASE
721
                WHEN atttypid IN (21, 23, 20) THEN 0
722
                WHEN atttypid IN (1700) THEN
723
            CASE
724
                WHEN atttypmod = -1 THEN null
725
                    ELSE (atttypmod - 4) & 65535
726
                    END
727
                    ELSE null
728
                    END AS numeric_scale,
729
                    CAST(
730
                        information_schema._pg_char_max_length(
731
                        information_schema._pg_truetypid(a, t),
732
                        information_schema._pg_truetypmod(a, t)
733
                        ) AS numeric
734
                    ) AS size,
735
                    a.attnum = any (ct.conkey) as is_pkey,
736
                    COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
737
            FROM
738
                pg_class c
739
                LEFT JOIN pg_attribute a ON a.attrelid = c.oid
740
                LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
741
                LEFT JOIN pg_type t ON a.atttypid = t.oid
742
                LEFT JOIN pg_type tb ON (a.attndims > 0 OR t.typcategory='A') AND t.typelem > 0 AND t.typelem = tb.oid
743
                                            OR t.typbasetype > 0 AND t.typbasetype = tb.oid
744
                LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
745
                LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
746
                LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
747
            WHERE
748
                a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped
749
                AND c.relname = :tableName
750
                AND d.nspname = :schemaName
751
            ORDER BY
752
                a.attnum;
753 165
        SQL;
754
755 165
        $columns = $this->db->createCommand($sql, [
756 165
            ':schemaName' => $table->getSchemaName(),
757 165
            ':tableName' => $table->getName(),
758 165
        ])->queryAll();
759
760 165
        if (empty($columns)) {
761 41
            return false;
762
        }
763
764
        /** @var array $column */
765 143
        foreach ($columns as $column) {
766
            /** @psalm-var ColumnArray $column */
767 143
            $column = $this->normalizeRowKeyCase($column, false);
768
769
            /** @psalm-var ColumnSchema $loadColumnSchema */
770 143
            $loadColumnSchema = $this->loadColumnSchema($column);
771
772 143
            $table->columns($loadColumnSchema->getName(), $loadColumnSchema);
773
774
            /** @psalm-var mixed $defaultValue */
775 143
            $defaultValue = $loadColumnSchema->getDefaultValue();
776
777 143
            if ($loadColumnSchema->isPrimaryKey()) {
778 89
                $table->primaryKey($loadColumnSchema->getName());
779
780 89
                if ($table->getSequenceName() === null) {
781 89
                    $table->sequenceName($loadColumnSchema->getSequenceName());
782
                }
783
784 89
                $loadColumnSchema->defaultValue(null);
785 141
            } elseif ($defaultValue) {
786 68
                $columnCategory = $this->createColumnSchemaBuilder(
787 68
                        $loadColumnSchema->getType(),
788 68
                        $loadColumnSchema->getSize()
789 68
                    )->getCategoryMap()[$loadColumnSchema->getType()] ?? '';
790
791
                if (
792 68
                    is_string($defaultValue) &&
793 68
                    $columnCategory === AbstractColumnSchemaBuilder::CATEGORY_TIME &&
794 68
                    in_array(
795 68
                        strtoupper($defaultValue),
796 68
                        ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'],
797 68
                        true
798 68
                    )
799
                ) {
800 33
                    $loadColumnSchema->defaultValue(new Expression($defaultValue));
801 68
                } elseif ($loadColumnSchema->getType() === 'boolean') {
802 62
                    $loadColumnSchema->defaultValue(($defaultValue  === 'true'));
803 39
                } elseif (is_string($defaultValue) && preg_match("/^B'(.*?)'::/", $defaultValue, $matches)) {
804
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
805 39
                } elseif (is_string($defaultValue) && preg_match("/^'(\d+)'::\"bit\"$/", $defaultValue, $matches)) {
806 32
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
807 39
                } elseif (is_string($defaultValue) && preg_match("/^'(.*?)'::/", $defaultValue, $matches)) {
808 36
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[1]));
809
                } elseif (
810 36
                    is_string($defaultValue) &&
811 36
                    preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $defaultValue, $matches)
812
                ) {
813 36
                    if ($matches[2] === 'NULL') {
814 5
                        $loadColumnSchema->defaultValue(null);
815
                    } elseif(
816 34
                        in_array($columnCategory, [
817 34
                            AbstractColumnSchemaBuilder::CATEGORY_STRING,
818 34
                            AbstractColumnSchemaBuilder::CATEGORY_TIME
819 34
                        ], true) &&
820 34
                        !str_starts_with('\'', $defaultValue)
821
                    ) {
822 4
                        $loadColumnSchema->defaultValue(new Expression($matches[2]));
823
                    } else {
824 36
                        $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[2]));
825
                    }
826
                } else {
827
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($defaultValue));
828
                }
829
            }
830
        }
831
832 143
        return true;
833
    }
834
835
    /**
836
     * Loads the column information into a {@see ColumnSchemaInterface} object.
837
     *
838
     * @psalm-param array{
839
     *   table_schema: string,
840
     *   table_name: string,
841
     *   column_name: string,
842
     *   data_type: string,
843
     *   type_type: string|null,
844
     *   type_scheme: string|null,
845
     *   character_maximum_length: int,
846
     *   column_comment: string|null,
847
     *   modifier: int,
848
     *   is_nullable: bool,
849
     *   column_default: mixed,
850
     *   is_autoinc: bool,
851
     *   sequence_name: string|null,
852
     *   enum_values: array<array-key, float|int|string>|string|null,
853
     *   numeric_precision: int|null,
854
     *   numeric_scale: int|null,
855
     *   size: string|null,
856
     *   is_pkey: bool|null,
857
     *   dimension: int
858
     * } $info column information.
859
     *
860
     * @return ColumnSchemaInterface the column schema object.
861
     */
862 143
    protected function loadColumnSchema(array $info): ColumnSchemaInterface
863
    {
864 143
        $column = $this->createColumnSchema();
865 143
        $column->allowNull($info['is_nullable']);
866 143
        $column->autoIncrement($info['is_autoinc']);
867 143
        $column->comment($info['column_comment']);
868
869 143
        if (!in_array($info['type_scheme'], [$this->defaultSchema, 'pg_catalog'], true)
870
        ) {
871 1
            $column->dbType($info['type_scheme'] . '.' . $info['data_type']);
872
        } else {
873 143
            $column->dbType($info['data_type']);
874
        }
875
876 143
        $column->defaultValue($info['column_default']);
877 143
        $column->enumValues(($info['enum_values'] !== null)
878 143
            ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) : null);
879 143
        $column->unsigned(false); // has no meaning in PG
880 143
        $column->primaryKey((bool) $info['is_pkey']);
881 143
        $column->name($info['column_name']);
882 143
        $column->precision($info['numeric_precision']);
883 143
        $column->scale($info['numeric_scale']);
884 143
        $column->size($info['size'] === null ? null : (int) $info['size']);
885 143
        $column->dimension($info['dimension']);
886
887
        /**
888
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
889
         * default value.
890
         *
891
         * @psalm-var mixed $defaultValue
892
         */
893 143
        $defaultValue = $column->getDefaultValue();
894 143
        $sequenceName = $info['sequence_name'] ?? null;
895
896
        if (
897 143
            isset($defaultValue) &&
898 143
            is_string($defaultValue) &&
899 143
            preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1
900
        ) {
901 78
            $column->sequenceName(preg_replace(
902 78
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
903 78
                '',
904 78
                $defaultValue
905 78
            ));
906 143
        } elseif ($sequenceName !== null) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sequenceName seems to be never defined.
Loading history...
907 5
            $column->sequenceName($this->resolveTableName($sequenceName)->getFullName());
0 ignored issues
show
Bug Best Practice introduced by
The property resolveTableName does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
Comprehensibility Best Practice introduced by
The variable $column seems to be never defined.
Loading history...
908
        }
909
910 143
        if (isset($this->typeMap[$column->getDbType()])) {
911 143
            $column->type($this->typeMap[$column->getDbType()]);
912
        } else {
913 5
            $column->type(self::TYPE_STRING);
914
        }
915
916 143
        $column->phpType($this->getColumnPhpType($column));
0 ignored issues
show
Bug Best Practice introduced by
The property getColumnPhpType does not exist on Yiisoft\Db\Pgsql\Schema. Did you maybe forget to declare it?
Loading history...
917
918 143
        return $column;
919
    }
920
921
    /**
922
     * Loads multiple types of constraints and returns the specified ones.
923
     *
924
     * @param string $tableName table name.
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $tableName does not exist. Did you maybe mean $table?
Loading history...
925
     * @param string $returnType return type:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $returnType seems to be never defined.
Loading history...
926
     * - primaryKey
927
     * - foreignKeys
928
     * - uniques
929
     * - checks
930
     *
931
     * @throws Exception
932
     * @throws InvalidConfigException
933
     * @throws Throwable
934
     *
935
     * @return array|Constraint|null (CheckConstraint|Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
936
     */
937 82
    private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null
938
    {
939 82
        $sql = <<<SQL
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sql seems to be never defined.
Loading history...
940
        SELECT
941
            "c"."conname" AS "name",
942
            "a"."attname" AS "column_name",
943
            "c"."contype" AS "type",
944
            "ftcns"."nspname" AS "foreign_table_schema",
945
            "ftc"."relname" AS "foreign_table_name",
946
            "fa"."attname" AS "foreign_column_name",
947
            "c"."confupdtype" AS "on_update",
948
            "c"."confdeltype" AS "on_delete",
949
            pg_get_constraintdef("c"."oid") AS "check_expr"
950
        FROM "pg_class" AS "tc"
951
        INNER JOIN "pg_namespace" AS "tcns"
952
            ON "tcns"."oid" = "tc"."relnamespace"
953
        INNER JOIN "pg_constraint" AS "c"
954
            ON "c"."conrelid" = "tc"."oid"
955
        INNER JOIN "pg_attribute" AS "a"
956
            ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
957
        LEFT JOIN "pg_class" AS "ftc"
958
            ON "ftc"."oid" = "c"."confrelid"
959
        LEFT JOIN "pg_namespace" AS "ftcns"
960
            ON "ftcns"."oid" = "ftc"."relnamespace"
961
        LEFT JOIN "pg_attribute" "fa"
962
            ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
963
        WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
964
        ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
965 82
        SQL;
966
967
        /** @psalm-var array<array-key, string> $actionTypes */
968 82
        $actionTypes = [
969 82
            'a' => 'NO ACTION',
970 82
            'r' => 'RESTRICT',
971 82
            'c' => 'CASCADE',
972 82
            'n' => 'SET NULL',
973 82
            'd' => 'SET DEFAULT',
974 82
        ];
975
976 82
        $resolvedName = $this->resolveTableName($tableName);
977
978 82
        $constraints = $this->db->createCommand($sql, [
979 82
            ':schemaName' => $resolvedName->getSchemaName(),
980 82
            ':tableName' => $resolvedName->getName(),
981 82
        ])->queryAll();
982
983
        /** @var array<array-key, array> $constraints */
984 82
        $constraints = $this->normalizeRowKeyCase($constraints, true);
985 82
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
986
987 82
        $result = [
988 82
            self::PRIMARY_KEY => null,
989 82
            self::FOREIGN_KEYS => [],
990 82
            self::UNIQUES => [],
991 82
            self::CHECKS => [],
992 82
        ];
993
994
        /**
995
         * @var string $type
996
         * @var array $names
997
         */
998 82
        foreach ($constraints as $type => $names) {
999
            /**
1000
             * @psalm-var object|string|null $name
1001
             * @psalm-var ConstraintArray $constraint
1002
             */
1003 82
            foreach ($names as $name => $constraint) {
1004
                switch ($type) {
1005 82
                    case 'p':
1006 57
                        $result[self::PRIMARY_KEY] = (new Constraint())
1007 57
                            ->name($name)
1008 57
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1009 57
                        break;
1010 74
                    case 'f':
1011 19
                        $onDelete = $actionTypes[$constraint[0]['on_delete']] ?? null;
1012 19
                        $onUpdate = $actionTypes[$constraint[0]['on_update']] ?? null;
1013
1014 19
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
1015 19
                            ->name($name)
1016 19
                            ->columnNames(array_values(
1017 19
                                array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
1018 19
                            ))
1019 19
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
1020 19
                            ->foreignTableName($constraint[0]['foreign_table_name'])
1021 19
                            ->foreignColumnNames(array_values(
1022 19
                                array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
1023 19
                            ))
1024 19
                            ->onDelete($onDelete)
1025 19
                            ->onUpdate($onUpdate);
1026 19
                        break;
1027 61
                    case 'u':
1028 58
                        $result[self::UNIQUES][] = (new Constraint())
1029 58
                            ->name($name)
1030 58
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1031 58
                        break;
1032 15
                    case 'c':
1033 15
                        $result[self::CHECKS][] = (new CheckConstraint())
1034 15
                            ->name($name)
1035 15
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
1036 15
                            ->expression($constraint[0]['check_expr']);
1037 15
                        break;
1038
                }
1039
            }
1040
        }
1041
1042 82
        foreach ($result as $type => $data) {
1043 82
            $this->setTableMetadata($tableName, $type, $data);
1044
        }
1045
1046 82
        return $result[$returnType];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result[$returnType] could return the type array which is incompatible with the type-hinted return void. Consider adding an additional type-check to rule them out.
Loading history...
1047
    }
1048
1049
    /**
1050
     * Creates a column schema for the database.
1051
     *
1052
     * This method may be overridden by child classes to create a DBMS-specific column schema.
1053
     *
1054
     * @return ColumnSchema column schema instance.
1055
     */
1056 143
    private function createColumnSchema(): ColumnSchema
0 ignored issues
show
Unused Code introduced by
The method createColumnSchema() is not used, and could be removed.

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

Loading history...
1057
    {
1058 143
        return new ColumnSchema();
1059
    }
1060
1061 74
    public function createColumnSchemaBuilder(
1062
        string $type,
1063
        int|string|array|null $length = null
1064
    ): ColumnSchemaBuilderInterface {
1065 74
        return new ColumnSchemaBuilder($type, $length);
1066
    }
1067
1068
    /**
1069
     * Returns the cache key for the specified table name.
1070
     *
1071
     * @param string $name the table name.
1072
     *
1073
     * @return array the cache key.
1074
     */
1075 255
    protected function getCacheKey(string $name): array
1076
    {
1077 255
        return array_merge([self::class], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
1078
    }
1079
1080
    /**
1081
     * Returns the cache tag name.
1082
     *
1083
     * This allows {@see refresh()} to invalidate all cached table schemas.
1084
     *
1085
     * @return string the cache tag name.
1086
     */
1087 256
    protected function getCacheTag(): string
1088
    {
1089 256
        return md5(serialize(array_merge([self::class], $this->db->getCacheKey())));
1090
    }
1091
}
1092