Passed
Push — master ( 03d3d0...cea9c0 )
by Alexander
07:36 queued 05:58
created

Schema::findConstraints()   C

Complexity

Conditions 10
Paths 56

Size

Total Lines 570
Code Lines 527

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 187
CRAP Score 10.0047

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 527
c 1
b 0
f 0
nc 56
nop 1
dl 0
loc 570
ccs 187
cts 194
cp 0.9639
crap 10.0047
rs 6.1333

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Pgsql;
6
7
use JsonException;
8
use 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
 * @psalm-type FindConstraintArray = array{
79
 *   constraint_name: string,
80
 *   column_name: string,
81
 *   foreign_table_name: string,
82
 *   foreign_table_schema: string,
83
 *   foreign_column_name: string,
84
 * }
85
 */
86
final class Schema extends AbstractSchema implements ConstraintFinderInterface
87
{
88
    use ConstraintFinderTrait;
89
    use ViewFinderTrait;
90
91
    public const TYPE_JSONB = 'jsonb';
92
93
    /**
94
     * @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...
95
     *
96
     * {@see http://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE}
97
     */
98
    private array $typeMap = [
0 ignored issues
show
introduced by
The private property $typeMap is not used, and could be removed.
Loading history...
99
        'bit' => self::TYPE_INTEGER,
100
        'bit varying' => self::TYPE_INTEGER,
101
        'varbit' => self::TYPE_INTEGER,
102
        'bool' => self::TYPE_BOOLEAN,
103
        'boolean' => self::TYPE_BOOLEAN,
104
        'box' => self::TYPE_STRING,
105
        'circle' => self::TYPE_STRING,
106
        'point' => self::TYPE_STRING,
107
        'line' => self::TYPE_STRING,
108
        'lseg' => self::TYPE_STRING,
109
        'polygon' => self::TYPE_STRING,
110
        'path' => self::TYPE_STRING,
111
        'character' => self::TYPE_CHAR,
112
        'char' => self::TYPE_CHAR,
113
        'bpchar' => self::TYPE_CHAR,
114
        'character varying' => self::TYPE_STRING,
115
        'varchar' => self::TYPE_STRING,
116
        'text' => self::TYPE_TEXT,
117
        'bytea' => self::TYPE_BINARY,
118
        'cidr' => self::TYPE_STRING,
119
        'inet' => self::TYPE_STRING,
120
        'macaddr' => self::TYPE_STRING,
121
        'real' => self::TYPE_FLOAT,
122
        'float4' => self::TYPE_FLOAT,
123
        'double precision' => self::TYPE_DOUBLE,
124
        'float8' => self::TYPE_DOUBLE,
125
        'decimal' => self::TYPE_DECIMAL,
126
        'numeric' => self::TYPE_DECIMAL,
127
        'money' => self::TYPE_MONEY,
128
        'smallint' => self::TYPE_SMALLINT,
129
        'int2' => self::TYPE_SMALLINT,
130
        'int4' => self::TYPE_INTEGER,
131
        'int' => self::TYPE_INTEGER,
132
        'integer' => self::TYPE_INTEGER,
133
        'bigint' => self::TYPE_BIGINT,
134
        'int8' => self::TYPE_BIGINT,
135
        'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal!
136
        'smallserial' => self::TYPE_SMALLINT,
137
        'serial2' => self::TYPE_SMALLINT,
138
        'serial4' => self::TYPE_INTEGER,
139
        'serial' => self::TYPE_INTEGER,
140
        'bigserial' => self::TYPE_BIGINT,
141
        'serial8' => self::TYPE_BIGINT,
142
        'pg_lsn' => self::TYPE_BIGINT,
143
        'date' => self::TYPE_DATE,
144
        'interval' => self::TYPE_STRING,
145
        'time without time zone' => self::TYPE_TIME,
146
        'time' => self::TYPE_TIME,
147
        'time with time zone' => self::TYPE_TIME,
148
        'timetz' => self::TYPE_TIME,
149
        'timestamp without time zone' => self::TYPE_TIMESTAMP,
150
        'timestamp' => self::TYPE_TIMESTAMP,
151
        'timestamp with time zone' => self::TYPE_TIMESTAMP,
152
        'timestamptz' => self::TYPE_TIMESTAMP,
153
        'abstime' => self::TYPE_TIMESTAMP,
154
        'tsquery' => self::TYPE_STRING,
155
        'tsvector' => self::TYPE_STRING,
156
        'txid_snapshot' => self::TYPE_STRING,
157
        'unknown' => self::TYPE_STRING,
158
        'uuid' => self::TYPE_STRING,
159
        'json' => self::TYPE_JSON,
160
        'jsonb' => self::TYPE_JSON,
161
        'xml' => self::TYPE_STRING,
162
    ];
163
164
    /**
165
     * @var string|null the default schema used for the current session.
166
     */
167
    protected ?string $defaultSchema = 'public';
168
169
    /**
170
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
171
     * case starting and ending characters are different.
172
     */
173
    protected $tableQuoteCharacter = '"';
174
175
    /**
176
     * Resolves the table name and schema name (if any).
177
     *
178
     * @param string $name the table name.
179
     *
180
     * @return TableSchema with resolved table, schema, etc. names.
181
     *
182
     * {@see TableSchema}
183
     */
184 71
    protected function resolveTableName(string $name): TableSchema
185
    {
186 71
        $resolvedName = new TableSchema();
187
188 71
        $parts = explode('.', str_replace('"', '', $name));
189
190 71
        if (isset($parts[1])) {
191
            $resolvedName->schemaName($parts[0]);
192
            $resolvedName->name($parts[1]);
193
        } else {
194 71
            $resolvedName->schemaName($this->defaultSchema);
195 71
            $resolvedName->name($name);
196
        }
197
198 71
        $resolvedName->fullName(
199
            (
200 71
                $resolvedName->getSchemaName() !== $this->defaultSchema ?
201
                    (string) $resolvedName->getSchemaName() . '.' :
202 71
                    ''
203 71
            ) . (string) $resolvedName->getName()
204
        );
205
206 71
        return $resolvedName;
207
    }
208
209
    /**
210
     * Returns all schema names in the database, including the default one but not system schemas.
211
     *
212
     * This method should be overridden by child classes in order to support this feature because the default
213
     * implementation simply throws an exception.
214
     *
215
     * @throws Exception|InvalidConfigException|Throwable
216
     *
217
     * @return array all schema names in the database, except system schemas.
218
     */
219 2
    protected function findSchemaNames(): array
220
    {
221
        $sql = <<<'SQL'
222 2
SELECT "ns"."nspname"
223
FROM "pg_namespace" AS "ns"
224
WHERE "ns"."nspname" != 'information_schema' AND "ns"."nspname" NOT LIKE 'pg_%'
225
ORDER BY "ns"."nspname" ASC
226
SQL;
227
228 2
        return $this->getDb()->createCommand($sql)->queryColumn();
229
    }
230
231
    /**
232
     * Returns all table names in the database.
233
     *
234
     * This method should be overridden by child classes in order to support this feature because the default
235
     * implementation simply throws an exception.
236
     *
237
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
238
     *
239
     * @throws Exception|InvalidConfigException|Throwable
240
     *
241
     * @return array all table names in the database. The names have NO schema name prefix.
242
     */
243 5
    protected function findTableNames(string $schema = ''): array
244
    {
245 5
        if ($schema === '') {
246 5
            $schema = $this->defaultSchema;
247
        }
248
249
        $sql = <<<'SQL'
250 5
SELECT c.relname AS table_name
251
FROM pg_class c
252
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
253
WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f', 'p')
254
ORDER BY c.relname
255
SQL;
256
257 5
        return $this->getDb()->createCommand($sql, [':schemaName' => $schema])->queryColumn();
258
    }
259
260
    /**
261
     * Loads the metadata for the specified table.
262
     *
263
     * @param string $name table name.
264
     *
265
     * @throws Exception|InvalidConfigException
266
     *
267
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
268
     */
269 101
    protected function loadTableSchema(string $name): ?TableSchema
270
    {
271 101
        $table = new TableSchema();
272
273 101
        $this->resolveTableNames($table, $name);
274
275 101
        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

275
        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...
276 95
            $this->findConstraints($table);
277 95
            return $table;
278
        }
279
280 17
        return null;
281
    }
282
283
    /**
284
     * Loads a primary key for the given table.
285
     *
286
     * @param string $tableName table name.
287
     *
288
     * @throws Exception|InvalidConfigException
289
     *
290
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
291
     */
292 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
293
    {
294 31
        $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

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

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

980
        $resolvedName = $this->resolveTableName(/** @scrutinizer ignore-type */ $tableName);
Loading history...
981
982 61
        $constraints = $this->getDb()->createCommand($sql, [
983 61
            ':schemaName' => $resolvedName->getSchemaName(),
984 61
            ':tableName' => $resolvedName->getName(),
985 61
        ])->queryAll();
986
987
        /** @var array<array-key, array> $constraints */
988 61
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
989 61
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
990
991
        $result = [
992 61
            'primaryKey' => null,
993
            'foreignKeys' => [],
994
            'uniques' => [],
995
            'checks' => [],
996
        ];
997
998
        /**
999
         * @var string $type
1000
         * @var array $names
1001
         */
1002 61
        foreach ($constraints as $type => $names) {
1003
            /**
1004
             * @psalm-var object|string|null $name
1005
             * @psalm-var ConstraintArray $constraint
1006
             */
1007 61
            foreach ($names as $name => $constraint) {
1008
                switch ($type) {
1009 61
                    case 'p':
1010 46
                        $ct = (new Constraint())
1011 46
                            ->name($name)
1012 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1013
1014 46
                        $result['primaryKey'] = $ct;
1015 46
                        break;
1016 59
                    case 'f':
1017 13
                        $onDelete = $actionTypes[$constraint[0]['on_delete']] ?? null;
1018 13
                        $onUpdate = $actionTypes[$constraint[0]['on_update']] ?? null;
1019
1020 13
                        $fk = (new ForeignKeyConstraint())
1021 13
                            ->name($name)
1022 13
                            ->columnNames(array_values(
1023 13
                                array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
1024
                            ))
1025 13
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
1026 13
                            ->foreignTableName($constraint[0]['foreign_table_name'])
1027 13
                            ->foreignColumnNames(array_values(
1028 13
                                array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
1029
                            ))
1030 13
                            ->onDelete($onDelete)
1031 13
                            ->onUpdate($onUpdate);
1032
1033 13
                        $result['foreignKeys'][] = $fk;
1034 13
                        break;
1035 47
                    case 'u':
1036 46
                        $ct = (new Constraint())
1037 46
                            ->name($name)
1038 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1039
1040 46
                        $result['uniques'][] = $ct;
1041 46
                        break;
1042 10
                    case 'c':
1043 10
                        $ck = (new CheckConstraint())
1044 10
                            ->name($name)
1045 10
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
1046 10
                            ->expression($constraint[0]['check_expr']);
1047
1048 10
                        $result['checks'][] = $ck;
1049 10
                        break;
1050
                }
1051
            }
1052
        }
1053
1054 61
        foreach ($result as $type => $data) {
1055 61
            $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

1055
            $this->setTableMetadata(/** @scrutinizer ignore-type */ $tableName, $type, $data);
Loading history...
1056
        }
1057
1058 61
        return $result[$returnType];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $returnType seems to be never defined.
Loading history...
1059
    }
1060
1061
    /**
1062
     * Creates a column schema for the database.
1063
     *
1064
     * This method may be overridden by child classes to create a DBMS-specific column schema.
1065
     *
1066
     * @return ColumnSchema column schema instance.
1067
     */
1068 95
    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...
1069
    {
1070 95
        return new ColumnSchema();
1071
    }
1072
1073
    /**
1074
     * Create a column schema builder instance giving the type and value precision.
1075
     *
1076
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
1077
     *
1078
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
1079
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
1080
     *
1081
     * @return ColumnSchemaBuilder column schema builder instance
1082
     */
1083 4
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
1084
    {
1085 4
        return new ColumnSchemaBuilder($type, $length);
1086
    }
1087
}
1088