Passed
Pull Request — master (#241)
by Def
09:05 queued 05:23
created

Schema::findColumns()   D

Complexity

Conditions 20
Paths 26

Size

Total Lines 151
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 20.0228

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 20
eloc 65
c 5
b 1
f 0
nc 26
nop 1
dl 0
loc 151
ccs 50
cts 52
cp 0.9615
crap 20.0228
rs 4.1666

How to fix   Long Method    Complexity   

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
                /** @var string $columnCategory */
787 68
                $columnCategory = $this->createColumnSchemaBuilder(
788 68
                    $loadColumnSchema->getType(),
789 68
                    $loadColumnSchema->getSize()
790 68
                )->getCategoryMap()[$loadColumnSchema->getType()] ?? '';
791
792
                if (
793 68
                    is_string($defaultValue) &&
794 68
                    $columnCategory === AbstractColumnSchemaBuilder::CATEGORY_TIME &&
795 68
                    in_array(
796 68
                        strtoupper($defaultValue),
797 68
                        ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'],
798 68
                        true
799 68
                    )
800
                ) {
801 33
                    $loadColumnSchema->defaultValue(new Expression($defaultValue));
802 68
                } elseif ($loadColumnSchema->getType() === 'boolean') {
803 62
                    $loadColumnSchema->defaultValue(($defaultValue  === 'true'));
804 39
                } elseif (is_string($defaultValue) && preg_match("/^B'(.*?)'::/", $defaultValue, $matches)) {
805
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
806 39
                } elseif (is_string($defaultValue) && preg_match("/^'(\d+)'::\"bit\"$/", $defaultValue, $matches)) {
807 32
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
808 39
                } elseif (is_string($defaultValue) && preg_match("/^'(.*?)'::/", $defaultValue, $matches)) {
809 36
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[1]));
810
                } elseif (
811 36
                    is_string($defaultValue) &&
812 36
                    preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $defaultValue, $matches)
813
                ) {
814 36
                    if ($matches[2] === 'NULL') {
815 5
                        $loadColumnSchema->defaultValue(null);
816
                    } elseif (
817 34
                        in_array($columnCategory, [
818 34
                            AbstractColumnSchemaBuilder::CATEGORY_STRING,
819 34
                            AbstractColumnSchemaBuilder::CATEGORY_TIME,
820 34
                        ], true) &&
821 34
                        !str_starts_with($defaultValue, "'")
822
                    ) {
823 4
                        $loadColumnSchema->defaultValue(new Expression($matches[2]));
824
                    } else {
825 36
                        $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[2]));
826
                    }
827
                } else {
828
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($defaultValue));
829
                }
830
            }
831
        }
832
833 143
        return true;
834
    }
835
836
    /**
837
     * Loads the column information into a {@see ColumnSchemaInterface} object.
838
     *
839
     * @psalm-param array{
840
     *   table_schema: string,
841
     *   table_name: string,
842
     *   column_name: string,
843
     *   data_type: string,
844
     *   type_type: string|null,
845
     *   type_scheme: string|null,
846
     *   character_maximum_length: int,
847
     *   column_comment: string|null,
848
     *   modifier: int,
849
     *   is_nullable: bool,
850
     *   column_default: mixed,
851
     *   is_autoinc: bool,
852
     *   sequence_name: string|null,
853
     *   enum_values: array<array-key, float|int|string>|string|null,
854
     *   numeric_precision: int|null,
855
     *   numeric_scale: int|null,
856
     *   size: string|null,
857
     *   is_pkey: bool|null,
858
     *   dimension: int
859
     * } $info column information.
860
     *
861
     * @return ColumnSchemaInterface the column schema object.
862
     */
863 143
    protected function loadColumnSchema(array $info): ColumnSchemaInterface
864
    {
865 143
        $column = $this->createColumnSchema();
866 143
        $column->allowNull($info['is_nullable']);
867 143
        $column->autoIncrement($info['is_autoinc']);
868 143
        $column->comment($info['column_comment']);
869
870 143
        if (!in_array($info['type_scheme'], [$this->defaultSchema, 'pg_catalog'], true)
871
        ) {
872 1
            $column->dbType($info['type_scheme'] . '.' . $info['data_type']);
873
        } else {
874 143
            $column->dbType($info['data_type']);
875
        }
876
877 143
        $column->defaultValue($info['column_default']);
878 143
        $column->enumValues(($info['enum_values'] !== null)
879 143
            ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) : null);
880 143
        $column->unsigned(false); // has no meaning in PG
881 143
        $column->primaryKey((bool) $info['is_pkey']);
882 143
        $column->name($info['column_name']);
883 143
        $column->precision($info['numeric_precision']);
884 143
        $column->scale($info['numeric_scale']);
885 143
        $column->size($info['size'] === null ? null : (int) $info['size']);
886 143
        $column->dimension($info['dimension']);
887
888
        /**
889
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
890
         * default value.
891
         *
892
         * @psalm-var mixed $defaultValue
893
         */
894 143
        $defaultValue = $column->getDefaultValue();
895 143
        $sequenceName = $info['sequence_name'] ?? null;
896
897
        if (
898 143
            isset($defaultValue) &&
899 143
            is_string($defaultValue) &&
900 143
            preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1
901
        ) {
902 78
            $column->sequenceName(preg_replace(
903 78
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
904 78
                '',
905 78
                $defaultValue
906 78
            ));
907 143
        } elseif ($sequenceName !== null) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sequenceName seems to be never defined.
Loading history...
908 5
            $column->sequenceName($this->resolveTableName($sequenceName)->getFullName());
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $column seems to be never defined.
Loading history...
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...
909
        }
910
911 143
        if (isset($this->typeMap[$column->getDbType()])) {
912 143
            $column->type($this->typeMap[$column->getDbType()]);
913
        } else {
914 5
            $column->type(self::TYPE_STRING);
915
        }
916
917 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...
918
919 143
        return $column;
920
    }
921
922
    /**
923
     * Loads multiple types of constraints and returns the specified ones.
924
     *
925
     * @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...
926
     * @param string $returnType return type:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $returnType seems to be never defined.
Loading history...
927
     * - primaryKey
928
     * - foreignKeys
929
     * - uniques
930
     * - checks
931
     *
932
     * @throws Exception
933
     * @throws InvalidConfigException
934
     * @throws Throwable
935
     *
936
     * @return array|Constraint|null (CheckConstraint|Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
937
     */
938 82
    private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null
939
    {
940 82
        $sql = <<<SQL
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sql seems to be never defined.
Loading history...
941
        SELECT
942
            "c"."conname" AS "name",
943
            "a"."attname" AS "column_name",
944
            "c"."contype" AS "type",
945
            "ftcns"."nspname" AS "foreign_table_schema",
946
            "ftc"."relname" AS "foreign_table_name",
947
            "fa"."attname" AS "foreign_column_name",
948
            "c"."confupdtype" AS "on_update",
949
            "c"."confdeltype" AS "on_delete",
950
            pg_get_constraintdef("c"."oid") AS "check_expr"
951
        FROM "pg_class" AS "tc"
952
        INNER JOIN "pg_namespace" AS "tcns"
953
            ON "tcns"."oid" = "tc"."relnamespace"
954
        INNER JOIN "pg_constraint" AS "c"
955
            ON "c"."conrelid" = "tc"."oid"
956
        INNER JOIN "pg_attribute" AS "a"
957
            ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
958
        LEFT JOIN "pg_class" AS "ftc"
959
            ON "ftc"."oid" = "c"."confrelid"
960
        LEFT JOIN "pg_namespace" AS "ftcns"
961
            ON "ftcns"."oid" = "ftc"."relnamespace"
962
        LEFT JOIN "pg_attribute" "fa"
963
            ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
964
        WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
965
        ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
966 82
        SQL;
967
968
        /** @psalm-var array<array-key, string> $actionTypes */
969 82
        $actionTypes = [
970 82
            'a' => 'NO ACTION',
971 82
            'r' => 'RESTRICT',
972 82
            'c' => 'CASCADE',
973 82
            'n' => 'SET NULL',
974 82
            'd' => 'SET DEFAULT',
975 82
        ];
976
977 82
        $resolvedName = $this->resolveTableName($tableName);
978
979 82
        $constraints = $this->db->createCommand($sql, [
980 82
            ':schemaName' => $resolvedName->getSchemaName(),
981 82
            ':tableName' => $resolvedName->getName(),
982 82
        ])->queryAll();
983
984
        /** @var array<array-key, array> $constraints */
985 82
        $constraints = $this->normalizeRowKeyCase($constraints, true);
986 82
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
987
988 82
        $result = [
989 82
            self::PRIMARY_KEY => null,
990 82
            self::FOREIGN_KEYS => [],
991 82
            self::UNIQUES => [],
992 82
            self::CHECKS => [],
993 82
        ];
994
995
        /**
996
         * @var string $type
997
         * @var array $names
998
         */
999 82
        foreach ($constraints as $type => $names) {
1000
            /**
1001
             * @psalm-var object|string|null $name
1002
             * @psalm-var ConstraintArray $constraint
1003
             */
1004 82
            foreach ($names as $name => $constraint) {
1005
                switch ($type) {
1006 82
                    case 'p':
1007 57
                        $result[self::PRIMARY_KEY] = (new Constraint())
1008 57
                            ->name($name)
1009 57
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1010 57
                        break;
1011 74
                    case 'f':
1012 19
                        $onDelete = $actionTypes[$constraint[0]['on_delete']] ?? null;
1013 19
                        $onUpdate = $actionTypes[$constraint[0]['on_update']] ?? null;
1014
1015 19
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
1016 19
                            ->name($name)
1017 19
                            ->columnNames(array_values(
1018 19
                                array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
1019 19
                            ))
1020 19
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
1021 19
                            ->foreignTableName($constraint[0]['foreign_table_name'])
1022 19
                            ->foreignColumnNames(array_values(
1023 19
                                array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
1024 19
                            ))
1025 19
                            ->onDelete($onDelete)
1026 19
                            ->onUpdate($onUpdate);
1027 19
                        break;
1028 61
                    case 'u':
1029 58
                        $result[self::UNIQUES][] = (new Constraint())
1030 58
                            ->name($name)
1031 58
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1032 58
                        break;
1033 15
                    case 'c':
1034 15
                        $result[self::CHECKS][] = (new CheckConstraint())
1035 15
                            ->name($name)
1036 15
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
1037 15
                            ->expression($constraint[0]['check_expr']);
1038 15
                        break;
1039
                }
1040
            }
1041
        }
1042
1043 82
        foreach ($result as $type => $data) {
1044 82
            $this->setTableMetadata($tableName, $type, $data);
1045
        }
1046
1047 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...
1048
    }
1049
1050
    /**
1051
     * Creates a column schema for the database.
1052
     *
1053
     * This method may be overridden by child classes to create a DBMS-specific column schema.
1054
     *
1055
     * @return ColumnSchema column schema instance.
1056
     */
1057 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...
1058
    {
1059 143
        return new ColumnSchema();
1060
    }
1061
1062 74
    public function createColumnSchemaBuilder(
1063
        string $type,
1064
        int|string|array|null $length = null
1065
    ): ColumnSchemaBuilderInterface {
1066 74
        return new ColumnSchemaBuilder($type, $length);
1067
    }
1068
1069
    /**
1070
     * Returns the cache key for the specified table name.
1071
     *
1072
     * @param string $name the table name.
1073
     *
1074
     * @return array the cache key.
1075
     */
1076 255
    protected function getCacheKey(string $name): array
1077
    {
1078 255
        return array_merge([self::class], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
1079
    }
1080
1081
    /**
1082
     * Returns the cache tag name.
1083
     *
1084
     * This allows {@see refresh()} to invalidate all cached table schemas.
1085
     *
1086
     * @return string the cache tag name.
1087
     */
1088 256
    protected function getCacheTag(): string
1089
    {
1090 256
        return md5(serialize(array_merge([self::class], $this->db->getCacheKey())));
1091
    }
1092
}
1093