Passed
Pull Request — master (#32)
by Wilmer
13:13
created

PgsqlSchema::findConstraints()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 62
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 40
nc 18
nop 1
dl 0
loc 62
rs 8.6577
c 0
b 0
f 0

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\Schema;
6
7
use PDO;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Db\Constraint\CheckConstraint;
10
use Yiisoft\Db\Constraint\Constraint;
11
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
12
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
13
use Yiisoft\Db\Constraint\DefaultValueConstraint;
14
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
15
use Yiisoft\Db\Constraint\IndexConstraint;
16
use Yiisoft\Db\Exception\Exception;
17
use Yiisoft\Db\Exception\InvalidArgumentException;
18
use Yiisoft\Db\Exception\InvalidConfigException;
19
use Yiisoft\Db\Exception\NotSupportedException;
20
use Yiisoft\Db\Expression\Expression;
21
use Yiisoft\Db\Pgsql\Query\PgsqlQueryBuilder;
22
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
23
use Yiisoft\Db\Schema\Schema;
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 PgsqlSchema extends Schema implements ConstraintFinderInterface
39
{
40
    use ViewFinderTrait;
41
    use ConstraintFinderTrait;
42
43
    public const TYPE_JSONB = 'jsonb';
44
45
    /**
46
     * @var array mapping from physical column types (keys) to abstract column types (values).
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 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 PgsqlTableSchema with resolved table, schema, etc. names.
133
     *
134
     * {@see PgsqlTableSchema}
135
     */
136
    protected function resolveTableName(string $name): PgsqlTableSchema
137
    {
138
        $resolvedName = new PgsqlTableSchema();
139
140
        $parts = explode('.', str_replace('"', '', $name));
141
142
        if (isset($parts[1])) {
143
            $resolvedName->schemaName($parts[0]);
144
            $resolvedName->name($parts[1]);
145
        } else {
146
            $resolvedName->schemaName($this->defaultSchema);
147
            $resolvedName->name($name);
148
        }
149
150
        $resolvedName->fullName(
151
            (
152
                $resolvedName->getSchemaName() !== $this->defaultSchema ? $resolvedName->getSchemaName() . '.' : ''
153
            ) . $resolvedName->getName()
154
        );
155
156
        return $resolvedName;
157
    }
158
159
    /**
160
     * Returns all schema names in the database, including the default one but not system schemas.
161
     *
162
     * This method should be overridden by child classes in order to support this feature because the default
163
     * implementation simply throws an exception.
164
     *
165
     * @throws Exception
166
     * @throws InvalidArgumentException
167
     * @throws InvalidConfigException
168
     *
169
     * @return array all schema names in the database, except system schemas.
170
     */
171
    protected function findSchemaNames(): array
172
    {
173
        static $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
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
192
     * @throws InvalidArgumentException
193
     * @throws InvalidConfigException
194
     *
195
     * @return array all table names in the database. The names have NO schema name prefix.
196
     */
197
    protected function findTableNames(string $schema = ''): array
198
    {
199
        if ($schema === '') {
200
            $schema = $this->defaultSchema;
201
        }
202
203
        $sql = <<<'SQL'
204
SELECT c.relname AS table_name
205
FROM pg_class c
206
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
207
WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f', 'p')
208
ORDER BY c.relname
209
SQL;
210
211
        return $this->getDb()->createCommand($sql, [':schemaName' => $schema])->queryColumn();
212
    }
213
214
    /**
215
     * Loads the metadata for the specified table.
216
     *
217
     * @param string $name table name.
218
     *
219
     * @throws Exception
220
     * @throws InvalidArgumentException
221
     * @throws InvalidConfigException
222
     * @throws NotSupportedException
223
     *
224
     * @return PgsqlTableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
225
     */
226
    protected function loadTableSchema(string $name): ?PgsqlTableSchema
227
    {
228
        $table = new PgsqlTableSchema();
229
230
        $this->resolveTableNames($table, $name);
231
232
        if ($this->findColumns($table)) {
233
            $this->findConstraints($table);
234
            return $table;
235
        }
236
237
        return null;
238
    }
239
240
    /**
241
     * Loads a primary key for the given table.
242
     *
243
     * @param string $tableName table name.
244
     *
245
     * @throws Exception
246
     * @throws InvalidArgumentException
247
     * @throws InvalidConfigException
248
     *
249
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
250
     */
251
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
252
    {
253
        return $this->loadTableConstraints($tableName, 'primaryKey');
254
    }
255
256
    /**
257
     * Loads all foreign keys for the given table.
258
     *
259
     * @param string $tableName table name.
260
     *
261
     * @throws Exception
262
     * @throws InvalidArgumentException
263
     * @throws InvalidConfigException
264
     *
265
     * @return ForeignKeyConstraint[] foreign keys for the given table.
266
     */
267
    protected function loadTableForeignKeys($tableName): array
268
    {
269
        return $this->loadTableConstraints($tableName, 'foreignKeys');
270
    }
271
272
    /**
273
     * Loads all indexes for the given table.
274
     *
275
     * @param string $tableName table name.
276
     *
277
     * @throws Exception
278
     * @throws InvalidArgumentException
279
     * @throws InvalidConfigException
280
     *
281
     * @return IndexConstraint[] indexes for the given table.
282
     */
283
    protected function loadTableIndexes(string $tableName): array
284
    {
285
        static $sql = <<<'SQL'
286
SELECT
287
    "ic"."relname" AS "name",
288
    "ia"."attname" AS "column_name",
289
    "i"."indisunique" AS "index_is_unique",
290
    "i"."indisprimary" AS "index_is_primary"
291
FROM "pg_class" AS "tc"
292
INNER JOIN "pg_namespace" AS "tcns"
293
    ON "tcns"."oid" = "tc"."relnamespace"
294
INNER JOIN "pg_index" AS "i"
295
    ON "i"."indrelid" = "tc"."oid"
296
INNER JOIN "pg_class" AS "ic"
297
    ON "ic"."oid" = "i"."indexrelid"
298
INNER JOIN "pg_attribute" AS "ia"
299
    ON "ia"."attrelid" = "i"."indrelid" AND "ia"."attnum" = ANY ("i"."indkey")
300
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
301
ORDER BY "ia"."attnum" ASC
302
SQL;
303
304
        $resolvedName = $this->resolveTableName($tableName);
305
306
        $indexes = $this->getDb()->createCommand($sql, [
307
            ':schemaName' => $resolvedName->getSchemaName(),
308
            ':tableName' => $resolvedName->getName(),
309
        ])->queryAll();
310
311
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
312
        $indexes = ArrayHelper::index($indexes, null, 'name');
313
        $result = [];
314
315
        foreach ($indexes as $name => $index) {
316
            $ic = (new IndexConstraint())
317
                ->name($name)
318
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
319
                ->primary((bool) $index[0]['index_is_primary'])
320
                ->unique((bool) $index[0]['index_is_unique']);
321
322
            $result[] = $ic;
323
        }
324
325
        return $result;
326
    }
327
328
    /**
329
     * Loads all unique constraints for the given table.
330
     *
331
     * @param string $tableName table name.
332
     *
333
     * @throws Exception
334
     * @throws InvalidArgumentException
335
     * @throws InvalidConfigException
336
     *
337
     * @return Constraint[] unique constraints for the given table.
338
     */
339
    protected function loadTableUniques(string $tableName): array
340
    {
341
        return $this->loadTableConstraints($tableName, 'uniques');
342
    }
343
344
    /**
345
     * Loads all check constraints for the given table.
346
     *
347
     * @param string $tableName table name.
348
     *
349
     * @throws Exception
350
     * @throws InvalidArgumentException
351
     * @throws InvalidConfigException
352
     *
353
     * @return CheckConstraint[] check constraints for the given table.
354
     */
355
    protected function loadTableChecks(string $tableName): array
356
    {
357
        return $this->loadTableConstraints($tableName, 'checks');
358
    }
359
360
    /**
361
     * Loads all default value constraints for the given table.
362
     *
363
     * @param string $tableName table name.
364
     *
365
     * @throws NotSupportedException
366
     *
367
     * @return DefaultValueConstraint[] default value constraints for the given table.
368
     */
369
    protected function loadTableDefaultValues($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

369
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ $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...
370
    {
371
        throw new NotSupportedException('PostgreSQL does not support default value constraints.');
372
    }
373
374
    /**
375
     * Creates a query builder for the PostgreSQL database.
376
     *
377
     * @return PgsqlQueryBuilder query builder instance
378
     */
379
    public function createQueryBuilder(): PgsqlQueryBuilder
380
    {
381
        return new PgsqlQueryBuilder($this->getDb());
0 ignored issues
show
Bug introduced by
It seems like $this->getDb() can also be of type null; however, parameter $db of Yiisoft\Db\Pgsql\Query\P...yBuilder::__construct() does only seem to accept Yiisoft\Db\Connection\Connection, maybe add an additional type check? ( Ignorable by Annotation )

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

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