Passed
Pull Request — master (#24)
by Wilmer
10:45
created

Schema::resolveTableName()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.054

Importance

Changes 0
Metric Value
cc 3
eloc 11
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 15
ccs 9
cts 11
cp 0.8182
crap 3.054
rs 9.9
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Pgsql\Schema;
6
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\Db\Constraint\CheckConstraint;
9
use Yiisoft\Db\Constraint\Constraint;
10
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
11
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
12
use Yiisoft\Db\Constraint\DefaultValueConstraint;
13
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
14
use Yiisoft\Db\Constraint\IndexConstraint;
15
use Yiisoft\Db\Exception\Exception;
16
use Yiisoft\Db\Exception\InvalidArgumentException;
17
use Yiisoft\Db\Exception\InvalidConfigException;
18
use Yiisoft\Db\Exception\NotSupportedException;
19
use Yiisoft\Db\Expression\Expression;
20
use Yiisoft\Db\Pgsql\Query\QueryBuilder;
21
use Yiisoft\Db\Schema\Schema as AbstractSchema;
22
use Yiisoft\Db\View\ViewFinderTrait;
23
24
class Schema extends AbstractSchema implements ConstraintFinderInterface
25
{
26
    use ViewFinderTrait;
27
    use ConstraintFinderTrait;
28
29
    public const TYPE_JSONB = 'jsonb';
30
31
    /**
32
     * @var string the default schema used for the current session.
33
     */
34
    protected ?string $defaultSchema = 'public';
35
    protected string $tableQuoteCharacter = '"';
36
37
    /**
38
     * @var array mapping from physical column types (keys) to abstract column types (values).
39
     *
40
     * {@see http://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE}
41
     */
42
    private array $typeMap = [
43
        'bit' => self::TYPE_INTEGER,
44
        'bit varying' => self::TYPE_INTEGER,
45
        'varbit' => self::TYPE_INTEGER,
46
        'bool' => self::TYPE_BOOLEAN,
47
        'boolean' => self::TYPE_BOOLEAN,
48
        'box' => self::TYPE_STRING,
49
        'circle' => self::TYPE_STRING,
50
        'point' => self::TYPE_STRING,
51
        'line' => self::TYPE_STRING,
52
        'lseg' => self::TYPE_STRING,
53
        'polygon' => self::TYPE_STRING,
54
        'path' => self::TYPE_STRING,
55
        'character' => self::TYPE_CHAR,
56
        'char' => self::TYPE_CHAR,
57
        'bpchar' => self::TYPE_CHAR,
58
        'character varying' => self::TYPE_STRING,
59
        'varchar' => self::TYPE_STRING,
60
        'text' => self::TYPE_TEXT,
61
        'bytea' => self::TYPE_BINARY,
62
        'cidr' => self::TYPE_STRING,
63
        'inet' => self::TYPE_STRING,
64
        'macaddr' => self::TYPE_STRING,
65
        'real' => self::TYPE_FLOAT,
66
        'float4' => self::TYPE_FLOAT,
67
        'double precision' => self::TYPE_DOUBLE,
68
        'float8' => self::TYPE_DOUBLE,
69
        'decimal' => self::TYPE_DECIMAL,
70
        'numeric' => self::TYPE_DECIMAL,
71
        'money' => self::TYPE_MONEY,
72
        'smallint' => self::TYPE_SMALLINT,
73
        'int2' => self::TYPE_SMALLINT,
74
        'int4' => self::TYPE_INTEGER,
75
        'int' => self::TYPE_INTEGER,
76
        'integer' => self::TYPE_INTEGER,
77
        'bigint' => self::TYPE_BIGINT,
78
        'int8' => self::TYPE_BIGINT,
79
        'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal!
80
        'smallserial' => self::TYPE_SMALLINT,
81
        'serial2' => self::TYPE_SMALLINT,
82
        'serial4' => self::TYPE_INTEGER,
83
        'serial' => self::TYPE_INTEGER,
84
        'bigserial' => self::TYPE_BIGINT,
85
        'serial8' => self::TYPE_BIGINT,
86
        'pg_lsn' => self::TYPE_BIGINT,
87
        'date' => self::TYPE_DATE,
88
        'interval' => self::TYPE_STRING,
89
        'time without time zone' => self::TYPE_TIME,
90
        'time' => self::TYPE_TIME,
91
        'time with time zone' => self::TYPE_TIME,
92
        'timetz' => self::TYPE_TIME,
93
        'timestamp without time zone' => self::TYPE_TIMESTAMP,
94
        'timestamp' => self::TYPE_TIMESTAMP,
95
        'timestamp with time zone' => self::TYPE_TIMESTAMP,
96
        'timestamptz' => self::TYPE_TIMESTAMP,
97
        'abstime' => self::TYPE_TIMESTAMP,
98
        'tsquery' => self::TYPE_STRING,
99
        'tsvector' => self::TYPE_STRING,
100
        'txid_snapshot' => self::TYPE_STRING,
101
        'unknown' => self::TYPE_STRING,
102
        'uuid' => self::TYPE_STRING,
103
        'json' => self::TYPE_JSON,
104
        'jsonb' => self::TYPE_JSON,
105
        'xml' => self::TYPE_STRING,
106
    ];
107
108
    /**
109
     * Resolves the table name and schema name (if any).
110
     *
111
     * @param string $name the table name.
112
     *
113
     * @return TableSchema with resolved table, schema, etc. names.
114
     *
115
     * {@see \Yiisoft\Db\Schema\TableSchema}
116
     */
117 71
    protected function resolveTableName(string $name): TableSchema
118
    {
119 71
        $resolvedName = new TableSchema();
120 71
        $parts = \explode('.', \str_replace('"', '', $name));
121
122 71
        if (isset($parts[1])) {
123
            $resolvedName->schemaName($parts[0]);
124
            $resolvedName->name($parts[1]);
125
        } else {
126 71
            $resolvedName->schemaName($this->defaultSchema);
127 71
            $resolvedName->name($name);
128
        }
129 71
        $resolvedName->fullName(($resolvedName->getSchemaName() !== $this->defaultSchema ? $resolvedName->getSchemaName() . '.' : '')
130 71
            . $resolvedName->getName());
131 71
        return $resolvedName;
132
    }
133
134
    /**
135
     * Returns all schema names in the database, including the default one but not system schemas.
136
     *
137
     * This method should be overridden by child classes in order to support this feature because the default
138
     * implementation simply throws an exception.
139
     *
140
     * @throws Exception
141
     * @throws InvalidArgumentException
142
     * @throws InvalidConfigException
143
     *
144
     * @return array all schema names in the database, except system schemas.
145
     */
146 2
    protected function findSchemaNames(): array
147
    {
148 2
        static $sql = <<<'SQL'
149
SELECT "ns"."nspname"
150
FROM "pg_namespace" AS "ns"
151
WHERE "ns"."nspname" != 'information_schema' AND "ns"."nspname" NOT LIKE 'pg_%'
152
ORDER BY "ns"."nspname" ASC
153
SQL;
154
155 2
        return $this->getDb()->createCommand($sql)->queryColumn();
156
    }
157
158
    /**
159
     * Returns all table names in the database.
160
     *
161
     * This method should be overridden by child classes in order to support this feature because the default
162
     * implementation simply throws an exception.
163
     *
164
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
165
     *
166
     * @throws Exception
167
     * @throws InvalidArgumentException
168
     * @throws InvalidConfigException
169
     *
170
     * @return array all table names in the database. The names have NO schema name prefix.
171
     */
172 5
    protected function findTableNames(string $schema = ''): array
173
    {
174 5
        if ($schema === '') {
175 5
            $schema = $this->defaultSchema;
176
        }
177
178
        $sql = <<<'SQL'
179 5
SELECT c.relname AS table_name
180
FROM pg_class c
181
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
182
WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f', 'p')
183
ORDER BY c.relname
184
SQL;
185 5
        return $this->getDb()->createCommand($sql, [':schemaName' => $schema])->queryColumn();
186
    }
187
188
    /**
189
     * Loads the metadata for the specified table.
190
     *
191
     * @param string $name table name.
192
     *
193
     * @throws Exception
194
     * @throws InvalidArgumentException
195
     * @throws InvalidConfigException
196
     * @throws NotSupportedException
197
     *
198
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
199
     */
200 98
    protected function loadTableSchema(string $name): ?TableSchema
201
    {
202 98
        $table = new TableSchema();
203
204 98
        $this->resolveTableNames($table, $name);
205
206 98
        if ($this->findColumns($table)) {
207 91
            $this->findConstraints($table);
208 91
            return $table;
209
        }
210
211 18
        return null;
212
    }
213
214
    /**
215
     * Loads a primary key for the given table.
216
     *
217
     * @param string $tableName table name.
218
     *
219
     * @throws Exception
220
     * @throws InvalidArgumentException
221
     * @throws InvalidConfigException
222
     *
223
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
224
     */
225 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
226
    {
227 31
        return $this->loadTableConstraints($tableName, 'primaryKey');
228
    }
229
230
    /**
231
     * Loads all foreign keys for the given table.
232
     *
233
     * @param string $tableName table name.
234
     *
235
     * @throws Exception
236
     * @throws InvalidArgumentException
237
     * @throws InvalidConfigException
238
     *
239
     * @return ForeignKeyConstraint[] foreign keys for the given table.
240
     */
241 4
    protected function loadTableForeignKeys($tableName): array
242
    {
243 4
        return $this->loadTableConstraints($tableName, 'foreignKeys');
244
    }
245
246
    /**
247
     * Loads all indexes for the given table.
248
     *
249
     * @param string $tableName table name.
250
     *
251
     * @throws Exception
252
     * @throws InvalidArgumentException
253
     * @throws InvalidConfigException
254
     *
255
     * @return IndexConstraint[] indexes for the given table.
256
     */
257 28
    protected function loadTableIndexes(string $tableName): array
258
    {
259 28
        static $sql = <<<'SQL'
260
SELECT
261
    "ic"."relname" AS "name",
262
    "ia"."attname" AS "column_name",
263
    "i"."indisunique" AS "index_is_unique",
264
    "i"."indisprimary" AS "index_is_primary"
265
FROM "pg_class" AS "tc"
266
INNER JOIN "pg_namespace" AS "tcns"
267
    ON "tcns"."oid" = "tc"."relnamespace"
268
INNER JOIN "pg_index" AS "i"
269
    ON "i"."indrelid" = "tc"."oid"
270
INNER JOIN "pg_class" AS "ic"
271
    ON "ic"."oid" = "i"."indexrelid"
272
INNER JOIN "pg_attribute" AS "ia"
273
    ON "ia"."attrelid" = "i"."indrelid" AND "ia"."attnum" = ANY ("i"."indkey")
274
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
275
ORDER BY "ia"."attnum" ASC
276
SQL;
277
278 28
        $resolvedName = $this->resolveTableName($tableName);
279
280 28
        $indexes = $this->getDb()->createCommand($sql, [
281 28
            ':schemaName' => $resolvedName->getSchemaName(),
282 28
            ':tableName' => $resolvedName->getName(),
283 28
        ])->queryAll();
284
285 28
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
286 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
287 28
        $result = [];
288
289 28
        foreach ($indexes as $name => $index) {
290 25
            $ic = new IndexConstraint();
291
292 25
            $ic->name($name);
293 25
            $ic->columnNames(ArrayHelper::getColumn($index, 'column_name'));
294 25
            $ic->primary((bool) $index[0]['index_is_primary']);
295 25
            $ic->unique((bool) $index[0]['index_is_unique']);
296
297 25
            $result[] = $ic;
298
        }
299
300 28
        return $result;
301
    }
302
303
    /**
304
     * Loads all unique constraints for the given table.
305
     *
306
     * @param string $tableName table name.
307
     *
308
     * @throws Exception
309
     * @throws InvalidArgumentException
310
     * @throws InvalidConfigException
311
     *
312
     * @return Constraint[] unique constraints for the given table.
313
     */
314 13
    protected function loadTableUniques(string $tableName): array
315
    {
316 13
        return $this->loadTableConstraints($tableName, 'uniques');
317
    }
318
319
    /**
320
     * Loads all check constraints for the given table.
321
     *
322
     * @param string $tableName table name.
323
     *
324
     * @throws Exception
325
     * @throws InvalidArgumentException
326
     * @throws InvalidConfigException
327
     *
328
     * @return CheckConstraint[] check constraints for the given table.
329
     */
330 13
    protected function loadTableChecks(string $tableName): array
331
    {
332 13
        return $this->loadTableConstraints($tableName, 'checks');
333
    }
334
335
    /**
336
     * Loads all default value constraints for the given table.
337
     *
338
     * @param string $tableName table name.
339
     *
340
     * @throws NotSupportedException
341
     *
342
     * @return DefaultValueConstraint[] default value constraints for the given table.
343
     */
344 12
    protected function loadTableDefaultValues($tableName): array
345
    {
346 12
        throw new NotSupportedException('PostgreSQL does not support default value constraints.');
347
    }
348
349
    /**
350
     * Creates a query builder for the PostgreSQL database.
351
     *
352
     * @return QueryBuilder query builder instance
353
     */
354 68
    public function createQueryBuilder(): QueryBuilder
355
    {
356 68
        return new QueryBuilder($this->getDb());
357
    }
358
359
    /**
360
     * Resolves the table name and schema name (if any).
361
     *
362
     * @param TableSchema $table the table metadata object.
363
     * @param string $name the table name
364
     */
365 98
    protected function resolveTableNames(TableSchema $table, string $name): void
366
    {
367 98
        $parts = \explode('.', \str_replace('"', '', $name));
368
369 98
        if (isset($parts[1])) {
370
            $table->schemaName($parts[0]);
371
            $table->name($parts[1]);
372
        } else {
373 98
            $table->schemaName($this->defaultSchema);
374 98
            $table->name($parts[0]);
375
        }
376
377 98
        $table->fullName($table->getSchemaName() !== $this->defaultSchema ? $table->getSchemaName() . '.'
378 98
            . $table->getName() : $table->getName());
379 98
    }
380
381
    protected function findViewNames(string $schema = '')
382
    {
383
        if ($schema === '') {
384
            $schema = $this->defaultSchema;
385
        }
386
        $sql = <<<'SQL'
387
SELECT c.relname AS table_name
388
FROM pg_class c
389
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
390
WHERE ns.nspname = :schemaName AND (c.relkind = 'v' OR c.relkind = 'm')
391
ORDER BY c.relname
392
SQL;
393
        return $this->getDb()->createCommand($sql, [':schemaName' => $schema])->queryColumn();
394
    }
395
396
    /**
397
     * Collects the foreign key column details for the given table.
398
     *
399
     * @param TableSchema $table the table metadata
400
     *
401
     * @throws Exception
402
     * @throws InvalidArgumentException
403
     * @throws InvalidConfigException
404
     */
405 91
    protected function findConstraints(TableSchema $table)
406
    {
407 91
        $tableName = $this->quoteValue($table->getName());
408 91
        $tableSchema = $this->quoteValue($table->getSchemaName());
409
410
        /**
411
         * We need to extract the constraints de hard way since:
412
         * {@see http://www.postgresql.org/message-id/[email protected]}
413
         */
414
415
        $sql = <<<SQL
416 91
select
417
    ct.conname as constraint_name,
418
    a.attname as column_name,
419
    fc.relname as foreign_table_name,
420
    fns.nspname as foreign_table_schema,
421
    fa.attname as foreign_column_name
422
from
423
    (SELECT ct.conname, ct.conrelid, ct.confrelid, ct.conkey, ct.contype, ct.confkey, generate_subscripts(ct.conkey, 1) AS s
424
       FROM pg_constraint ct
425
    ) AS ct
426
    inner join pg_class c on c.oid=ct.conrelid
427
    inner join pg_namespace ns on c.relnamespace=ns.oid
428
    inner join pg_attribute a on a.attrelid=ct.conrelid and a.attnum = ct.conkey[ct.s]
429
    left join pg_class fc on fc.oid=ct.confrelid
430
    left join pg_namespace fns on fc.relnamespace=fns.oid
431
    left join pg_attribute fa on fa.attrelid=ct.confrelid and fa.attnum = ct.confkey[ct.s]
432
where
433
    ct.contype='f'
434 91
    and c.relname={$tableName}
435 91
    and ns.nspname={$tableSchema}
436
order by
437
    fns.nspname, fc.relname, a.attnum
438
SQL;
439
440 91
        $constraints = [];
441
442 91
        foreach ($this->getDb()->createCommand($sql)->queryAll() as $constraint) {
443 9
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
444
                $constraint = \array_change_key_case($constraint, CASE_LOWER);
445
            }
446 9
            if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
447
                $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
448
            } else {
449 9
                $foreignTable = $constraint['foreign_table_name'];
450
            }
451 9
            $name = $constraint['constraint_name'];
452 9
            if (!isset($constraints[$name])) {
453 9
                $constraints[$name] = [
454 9
                    'tableName' => $foreignTable,
455
                    'columns' => [],
456
                ];
457
            }
458 9
            $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name'];
459
        }
460
461 91
        foreach ($constraints as $name => $constraint) {
462 9
            $table->foreignKey($name, \array_merge([$constraint['tableName']], $constraint['columns']));
463
        }
464 91
    }
465
466
    /**
467
     * Gets information about given table unique indexes.
468
     *
469
     * @param TableSchema $table the table metadata
470
     *
471
     * @throws Exception
472
     * @throws InvalidArgumentException
473
     * @throws InvalidConfigException
474
     *
475
     * @return array with index and column names
476
     */
477 1
    protected function getUniqueIndexInformation(TableSchema $table): array
478
    {
479
        $sql = <<<'SQL'
480 1
SELECT
481
    i.relname as indexname,
482
    pg_get_indexdef(idx.indexrelid, k + 1, TRUE) AS columnname
483
FROM (
484
  SELECT *, generate_subscripts(indkey, 1) AS k
485
  FROM pg_index
486
) idx
487
INNER JOIN pg_class i ON i.oid = idx.indexrelid
488
INNER JOIN pg_class c ON c.oid = idx.indrelid
489
INNER JOIN pg_namespace ns ON c.relnamespace = ns.oid
490
WHERE idx.indisprimary = FALSE AND idx.indisunique = TRUE
491
AND c.relname = :tableName AND ns.nspname = :schemaName
492
ORDER BY i.relname, k
493
SQL;
494
495 1
        return $this->getDb()->createCommand($sql, [
496 1
            ':schemaName' => $table->getSchemaName(),
497 1
            ':tableName' => $table->getName(),
498 1
        ])->queryAll();
499
    }
500
501
    /**
502
     * Returns all unique indexes for the given table.
503
     *
504
     * Each array element is of the following structure:
505
     *
506
     * ```php
507
     * [
508
     *     'IndexName1' => ['col1' [, ...]],
509
     *     'IndexName2' => ['col2' [, ...]],
510
     * ]
511
     * ```
512
     *
513
     * @param TableSchema $table the table metadata
514
     *
515
     * @throws Exception
516
     * @throws InvalidArgumentException
517
     * @throws InvalidConfigException
518
     *
519
     * @return array all unique indexes for the given table.
520
     */
521 1
    public function findUniqueIndexes($table): array
522
    {
523 1
        $uniqueIndexes = [];
524
525 1
        foreach ($this->getUniqueIndexInformation($table) as $row) {
526 1
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
527
                $row = \array_change_key_case($row, CASE_LOWER);
528
            }
529 1
            $column = $row['columnname'];
530 1
            if (!empty($column) && $column[0] === '"') {
531
                /**
532
                 * postgres will quote names that are not lowercase-only.
533
                 *
534
                 * {@see https://github.com/yiisoft/yii2/issues/10613}
535
                 */
536 1
                $column = \substr($column, 1, -1);
537
            }
538 1
            $uniqueIndexes[$row['indexname']][] = $column;
539
        }
540
541 1
        return $uniqueIndexes;
542
    }
543
544
    /**
545
     * Collects the metadata of table columns.
546
     *
547
     * @param TableSchema $table the table metadata
548
     *
549
     * @throws Exception
550
     * @throws InvalidArgumentException
551
     * @throws InvalidConfigException
552
     * @throws NotSupportedException
553
     *
554
     * @return bool whether the table exists in the database
555
     */
556 98
    protected function findColumns(TableSchema $table): bool
557
    {
558 98
        $tableName = $this->getDb()->quoteValue($table->getName());
559 98
        $schemaName = $this->getDb()->quoteValue($table->getSchemaName());
560
561 98
        $orIdentity = '';
562 98
        if (\version_compare($this->getDb()->getServerVersion(), '12.0', '>=')) {
563
            $orIdentity = 'OR attidentity != \'\'';
564
        }
565
566
        $sql = <<<SQL
567 98
SELECT
568
    d.nspname AS table_schema,
569
    c.relname AS table_name,
570
    a.attname AS column_name,
571
    COALESCE(td.typname, tb.typname, t.typname) AS data_type,
572
    COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
573
    a.attlen AS character_maximum_length,
574
    pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
575
    a.atttypmod AS modifier,
576
    a.attnotnull = false AS is_nullable,
577
    CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
578 98
    coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) {$orIdentity} AS is_autoinc,
579
    pg_get_serial_sequence(quote_ident(d.nspname) || '.' || quote_ident(c.relname), a.attname) AS sequence_name,
580
    CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
581
        THEN array_to_string((SELECT array_agg(enumlabel) FROM pg_enum WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid))::varchar[], ',')
582
        ELSE NULL
583
    END AS enum_values,
584
    CASE atttypid
585
         WHEN 21 /*int2*/ THEN 16
586
         WHEN 23 /*int4*/ THEN 32
587
         WHEN 20 /*int8*/ THEN 64
588
         WHEN 1700 /*numeric*/ THEN
589
              CASE WHEN atttypmod = -1
590
               THEN null
591
               ELSE ((atttypmod - 4) >> 16) & 65535
592
               END
593
         WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/
594
         WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/
595
         ELSE null
596
      END   AS numeric_precision,
597
      CASE
598
        WHEN atttypid IN (21, 23, 20) THEN 0
599
        WHEN atttypid IN (1700) THEN
600
        CASE
601
            WHEN atttypmod = -1 THEN null
602
            ELSE (atttypmod - 4) & 65535
603
        END
604
           ELSE null
605
      END AS numeric_scale,
606
    CAST(
607
             information_schema._pg_char_max_length(information_schema._pg_truetypid(a, t), information_schema._pg_truetypmod(a, t))
608
             AS numeric
609
    ) AS size,
610
    a.attnum = any (ct.conkey) as is_pkey,
611
    COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
612
FROM
613
    pg_class c
614
    LEFT JOIN pg_attribute a ON a.attrelid = c.oid
615
    LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
616
    LEFT JOIN pg_type t ON a.atttypid = t.oid
617
    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
618
    LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
619
    LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
620
    LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
621
WHERE
622
    a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped
623 98
    AND c.relname = {$tableName}
624 98
    AND d.nspname = {$schemaName}
625
ORDER BY
626
    a.attnum;
627
SQL;
628 98
        $columns = $this->getDb()->createCommand($sql)->queryAll();
629 98
        if (empty($columns)) {
630 18
            return false;
631
        }
632 91
        foreach ($columns as $column) {
633 91
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
634
                $column = \array_change_key_case($column, CASE_LOWER);
635
            }
636 91
            $column = $this->loadColumnSchema($column);
637 91
            $table->columns($column->getName(), $column);
638 91
            if ($column->isPrimaryKey()) {
639 62
                $table->primaryKey($column->getName());
640 62
                if ($table->getSequenceName() === null) {
641 62
                    $table->sequenceName($column->getSequenceName());
642
                }
643 62
                $column->defaultValue(null);
644 90
            } elseif ($column->getDefaultValue()) {
645 54
                if ($column->getType() === 'timestamp' && $column->getDefaultValue() === 'now()') {
646 27
                    $column->defaultValue(new Expression($column->getDefaultValue()));
647 54
                } elseif ($column->getType() === 'boolean') {
648 51
                    $column->defaultValue(($column->getDefaultValue() === 'true'));
649 30
                } elseif (\preg_match("/^B'(.*?)'::/", $column->getDefaultValue(), $matches)) {
650 27
                    $column->defaultValue(\bindec($matches[1]));
651 30
                } elseif (\strncasecmp($column->getDbType(), 'bit', 3) === 0 || \strncasecmp($column->getDbType(), 'varbit', 6) === 0) {
652
                    $column->defaultValue(\bindec(\trim($column->getDefaultValue(), 'B\'')));
653 30
                } elseif (\preg_match("/^'(.*?)'::/", $column->getDefaultValue(), $matches)) {
654 28
                    $column->defaultValue($column->phpTypecast($matches[1]));
655 29
                } elseif (\preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $column->getDefaultValue(), $matches)) {
656 29
                    if ($matches[2] === 'NULL') {
657 4
                        $column->defaultValue(null);
658
                    } else {
659 29
                        $column->defaultValue($column->phpTypecast($matches[2]));
660
                    }
661
                } else {
662
                    $column->defaultValue($column->phpTypecast($column->getDefaultValue()));
663
                }
664
            }
665
        }
666
667 91
        return true;
668
    }
669
670
    /**
671
     * Loads the column information into a {@see ColumnSchema} object.
672
     *
673
     * @param array $info column information
674
     *
675
     * @return ColumnSchema the column schema object
676
     */
677 91
    protected function loadColumnSchema(array $info): ColumnSchema
678
    {
679
        /** @var ColumnSchema $column */
680 91
        $column = $this->createColumnSchema();
681 91
        $column->allowNull($info['is_nullable']);
682 91
        $column->autoIncrement($info['is_autoinc']);
683 91
        $column->comment($info['column_comment']);
684 91
        $column->dbType($info['data_type']);
685 91
        $column->defaultValue($info['column_default']);
686 91
        $column->enumValues(($info['enum_values'] !== null)
687 91
            ? \explode(',', \str_replace(["''"], ["'"], $info['enum_values'])) : null);
688 91
        $column->unsigned(false); // has no meaning in PG
689 91
        $column->primaryKey((bool) $info['is_pkey']);
690 91
        $column->name($info['column_name']);
691 91
        $column->precision($info['numeric_precision']);
692 91
        $column->scale($info['numeric_scale']);
693 91
        $column->size($info['size'] === null ? null : (int) $info['size']);
694 91
        $column->dimension((int) $info['dimension']);
695
696
        /**
697
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
698
         * default value
699
         */
700
701 91
        $defaultValue = $column->getDefaultValue();
702 91
        if (isset($defaultValue) && \preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1) {
703 59
            $column->sequenceName(\preg_replace(
704 59
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
705 59
                '',
706 59
                $defaultValue
707
            ));
708 91
        } elseif (isset($info['sequence_name'])) {
709
            $column->sequenceName($this->resolveTableName($info['sequence_name'])->getFullName());
710
        }
711
712 91
        if (isset($this->typeMap[$column->getDbType()])) {
713 91
            $column->type($this->typeMap[$column->getDbType()]);
714
        } else {
715
            $column->type(self::TYPE_STRING);
716
        }
717 91
        $column->phpType($this->getColumnPhpType($column));
718
719 91
        return $column;
720
    }
721
722
    /**
723
     * Executes the INSERT command, returning primary key values.
724
     *
725
     * @param string $table the table that new rows will be inserted into.
726
     * @param array $columns the column data (name => value) to be inserted into the table.
727
     *
728
     * @throws Exception
729
     * @throws InvalidArgumentException
730
     * @throws InvalidConfigException
731
     * @throws NotSupportedException
732
     *
733
     * @return array|false primary key values or false if the command fails.
734
     */
735
    public function insert(string $table, array $columns)
736
    {
737
        $params = [];
738
        $sql = $this->getDb()->getQueryBuilder()->insert($table, $columns, $params);
739
        $returnColumns = $this->getTableSchema($table)->getPrimaryKey();
740
        if (!empty($returnColumns)) {
741
            $returning = [];
742
            foreach ((array) $returnColumns as $name) {
743
                $returning[] = $this->quoteColumnName($name);
744
            }
745
            $sql .= ' RETURNING ' . \implode(', ', $returning);
746
        }
747
748
        $command = $this->getDb()->createCommand($sql, $params);
749
        $command->prepare(false);
750
        $result = $command->queryOne();
751
752
        return !$command->getPdoStatement()->rowCount() ? false : $result;
753
    }
754
755
    /**
756
     * Loads multiple types of constraints and returns the specified ones.
757
     *
758
     * @param string $tableName table name.
759
     * @param string $returnType return type:
760
     * - primaryKey
761
     * - foreignKeys
762
     * - uniques
763
     * - checks
764
     *
765
     * @throws Exception
766
     * @throws InvalidArgumentException
767
     * @throws InvalidConfigException
768
     *
769
     * @return mixed constraints.
770
     */
771 61
    private function loadTableConstraints(string $tableName, string $returnType)
772
    {
773 61
        static $sql = <<<'SQL'
774
SELECT
775
    "c"."conname" AS "name",
776
    "a"."attname" AS "column_name",
777
    "c"."contype" AS "type",
778
    "ftcns"."nspname" AS "foreign_table_schema",
779
    "ftc"."relname" AS "foreign_table_name",
780
    "fa"."attname" AS "foreign_column_name",
781
    "c"."confupdtype" AS "on_update",
782
    "c"."confdeltype" AS "on_delete",
783
    pg_get_constraintdef("c"."oid") AS "check_expr"
784
FROM "pg_class" AS "tc"
785
INNER JOIN "pg_namespace" AS "tcns"
786
    ON "tcns"."oid" = "tc"."relnamespace"
787
INNER JOIN "pg_constraint" AS "c"
788
    ON "c"."conrelid" = "tc"."oid"
789
INNER JOIN "pg_attribute" AS "a"
790
    ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
791
LEFT JOIN "pg_class" AS "ftc"
792
    ON "ftc"."oid" = "c"."confrelid"
793
LEFT JOIN "pg_namespace" AS "ftcns"
794
    ON "ftcns"."oid" = "ftc"."relnamespace"
795
LEFT JOIN "pg_attribute" "fa"
796
    ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
797
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
798
ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
799
SQL;
800 61
        static $actionTypes = [
801
            'a' => 'NO ACTION',
802
            'r' => 'RESTRICT',
803
            'c' => 'CASCADE',
804
            'n' => 'SET NULL',
805
            'd' => 'SET DEFAULT',
806
        ];
807
808 61
        $resolvedName = $this->resolveTableName($tableName);
809 61
        $constraints = $this->getDb()->createCommand($sql, [
810 61
            ':schemaName' => $resolvedName->getSchemaName(),
811 61
            ':tableName' => $resolvedName->getName(),
812 61
        ])->queryAll();
813 61
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
814 61
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
815
        $result = [
816 61
            'primaryKey' => null,
817
            'foreignKeys' => [],
818
            'uniques' => [],
819
            'checks' => [],
820
        ];
821 61
        foreach ($constraints as $type => $names) {
822 61
            foreach ($names as $name => $constraint) {
823 61
                switch ($type) {
824 61
                    case 'p':
825 46
                        $ct = new Constraint();
826 46
                        $ct->name($name);
827 46
                        $ct->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
828
829 46
                        $result['primaryKey'] = $ct;
830 46
                        break;
831 59
                    case 'f':
832 13
                        $fk = new ForeignKeyConstraint();
833 13
                        $fk->name($name);
834 13
                        $fk->columnNames(\array_values(
835 13
                            \array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
836
                        ));
837 13
                        $fk->foreignColumnNames($constraint[0]['foreign_table_schema']);
838 13
                        $fk->foreignTableName($constraint[0]['foreign_table_name']);
839 13
                        $fk->foreignColumnNames(\array_values(
840 13
                            \array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
841
                        ));
842 13
                        $fk->onDelete($actionTypes[$constraint[0]['on_delete']] ?? null);
843 13
                        $fk->onUpdate($actionTypes[$constraint[0]['on_update']] ?? null);
844
845 13
                        $result['foreignKeys'][] = $fk;
846 13
                        break;
847 47
                    case 'u':
848 46
                        $ct = new Constraint();
849 46
                        $ct->name($name);
850 46
                        $ct->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
851
852 46
                        $result['uniques'][] = $ct;
853 46
                        break;
854 10
                    case 'c':
855 10
                        $ck = new CheckConstraint();
856 10
                        $ck->name($name);
857 10
                        $ck->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
858 10
                        $ck->expression($constraint[0]['check_expr']);
859
860 10
                        $result['checks'][] = $ck;
861 10
                        break;
862
                }
863
            }
864
        }
865 61
        foreach ($result as $type => $data) {
866 61
            $this->setTableMetadata($tableName, $type, $data);
867
        }
868
869 61
        return $result[$returnType];
870
    }
871
872
    /**
873
     * Creates a column schema for the database.
874
     *
875
     * This method may be overridden by child classes to create a DBMS-specific column schema.
876
     *
877
     * @return ColumnSchema column schema instance.
878
     */
879 91
    protected function createColumnSchema(): ColumnSchema
880
    {
881 91
        return new ColumnSchema();
882
    }
883
}
884