Passed
Pull Request — master (#70)
by Wilmer
01:58
created

Schema::findConstraints()   B

Complexity

Conditions 9
Paths 72

Size

Total Lines 97
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 17.1073

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 47
c 1
b 0
f 0
nc 72
nop 1
dl 0
loc 97
ccs 15
cts 28
cp 0.5356
crap 17.1073
rs 7.6008

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Pgsql;
6
7
use JsonException;
8
use PDO;
9
use Throwable;
10
use Yiisoft\Arrays\ArrayHelper;
11
use Yiisoft\Db\Constraint\CheckConstraint;
12
use Yiisoft\Db\Constraint\Constraint;
13
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
14
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
15
use Yiisoft\Db\Constraint\DefaultValueConstraint;
16
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
17
use Yiisoft\Db\Constraint\IndexConstraint;
18
use Yiisoft\Db\Exception\Exception;
19
use Yiisoft\Db\Exception\InvalidConfigException;
20
use Yiisoft\Db\Exception\NotSupportedException;
21
use Yiisoft\Db\Expression\Expression;
22
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
23
use Yiisoft\Db\Schema\Schema as AbstractSchema;
24
use Yiisoft\Db\View\ViewFinderTrait;
25
26
use function array_change_key_case;
27
use function array_merge;
28
use function array_unique;
29
use function array_values;
30
use function bindec;
31
use function explode;
32
use function implode;
33
use function preg_match;
34
use function preg_replace;
35
use function str_replace;
36
use function substr;
37
38
final class Schema extends AbstractSchema implements ConstraintFinderInterface
39
{
40
    use ConstraintFinderTrait;
41
    use ViewFinderTrait;
42
43
    public const TYPE_JSONB = 'jsonb';
44
45
    /**
46
     * @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...
47
     *
48
     * {@see http://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE}
49
     */
50
    private array $typeMap = [
51
        'bit' => self::TYPE_INTEGER,
52
        'bit varying' => self::TYPE_INTEGER,
53
        'varbit' => self::TYPE_INTEGER,
54
        'bool' => self::TYPE_BOOLEAN,
55
        'boolean' => self::TYPE_BOOLEAN,
56
        'box' => self::TYPE_STRING,
57
        'circle' => self::TYPE_STRING,
58
        'point' => self::TYPE_STRING,
59
        'line' => self::TYPE_STRING,
60
        'lseg' => self::TYPE_STRING,
61
        'polygon' => self::TYPE_STRING,
62
        'path' => self::TYPE_STRING,
63
        'character' => self::TYPE_CHAR,
64
        'char' => self::TYPE_CHAR,
65
        'bpchar' => self::TYPE_CHAR,
66
        'character varying' => self::TYPE_STRING,
67
        'varchar' => self::TYPE_STRING,
68
        'text' => self::TYPE_TEXT,
69
        'bytea' => self::TYPE_BINARY,
70
        'cidr' => self::TYPE_STRING,
71
        'inet' => self::TYPE_STRING,
72
        'macaddr' => self::TYPE_STRING,
73
        'real' => self::TYPE_FLOAT,
74
        'float4' => self::TYPE_FLOAT,
75
        'double precision' => self::TYPE_DOUBLE,
76
        'float8' => self::TYPE_DOUBLE,
77
        'decimal' => self::TYPE_DECIMAL,
78
        'numeric' => self::TYPE_DECIMAL,
79
        'money' => self::TYPE_MONEY,
80
        'smallint' => self::TYPE_SMALLINT,
81
        'int2' => self::TYPE_SMALLINT,
82
        'int4' => self::TYPE_INTEGER,
83
        'int' => self::TYPE_INTEGER,
84
        'integer' => self::TYPE_INTEGER,
85
        'bigint' => self::TYPE_BIGINT,
86
        'int8' => self::TYPE_BIGINT,
87
        'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal!
88
        'smallserial' => self::TYPE_SMALLINT,
89
        'serial2' => self::TYPE_SMALLINT,
90
        'serial4' => self::TYPE_INTEGER,
91
        'serial' => self::TYPE_INTEGER,
92
        'bigserial' => self::TYPE_BIGINT,
93
        'serial8' => self::TYPE_BIGINT,
94
        'pg_lsn' => self::TYPE_BIGINT,
95
        'date' => self::TYPE_DATE,
96
        'interval' => self::TYPE_STRING,
97
        'time without time zone' => self::TYPE_TIME,
98
        'time' => self::TYPE_TIME,
99
        'time with time zone' => self::TYPE_TIME,
100
        'timetz' => self::TYPE_TIME,
101
        'timestamp without time zone' => self::TYPE_TIMESTAMP,
102
        'timestamp' => self::TYPE_TIMESTAMP,
103
        'timestamp with time zone' => self::TYPE_TIMESTAMP,
104
        'timestamptz' => self::TYPE_TIMESTAMP,
105
        'abstime' => self::TYPE_TIMESTAMP,
106
        'tsquery' => self::TYPE_STRING,
107
        'tsvector' => self::TYPE_STRING,
108
        'txid_snapshot' => self::TYPE_STRING,
109
        'unknown' => self::TYPE_STRING,
110
        'uuid' => self::TYPE_STRING,
111
        'json' => self::TYPE_JSON,
112
        'jsonb' => self::TYPE_JSON,
113
        'xml' => self::TYPE_STRING,
114
    ];
115
116
    /**
117
     * @var string|null the default schema used for the current session.
118
     */
119
    protected ?string $defaultSchema = 'public';
120
121
    /**
122
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
123
     * case starting and ending characters are different.
124
     */
125
    protected $tableQuoteCharacter = '"';
126
127
    /**
128
     * Resolves the table name and schema name (if any).
129
     *
130
     * @param string $name the table name.
131
     *
132
     * @return TableSchema with resolved table, schema, etc. names.
133
     *
134
     * {@see TableSchema}
135
     */
136
    protected function resolveTableName(string $name): TableSchema
137 71
    {
138
        $resolvedName = new TableSchema();
139 71
140
        $parts = explode('.', str_replace('"', '', $name));
141 71
142
        if (isset($parts[1])) {
143 71
            $resolvedName->schemaName($parts[0]);
144
            $resolvedName->name($parts[1]);
145
        } else {
146
            $resolvedName->schemaName($this->defaultSchema);
147 71
            $resolvedName->name($name);
148 71
        }
149
150
        $resolvedName->fullName(
151 71
            (
152
                $resolvedName->getSchemaName() !== $this->defaultSchema ?
153 71
                    (string) $resolvedName->getSchemaName() . '.' :
154 71
                    ''
155
            ) . (string) $resolvedName->getName()
156
        );
157 71
158
        return $resolvedName;
159
    }
160
161
    /**
162
     * Returns all schema names in the database, including the default one but not system schemas.
163
     *
164
     * This method should be overridden by child classes in order to support this feature because the default
165
     * implementation simply throws an exception.
166
     *
167
     * @throws Exception|InvalidConfigException|Throwable
168
     *
169
     * @return array all schema names in the database, except system schemas.
170 2
     */
171
    protected function findSchemaNames(): array
172 2
    {
173
        $sql = <<<'SQL'
174
SELECT "ns"."nspname"
175
FROM "pg_namespace" AS "ns"
176
WHERE "ns"."nspname" != 'information_schema' AND "ns"."nspname" NOT LIKE 'pg_%'
177
ORDER BY "ns"."nspname" ASC
178
SQL;
179 2
180
        return $this->getDb()->createCommand($sql)->queryColumn();
181
    }
182
183
    /**
184
     * Returns all table names in the database.
185
     *
186
     * This method should be overridden by child classes in order to support this feature because the default
187
     * implementation simply throws an exception.
188
     *
189
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
190
     *
191
     * @throws Exception|InvalidConfigException|Throwable
192
     *
193
     * @return array all table names in the database. The names have NO schema name prefix.
194 5
     */
195
    protected function findTableNames(string $schema = ''): array
196 5
    {
197 5
        if ($schema === '') {
198
            $schema = $this->defaultSchema;
199
        }
200
201 5
        $sql = <<<'SQL'
202
SELECT c.relname AS table_name
203
FROM pg_class c
204
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
205
WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f', 'p')
206
ORDER BY c.relname
207
SQL;
208 5
209
        return $this->getDb()->createCommand($sql, [':schemaName' => $schema])->queryColumn();
210
    }
211
212
    /**
213
     * Loads the metadata for the specified table.
214
     *
215
     * @param string $name table name.
216
     *
217
     * @throws Exception|InvalidConfigException
218
     *
219
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
220 97
     */
221
    protected function loadTableSchema(string $name): ?TableSchema
222 97
    {
223
        $table = new TableSchema();
224 97
225
        $this->resolveTableNames($table, $name);
226 97
227 91
        if ($this->findColumns($table)) {
228 91
            $this->findConstraints($table);
229
            return $table;
230
        }
231 16
232
        return null;
233
    }
234
235
    /**
236
     * Loads a primary key for the given table.
237
     *
238
     * @param string $tableName table name.
239
     *
240
     * @throws Exception|InvalidConfigException
241
     *
242
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
243 31
     */
244
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
245 31
    {
246
        $tablePrimaryKey = $this->loadTableConstraints($tableName, 'primaryKey');
247
248
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
249
    }
250
251
    /**
252
     * Loads all foreign keys for the given table.
253
     *
254
     * @param string $tableName table name.
255
     *
256
     * @throws Exception|InvalidConfigException
257 4
     *
258
     * @return array|ForeignKeyConstraint[] foreign keys for the given table.
259 4
     */
260
    protected function loadTableForeignKeys(string $tableName): array
261
    {
262
        $tableForeignKeys = $this->loadTableConstraints($tableName, 'foreignKeys');
263
264
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
265
    }
266
267
    /**
268
     * Loads all indexes for the given table.
269
     *
270
     * @param string $tableName table name.
271 28
     *
272
     * @throws Exception|InvalidConfigException|Throwable
273 28
     *
274
     * @return IndexConstraint[] indexes for the given table.
275
     */
276
    protected function loadTableIndexes(string $tableName): array
277
    {
278
        $sql = <<<'SQL'
279
SELECT
280
    "ic"."relname" AS "name",
281
    "ia"."attname" AS "column_name",
282
    "i"."indisunique" AS "index_is_unique",
283
    "i"."indisprimary" AS "index_is_primary"
284
FROM "pg_class" AS "tc"
285
INNER JOIN "pg_namespace" AS "tcns"
286
    ON "tcns"."oid" = "tc"."relnamespace"
287
INNER JOIN "pg_index" AS "i"
288
    ON "i"."indrelid" = "tc"."oid"
289
INNER JOIN "pg_class" AS "ic"
290
    ON "ic"."oid" = "i"."indexrelid"
291
INNER JOIN "pg_attribute" AS "ia"
292 28
    ON "ia"."attrelid" = "i"."indrelid" AND "ia"."attnum" = ANY ("i"."indkey")
293
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
294 28
ORDER BY "ia"."attnum" ASC
295 28
SQL;
296 28
297 28
        $resolvedName = $this->resolveTableName($tableName);
298
299 28
        $indexes = $this->getDb()->createCommand($sql, [
300 28
            ':schemaName' => $resolvedName->getSchemaName(),
301 28
            ':tableName' => $resolvedName->getName(),
302
        ])->queryAll();
303 28
304 25
        /** @var array<array-key, array<array-key, mixed>> @indexes */
305 25
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
306 25
        $indexes = ArrayHelper::index($indexes, null, 'name');
307 25
        $result = [];
308 25
309
        /**
310 25
         * @var object|string|null $name
311
         * @var array<
312
         *   array-key,
313 28
         *   array{
314
         *     name: string,
315
         *     column_name: string,
316
         *     index_is_unique: bool,
317
         *     index_is_primary: bool
318
         *   }
319
         * > $index
320
         */
321
        foreach ($indexes as $name => $index) {
322
            $ic = (new IndexConstraint())
323
                ->name($name)
324
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
325 13
                ->primary($index[0]['index_is_primary'])
326
                ->unique($index[0]['index_is_unique']);
327 13
328
            $result[] = $ic;
329
        }
330
331
        return $result;
332
    }
333
334
    /**
335
     * Loads all unique constraints for the given table.
336
     *
337
     * @param string $tableName table name.
338
     *
339 13
     * @throws Exception|InvalidConfigException
340
     *
341 13
     * @return array|Constraint[] unique constraints for the given table.
342
     */
343
    protected function loadTableUniques(string $tableName): array
344
    {
345
        $tableUniques = $this->loadTableConstraints($tableName, 'uniques');
346
347
        return is_array($tableUniques) ? $tableUniques : [];
348
    }
349
350
    /**
351
     * Loads all check constraints for the given table.
352
     *
353 12
     * @param string $tableName table name.
354
     *
355 12
     * @throws Exception|InvalidConfigException
356
     *
357
     * @return array|CheckConstraint[] check constraints for the given table.
358
     */
359
    protected function loadTableChecks(string $tableName): array
360
    {
361
        $tableChecks = $this->loadTableConstraints($tableName, 'checks');
362
363 69
        return is_array($tableChecks) ? $tableChecks : [];
364
    }
365 69
366
    /**
367
     * Loads all default value constraints for the given table.
368
     *
369
     * @param string $tableName table name.
370
     *
371
     * @throws NotSupportedException
372
     *
373
     * @return DefaultValueConstraint[] default value constraints for the given table.
374 97
     */
375
    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

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