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

Schema::findColumns()   F

Complexity

Conditions 24
Paths 192

Size

Total Lines 167
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 61
CRAP Score 30.809

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 24
eloc 67
c 1
b 0
f 0
nc 192
nop 1
dl 0
loc 167
ccs 61
cts 79
cp 0.7722
crap 30.809
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
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 ForeignKeyConstraint[]|array 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 Constraint[]|array 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 CheckConstraint[]|array 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 string|int $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
633 97
        $tableName = $table->getName();
634 97
        $schemaName = $table->getSchemaName();
635
        $orIdentity = '';
636
637
        if ($tableName !== null) {
638
            $tableName = $this->getDb()->quoteValue($tableName);
639 97
        }
640
641 97
        if ($schemaName !== null) {
642 16
            $schemaName = $this->getDb()->quoteValue($schemaName);
643
        }
644
645 91
        if (version_compare($this->getDb()->getServerVersion(), '12.0', '>=')) {
646 91
            $orIdentity = 'OR a.attidentity != \'\'';
647
        }
648
649
        $sql = <<<SQL
650 91
SELECT
651 91
    d.nspname AS table_schema,
652
    c.relname AS table_name,
653 91
    a.attname AS column_name,
654 62
    COALESCE(td.typname, tb.typname, t.typname) AS data_type,
655
    COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
656 62
    a.attlen AS character_maximum_length,
657 62
    pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
658
    a.atttypmod AS modifier,
659
    a.attnotnull = false AS is_nullable,
660 62
    CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
661 88
    coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) {$orIdentity} AS is_autoinc,
662
    pg_get_serial_sequence(quote_ident(d.nspname) || '.' || quote_ident(c.relname), a.attname) AS sequence_name,
663 54
    CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
664 29
        THEN array_to_string((SELECT array_agg(enumlabel) FROM pg_enum WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid))::varchar[], ',')
665 29
        ELSE NULL
666 29
    END AS enum_values,
667 54
    CASE atttypid
668
         WHEN 21 /*int2*/ THEN 16
669
         WHEN 23 /*int4*/ THEN 32
670 27
         WHEN 20 /*int8*/ THEN 64
671 54
         WHEN 1700 /*numeric*/ THEN
672 51
              CASE WHEN atttypmod = -1
673 31
               THEN null
674
               ELSE ((atttypmod - 4) >> 16) & 65535
675 31
               END
676 27
         WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/
677 31
         WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/
678 28
         ELSE null
679 30
      END   AS numeric_precision,
680 30
      CASE
681 5
        WHEN atttypid IN (21, 23, 20) THEN 0
682
        WHEN atttypid IN (1700) THEN
683 30
        CASE
684
            WHEN atttypmod = -1 THEN null
685
            ELSE (atttypmod - 4) & 65535
686
        END
687
           ELSE null
688
      END AS numeric_scale,
689
    CAST(
690
             information_schema._pg_char_max_length(information_schema._pg_truetypid(a, t), information_schema._pg_truetypmod(a, t))
691 91
             AS numeric
692
    ) AS size,
693
    a.attnum = any (ct.conkey) as is_pkey,
694
    COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
695
FROM
696
    pg_class c
697
    LEFT JOIN pg_attribute a ON a.attrelid = c.oid
698
    LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
699
    LEFT JOIN pg_type t ON a.atttypid = t.oid
700
    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
701 91
    LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
702
    LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
703 91
    LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
704 91
WHERE
705 91
    a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped
706 91
    AND c.relname = {$tableName}
707 91
    AND d.nspname = {$schemaName}
708 91
ORDER BY
709 91
    a.attnum;
710 91
SQL;
711 91
712 91
        /** @var array columns */
713 91
        $columns = $this->getDb()->createCommand($sql)->queryAll();
714 91
        $slavePdo = $this->getDb()->getSlavePdo();
715 91
716 91
        if (empty($columns)) {
717 91
            return false;
718
        }
719
720
        /** @var array<array-key, mixed> $column */
721
        foreach ($columns as $column) {
722
            if ($slavePdo !== null && $slavePdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
723
                $column = array_change_key_case($column, CASE_LOWER);
724 91
            }
725
726 91
            /**
727 91
             * @var array{
728
             *   table_schema: string,
729 59
             *   table_name: string,
730
             *   column_name: string,
731
             *   data_type: string,
732 59
             *   type_type: string|null,
733
             *   character_maximum_length: int,
734 89
             *   column_comment: string|null,
735
             *   modifier: int,
736
             *   is_nullable: bool,
737
             *   column_default: mixed,
738 91
             *   is_autoinc: bool,
739 91
             *   sequence_name: string|null,
740
             *   enum_values: array<array-key, float|int|string>|string|null,
741
             *   numeric_precision: int|null,
742
             *   numeric_scale: int|null,
743
             *   size: string|null,
744 91
             *   is_pkey: bool|null,
745
             *   dimension: int
746 91
             * } $column
747
             */
748
            $loadColumnSchema = $this->loadColumnSchema($column);
749
            $table->columns($loadColumnSchema->getName(), $loadColumnSchema);
750
751
            /** @var mixed $defaultValue */
752
            $defaultValue = $loadColumnSchema->getDefaultValue();
753
754
            if ($loadColumnSchema->isPrimaryKey()) {
755
                $table->primaryKey($loadColumnSchema->getName());
756
757
                if ($table->getSequenceName() === null) {
758
                    $table->sequenceName($loadColumnSchema->getSequenceName());
759
                }
760
761
                $loadColumnSchema->defaultValue(null);
762
            } elseif ($defaultValue) {
763
                if (
764
                    is_string($defaultValue) &&
765
                    in_array($loadColumnSchema->getType(), [self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME], true) &&
766
                    in_array(
767
                        strtoupper($defaultValue),
768
                        ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'],
769
                        true
770
                    )
771
                ) {
772
                    $loadColumnSchema->defaultValue(new Expression($defaultValue));
773
                } elseif ($loadColumnSchema->getType() === 'boolean') {
774
                    $loadColumnSchema->defaultValue(($defaultValue  === 'true'));
775
                } elseif (is_string($defaultValue) && preg_match("/^B'(.*?)'::/", $defaultValue, $matches)) {
776
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
777
                } elseif (is_string($defaultValue) && preg_match("/^'(\d+)'::\"bit\"$/", $defaultValue, $matches)) {
778
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
779
                } elseif (is_string($defaultValue) && preg_match("/^'(.*?)'::/", $defaultValue, $matches)) {
780
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[1]));
781
                } elseif (
782
                    is_string($defaultValue) &&
783
                    preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $defaultValue, $matches)
784
                ) {
785
                    if ($matches[2] === 'NULL') {
786
                        $loadColumnSchema->defaultValue(null);
787
                    } else {
788
                        $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[2]));
789
                    }
790
                } else {
791
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($defaultValue));
792
                }
793
            }
794 61
        }
795
796 61
        return true;
797
    }
798
799
    /**
800
     * Loads the column information into a {@see ColumnSchema} object.
801
     *
802
     * @param array{
803
     *   table_schema: string,
804
     *   table_name: string,
805
     *   column_name: string,
806
     *   data_type: string,
807
     *   type_type: string|null,
808
     *   character_maximum_length: int,
809
     *   column_comment: string|null,
810
     *   modifier: int,
811
     *   is_nullable: bool,
812
     *   column_default: mixed,
813
     *   is_autoinc: bool,
814
     *   sequence_name: string|null,
815
     *   enum_values: array<array-key, float|int|string>|string|null,
816
     *   numeric_precision: int|null,
817
     *   numeric_scale: int|null,
818
     *   size: string|null,
819
     *   is_pkey: bool|null,
820
     *   dimension: int
821
     * } $info column information.
822
     *
823
     * @return ColumnSchema the column schema object.
824 61
     */
825
    protected function loadColumnSchema(array $info): ColumnSchema
826
    {
827
        $column = $this->createColumnSchema();
828
        $column->allowNull($info['is_nullable']);
829
        $column->autoIncrement($info['is_autoinc']);
830
        $column->comment($info['column_comment']);
831
        $column->dbType($info['data_type']);
832 61
        $column->defaultValue($info['column_default']);
833 61
        $column->enumValues(($info['enum_values'] !== null)
834 61
            ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) : null);
835 61
        $column->unsigned(false); // has no meaning in PG
836 61
        $column->primaryKey((bool) $info['is_pkey']);
837 61
        $column->name($info['column_name']);
838 61
        $column->precision($info['numeric_precision']);
839
        $column->scale($info['numeric_scale']);
840 61
        $column->size($info['size'] === null ? null : (int) $info['size']);
841
        $column->dimension($info['dimension']);
842
843
        /**
844
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
845
         * default value.
846 61
         *
847 61
         * @var mixed $defaultValue
848
         */
849 61
        $defaultValue = $column->getDefaultValue();
850 46
        $sequenceName = $info['sequence_name'] ?? null;
851 46
852 46
        if (
853
            isset($defaultValue) &&
854 46
            is_string($defaultValue) &&
855 46
            preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1
856 59
        ) {
857 13
            $column->sequenceName(preg_replace(
858 13
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
859 13
                '',
860 13
                $defaultValue
861
            ));
862 13
        } elseif ($sequenceName !== null) {
863 13
            $column->sequenceName($this->resolveTableName($sequenceName)->getFullName());
864 13
        }
865 13
866
        if (isset($this->typeMap[$column->getDbType()])) {
867 13
            $column->type($this->typeMap[$column->getDbType()]);
868 13
        } else {
869
            $column->type(self::TYPE_STRING);
870 13
        }
871 13
872 47
        $column->phpType($this->getColumnPhpType($column));
873 46
874 46
        return $column;
875 46
    }
876
877 46
    /**
878 46
     * Executes the INSERT command, returning primary key values.
879 10
     *
880 10
     * @param string $table the table that new rows will be inserted into.
881 10
     * @param array $columns the column data (name => value) to be inserted into the table.
882 10
     *
883 10
     * @throws Exception|InvalidConfigException|Throwable
884
     *
885 10
     * @return array|false primary key values or false if the command fails.
886 10
     */
887
    public function insert(string $table, array $columns)
888
    {
889
        $params = [];
890
        $returnColumns = [];
891 61
        $sql = $this->getDb()->getQueryBuilder()->insert($table, $columns, $params);
892 61
        $tableSchema = $this->getTableSchema($table);
893
894
        if ($tableSchema !== null) {
895 61
            $returnColumns = $tableSchema->getPrimaryKey();
896
        }
897
898
        if (!empty($returnColumns)) {
899
            $returning = [];
900
            /** @var string $name */
901
            foreach ($returnColumns as $name) {
902
                $returning[] = $this->quoteColumnName($name);
903
            }
904
            $sql .= ' RETURNING ' . implode(', ', $returning);
905 91
        }
906
907 91
        $command = $this->getDb()->createCommand($sql, $params);
908
        $command->prepare(false);
909
        $result = $command->queryOne();
910
911
        $pdoStatement = $command->getPdoStatement();
912
913
        return $pdoStatement !== null && !$pdoStatement->rowCount() ? false : $result;
914
    }
915
916
    /**
917
     * Loads multiple types of constraints and returns the specified ones.
918
     *
919
     * @param string $tableName table name.
920 4
     * @param string $returnType return type:
921
     * - primaryKey
922 4
     * - foreignKeys
923
     * - uniques
924
     * - checks
925
     *
926
     * @throws Exception|InvalidConfigException|Throwable
927
     *
928
     * @return (CheckConstraint|Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
929
     *
930
     * @psalm-return Constraint|list<CheckConstraint|Constraint|ForeignKeyConstraint>|null
931
     */
932
    private function loadTableConstraints(string $tableName, string $returnType)
933
    {
934
        /** @var string $sql */
935
        $sql = <<<'SQL'
936
SELECT
937
    "c"."conname" AS "name",
938
    "a"."attname" AS "column_name",
939
    "c"."contype" AS "type",
940
    "ftcns"."nspname" AS "foreign_table_schema",
941
    "ftc"."relname" AS "foreign_table_name",
942
    "fa"."attname" AS "foreign_column_name",
943
    "c"."confupdtype" AS "on_update",
944
    "c"."confdeltype" AS "on_delete",
945
    pg_get_constraintdef("c"."oid") AS "check_expr"
946
FROM "pg_class" AS "tc"
947
INNER JOIN "pg_namespace" AS "tcns"
948
    ON "tcns"."oid" = "tc"."relnamespace"
949
INNER JOIN "pg_constraint" AS "c"
950
    ON "c"."conrelid" = "tc"."oid"
951
INNER JOIN "pg_attribute" AS "a"
952
    ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
953
LEFT JOIN "pg_class" AS "ftc"
954
    ON "ftc"."oid" = "c"."confrelid"
955
LEFT JOIN "pg_namespace" AS "ftcns"
956
    ON "ftcns"."oid" = "ftc"."relnamespace"
957
LEFT JOIN "pg_attribute" "fa"
958
    ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
959
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
960
ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
961
SQL;
962
963
        /** @var array<array-key, string> $actionTypes */
964
        $actionTypes = [
965
            'a' => 'NO ACTION',
966
            'r' => 'RESTRICT',
967
            'c' => 'CASCADE',
968
            'n' => 'SET NULL',
969
            'd' => 'SET DEFAULT',
970
        ];
971
972
        $resolvedName = $this->resolveTableName($tableName);
973
974
        $constraints = $this->getDb()->createCommand($sql, [
975
            ':schemaName' => $resolvedName->getSchemaName(),
976
            ':tableName' => $resolvedName->getName(),
977
        ])->queryAll();
978
979
        /** @var array<array-key, array> $constraints */
980
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
981
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
982
983
        $result = [
984
            'primaryKey' => null,
985
            'foreignKeys' => [],
986
            'uniques' => [],
987
            'checks' => [],
988
        ];
989
990
        /**
991
         * @var string $type
992
         * @var array $names
993
         */
994
        foreach ($constraints as $type => $names) {
995
            /**
996
             * @var object|string|null $name
997
             * @var array<
998
             *   array-key,
999
             *   array {
1000
             *     name: string,
1001
             *     column_name: string,
1002
             *     type: string,
1003
             *     foreign_table_schema: string|null,
1004
             *     foreign_table_name: string|null,
1005
             *     foreign_column_name: string|null,
1006
             *     on_update: string,
1007
             *     on_delete: string,
1008
             *     check_expr: string
1009
             *   }
1010
             * > $constraint
1011
             */
1012
            foreach ($names as $name => $constraint) {
1013
                switch ($type) {
1014
                    case 'p':
1015
                        $ct = (new Constraint())
1016
                            ->name($name)
1017
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1018
1019
                        $result['primaryKey'] = $ct;
1020
                        break;
1021
                    case 'f':
1022
                        $onDelete = $actionTypes[$constraint[0]['on_delete']] ?? null;
1023
                        $onUpdate = $actionTypes[$constraint[0]['on_update']] ?? null;
1024
1025
                        $fk = (new ForeignKeyConstraint())
1026
                            ->name($name)
1027
                            ->columnNames(array_values(
1028
                                array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
1029
                            ))
1030
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
1031
                            ->foreignTableName($constraint[0]['foreign_table_name'])
1032
                            ->foreignColumnNames(array_values(
1033
                                array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
1034
                            ))
1035
                            ->onDelete($onDelete)
1036
                            ->onUpdate($onUpdate);
1037
1038
                        $result['foreignKeys'][] = $fk;
1039
                        break;
1040
                    case 'u':
1041
                        $ct = (new Constraint())
1042
                            ->name($name)
1043
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1044
1045
                        $result['uniques'][] = $ct;
1046
                        break;
1047
                    case 'c':
1048
                        $ck = (new CheckConstraint())
1049
                            ->name($name)
1050
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
1051
                            ->expression($constraint[0]['check_expr']);
1052
1053
                        $result['checks'][] = $ck;
1054
                        break;
1055
                }
1056
            }
1057
        }
1058
1059
        foreach ($result as $type => $data) {
1060
            $this->setTableMetadata($tableName, $type, $data);
1061
        }
1062
1063
        return $result[$returnType];
1064
    }
1065
1066
    /**
1067
     * Creates a column schema for the database.
1068
     *
1069
     * This method may be overridden by child classes to create a DBMS-specific column schema.
1070
     *
1071
     * @return ColumnSchema column schema instance.
1072
     */
1073
    private function createColumnSchema(): ColumnSchema
1074
    {
1075
        return new ColumnSchema();
1076
    }
1077
1078
    /**
1079
     * Create a column schema builder instance giving the type and value precision.
1080
     *
1081
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
1082
     *
1083
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
1084
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
1085
     *
1086
     * @return ColumnSchemaBuilder column schema builder instance
1087
     */
1088
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
1089
    {
1090
        return new ColumnSchemaBuilder($type, $length);
1091
    }
1092
}
1093