Passed
Pull Request — master (#29)
by Wilmer
11:34
created

Schema::findUniqueIndexes()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 5
nop 1
dl 0
loc 21
ccs 9
cts 10
cp 0.9
crap 5.025
rs 9.6111
c 0
b 0
f 0
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 25
                ->name($name)
292 25
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
293 25
                ->primary((bool) $index[0]['index_is_primary'])
294 25
                ->unique((bool) $index[0]['index_is_unique']);
295
296 25
            $result[] = $ic;
297
        }
298
299 28
        return $result;
300
    }
301
302
    /**
303
     * Loads all unique constraints for the given table.
304
     *
305
     * @param string $tableName table name.
306
     *
307
     * @throws Exception
308
     * @throws InvalidArgumentException
309
     * @throws InvalidConfigException
310
     *
311
     * @return Constraint[] unique constraints for the given table.
312
     */
313 13
    protected function loadTableUniques(string $tableName): array
314
    {
315 13
        return $this->loadTableConstraints($tableName, 'uniques');
316
    }
317
318
    /**
319
     * Loads all check constraints for the given table.
320
     *
321
     * @param string $tableName table name.
322
     *
323
     * @throws Exception
324
     * @throws InvalidArgumentException
325
     * @throws InvalidConfigException
326
     *
327
     * @return CheckConstraint[] check constraints for the given table.
328
     */
329 13
    protected function loadTableChecks(string $tableName): array
330
    {
331 13
        return $this->loadTableConstraints($tableName, 'checks');
332
    }
333
334
    /**
335
     * Loads all default value constraints for the given table.
336
     *
337
     * @param string $tableName table name.
338
     *
339
     * @throws NotSupportedException
340
     *
341
     * @return DefaultValueConstraint[] default value constraints for the given table.
342
     */
343 12
    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

343
    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...
344
    {
345 12
        throw new NotSupportedException('PostgreSQL does not support default value constraints.');
346
    }
347
348
    /**
349
     * Creates a query builder for the PostgreSQL database.
350
     *
351
     * @return QueryBuilder query builder instance
352
     */
353 68
    public function createQueryBuilder(): QueryBuilder
354
    {
355 68
        return new QueryBuilder($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\QueryBuilder::__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

355
        return new QueryBuilder(/** @scrutinizer ignore-type */ $this->getDb());
Loading history...
356
    }
357
358
    /**
359
     * Resolves the table name and schema name (if any).
360
     *
361
     * @param TableSchema $table the table metadata object.
362
     * @param string $name the table name
363
     */
364 98
    protected function resolveTableNames(TableSchema $table, string $name): void
365
    {
366 98
        $parts = \explode('.', \str_replace('"', '', $name));
367
368 98
        if (isset($parts[1])) {
369
            $table->schemaName($parts[0]);
370
            $table->name($parts[1]);
371
        } else {
372 98
            $table->schemaName($this->defaultSchema);
373 98
            $table->name($parts[0]);
374
        }
375
376 98
        $table->fullName($table->getSchemaName() !== $this->defaultSchema ? $table->getSchemaName() . '.'
377 98
            . $table->getName() : $table->getName());
378 98
    }
379
380
    protected function findViewNames(string $schema = ''): array
381
    {
382
        if ($schema === '') {
383
            $schema = $this->defaultSchema;
384
        }
385
        $sql = <<<'SQL'
386
SELECT c.relname AS table_name
387
FROM pg_class c
388
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
389
WHERE ns.nspname = :schemaName AND (c.relkind = 'v' OR c.relkind = 'm')
390
ORDER BY c.relname
391
SQL;
392
        return $this->getDb()->createCommand($sql, [':schemaName' => $schema])->queryColumn();
393
    }
394
395
    /**
396
     * Collects the foreign key column details for the given table.
397
     *
398
     * @param TableSchema $table the table metadata
399
     *
400
     * @throws Exception
401
     * @throws InvalidArgumentException
402
     * @throws InvalidConfigException
403
     */
404 91
    protected function findConstraints(TableSchema $table)
405
    {
406 91
        $tableName = $this->quoteValue($table->getName());
407 91
        $tableSchema = $this->quoteValue($table->getSchemaName());
408
409
        /**
410
         * We need to extract the constraints de hard way since:
411
         * {@see http://www.postgresql.org/message-id/[email protected]}
412
         */
413
414
        $sql = <<<SQL
415 91
select
416
    ct.conname as constraint_name,
417
    a.attname as column_name,
418
    fc.relname as foreign_table_name,
419
    fns.nspname as foreign_table_schema,
420
    fa.attname as foreign_column_name
421
from
422
    (SELECT ct.conname, ct.conrelid, ct.confrelid, ct.conkey, ct.contype, ct.confkey, generate_subscripts(ct.conkey, 1) AS s
423
       FROM pg_constraint ct
424
    ) AS ct
425
    inner join pg_class c on c.oid=ct.conrelid
426
    inner join pg_namespace ns on c.relnamespace=ns.oid
427
    inner join pg_attribute a on a.attrelid=ct.conrelid and a.attnum = ct.conkey[ct.s]
428
    left join pg_class fc on fc.oid=ct.confrelid
429
    left join pg_namespace fns on fc.relnamespace=fns.oid
430
    left join pg_attribute fa on fa.attrelid=ct.confrelid and fa.attnum = ct.confkey[ct.s]
431
where
432
    ct.contype='f'
433 91
    and c.relname={$tableName}
434 91
    and ns.nspname={$tableSchema}
435
order by
436
    fns.nspname, fc.relname, a.attnum
437
SQL;
438
439 91
        $constraints = [];
440
441 91
        foreach ($this->getDb()->createCommand($sql)->queryAll() as $constraint) {
442 9
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
443
                $constraint = \array_change_key_case($constraint, CASE_LOWER);
444
            }
445 9
            if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
446
                $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
447
            } else {
448 9
                $foreignTable = $constraint['foreign_table_name'];
449
            }
450 9
            $name = $constraint['constraint_name'];
451 9
            if (!isset($constraints[$name])) {
452 9
                $constraints[$name] = [
453 9
                    'tableName' => $foreignTable,
454
                    'columns' => [],
455
                ];
456
            }
457 9
            $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name'];
458
        }
459
460 91
        foreach ($constraints as $name => $constraint) {
461 9
            $table->foreignKey($name, \array_merge([$constraint['tableName']], $constraint['columns']));
462
        }
463 91
    }
464
465
    /**
466
     * Gets information about given table unique indexes.
467
     *
468
     * @param TableSchema $table the table metadata
469
     *
470
     * @throws Exception
471
     * @throws InvalidArgumentException
472
     * @throws InvalidConfigException
473
     *
474
     * @return array with index and column names
475
     */
476 1
    protected function getUniqueIndexInformation(TableSchema $table): array
477
    {
478
        $sql = <<<'SQL'
479 1
SELECT
480
    i.relname as indexname,
481
    pg_get_indexdef(idx.indexrelid, k + 1, TRUE) AS columnname
482
FROM (
483
  SELECT *, generate_subscripts(indkey, 1) AS k
484
  FROM pg_index
485
) idx
486
INNER JOIN pg_class i ON i.oid = idx.indexrelid
487
INNER JOIN pg_class c ON c.oid = idx.indrelid
488
INNER JOIN pg_namespace ns ON c.relnamespace = ns.oid
489
WHERE idx.indisprimary = FALSE AND idx.indisunique = TRUE
490
AND c.relname = :tableName AND ns.nspname = :schemaName
491
ORDER BY i.relname, k
492
SQL;
493
494 1
        return $this->getDb()->createCommand($sql, [
495 1
            ':schemaName' => $table->getSchemaName(),
496 1
            ':tableName' => $table->getName(),
497 1
        ])->queryAll();
498
    }
499
500
    /**
501
     * Returns all unique indexes for the given table.
502
     *
503
     * Each array element is of the following structure:
504
     *
505
     * ```php
506
     * [
507
     *     'IndexName1' => ['col1' [, ...]],
508
     *     'IndexName2' => ['col2' [, ...]],
509
     * ]
510
     * ```
511
     *
512
     * @param TableSchema $table the table metadata
513
     *
514
     * @throws Exception
515
     * @throws InvalidArgumentException
516
     * @throws InvalidConfigException
517
     *
518
     * @return array all unique indexes for the given table.
519
     */
520 1
    public function findUniqueIndexes($table): array
521
    {
522 1
        $uniqueIndexes = [];
523
524 1
        foreach ($this->getUniqueIndexInformation($table) as $row) {
525 1
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
526
                $row = \array_change_key_case($row, CASE_LOWER);
527
            }
528 1
            $column = $row['columnname'];
529 1
            if (!empty($column) && $column[0] === '"') {
530
                /**
531
                 * postgres will quote names that are not lowercase-only.
532
                 *
533
                 * {@see https://github.com/yiisoft/yii2/issues/10613}
534
                 */
535 1
                $column = \substr($column, 1, -1);
536
            }
537 1
            $uniqueIndexes[$row['indexname']][] = $column;
538
        }
539
540 1
        return $uniqueIndexes;
541
    }
542
543
    /**
544
     * Collects the metadata of table columns.
545
     *
546
     * @param TableSchema $table the table metadata
547
     *
548
     * @throws Exception
549
     * @throws InvalidArgumentException
550
     * @throws InvalidConfigException
551
     * @throws NotSupportedException
552
     *
553
     * @return bool whether the table exists in the database
554
     */
555 98
    protected function findColumns(TableSchema $table): bool
556
    {
557 98
        $tableName = $this->getDb()->quoteValue($table->getName());
558 98
        $schemaName = $this->getDb()->quoteValue($table->getSchemaName());
559
560 98
        $orIdentity = '';
561 98
562
        if (version_compare($this->getDb()->getServerVersion(), '12.0', '>=')) {
563
            $orIdentity = 'OR a.attidentity != \'\'';
564
        }
565
566 98
        $sql = <<<SQL
567
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 98
    CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
578
    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 98
    a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped
623 98
    AND c.relname = {$tableName}
624
    AND d.nspname = {$schemaName}
625
ORDER BY
626
    a.attnum;
627 98
SQL;
628 98
629 18
        $columns = $this->getDb()->createCommand($sql)->queryAll();
630
631 91
        if (empty($columns)) {
632 91
            return false;
633
        }
634
635 91
        foreach ($columns as $column) {
636 91
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
637 91
                $column = array_change_key_case($column, CASE_LOWER);
638 62
            }
639 62
640 62
            $column = $this->loadColumnSchema($column);
641
            $table->columns($column->getName(), $column);
642 62
643 90
            if ($column->isPrimaryKey()) {
644 54
                $table->primaryKey($column->getName());
645 27
646 54
                if ($table->getSequenceName() === null) {
647 51
                    $table->sequenceName($column->getSequenceName());
648 30
                }
649 27
650 30
                $column->defaultValue(null);
651
            } elseif ($column->getDefaultValue()) {
652 30
                if (
653 28
                    in_array(
654 29
                        $column->getType(),
655 29
                        [self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME]
656 4
                    ) &&
657
                    in_array(
658 29
                        $column->getDefaultValue(),
659
                        ['now()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME']
660
                    )
661
                ) {
662
                    $column->defaultValue(new Expression($column->getDefaultValue()));
663
                } elseif ($column->getType() === 'boolean') {
664
                    $column->defaultValue(($column->getDefaultValue() === 'true'));
665
                } elseif (preg_match("/^B'(.*?)'::/", $column->getDefaultValue(), $matches)) {
666 91
                    $column->defaultValue(bindec($matches[1]));
667
                } elseif (
668
                    (
669
                        strncasecmp($column->getDbType(), 'bit', 3) === 0 ||
670
                        strncasecmp($column->getDbType(), 'varbit', 6) === 0
671
                    ) &&
672
                    preg_match("#^'(\d+)'::\"bit\"$#", $column->getDefaultValue(), $matches)
673
                ) {
674
                    $column->defaultValue(bindec($matches[1]));
675
                } elseif (\preg_match("/^'(.*?)'::/", $column->getDefaultValue(), $matches)) {
676 91
                    $column->defaultValue($column->phpTypecast($matches[1]));
677
                } elseif (\preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $column->getDefaultValue(), $matches)) {
678
                    if ($matches[2] === 'NULL') {
679 91
                        $column->defaultValue(null);
680 91
                    } else {
681 91
                        $column->defaultValue($column->phpTypecast($matches[2]));
682 91
                    }
683 91
                } else {
684 91
                    $column->defaultValue($column->phpTypecast($column->getDefaultValue()));
685 91
                }
686 91
            }
687 91
        }
688 91
689 91
        return true;
690 91
    }
691 91
692 91
    /**
693 91
     * Loads the column information into a {@see ColumnSchema} object.
694
     *
695
     * @param array $info column information
696
     *
697
     * @return ColumnSchema the column schema object
698
     */
699
    protected function loadColumnSchema(array $info): ColumnSchema
700 91
    {
701 91
        /** @var ColumnSchema $column */
702 59
        $column = $this->createColumnSchema();
703 59
        $column->allowNull($info['is_nullable']);
704 59
        $column->autoIncrement($info['is_autoinc']);
705 59
        $column->comment($info['column_comment']);
706
        $column->dbType($info['data_type']);
707 91
        $column->defaultValue($info['column_default']);
708
        $column->enumValues(($info['enum_values'] !== null)
709
            ? \explode(',', \str_replace(["''"], ["'"], $info['enum_values'])) : null);
710
        $column->unsigned(false); // has no meaning in PG
711 91
        $column->primaryKey((bool) $info['is_pkey']);
712 91
        $column->name($info['column_name']);
713
        $column->precision($info['numeric_precision']);
714
        $column->scale($info['numeric_scale']);
715
        $column->size($info['size'] === null ? null : (int) $info['size']);
716 91
        $column->dimension((int) $info['dimension']);
717
718 91
        /**
719
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
720
         * default value
721
         */
722
723
        $defaultValue = $column->getDefaultValue();
724
        if (isset($defaultValue) && \preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1) {
725
            $column->sequenceName(\preg_replace(
726
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
727
                '',
728
                $defaultValue
729
            ));
730
        } elseif (isset($info['sequence_name'])) {
731
            $column->sequenceName($this->resolveTableName($info['sequence_name'])->getFullName());
732
        }
733
734
        if (isset($this->typeMap[$column->getDbType()])) {
735
            $column->type($this->typeMap[$column->getDbType()]);
736
        } else {
737
            $column->type(self::TYPE_STRING);
738
        }
739
        $column->phpType($this->getColumnPhpType($column));
740
741
        return $column;
742
    }
743
744
    /**
745
     * Executes the INSERT command, returning primary key values.
746
     *
747
     * @param string $table the table that new rows will be inserted into.
748
     * @param array $columns the column data (name => value) to be inserted into the table.
749
     *
750
     * @throws Exception
751
     * @throws InvalidArgumentException
752
     * @throws InvalidConfigException
753
     * @throws NotSupportedException
754
     *
755
     * @return array|false primary key values or false if the command fails.
756
     */
757
    public function insert(string $table, array $columns)
758
    {
759
        $params = [];
760
        $sql = $this->getDb()->getQueryBuilder()->insert($table, $columns, $params);
761
        $returnColumns = $this->getTableSchema($table)->getPrimaryKey();
762
        if (!empty($returnColumns)) {
763
            $returning = [];
764
            foreach ((array) $returnColumns as $name) {
765
                $returning[] = $this->quoteColumnName($name);
766
            }
767
            $sql .= ' RETURNING ' . \implode(', ', $returning);
768
        }
769
770 61
        $command = $this->getDb()->createCommand($sql, $params);
771
        $command->prepare(false);
772 61
        $result = $command->queryOne();
773
774
        return !$command->getPdoStatement()->rowCount() ? false : $result;
775
    }
776
777
    /**
778
     * Loads multiple types of constraints and returns the specified ones.
779
     *
780
     * @param string $tableName table name.
781
     * @param string $returnType return type:
782
     * - primaryKey
783
     * - foreignKeys
784
     * - uniques
785
     * - checks
786
     *
787
     * @throws Exception
788
     * @throws InvalidArgumentException
789
     * @throws InvalidConfigException
790
     *
791
     * @return mixed constraints.
792
     */
793
    private function loadTableConstraints(string $tableName, string $returnType)
794
    {
795
        static $sql = <<<'SQL'
796
SELECT
797
    "c"."conname" AS "name",
798
    "a"."attname" AS "column_name",
799 61
    "c"."contype" AS "type",
800
    "ftcns"."nspname" AS "foreign_table_schema",
801
    "ftc"."relname" AS "foreign_table_name",
802
    "fa"."attname" AS "foreign_column_name",
803
    "c"."confupdtype" AS "on_update",
804
    "c"."confdeltype" AS "on_delete",
805
    pg_get_constraintdef("c"."oid") AS "check_expr"
806
FROM "pg_class" AS "tc"
807 61
INNER JOIN "pg_namespace" AS "tcns"
808 61
    ON "tcns"."oid" = "tc"."relnamespace"
809 61
INNER JOIN "pg_constraint" AS "c"
810 61
    ON "c"."conrelid" = "tc"."oid"
811 61
INNER JOIN "pg_attribute" AS "a"
812 61
    ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
813 61
LEFT JOIN "pg_class" AS "ftc"
814
    ON "ftc"."oid" = "c"."confrelid"
815 61
LEFT JOIN "pg_namespace" AS "ftcns"
816
    ON "ftcns"."oid" = "ftc"."relnamespace"
817
LEFT JOIN "pg_attribute" "fa"
818
    ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
819
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
820 61
ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
821 61
SQL;
822 61
        static $actionTypes = [
823 61
            'a' => 'NO ACTION',
824 46
            'r' => 'RESTRICT',
825 46
            'c' => 'CASCADE',
826 46
            'n' => 'SET NULL',
827
            'd' => 'SET DEFAULT',
828 46
        ];
829 46
830 59
        $resolvedName = $this->resolveTableName($tableName);
831 13
        $constraints = $this->getDb()->createCommand($sql, [
832 13
            ':schemaName' => $resolvedName->getSchemaName(),
833 13
            ':tableName' => $resolvedName->getName(),
834 13
        ])->queryAll();
835
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
836 13
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
837 13
        $result = [
838 13
            'primaryKey' => null,
839 13
            'foreignKeys' => [],
840
            'uniques' => [],
841 13
            'checks' => [],
842 13
        ];
843
        foreach ($constraints as $type => $names) {
844 13
            foreach ($names as $name => $constraint) {
845 13
                switch ($type) {
846 47
                    case 'p':
847 46
                        $ct = (new Constraint())
848 46
                            ->name($name)
849 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
850
851 46
                        $result['primaryKey'] = $ct;
852 46
                        break;
853 10
                    case 'f':
854 10
                        $fk = (new ForeignKeyConstraint())
855 10
                            ->name($name)
856 10
                            ->columnNames(\array_values(
857 10
                                \array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
858
                            ))
859 10
                            ->foreignColumnNames($constraint[0]['foreign_table_schema'])
860 10
                            ->foreignTableName($constraint[0]['foreign_table_name'])
861
                            ->foreignColumnNames(\array_values(
862
                                \array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
863
                            ))
864 61
                            ->onDelete($actionTypes[$constraint[0]['on_delete']] ?? null)
865 61
                            ->onUpdate($actionTypes[$constraint[0]['on_update']] ?? null);
866
867
                        $result['foreignKeys'][] = $fk;
868 61
                        break;
869
                    case 'u':
870
                        $ct = (new Constraint())
871
                            ->name($name)
872
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
873
874
                        $result['uniques'][] = $ct;
875
                        break;
876
                    case 'c':
877
                        $ck = (new CheckConstraint())
878 91
                            ->name($name)
879
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
880 91
                            ->expression($constraint[0]['check_expr']);
881
882
                        $result['checks'][] = $ck;
883 1
                        break;
884
                }
885
            }
886
        }
887
        foreach ($result as $type => $data) {
888
            $this->setTableMetadata($tableName, $type, $data);
889
        }
890
891
        return $result[$returnType];
892
    }
893
894
    /**
895
     * Creates a column schema for the database.
896
     *
897
     * This method may be overridden by child classes to create a DBMS-specific column schema.
898
     *
899
     * @return ColumnSchema column schema instance.
900
     */
901
    protected function createColumnSchema(): ColumnSchema
902
    {
903
        return new ColumnSchema();
904
    }
905
}
906