Test Failed
Push — remove-test-batch-query-result... ( 8f35cd...ed4016 )
by Wilmer
12:01 queued 09:19
created

Schema::findConstraints()   C

Complexity

Conditions 8
Paths 14

Size

Total Lines 541
Code Lines 481

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 146
CRAP Score 8.0023

Importance

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

289
        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...
290 34
            $this->findConstraints($table);
291
            return $table;
292 34
        }
293
294
        return null;
295
    }
296
297
    /**
298
     * Loads a primary key for the given table.
299
     *
300
     * @param string $tableName table name.
301
     *
302
     * @throws Exception
303
     * @throws InvalidConfigException
304
     * @throws Throwable
305
     *
306
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
307
     */
308 9
    protected function loadTablePrimaryKey(string $tableName): Constraint|null
309
    {
310 9
        $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

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

489
    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...
490
    {
491
        /**
492
         * We need to extract the constraints de hard way since:
493
         * {@see http://www.postgresql.org/message-id/[email protected]}
494
         */
495
496
        $sql = <<<SQL
497
        SELECT
498
            ct.conname as constraint_name,
499
            a.attname as column_name,
500
            fc.relname as foreign_table_name,
501
            fns.nspname as foreign_table_schema,
502
            fa.attname as foreign_column_name
503 117
            FROM
504
            (SELECT ct.conname, ct.conrelid, ct.confrelid, ct.conkey, ct.contype, ct.confkey,
505
                generate_subscripts(ct.conkey, 1) AS s
506
                FROM pg_constraint ct
507
            ) AS ct
508
            inner join pg_class c on c.oid=ct.conrelid
509
            inner join pg_namespace ns on c.relnamespace=ns.oid
510
            inner join pg_attribute a on a.attrelid=ct.conrelid and a.attnum = ct.conkey[ct.s]
511
            left join pg_class fc on fc.oid=ct.confrelid
512
            left join pg_namespace fns on fc.relnamespace=fns.oid
513
            left join pg_attribute fa on fa.attrelid=ct.confrelid and fa.attnum = ct.confkey[ct.s]
514
        WHERE
515
            ct.contype='f'
516 117
            and c.relname=:tableName
517 117
            and ns.nspname=:schemaName
518 117
        ORDER BY
519 117
            fns.nspname, fc.relname, a.attnum
520
        SQL;
521 117
522
        /** @var array{array{tableName: string, columns: array}} $constraints */
523
        $constraints = [];
524
525
        /**
526
         * @psalm-var array<
527
         *   array{
528
         *     constraint_name: string,
529 12
         *     column_name: string,
530
         *     foreign_table_name: string,
531 12
         *     foreign_table_schema: string,
532
         *     foreign_column_name: string,
533
         *   }
534 12
         * > $rows
535
         */
536
        $rows = $this->db->createCommand($sql, [
537 12
            ':schemaName' => $table->getSchemaName(),
538
            ':tableName' => $table->getName(),
539 12
        ])->queryAll();
540 12
541
        foreach ($rows as $constraint) {
542
            /** @psalm-var array{
543
             *     constraint_name: string,
544
             *     column_name: string,
545
             *     foreign_table_name: string,
546 12
             *     foreign_table_schema: string,
547
             *     foreign_column_name: string,
548
             *   } $constraint */
549
            $constraint = $this->normalizeRowKeyCase($constraint, false);
550
551
            if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
552
                $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
553 117
            } else {
554 12
                $foreignTable = $constraint['foreign_table_name'];
555
            }
556 12
557
            $name = $constraint['constraint_name'];
558
559
            if (!isset($constraints[$name])) {
560
                $constraints[$name] = [
561
                    'tableName' => $foreignTable,
562
                    'columns' => [],
563
                ];
564
            }
565
566
            $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name'];
567
        }
568
569
        /**
570
         * @var int|string $foreingKeyName.
571
         * @var array{tableName: string, columns: array} $constraint
572 1
         */
573
        foreach ($constraints as $foreingKeyName => $constraint) {
574 1
            $table->foreignKey(
575
                (string) $foreingKeyName,
576
                array_merge([$constraint['tableName']], $constraint['columns'])
577
            );
578
        }
579
    }
580
581
    /**
582
     * Gets information about given table unique indexes.
583
     *
584
     * @param TableSchemaInterface $table the table metadata.
585
     *
586
     * @throws Exception
587
     * @throws InvalidConfigException
588
     * @throws Throwable
589
     *
590 1
     * @return array with index and column names.
591 1
     */
592 1
    protected function getUniqueIndexInformation(TableSchemaInterface $table): array
593 1
    {
594
        $sql = <<<'SQL'
595
        SELECT
596
            i.relname as indexname,
597
            pg_get_indexdef(idx.indexrelid, k + 1, TRUE) AS columnname
598
        FROM (
599
            SELECT *, generate_subscripts(indkey, 1) AS k
600
            FROM pg_index
601
        ) idx
602
        INNER JOIN pg_class i ON i.oid = idx.indexrelid
603
        INNER JOIN pg_class c ON c.oid = idx.indrelid
604
        INNER JOIN pg_namespace ns ON c.relnamespace = ns.oid
605
        WHERE idx.indisprimary = FALSE AND idx.indisunique = TRUE
606
        AND c.relname = :tableName AND ns.nspname = :schemaName
607
        ORDER BY i.relname, k
608
        SQL;
609
610
        return $this->db->createCommand($sql, [
611
            ':schemaName' => $table->getSchemaName(),
612
            ':tableName' => $table->getName(),
613
        ])->queryAll();
614
    }
615
616 1
    /**
617
     * Returns all unique indexes for the given table.
618 1
     *
619
     * Each array element is of the following structure:
620
     *
621 1
     * ```php
622
     * [
623 1
     *     'IndexName1' => ['col1' [, ...]],
624
     *     'IndexName2' => ['col2' [, ...]],
625 1
     * ]
626
     * ```
627 1
     *
628
     * @param TableSchemaInterface $table the table metadata
629
     *
630
     * @throws Exception
631
     * @throws InvalidConfigException
632
     * @throws Throwable
633 1
     *
634
     * @return array all unique indexes for the given table.
635
     */
636 1
    public function findUniqueIndexes(TableSchemaInterface $table): array
637
    {
638
        $uniqueIndexes = [];
639 1
640
        /** @var array{indexname: string, columnname: string} $row */
641
        foreach ($this->getUniqueIndexInformation($table) as $row) {
642
            /** @var array{indexname: string, columnname: string} $row */
643
            $row = $this->normalizeRowKeyCase($row, false);
644
645
            $column = $row['columnname'];
646
647
            if (!empty($column) && $column[0] === '"') {
648
                /**
649
                 * postgres will quote names that are not lowercase-only.
650
                 *
651
                 * {@see https://github.com/yiisoft/yii2/issues/10613}
652
                 */
653
                $column = substr($column, 1, -1);
654 137
            }
655
656 137
            $uniqueIndexes[$row['indexname']][] = $column;
657
        }
658 137
659 130
        return $uniqueIndexes;
660
    }
661
662 137
    /**
663
     * Collects the metadata of table columns.
664
     *
665
     * @param TableSchemaInterface $table the table metadata.
666
     *
667
     * @throws Exception
668
     * @throws InvalidConfigException
669
     * @throws JsonException
670
     * @throws Throwable
671
     *
672
     * @return bool whether the table exists in the database.
673
     */
674
    protected function findColumns(TableSchemaInterface $table): bool
675
    {
676
        $orIdentity = '';
677
678
        if (version_compare($this->db->getServerVersion(), '12.0', '>=')) {
679
            $orIdentity = 'OR a.attidentity != \'\'';
680
        }
681
682
        $sql = <<<SQL
683
        SELECT
684
            d.nspname AS table_schema,
685
            c.relname AS table_name,
686
            a.attname AS column_name,
687
            COALESCE(td.typname, tb.typname, t.typname) AS data_type,
688
            COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
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
            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 137
                    a.attnum = any (ct.conkey) as is_pkey,
736 137
                    COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
737 137
            FROM
738 137
                pg_class c
739
                LEFT JOIN pg_attribute a ON a.attrelid = c.oid
740 137
                LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
741 31
                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 117
                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 117
            WHERE
748
                a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped
749
                AND c.relname = :tableName
750 117
                AND d.nspname = :schemaName
751
            ORDER BY
752 117
                a.attnum;
753
        SQL;
754
755 117
        $columns = $this->db->createCommand($sql, [
756
            ':schemaName' => $table->getSchemaName(),
757 117
            ':tableName' => $table->getName(),
758 76
        ])->queryAll();
759
760 76
        if (empty($columns)) {
761 76
            return false;
762
        }
763
764 76
        /** @var array $column */
765 114
        foreach ($columns as $column) {
766
            /** @psalm-var ColumnArray $column */
767 62
            $column = $this->normalizeRowKeyCase($column, false);
768 62
769 62
            /** @psalm-var ColumnSchema $loadColumnSchema */
770
            $loadColumnSchema = $this->loadColumnSchema($column);
771
772
            $table->columns($loadColumnSchema->getName(), $loadColumnSchema);
773 30
774 30
            /** @psalm-var mixed $defaultValue */
775
            $defaultValue = $loadColumnSchema->getDefaultValue();
776
777
            if ($loadColumnSchema->isPrimaryKey()) {
778
                $table->primaryKey($loadColumnSchema->getName());
779 28
780 62
                if ($table->getSequenceName() === null) {
781 58
                    $table->sequenceName($loadColumnSchema->getSequenceName());
782 33
                }
783
784 33
                $loadColumnSchema->defaultValue(null);
785 28
            } elseif ($defaultValue) {
786 33
                if (
787 30
                    is_string($defaultValue) &&
788
                    in_array(
789 31
                        $loadColumnSchema->getType(),
790 31
                        [self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME],
791
                        true
792 31
                    ) &&
793 5
                    in_array(
794
                        strtoupper($defaultValue),
795 31
                        ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'],
796
                        true
797
                    )
798
                ) {
799
                    $loadColumnSchema->defaultValue(new Expression($defaultValue));
800
                } elseif ($loadColumnSchema->getType() === 'boolean') {
801
                    $loadColumnSchema->defaultValue(($defaultValue  === 'true'));
802
                } elseif (is_string($defaultValue) && preg_match("/^B'(.*?)'::/", $defaultValue, $matches)) {
803 117
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
804
                } elseif (is_string($defaultValue) && preg_match("/^'(\d+)'::\"bit\"$/", $defaultValue, $matches)) {
805
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
806
                } elseif (is_string($defaultValue) && preg_match("/^'(.*?)'::/", $defaultValue, $matches)) {
807
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[1]));
808
                } elseif (
809
                    is_string($defaultValue) &&
810
                    preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $defaultValue, $matches)
811
                ) {
812
                    if ($matches[2] === 'NULL') {
813
                        $loadColumnSchema->defaultValue(null);
814
                    } else {
815
                        $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[2]));
816
                    }
817
                } else {
818
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($defaultValue));
819
                }
820
            }
821
        }
822
823
        return true;
824
    }
825
826
    /**
827
     * Loads the column information into a {@see ColumnSchemaInterface} object.
828
     *
829
     * @psalm-param array{
830
     *   table_schema: string,
831
     *   table_name: string,
832 117
     *   column_name: string,
833
     *   data_type: string,
834 117
     *   type_type: string|null,
835 117
     *   character_maximum_length: int,
836 117
     *   column_comment: string|null,
837 117
     *   modifier: int,
838 117
     *   is_nullable: bool,
839 117
     *   column_default: mixed,
840 117
     *   is_autoinc: bool,
841 117
     *   sequence_name: string|null,
842 117
     *   enum_values: array<array-key, float|int|string>|string|null,
843 117
     *   numeric_precision: int|null,
844 117
     *   numeric_scale: int|null,
845 117
     *   size: string|null,
846 117
     *   is_pkey: bool|null,
847 117
     *   dimension: int
848 117
     * } $info column information.
849
     *
850
     * @return ColumnSchemaInterface the column schema object.
851
     */
852
    protected function loadColumnSchema(array $info): ColumnSchemaInterface
853
    {
854
        $column = $this->createColumnSchema();
855
        $column->allowNull($info['is_nullable']);
856 117
        $column->autoIncrement($info['is_autoinc']);
857 117
        $column->comment($info['column_comment']);
858
        $column->dbType($info['data_type']);
859
        $column->defaultValue($info['column_default']);
860
        $column->enumValues(($info['enum_values'] !== null)
861 117
            ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) : null);
862 117
        $column->unsigned(false); // has no meaning in PG
863
        $column->primaryKey((bool) $info['is_pkey']);
864 70
        $column->name($info['column_name']);
865
        $column->precision($info['numeric_precision']);
866
        $column->scale($info['numeric_scale']);
867
        $column->size($info['size'] === null ? null : (int) $info['size']);
868
        $column->dimension($info['dimension']);
869 115
870
        /**
871
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
872
         * default value.
873 117
         *
874 117
         * @psalm-var mixed $defaultValue
875
         */
876
        $defaultValue = $column->getDefaultValue();
877
        $sequenceName = $info['sequence_name'] ?? null;
878
879 117
        if (
880
            isset($defaultValue) &&
881 117
            is_string($defaultValue) &&
882
            preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1
883
        ) {
884
            $column->sequenceName(preg_replace(
885
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
886
                '',
887
                $defaultValue
888
            ));
889
        } elseif ($sequenceName !== null) {
890
            $column->sequenceName($this->resolveTableName($sequenceName)->getFullName());
891
        }
892
893
        if (isset($this->typeMap[$column->getDbType()])) {
894
            $column->type($this->typeMap[$column->getDbType()]);
895
        } else {
896
            $column->type(self::TYPE_STRING);
897
        }
898
899
        $column->phpType($this->getColumnPhpType($column));
900 75
901
        return $column;
902 75
    }
903
904
    /**
905
     * Loads multiple types of constraints and returns the specified ones.
906
     *
907
     * @param string $tableName table name.
908
     * @param string $returnType return type:
909
     * - primaryKey
910
     * - foreignKeys
911
     * - uniques
912
     * - checks
913
     *
914
     * @throws Exception
915
     * @throws InvalidConfigException
916
     * @throws Throwable
917
     *
918
     * @return array|Constraint|null (CheckConstraint|Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
919
     */
920
    private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $returnType seems to be never defined.
Loading history...
921
    {
922
        $sql = <<<SQL
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sql seems to be never defined.
Loading history...
923
        SELECT
924
            "c"."conname" AS "name",
925
            "a"."attname" AS "column_name",
926
            "c"."contype" AS "type",
927
            "ftcns"."nspname" AS "foreign_table_schema",
928
            "ftc"."relname" AS "foreign_table_name",
929
            "fa"."attname" AS "foreign_column_name",
930
            "c"."confupdtype" AS "on_update",
931 75
            "c"."confdeltype" AS "on_delete",
932
            pg_get_constraintdef("c"."oid") AS "check_expr"
933
        FROM "pg_class" AS "tc"
934
        INNER JOIN "pg_namespace" AS "tcns"
935
            ON "tcns"."oid" = "tc"."relnamespace"
936
        INNER JOIN "pg_constraint" AS "c"
937
            ON "c"."conrelid" = "tc"."oid"
938
        INNER JOIN "pg_attribute" AS "a"
939 75
            ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
940
        LEFT JOIN "pg_class" AS "ftc"
941 75
            ON "ftc"."oid" = "c"."confrelid"
942 75
        LEFT JOIN "pg_namespace" AS "ftcns"
943 75
            ON "ftcns"."oid" = "ftc"."relnamespace"
944 75
        LEFT JOIN "pg_attribute" "fa"
945
            ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
946
        WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
947 75
        ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
948 75
        SQL;
949
950 75
        /** @psalm-var array<array-key, string> $actionTypes */
951
        $actionTypes = [
952
            'a' => 'NO ACTION',
953
            'r' => 'RESTRICT',
954
            'c' => 'CASCADE',
955
            'n' => 'SET NULL',
956
            'd' => 'SET DEFAULT',
957
        ];
958
959
        $resolvedName = $this->resolveTableName($tableName);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $tableName does not exist. Did you maybe mean $table?
Loading history...
960
961 75
        $constraints = $this->db->createCommand($sql, [
962
            ':schemaName' => $resolvedName->getSchemaName(),
963
            ':tableName' => $resolvedName->getName(),
964
        ])->queryAll();
965
966 75
        /** @var array<array-key, array> $constraints */
967
        $constraints = $this->normalizeRowKeyCase($constraints, true);
968 75
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
969 53
970 53
        $result = [
971 53
            self::PRIMARY_KEY => null,
972 53
            self::FOREIGN_KEYS => [],
973 71
            self::UNIQUES => [],
974 21
            self::CHECKS => [],
975 21
        ];
976
977 21
        /**
978 21
         * @var string $type
979 21
         * @var array $names
980 21
         */
981
        foreach ($constraints as $type => $names) {
982 21
            /**
983 21
             * @psalm-var object|string|null $name
984 21
             * @psalm-var ConstraintArray $constraint
985 21
             */
986
            foreach ($names as $name => $constraint) {
987 21
                switch ($type) {
988 21
                    case 'p':
989 21
                        $result[self::PRIMARY_KEY] = (new Constraint())
990 58
                            ->name($name)
991 56
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
992 56
                        break;
993 56
                    case 'f':
994 56
                        $onDelete = $actionTypes[$constraint[0]['on_delete']] ?? null;
995 15
                        $onUpdate = $actionTypes[$constraint[0]['on_update']] ?? null;
996 15
997 15
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
998 15
                            ->name($name)
999 15
                            ->columnNames(array_values(
1000 15
                                array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
1001
                            ))
1002
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
1003
                            ->foreignTableName($constraint[0]['foreign_table_name'])
1004
                            ->foreignColumnNames(array_values(
1005 75
                                array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
1006 75
                            ))
1007
                            ->onDelete($onDelete)
1008
                            ->onUpdate($onUpdate);
1009 75
                        break;
1010
                    case 'u':
1011
                        $result[self::UNIQUES][] = (new Constraint())
1012
                            ->name($name)
1013
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1014
                        break;
1015
                    case 'c':
1016
                        $result[self::CHECKS][] = (new CheckConstraint())
1017
                            ->name($name)
1018
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
1019 117
                            ->expression($constraint[0]['check_expr']);
1020
                        break;
1021 117
                }
1022
            }
1023
        }
1024
1025
        foreach ($result as $type => $data) {
1026
            $this->setTableMetadata($tableName, $type, $data);
1027
        }
1028
1029
        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...
1030
    }
1031
1032
    /**
1033
     * Creates a column schema for the database.
1034
     *
1035
     * This method may be overridden by child classes to create a DBMS-specific column schema.
1036 4
     *
1037
     * @return ColumnSchema column schema instance.
1038 4
     */
1039
    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...
1040
    {
1041
        return new ColumnSchema();
1042
    }
1043
1044
    /**
1045
     * Create a column schema builder instance giving the type and value precision.
1046
     *
1047
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
1048 202
     *
1049
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
1050 202
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
1051
     *
1052
     * @return ColumnSchemaBuilder column schema builder instance
1053
     *
1054
     * @psalm-param int|string|string[]|null $length
1055
     */
1056
    public function createColumnSchemaBuilder(string $type, int|string|array|null $length = null): ColumnSchemaBuilder
1057
    {
1058
        return new ColumnSchemaBuilder($type, $length);
1059
    }
1060 202
1061
    /**
1062 202
     * Returns the cache key for the specified table name.
1063
     *
1064
     * @param string $name the table name.
1065
     *
1066
     * @return array the cache key.
1067
     */
1068
    protected function getCacheKey(string $name): array
1069
    {
1070
        return array_merge([self::class], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
1071
    }
1072
1073 176
    /**
1074
     * Returns the cache tag name.
1075 176
     *
1076 89
     * This allows {@see refresh()} to invalidate all cached table schemas.
1077
     *
1078
     * @return string the cache tag name.
1079 117
     */
1080
    protected function getCacheTag(): string
1081
    {
1082
        return md5(serialize(array_merge([self::class], $this->db->getCacheKey())));
1083
    }
1084
1085
    /**
1086
     * Changes row's array key case to lower.
1087
     *
1088
     * @param array $row row's array or an array of row's arrays.
1089
     * @param bool $multiple whether multiple rows or a single row passed.
1090
     *
1091
     * @return array normalized row or rows.
1092
     */
1093
    protected function normalizeRowKeyCase(array $row, bool $multiple): array
1094
    {
1095
        if ($multiple) {
1096
            return array_map(static fn (array $row) => array_change_key_case($row, CASE_LOWER), $row);
1097
        }
1098
1099
        return array_change_key_case($row, CASE_LOWER);
1100
    }
1101
}
1102