Passed
Pull Request — master (#70)
by Wilmer
27:59 queued 13:00
created

Schema::loadTableConstraints()   B

Complexity

Conditions 8
Paths 14

Size

Total Lines 102
Code Lines 83

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 47
CRAP Score 8

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 83
c 1
b 0
f 0
nc 14
nop 2
dl 0
loc 102
ccs 47
cts 47
cp 1
crap 8
rs 7.1264

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

267
        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...
268
            $this->findConstraints($table);
269
            return $table;
270
        }
271 28
272
        return null;
273 28
    }
274
275
    /**
276
     * Loads a primary key for the given table.
277
     *
278
     * @param string $tableName table name.
279
     *
280
     * @throws Exception|InvalidConfigException
281
     *
282
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
283
     */
284
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
285
    {
286
        $tablePrimaryKey = $this->loadTableConstraints($tableName, 'primaryKey');
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

286
        /** @scrutinizer ignore-call */ 
287
        $tablePrimaryKey = $this->loadTableConstraints($tableName, 'primaryKey');

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...
287
288
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
289
    }
290
291
    /**
292 28
     * Loads all foreign keys for the given table.
293
     *
294 28
     * @param string $tableName table name.
295 28
     *
296 28
     * @throws Exception|InvalidConfigException
297 28
     *
298
     * @return array|ForeignKeyConstraint[] foreign keys for the given table.
299 28
     */
300 28
    protected function loadTableForeignKeys(string $tableName): array
301 28
    {
302
        $tableForeignKeys = $this->loadTableConstraints($tableName, 'foreignKeys');
303 28
304 25
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
305 25
    }
306 25
307 25
    /**
308 25
     * Loads all indexes for the given table.
309
     *
310 25
     * @param string $tableName table name.
311
     *
312
     * @throws Exception|InvalidConfigException|Throwable
313 28
     *
314
     * @return IndexConstraint[] indexes for the given table.
315
     */
316
    protected function loadTableIndexes(string $tableName): array
317
    {
318
        $sql = <<<'SQL'
319
SELECT
320
    "ic"."relname" AS "name",
321
    "ia"."attname" AS "column_name",
322
    "i"."indisunique" AS "index_is_unique",
323
    "i"."indisprimary" AS "index_is_primary"
324
FROM "pg_class" AS "tc"
325 13
INNER JOIN "pg_namespace" AS "tcns"
326
    ON "tcns"."oid" = "tc"."relnamespace"
327 13
INNER JOIN "pg_index" AS "i"
328
    ON "i"."indrelid" = "tc"."oid"
329
INNER JOIN "pg_class" AS "ic"
330
    ON "ic"."oid" = "i"."indexrelid"
331
INNER JOIN "pg_attribute" AS "ia"
332
    ON "ia"."attrelid" = "i"."indrelid" AND "ia"."attnum" = ANY ("i"."indkey")
333
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
334
ORDER BY "ia"."attnum" ASC
335
SQL;
336
337
        $resolvedName = $this->resolveTableName($tableName);
338
339 13
        $indexes = $this->getDb()->createCommand($sql, [
340
            ':schemaName' => $resolvedName->getSchemaName(),
341 13
            ':tableName' => $resolvedName->getName(),
342
        ])->queryAll();
343
344
        /** @var array<array-key, array<array-key, mixed>> @indexes */
345
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
346
        $indexes = ArrayHelper::index($indexes, null, 'name');
347
        $result = [];
348
349
        /**
350
         * @var object|string|null $name
351
         * @var array<
352
         *   array-key,
353 12
         *   array{
354
         *     name: string,
355 12
         *     column_name: string,
356
         *     index_is_unique: bool,
357
         *     index_is_primary: bool
358
         *   }
359
         * > $index
360
         */
361
        foreach ($indexes as $name => $index) {
362
            $ic = (new IndexConstraint())
363 69
                ->name($name)
364
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
365 69
                ->primary($index[0]['index_is_primary'])
366
                ->unique($index[0]['index_is_unique']);
367
368
            $result[] = $ic;
369
        }
370
371
        return $result;
372
    }
373
374 97
    /**
375
     * Loads all unique constraints for the given table.
376 97
     *
377
     * @param string $tableName table name.
378 97
     *
379
     * @throws Exception|InvalidConfigException
380
     *
381
     * @return array|Constraint[] unique constraints for the given table.
382 97
     */
383 97
    protected function loadTableUniques(string $tableName): array
384
    {
385
        $tableUniques = $this->loadTableConstraints($tableName, 'uniques');
386 97
387 97
        return is_array($tableUniques) ? $tableUniques : [];
388 97
    }
389
390
    /**
391
     * Loads all check constraints for the given table.
392
     *
393
     * @param string $tableName table name.
394
     *
395
     * @throws Exception|InvalidConfigException
396
     *
397
     * @return array|CheckConstraint[] check constraints for the given table.
398
     */
399
    protected function loadTableChecks(string $tableName): array
400
    {
401
        $tableChecks = $this->loadTableConstraints($tableName, 'checks');
402
403
        return is_array($tableChecks) ? $tableChecks : [];
404
    }
405
406
    /**
407
     * Loads all default value constraints for the given table.
408
     *
409
     * @param string $tableName table name.
410
     *
411
     * @throws NotSupportedException
412
     *
413
     * @return DefaultValueConstraint[] default value constraints for the given table.
414 91
     */
415
    protected function loadTableDefaultValues(string $tableName): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName 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

415
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ string $tableName): array

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...
416 91
    {
417 91
        throw new NotSupportedException('PostgreSQL does not support default value constraints.');
418
    }
419
420
    /**
421
     * Creates a query builder for the PostgreSQL database.
422
     *
423
     * @return QueryBuilder query builder instance
424
     */
425 91
    public function createQueryBuilder(): QueryBuilder
426
    {
427
        return new QueryBuilder($this->getDb());
428
    }
429
430
    /**
431
     * Resolves the table name and schema name (if any).
432
     *
433
     * @param TableSchema $table the table metadata object.
434
     * @param string $name the table name
435
     */
436
    protected function resolveTableNames(TableSchema $table, string $name): void
437
    {
438
        $parts = explode('.', str_replace('"', '', $name));
439
440
        if (isset($parts[1])) {
441
            $table->schemaName($parts[0]);
442
            $table->name($parts[1]);
443 91
        } else {
444 91
            $table->schemaName($this->defaultSchema);
445
            $table->name($parts[0]);
446
        }
447
448
        if ($table->getSchemaName() !== $this->defaultSchema) {
449 91
            $name = (string) $table->getSchemaName() . '.' . (string) $table->getName();
450
        } else {
451 91
            $name = $table->getName();
452 8
        }
453
454
        $table->fullName($name);
455
    }
456 8
457
    protected function findViewNames(string $schema = ''): array
458
    {
459 8
        if ($schema === '') {
460
            $schema = $this->defaultSchema;
461
        }
462 8
463
        $sql = <<<'SQL'
464 8
SELECT c.relname AS table_name
465 8
FROM pg_class c
466 8
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
467
WHERE ns.nspname = :schemaName AND (c.relkind = 'v' OR c.relkind = 'm')
468
ORDER BY c.relname
469
SQL;
470
471 8
        return $this->getDb()->createCommand($sql, [':schemaName' => $schema])->queryColumn();
472
    }
473
474 91
    /**
475 8
     * Collects the foreign key column details for the given table.
476
     *
477 91
     * @param TableSchema $table the table metadata
478
     *
479
     * @throws Exception|InvalidConfigException|Throwable
480
     */
481
    protected function findConstraints(TableSchema $table): void
482
    {
483
        $tableName = $table->getName();
484
        $tableSchema = $table->getSchemaName();
485
486
        if ($tableName !== null) {
487
            $tableName = $this->quoteValue($tableName);
488
        }
489
490
        if ($tableSchema !== null) {
491
            $tableSchema = $this->quoteValue($tableSchema);
0 ignored issues
show
Unused Code introduced by
The assignment to $tableSchema is dead and can be removed.
Loading history...
492
        }
493
494
        /**
495
         * We need to extract the constraints de hard way since:
496
         * {@see http://www.postgresql.org/message-id/[email protected]}
497
         */
498
499
        $sql = <<<SQL
500
select
501
    ct.conname as constraint_name,
502
    a.attname as column_name,
503
    fc.relname as foreign_table_name,
504
    fns.nspname as foreign_table_schema,
505
    fa.attname as foreign_column_name
506
from
507
    (SELECT ct.conname, ct.conrelid, ct.confrelid, ct.conkey, ct.contype, ct.confkey, generate_subscripts(ct.conkey, 1) AS s
508
       FROM pg_constraint ct
509
    ) AS ct
510
    inner join pg_class c on c.oid=ct.conrelid
511
    inner join pg_namespace ns on c.relnamespace=ns.oid
512
    inner join pg_attribute a on a.attrelid=ct.conrelid and a.attnum = ct.conkey[ct.s]
513
    left join pg_class fc on fc.oid=ct.confrelid
514
    left join pg_namespace fns on fc.relnamespace=fns.oid
515
    left join pg_attribute fa on fa.attrelid=ct.confrelid and fa.attnum = ct.confkey[ct.s]
516
where
517
    ct.contype='f'
518
    and c.relname={$tableName}
519
    and ns.nspname={$tableSchema}
520
order by
521
    fns.nspname, fc.relname, a.attnum
522
SQL;
523
524
        /**
525
         * @var array{
526
         *   array{
527
         *     tableName: string,
528
         *     columns: array
529
         *   }
530
         * } $constraints
531
         */
532
        $constraints = [];
533
        $slavePdo = $this->getDb()->getSlavePdo();
534
535
        /**
536
         * @var array{
537
         *   constraint_name: string,
538
         *   column_name: string,
539
         *   foreign_table_name: string,
540
         *   foreign_table_schema: string,
541
         *   foreign_column_name: string,
542
         * } $constraint
543
         */
544
        foreach ($this->getDb()->createCommand($sql)->queryAll() as $constraint) {
545
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
546
                $constraint = array_change_key_case($constraint, CASE_LOWER);
547
            }
548
549
            if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
550
                $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
551
            } else {
552
                $foreignTable = $constraint['foreign_table_name'];
553
            }
554
555
            $name = $constraint['constraint_name'];
556
557
            if (!isset($constraints[$name])) {
558
                $constraints[$name] = [
559
                    'tableName' => $foreignTable,
560
                    'columns' => [],
561
                ];
562
            }
563
564
            $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name'];
565 97
        }
566
567 97
        /**
568 97
         * @var int|string $foreingKeyName.
569
         * @var array{tableName: string, columns: array} $constraint
570 97
         */
571
        foreach ($constraints as $foreingKeyName => $constraint) {
572 97
            $table->foreignKey(
573
                (string) $foreingKeyName,
574
                array_merge([$constraint['tableName']], $constraint['columns'])
575
            );
576
        }
577 97
    }
578
579
    /**
580
     * Gets information about given table unique indexes.
581
     *
582
     * @param TableSchema $table the table metadata.
583
     *
584
     * @throws Exception|InvalidConfigException|Throwable
585
     *
586
     * @return array with index and column names.
587
     */
588 97
    protected function getUniqueIndexInformation(TableSchema $table): array
589
    {
590
        $sql = <<<'SQL'
591
SELECT
592
    i.relname as indexname,
593
    pg_get_indexdef(idx.indexrelid, k + 1, TRUE) AS columnname
594
FROM (
595
  SELECT *, generate_subscripts(indkey, 1) AS k
596
  FROM pg_index
597
) idx
598
INNER JOIN pg_class i ON i.oid = idx.indexrelid
599
INNER JOIN pg_class c ON c.oid = idx.indrelid
600
INNER JOIN pg_namespace ns ON c.relnamespace = ns.oid
601
WHERE idx.indisprimary = FALSE AND idx.indisunique = TRUE
602
AND c.relname = :tableName AND ns.nspname = :schemaName
603
ORDER BY i.relname, k
604
SQL;
605
606
        return $this->getDb()->createCommand($sql, [
607
            ':schemaName' => $table->getSchemaName(),
608
            ':tableName' => $table->getName(),
609
        ])->queryAll();
610
    }
611
612
    /**
613
     * Returns all unique indexes for the given table.
614
     *
615
     * Each array element is of the following structure:
616
     *
617
     * ```php
618
     * [
619
     *     'IndexName1' => ['col1' [, ...]],
620
     *     'IndexName2' => ['col2' [, ...]],
621
     * ]
622
     * ```
623
     *
624
     * @param TableSchema $table the table metadata
625
     *
626
     * @throws Exception|InvalidConfigException|Throwable
627
     *
628
     * @return array all unique indexes for the given table.
629
     */
630
    public function findUniqueIndexes(TableSchema $table): array
631
    {
632
        $uniqueIndexes = [];
633 97
        $slavePdo = $this->getDb()->getSlavePdo();
634 97
635
        /** @var array{indexname: string, columnname: string} $row */
636
        foreach ($this->getUniqueIndexInformation($table) as $row) {
637
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
638
                $row = array_change_key_case($row, CASE_LOWER);
639 97
            }
640
641 97
            $column = $row['columnname'];
642 16
643
            if (!empty($column) && $column[0] === '"') {
644
                /**
645 91
                 * postgres will quote names that are not lowercase-only.
646 91
                 *
647
                 * {@see https://github.com/yiisoft/yii2/issues/10613}
648
                 */
649
                $column = substr($column, 1, -1);
650 91
            }
651 91
652
            $uniqueIndexes[$row['indexname']][] = $column;
653 91
        }
654 62
655
        return $uniqueIndexes;
656 62
    }
657 62
658
    /**
659
     * Collects the metadata of table columns.
660 62
     *
661 88
     * @param TableSchema $table the table metadata.
662
     *
663 54
     * @throws Exception|InvalidConfigException|JsonException|Throwable
664 29
     *
665 29
     * @return bool whether the table exists in the database.
666 29
     */
667 54
    protected function findColumns(TableSchema $table): bool
668
    {
669
        $tableName = $table->getName();
670 27
        $schemaName = $table->getSchemaName();
671 54
        $orIdentity = '';
672 51
673 31
        if ($tableName !== null) {
674
            $tableName = $this->getDb()->quoteValue($tableName);
675 31
        }
676 27
677 31
        if ($schemaName !== null) {
678 28
            $schemaName = $this->getDb()->quoteValue($schemaName);
679 30
        }
680 30
681 5
        if (version_compare($this->getDb()->getServerVersion(), '12.0', '>=')) {
682
            $orIdentity = 'OR a.attidentity != \'\'';
683 30
        }
684
685
        $sql = <<<SQL
686
SELECT
687
    d.nspname AS table_schema,
688
    c.relname AS table_name,
689
    a.attname AS column_name,
690
    COALESCE(td.typname, tb.typname, t.typname) AS data_type,
691 91
    COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
692
    a.attlen AS character_maximum_length,
693
    pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
694
    a.atttypmod AS modifier,
695
    a.attnotnull = false AS is_nullable,
696
    CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
697
    coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) {$orIdentity} AS is_autoinc,
698
    pg_get_serial_sequence(quote_ident(d.nspname) || '.' || quote_ident(c.relname), a.attname) AS sequence_name,
699
    CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
700
        THEN array_to_string((SELECT array_agg(enumlabel) FROM pg_enum WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid))::varchar[], ',')
701 91
        ELSE NULL
702
    END AS enum_values,
703 91
    CASE atttypid
704 91
         WHEN 21 /*int2*/ THEN 16
705 91
         WHEN 23 /*int4*/ THEN 32
706 91
         WHEN 20 /*int8*/ THEN 64
707 91
         WHEN 1700 /*numeric*/ THEN
708 91
              CASE WHEN atttypmod = -1
709 91
               THEN null
710 91
               ELSE ((atttypmod - 4) >> 16) & 65535
711 91
               END
712 91
         WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/
713 91
         WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/
714 91
         ELSE null
715 91
      END   AS numeric_precision,
716 91
      CASE
717 91
        WHEN atttypid IN (21, 23, 20) THEN 0
718
        WHEN atttypid IN (1700) THEN
719
        CASE
720
            WHEN atttypmod = -1 THEN null
721
            ELSE (atttypmod - 4) & 65535
722
        END
723
           ELSE null
724 91
      END AS numeric_scale,
725
    CAST(
726 91
             information_schema._pg_char_max_length(information_schema._pg_truetypid(a, t), information_schema._pg_truetypmod(a, t))
727 91
             AS numeric
728
    ) AS size,
729 59
    a.attnum = any (ct.conkey) as is_pkey,
730
    COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
731
FROM
732 59
    pg_class c
733
    LEFT JOIN pg_attribute a ON a.attrelid = c.oid
734 89
    LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
735
    LEFT JOIN pg_type t ON a.atttypid = t.oid
736
    LEFT JOIN pg_type tb ON (a.attndims > 0 OR t.typcategory='A') AND t.typelem > 0 AND t.typelem = tb.oid OR t.typbasetype > 0 AND t.typbasetype = tb.oid
737
    LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
738 91
    LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
739 91
    LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
740
WHERE
741
    a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped
742
    AND c.relname = {$tableName}
743
    AND d.nspname = {$schemaName}
744 91
ORDER BY
745
    a.attnum;
746 91
SQL;
747
748
        /** @var array columns */
749
        $columns = $this->getDb()->createCommand($sql)->queryAll();
750
        $slavePdo = $this->getDb()->getSlavePdo();
751
752
        if (empty($columns)) {
753
            return false;
754
        }
755
756
        /** @var array<array-key, mixed> $column */
757
        foreach ($columns as $column) {
758
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
759
                $column = array_change_key_case($column, CASE_LOWER);
760
            }
761
762
            /** @psalm-var ColumnArray $column */
763
            $loadColumnSchema = $this->loadColumnSchema($column);
764
            $table->columns($loadColumnSchema->getName(), $loadColumnSchema);
765
766
            /** @var mixed $defaultValue */
767
            $defaultValue = $loadColumnSchema->getDefaultValue();
768
769
            if ($loadColumnSchema->isPrimaryKey()) {
770
                $table->primaryKey($loadColumnSchema->getName());
771
772
                if ($table->getSequenceName() === null) {
773
                    $table->sequenceName($loadColumnSchema->getSequenceName());
774
                }
775
776
                $loadColumnSchema->defaultValue(null);
777
            } elseif ($defaultValue) {
778
                if (
779
                    is_string($defaultValue) &&
780
                    in_array($loadColumnSchema->getType(), [self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME], true) &&
781
                    in_array(
782
                        strtoupper($defaultValue),
783
                        ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'],
784
                        true
785
                    )
786
                ) {
787
                    $loadColumnSchema->defaultValue(new Expression($defaultValue));
788
                } elseif ($loadColumnSchema->getType() === 'boolean') {
789
                    $loadColumnSchema->defaultValue(($defaultValue  === 'true'));
790
                } elseif (is_string($defaultValue) && preg_match("/^B'(.*?)'::/", $defaultValue, $matches)) {
791
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
792
                } elseif (is_string($defaultValue) && preg_match("/^'(\d+)'::\"bit\"$/", $defaultValue, $matches)) {
793
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
794 61
                } elseif (is_string($defaultValue) && preg_match("/^'(.*?)'::/", $defaultValue, $matches)) {
795
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[1]));
796 61
                } elseif (
797
                    is_string($defaultValue) &&
798
                    preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $defaultValue, $matches)
799
                ) {
800
                    if ($matches[2] === 'NULL') {
801
                        $loadColumnSchema->defaultValue(null);
802
                    } else {
803
                        $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[2]));
804
                    }
805
                } else {
806
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($defaultValue));
807
                }
808
            }
809
        }
810
811
        return true;
812
    }
813
814
    /**
815
     * Loads the column information into a {@see ColumnSchema} object.
816
     *
817
     * @param array{
818
     *   table_schema: string,
819
     *   table_name: string,
820
     *   column_name: string,
821
     *   data_type: string,
822
     *   type_type: string|null,
823
     *   character_maximum_length: int,
824 61
     *   column_comment: string|null,
825
     *   modifier: int,
826
     *   is_nullable: bool,
827
     *   column_default: mixed,
828
     *   is_autoinc: bool,
829
     *   sequence_name: string|null,
830
     *   enum_values: array<array-key, float|int|string>|string|null,
831
     *   numeric_precision: int|null,
832 61
     *   numeric_scale: int|null,
833 61
     *   size: string|null,
834 61
     *   is_pkey: bool|null,
835 61
     *   dimension: int
836 61
     * } $info column information.
837 61
     *
838 61
     * @return ColumnSchema the column schema object.
839
     */
840 61
    protected function loadColumnSchema(array $info): ColumnSchema
841
    {
842
        $column = $this->createColumnSchema();
843
        $column->allowNull($info['is_nullable']);
844
        $column->autoIncrement($info['is_autoinc']);
845
        $column->comment($info['column_comment']);
846 61
        $column->dbType($info['data_type']);
847 61
        $column->defaultValue($info['column_default']);
848
        $column->enumValues(($info['enum_values'] !== null)
849 61
            ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) : null);
850 46
        $column->unsigned(false); // has no meaning in PG
851 46
        $column->primaryKey((bool) $info['is_pkey']);
852 46
        $column->name($info['column_name']);
853
        $column->precision($info['numeric_precision']);
854 46
        $column->scale($info['numeric_scale']);
855 46
        $column->size($info['size'] === null ? null : (int) $info['size']);
856 59
        $column->dimension($info['dimension']);
857 13
858 13
        /**
859 13
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
860 13
         * default value.
861
         *
862 13
         * @var mixed $defaultValue
863 13
         */
864 13
        $defaultValue = $column->getDefaultValue();
865 13
        $sequenceName = $info['sequence_name'] ?? null;
866
867 13
        if (
868 13
            isset($defaultValue) &&
869
            is_string($defaultValue) &&
870 13
            preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1
871 13
        ) {
872 47
            $column->sequenceName(preg_replace(
873 46
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
874 46
                '',
875 46
                $defaultValue
876
            ));
877 46
        } elseif ($sequenceName !== null) {
878 46
            $column->sequenceName($this->resolveTableName($sequenceName)->getFullName());
879 10
        }
880 10
881 10
        if (isset($this->typeMap[$column->getDbType()])) {
882 10
            $column->type($this->typeMap[$column->getDbType()]);
883 10
        } else {
884
            $column->type(self::TYPE_STRING);
885 10
        }
886 10
887
        $column->phpType($this->getColumnPhpType($column));
888
889
        return $column;
890
    }
891 61
892 61
    /**
893
     * Executes the INSERT command, returning primary key values.
894
     *
895 61
     * @param string $table the table that new rows will be inserted into.
896
     * @param array $columns the column data (name => value) to be inserted into the table.
897
     *
898
     * @throws Exception|InvalidConfigException|Throwable
899
     *
900
     * @return array|false primary key values or false if the command fails.
901
     */
902
    public function insert(string $table, array $columns)
903
    {
904
        $params = [];
905 91
        $returnColumns = [];
906
        $sql = $this->getDb()->getQueryBuilder()->insert($table, $columns, $params);
907 91
        $tableSchema = $this->getTableSchema($table);
908
909
        if ($tableSchema !== null) {
910
            $returnColumns = $tableSchema->getPrimaryKey();
911
        }
912
913
        if (!empty($returnColumns)) {
914
            $returning = [];
915
            /** @var string $name */
916
            foreach ($returnColumns as $name) {
917
                $returning[] = $this->quoteColumnName($name);
918
            }
919
            $sql .= ' RETURNING ' . implode(', ', $returning);
920 4
        }
921
922 4
        $command = $this->getDb()->createCommand($sql, $params);
923
        $command->prepare(false);
924
        $result = $command->queryOne();
925
926
        $pdoStatement = $command->getPdoStatement();
927
928
        return $pdoStatement !== null && !$pdoStatement->rowCount() ? false : $result;
929
    }
930
931
    /**
932
     * Loads multiple types of constraints and returns the specified ones.
933
     *
934
     * @param string $tableName table name.
935
     * @param string $returnType return type:
936
     * - primaryKey
937
     * - foreignKeys
938
     * - uniques
939
     * - checks
940
     *
941
     * @throws Exception|InvalidConfigException|Throwable
942
     *
943
     * @return (CheckConstraint|Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
944
     *
945
     * @psalm-return Constraint|list<CheckConstraint|Constraint|ForeignKeyConstraint>|null
946
     */
947
    private function loadTableConstraints(string $tableName, string $returnType)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $returnType seems to be never defined.
Loading history...
948
    {
949
        /** @var string $sql */
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sql seems to be never defined.
Loading history...
950
        $sql = <<<'SQL'
951
SELECT
952
    "c"."conname" AS "name",
953
    "a"."attname" AS "column_name",
954
    "c"."contype" AS "type",
955
    "ftcns"."nspname" AS "foreign_table_schema",
956
    "ftc"."relname" AS "foreign_table_name",
957
    "fa"."attname" AS "foreign_column_name",
958
    "c"."confupdtype" AS "on_update",
959
    "c"."confdeltype" AS "on_delete",
960
    pg_get_constraintdef("c"."oid") AS "check_expr"
961
FROM "pg_class" AS "tc"
962
INNER JOIN "pg_namespace" AS "tcns"
963
    ON "tcns"."oid" = "tc"."relnamespace"
964
INNER JOIN "pg_constraint" AS "c"
965
    ON "c"."conrelid" = "tc"."oid"
966
INNER JOIN "pg_attribute" AS "a"
967
    ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
968
LEFT JOIN "pg_class" AS "ftc"
969
    ON "ftc"."oid" = "c"."confrelid"
970
LEFT JOIN "pg_namespace" AS "ftcns"
971
    ON "ftcns"."oid" = "ftc"."relnamespace"
972
LEFT JOIN "pg_attribute" "fa"
973
    ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
974
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
975
ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
976
SQL;
977
978
        /** @var array<array-key, string> $actionTypes */
979
        $actionTypes = [
980
            'a' => 'NO ACTION',
981
            'r' => 'RESTRICT',
982
            'c' => 'CASCADE',
983
            'n' => 'SET NULL',
984
            'd' => 'SET DEFAULT',
985
        ];
986
987
        $resolvedName = $this->resolveTableName($tableName);
0 ignored issues
show
Bug introduced by
It seems like $tableName can also be of type null; however, parameter $name of Yiisoft\Db\Pgsql\Schema::resolveTableName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

987
        $resolvedName = $this->resolveTableName(/** @scrutinizer ignore-type */ $tableName);
Loading history...
988
989
        $constraints = $this->getDb()->createCommand($sql, [
990
            ':schemaName' => $resolvedName->getSchemaName(),
991
            ':tableName' => $resolvedName->getName(),
992
        ])->queryAll();
993
994
        /** @var array<array-key, array> $constraints */
995
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
996
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
997
998
        $result = [
999
            'primaryKey' => null,
1000
            'foreignKeys' => [],
1001
            'uniques' => [],
1002
            'checks' => [],
1003
        ];
1004
1005
        /**
1006
         * @var string $type
1007
         * @var array $names
1008
         */
1009
        foreach ($constraints as $type => $names) {
1010
            /**
1011
             * @psalm-var object|string|null $name
1012
             * @psalm-var ConstraintArray $constraint
1013
             */
1014
            foreach ($names as $name => $constraint) {
1015
                switch ($type) {
1016
                    case 'p':
1017
                        $ct = (new Constraint())
1018
                            ->name($name)
1019
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1020
1021
                        $result['primaryKey'] = $ct;
1022
                        break;
1023
                    case 'f':
1024
                        $onDelete = $actionTypes[$constraint[0]['on_delete']] ?? null;
1025
                        $onUpdate = $actionTypes[$constraint[0]['on_update']] ?? null;
1026
1027
                        $fk = (new ForeignKeyConstraint())
1028
                            ->name($name)
1029
                            ->columnNames(array_values(
1030
                                array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
1031
                            ))
1032
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
1033
                            ->foreignTableName($constraint[0]['foreign_table_name'])
1034
                            ->foreignColumnNames(array_values(
1035
                                array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
1036
                            ))
1037
                            ->onDelete($onDelete)
1038
                            ->onUpdate($onUpdate);
1039
1040
                        $result['foreignKeys'][] = $fk;
1041
                        break;
1042
                    case 'u':
1043
                        $ct = (new Constraint())
1044
                            ->name($name)
1045
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1046
1047
                        $result['uniques'][] = $ct;
1048
                        break;
1049
                    case 'c':
1050
                        $ck = (new CheckConstraint())
1051
                            ->name($name)
1052
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
1053
                            ->expression($constraint[0]['check_expr']);
1054
1055
                        $result['checks'][] = $ck;
1056
                        break;
1057
                }
1058
            }
1059
        }
1060
1061
        foreach ($result as $type => $data) {
1062
            $this->setTableMetadata($tableName, $type, $data);
0 ignored issues
show
Bug introduced by
It seems like $tableName can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::setTableMetadata() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1062
            $this->setTableMetadata(/** @scrutinizer ignore-type */ $tableName, $type, $data);
Loading history...
1063
        }
1064
1065
        return $result[$returnType];
1066
    }
1067
1068
    /**
1069
     * Creates a column schema for the database.
1070
     *
1071
     * This method may be overridden by child classes to create a DBMS-specific column schema.
1072
     *
1073
     * @return ColumnSchema column schema instance.
1074
     */
1075
    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...
1076
    {
1077
        return new ColumnSchema();
1078
    }
1079
1080
    /**
1081
     * Create a column schema builder instance giving the type and value precision.
1082
     *
1083
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
1084
     *
1085
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
1086
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
1087
     *
1088
     * @return ColumnSchemaBuilder column schema builder instance
1089
     */
1090
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
1091
    {
1092
        return new ColumnSchemaBuilder($type, $length);
1093
    }
1094
}
1095