Passed
Pull Request — master (#80)
by Wilmer
02:46
created

Schema::findColumns()   F

Complexity

Conditions 24
Paths 192

Size

Total Lines 159
Code Lines 71

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 54
CRAP Score 24.0262

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 24
eloc 71
c 3
b 0
f 0
nc 192
nop 1
dl 0
loc 159
ccs 54
cts 56
cp 0.9643
crap 24.0262
rs 3.4

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 = [
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 76
    protected function resolveTableName(string $name): TableSchema
185
    {
186 76
        $resolvedName = new TableSchema();
187
188 76
        $parts = explode('.', str_replace('"', '', $name));
189
190 76
        if (isset($parts[1])) {
191 5
            $resolvedName->schemaName($parts[0]);
192 5
            $resolvedName->name($parts[1]);
193
        } else {
194 71
            $resolvedName->schemaName($this->defaultSchema);
195 71
            $resolvedName->name($name);
196
        }
197
198 76
        $resolvedName->fullName(
199
            (
200 76
                $resolvedName->getSchemaName() !== $this->defaultSchema ?
201
                    (string) $resolvedName->getSchemaName() . '.' :
202 76
                    ''
203 76
            ) . (string) $resolvedName->getName()
204
        );
205
206 76
        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 2
        $sql = <<<'SQL'
222
            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 5
        $sql = <<<'SQL'
250
            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 103
    protected function loadTableSchema(string $name): ?TableSchema
270
    {
271 103
        $table = new TableSchema();
272
273 103
        $this->resolveTableNames($table, $name);
274
275 103
        if ($this->findColumns($table)) {
276 97
            $this->findConstraints($table);
277 97
            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');
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 28
        $sql = <<<'SQL'
327
            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"."indexrelid"
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 103
    protected function resolveTableNames(TableSchema $table, string $name): void
445
    {
446 103
        $parts = explode('.', str_replace('"', '', $name));
447
448 103
        if (isset($parts[1])) {
449
            $table->schemaName($parts[0]);
450
            $table->name($parts[1]);
451
        } else {
452 103
            $table->schemaName($this->defaultSchema);
453 103
            $table->name($parts[0]);
454
        }
455
456 103
        if ($table->getSchemaName() !== $this->defaultSchema) {
457
            $name = (string) $table->getSchemaName() . '.' . (string) $table->getName();
458
        } else {
459 103
            $name = $table->getName();
460
        }
461
462 103
        $table->fullName($name);
463 103
    }
464
465 1
    protected function findViewNames(string $schema = ''): array
466
    {
467 1
        if ($schema === '') {
468
            $schema = $this->defaultSchema;
469
        }
470
471 1
        $sql = <<<'SQL'
472
            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 97
    protected function findConstraints(TableSchema $table): void
490
    {
491 97
        $tableName = $table->getName();
492 97
        $tableSchema = $table->getSchemaName();
493
494 97
        if ($tableName !== null) {
495 97
            $tableName = $this->quoteValue($tableName);
496
        }
497
498 97
        if ($tableSchema !== null) {
499 97
            $tableSchema = $this->quoteValue($tableSchema);
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 97
        $sql = <<<SQL
508 97
            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,
516
                        generate_subscripts(ct.conkey, 1) AS s
517
                   FROM pg_constraint ct
518
                ) AS ct
519
                inner join pg_class c on c.oid=ct.conrelid
520
                inner join pg_namespace ns on c.relnamespace=ns.oid
521
                inner join pg_attribute a on a.attrelid=ct.conrelid and a.attnum = ct.conkey[ct.s]
522
                left join pg_class fc on fc.oid=ct.confrelid
523
                left join pg_namespace fns on fc.relnamespace=fns.oid
524
                left join pg_attribute fa on fa.attrelid=ct.confrelid and fa.attnum = ct.confkey[ct.s]
525
            WHERE
526
                ct.contype='f'
527 97
                and c.relname={$tableName}
528 97
                and ns.nspname={$tableSchema}
529
            ORDER BY
530
                fns.nspname, fc.relname, a.attnum
531
            SQL;
532
533
        /** @var array{array{tableName: string, columns: array}} $constraints */
534 97
        $constraints = [];
535 97
        $slavePdo = $this->getDb()->getSlavePdo();
536
537
        /** @var FindConstraintArray $constraint */
538 97
        foreach ($this->getDb()->createCommand($sql)->queryAll() as $constraint) {
539 9
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
540
                $constraint = array_change_key_case($constraint, CASE_LOWER);
541
            }
542
543 9
            if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
544
                $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
545
            } else {
546 9
                $foreignTable = $constraint['foreign_table_name'];
547
            }
548
549 9
            $name = $constraint['constraint_name'];
550
551 9
            if (!isset($constraints[$name])) {
552
                $constraints[$name] = [
553 9
                    'tableName' => $foreignTable,
554
                    'columns' => [],
555
                ];
556
            }
557
558 9
            $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name'];
559
        }
560
561
        /**
562
         * @var int|string $foreingKeyName.
563
         * @var array{tableName: string, columns: array} $constraint
564
         */
565 97
        foreach ($constraints as $foreingKeyName => $constraint) {
566 9
            $table->foreignKey(
567 9
                (string) $foreingKeyName,
568 9
                array_merge([$constraint['tableName']], $constraint['columns'])
569
            );
570
        }
571 97
    }
572
573
    /**
574
     * Gets information about given table unique indexes.
575
     *
576
     * @param TableSchema $table the table metadata.
577
     *
578
     * @throws Exception|InvalidConfigException|Throwable
579
     *
580
     * @return array with index and column names.
581
     */
582 1
    protected function getUniqueIndexInformation(TableSchema $table): array
583
    {
584 1
        $sql = <<<'SQL'
585
            SELECT
586
                i.relname as indexname,
587
                pg_get_indexdef(idx.indexrelid, k + 1, TRUE) AS columnname
588
            FROM (
589
              SELECT *, generate_subscripts(indkey, 1) AS k
590
              FROM pg_index
591
            ) idx
592
            INNER JOIN pg_class i ON i.oid = idx.indexrelid
593
            INNER JOIN pg_class c ON c.oid = idx.indrelid
594
            INNER JOIN pg_namespace ns ON c.relnamespace = ns.oid
595
            WHERE idx.indisprimary = FALSE AND idx.indisunique = TRUE
596
            AND c.relname = :tableName AND ns.nspname = :schemaName
597
            ORDER BY i.relname, k
598
            SQL;
599
600 1
        return $this->getDb()->createCommand($sql, [
601 1
            ':schemaName' => $table->getSchemaName(),
602 1
            ':tableName' => $table->getName(),
603 1
        ])->queryAll();
604
    }
605
606
    /**
607
     * Returns all unique indexes for the given table.
608
     *
609
     * Each array element is of the following structure:
610
     *
611
     * ```php
612
     * [
613
     *     'IndexName1' => ['col1' [, ...]],
614
     *     'IndexName2' => ['col2' [, ...]],
615
     * ]
616
     * ```
617
     *
618
     * @param TableSchema $table the table metadata
619
     *
620
     * @throws Exception|InvalidConfigException|Throwable
621
     *
622
     * @return array all unique indexes for the given table.
623
     */
624 1
    public function findUniqueIndexes(TableSchema $table): array
625
    {
626 1
        $uniqueIndexes = [];
627 1
        $slavePdo = $this->getDb()->getSlavePdo();
628
629
        /** @var array{indexname: string, columnname: string} $row */
630 1
        foreach ($this->getUniqueIndexInformation($table) as $row) {
631 1
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
632 1
                $row = array_change_key_case($row, CASE_LOWER);
633
            }
634
635 1
            $column = $row['columnname'];
636
637 1
            if (!empty($column) && $column[0] === '"') {
638
                /**
639
                 * postgres will quote names that are not lowercase-only.
640
                 *
641
                 * {@see https://github.com/yiisoft/yii2/issues/10613}
642
                 */
643 1
                $column = substr($column, 1, -1);
644
            }
645
646 1
            $uniqueIndexes[$row['indexname']][] = $column;
647
        }
648
649 1
        return $uniqueIndexes;
650
    }
651
652
    /**
653
     * Collects the metadata of table columns.
654
     *
655
     * @param TableSchema $table the table metadata.
656
     *
657
     * @throws Exception|InvalidConfigException|JsonException|Throwable
658
     *
659
     * @return bool whether the table exists in the database.
660
     */
661 103
    protected function findColumns(TableSchema $table): bool
662
    {
663 103
        $tableName = $table->getName();
664 103
        $schemaName = $table->getSchemaName();
665 103
        $orIdentity = '';
666
667 103
        if ($tableName !== null) {
668 103
            $tableName = $this->getDb()->quoteValue($tableName);
669
        }
670
671 103
        if ($schemaName !== null) {
672 103
            $schemaName = $this->getDb()->quoteValue($schemaName);
673
        }
674
675 103
        if (version_compare($this->getDb()->getServerVersion(), '12.0', '>=')) {
676 103
            $orIdentity = 'OR a.attidentity != \'\'';
677
        }
678
679 103
        $sql = <<<SQL
680 103
            SELECT
681
                d.nspname AS table_schema,
682
                c.relname AS table_name,
683
                a.attname AS column_name,
684
                COALESCE(td.typname, tb.typname, t.typname) AS data_type,
685
                COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
686
                a.attlen AS character_maximum_length,
687
                pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
688
                a.atttypmod AS modifier,
689
                a.attnotnull = false AS is_nullable,
690
                CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
691 103
                coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) {$orIdentity} AS is_autoinc,
692
                pg_get_serial_sequence(quote_ident(d.nspname) || '.' || quote_ident(c.relname), a.attname)
693
                    AS sequence_name,
694
                CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
695
                    THEN array_to_string(
696
                        (
697
                            SELECT array_agg(enumlabel)
698
                            FROM pg_enum
699
                            WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid)
700
                            )::varchar[],
701
                        ',')
702
                    ELSE NULL
703
                END AS enum_values,
704
                CASE atttypid
705
                     WHEN 21 /*int2*/ THEN 16
706
                     WHEN 23 /*int4*/ THEN 32
707
                     WHEN 20 /*int8*/ THEN 64
708
                     WHEN 1700 /*numeric*/ THEN
709
                          CASE WHEN atttypmod = -1
710
                           THEN null
711
                           ELSE ((atttypmod - 4) >> 16) & 65535
712
                           END
713
                     WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/
714
                     WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/
715
                     ELSE null
716
                  END   AS numeric_precision,
717
                  CASE
718
                    WHEN atttypid IN (21, 23, 20) THEN 0
719
                    WHEN atttypid IN (1700) THEN
720
                    CASE
721
                        WHEN atttypmod = -1 THEN null
722
                        ELSE (atttypmod - 4) & 65535
723
                    END
724
                       ELSE null
725
                  END AS numeric_scale,
726
                CAST(
727
                         information_schema._pg_char_max_length(
728
                             information_schema._pg_truetypid(a, t),
729
                             information_schema._pg_truetypmod(a, t)
730
                             ) AS numeric
731
                ) AS size,
732
                a.attnum = any (ct.conkey) as is_pkey,
733
                COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
734
            FROM
735
                pg_class c
736
                LEFT JOIN pg_attribute a ON a.attrelid = c.oid
737
                LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
738
                LEFT JOIN pg_type t ON a.atttypid = t.oid
739
                LEFT JOIN pg_type tb ON (a.attndims > 0 OR t.typcategory='A') AND t.typelem > 0 AND t.typelem = tb.oid
740
                                            OR t.typbasetype > 0 AND t.typbasetype = tb.oid
741
                LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
742
                LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
743
                LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
744
            WHERE
745
                a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped
746 103
                AND c.relname = {$tableName}
747 103
                AND d.nspname = {$schemaName}
748
            ORDER BY
749
                a.attnum;
750
            SQL;
751
752
        /** @var array columns */
753 103
        $columns = $this->getDb()->createCommand($sql)->queryAll();
754 103
        $slavePdo = $this->getDb()->getSlavePdo();
755
756 103
        if (empty($columns)) {
757 17
            return false;
758
        }
759
760
        /** @var array<array-key, mixed> $column */
761 97
        foreach ($columns as $column) {
762 97
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
763 1
                $column = array_change_key_case($column, CASE_LOWER);
764
            }
765
766
            /** @psalm-var ColumnArray $column */
767 97
            $loadColumnSchema = $this->loadColumnSchema($column);
768 97
            $table->columns($loadColumnSchema->getName(), $loadColumnSchema);
769
770
            /** @var mixed $defaultValue */
771 97
            $defaultValue = $loadColumnSchema->getDefaultValue();
772
773 97
            if ($loadColumnSchema->isPrimaryKey()) {
774 66
                $table->primaryKey($loadColumnSchema->getName());
775
776 66
                if ($table->getSequenceName() === null) {
777 66
                    $table->sequenceName($loadColumnSchema->getSequenceName());
778
                }
779
780 66
                $loadColumnSchema->defaultValue(null);
781 94
            } elseif ($defaultValue) {
782
                if (
783 55
                    is_string($defaultValue) &&
784 55
                    in_array(
785 55
                        $loadColumnSchema->getType(),
786 55
                        [self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME],
787 55
                        true
788
                    ) &&
789 30
                    in_array(
790 30
                        strtoupper($defaultValue),
791 30
                        ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'],
792 55
                        true
793
                    )
794
                ) {
795 28
                    $loadColumnSchema->defaultValue(new Expression($defaultValue));
796 55
                } elseif ($loadColumnSchema->getType() === 'boolean') {
797 52
                    $loadColumnSchema->defaultValue(($defaultValue  === 'true'));
798 32
                } elseif (is_string($defaultValue) && preg_match("/^B'(.*?)'::/", $defaultValue, $matches)) {
799
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
800 32
                } elseif (is_string($defaultValue) && preg_match("/^'(\d+)'::\"bit\"$/", $defaultValue, $matches)) {
801 28
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
802 32
                } elseif (is_string($defaultValue) && preg_match("/^'(.*?)'::/", $defaultValue, $matches)) {
803 29
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[1]));
804
                } elseif (
805 31
                    is_string($defaultValue) &&
806 31
                    preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $defaultValue, $matches)
807
                ) {
808 31
                    if ($matches[2] === 'NULL') {
809 5
                        $loadColumnSchema->defaultValue(null);
810
                    } else {
811 31
                        $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[2]));
812
                    }
813
                } else {
814
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($defaultValue));
815
                }
816
            }
817
        }
818
819 97
        return true;
820
    }
821
822
    /**
823
     * Loads the column information into a {@see ColumnSchema} object.
824
     *
825
     * @param array{
826
     *   table_schema: string,
827
     *   table_name: string,
828
     *   column_name: string,
829
     *   data_type: string,
830
     *   type_type: string|null,
831
     *   character_maximum_length: int,
832
     *   column_comment: string|null,
833
     *   modifier: int,
834
     *   is_nullable: bool,
835
     *   column_default: mixed,
836
     *   is_autoinc: bool,
837
     *   sequence_name: string|null,
838
     *   enum_values: array<array-key, float|int|string>|string|null,
839
     *   numeric_precision: int|null,
840
     *   numeric_scale: int|null,
841
     *   size: string|null,
842
     *   is_pkey: bool|null,
843
     *   dimension: int
844
     * } $info column information.
845
     *
846
     * @return ColumnSchema the column schema object.
847
     */
848 97
    protected function loadColumnSchema(array $info): ColumnSchema
849
    {
850 97
        $column = $this->createColumnSchema();
851 97
        $column->allowNull($info['is_nullable']);
852 97
        $column->autoIncrement($info['is_autoinc']);
853 97
        $column->comment($info['column_comment']);
854 97
        $column->dbType($info['data_type']);
855 97
        $column->defaultValue($info['column_default']);
856 97
        $column->enumValues(($info['enum_values'] !== null)
857 97
            ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) : null);
858 97
        $column->unsigned(false); // has no meaning in PG
859 97
        $column->primaryKey((bool) $info['is_pkey']);
860 97
        $column->name($info['column_name']);
861 97
        $column->precision($info['numeric_precision']);
862 97
        $column->scale($info['numeric_scale']);
863 97
        $column->size($info['size'] === null ? null : (int) $info['size']);
864 97
        $column->dimension($info['dimension']);
865
866
        /**
867
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
868
         * default value.
869
         *
870
         * @var mixed $defaultValue
871
         */
872 97
        $defaultValue = $column->getDefaultValue();
873 97
        $sequenceName = $info['sequence_name'] ?? null;
874
875
        if (
876 97
            isset($defaultValue) &&
877 97
            is_string($defaultValue) &&
878 97
            preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1
879
        ) {
880 60
            $column->sequenceName(preg_replace(
881 60
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
882 60
                '',
883 60
                $defaultValue
884
            ));
885 95
        } elseif ($sequenceName !== null) {
886 5
            $column->sequenceName($this->resolveTableName($sequenceName)->getFullName());
887
        }
888
889 97
        if (isset($this->typeMap[$column->getDbType()])) {
890 97
            $column->type($this->typeMap[$column->getDbType()]);
891
        } else {
892
            $column->type(self::TYPE_STRING);
893
        }
894
895 97
        $column->phpType($this->getColumnPhpType($column));
896
897 97
        return $column;
898
    }
899
900
    /**
901
     * Executes the INSERT command, returning primary key values.
902
     *
903
     * @param string $table the table that new rows will be inserted into.
904
     * @param array $columns the column data (name => value) to be inserted into the table.
905
     *
906
     * @throws Exception|InvalidConfigException|Throwable
907
     *
908
     * @return array|false primary key values or false if the command fails.
909
     */
910 1
    public function insert(string $table, array $columns)
911
    {
912 1
        $params = [];
913 1
        $returnColumns = [];
914 1
        $sql = $this->getDb()->getQueryBuilder()->insert($table, $columns, $params);
915 1
        $tableSchema = $this->getTableSchema($table);
916
917 1
        if ($tableSchema !== null) {
918 1
            $returnColumns = $tableSchema->getPrimaryKey();
919
        }
920
921 1
        if (!empty($returnColumns)) {
922 1
            $returning = [];
923
            /** @var string $name */
924 1
            foreach ($returnColumns as $name) {
925 1
                $returning[] = $this->quoteColumnName($name);
926
            }
927 1
            $sql .= ' RETURNING ' . implode(', ', $returning);
928
        }
929
930 1
        $command = $this->getDb()->createCommand($sql, $params);
931 1
        $command->prepare(false);
932 1
        $result = $command->queryOne();
933
934 1
        $pdoStatement = $command->getPdoStatement();
935
936 1
        return $pdoStatement !== null && !$pdoStatement->rowCount() ? false : $result;
937
    }
938
939
    /**
940
     * Loads multiple types of constraints and returns the specified ones.
941
     *
942
     * @param string $tableName table name.
943
     * @param string $returnType return type:
944
     * - primaryKey
945
     * - foreignKeys
946
     * - uniques
947
     * - checks
948
     *
949
     * @throws Exception|InvalidConfigException|Throwable
950
     *
951
     * @return (CheckConstraint|Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
952
     *
953
     * @psalm-return Constraint|list<CheckConstraint|Constraint|ForeignKeyConstraint>|null
954
     */
955 61
    private function loadTableConstraints(string $tableName, string $returnType)
956
    {
957
        /** @var string $sql */
958 61
        $sql = <<<'SQL'
959
            SELECT
960
                "c"."conname" AS "name",
961
                "a"."attname" AS "column_name",
962
                "c"."contype" AS "type",
963
                "ftcns"."nspname" AS "foreign_table_schema",
964
                "ftc"."relname" AS "foreign_table_name",
965
                "fa"."attname" AS "foreign_column_name",
966
                "c"."confupdtype" AS "on_update",
967
                "c"."confdeltype" AS "on_delete",
968
                pg_get_constraintdef("c"."oid") AS "check_expr"
969
            FROM "pg_class" AS "tc"
970
            INNER JOIN "pg_namespace" AS "tcns"
971
                ON "tcns"."oid" = "tc"."relnamespace"
972
            INNER JOIN "pg_constraint" AS "c"
973
                ON "c"."conrelid" = "tc"."oid"
974
            INNER JOIN "pg_attribute" AS "a"
975
                ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
976
            LEFT JOIN "pg_class" AS "ftc"
977
                ON "ftc"."oid" = "c"."confrelid"
978
            LEFT JOIN "pg_namespace" AS "ftcns"
979
                ON "ftcns"."oid" = "ftc"."relnamespace"
980
            LEFT JOIN "pg_attribute" "fa"
981
                ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
982
            WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
983
            ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
984
            SQL;
985
986
        /** @var array<array-key, string> $actionTypes */
987 61
        $actionTypes = [
988
            'a' => 'NO ACTION',
989
            'r' => 'RESTRICT',
990
            'c' => 'CASCADE',
991
            'n' => 'SET NULL',
992
            'd' => 'SET DEFAULT',
993
        ];
994
995 61
        $resolvedName = $this->resolveTableName($tableName);
996
997 61
        $constraints = $this->getDb()->createCommand($sql, [
998 61
            ':schemaName' => $resolvedName->getSchemaName(),
999 61
            ':tableName' => $resolvedName->getName(),
1000 61
        ])->queryAll();
1001
1002
        /** @var array<array-key, array> $constraints */
1003 61
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
1004 61
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
1005
1006 61
        $result = [
1007
            'primaryKey' => null,
1008
            'foreignKeys' => [],
1009
            'uniques' => [],
1010
            'checks' => [],
1011
        ];
1012
1013
        /**
1014
         * @var string $type
1015
         * @var array $names
1016
         */
1017 61
        foreach ($constraints as $type => $names) {
1018
            /**
1019
             * @psalm-var object|string|null $name
1020
             * @psalm-var ConstraintArray $constraint
1021
             */
1022 61
            foreach ($names as $name => $constraint) {
1023 61
                switch ($type) {
1024 61
                    case 'p':
1025 46
                        $ct = (new Constraint())
1026 46
                            ->name($name)
1027 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1028
1029 46
                        $result['primaryKey'] = $ct;
1030 46
                        break;
1031 59
                    case 'f':
1032 13
                        $onDelete = $actionTypes[$constraint[0]['on_delete']] ?? null;
1033 13
                        $onUpdate = $actionTypes[$constraint[0]['on_update']] ?? null;
1034
1035 13
                        $fk = (new ForeignKeyConstraint())
1036 13
                            ->name($name)
1037 13
                            ->columnNames(array_values(
1038 13
                                array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
1039
                            ))
1040 13
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
1041 13
                            ->foreignTableName($constraint[0]['foreign_table_name'])
1042 13
                            ->foreignColumnNames(array_values(
1043 13
                                array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
1044
                            ))
1045 13
                            ->onDelete($onDelete)
1046 13
                            ->onUpdate($onUpdate);
1047
1048 13
                        $result['foreignKeys'][] = $fk;
1049 13
                        break;
1050 47
                    case 'u':
1051 46
                        $ct = (new Constraint())
1052 46
                            ->name($name)
1053 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1054
1055 46
                        $result['uniques'][] = $ct;
1056 46
                        break;
1057 10
                    case 'c':
1058 10
                        $ck = (new CheckConstraint())
1059 10
                            ->name($name)
1060 10
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
1061 10
                            ->expression($constraint[0]['check_expr']);
1062
1063 10
                        $result['checks'][] = $ck;
1064 10
                        break;
1065
                }
1066
            }
1067
        }
1068
1069 61
        foreach ($result as $type => $data) {
1070 61
            $this->setTableMetadata($tableName, $type, $data);
1071
        }
1072
1073 61
        return $result[$returnType];
1074
    }
1075
1076
    /**
1077
     * Creates a column schema for the database.
1078
     *
1079
     * This method may be overridden by child classes to create a DBMS-specific column schema.
1080
     *
1081
     * @return ColumnSchema column schema instance.
1082
     */
1083 97
    private function createColumnSchema(): ColumnSchema
1084
    {
1085 97
        return new ColumnSchema();
1086
    }
1087
1088
    /**
1089
     * Create a column schema builder instance giving the type and value precision.
1090
     *
1091
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
1092
     *
1093
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
1094
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
1095
     *
1096
     * @return ColumnSchemaBuilder column schema builder instance
1097
     */
1098 4
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
1099
    {
1100 4
        return new ColumnSchemaBuilder($type, $length);
1101
    }
1102
}
1103