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

Schema::loadTableConstraints()   B

Complexity

Conditions 8
Paths 14

Size

Total Lines 99
Code Lines 83

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 83
c 0
b 0
f 0
nc 14
nop 2
dl 0
loc 99
ccs 49
cts 49
cp 1
crap 8
rs 7.1264

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 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
        if (\version_compare($this->getDb()->getServerVersion(), '12.0', '>=')) {
562
            $orIdentity = 'OR attidentity != \'\'';
563
        }
564
565
        $sql = <<<SQL
566 98
SELECT
567
    d.nspname AS table_schema,
568
    c.relname AS table_name,
569
    a.attname AS column_name,
570
    COALESCE(td.typname, tb.typname, t.typname) AS data_type,
571
    COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
572
    a.attlen AS character_maximum_length,
573
    pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
574
    a.atttypmod AS modifier,
575
    a.attnotnull = false AS is_nullable,
576
    CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
577 98
    coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) {$orIdentity} AS is_autoinc,
578
    pg_get_serial_sequence(quote_ident(d.nspname) || '.' || quote_ident(c.relname), a.attname) AS sequence_name,
579
    CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
580
        THEN array_to_string((SELECT array_agg(enumlabel) FROM pg_enum WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid))::varchar[], ',')
581
        ELSE NULL
582
    END AS enum_values,
583
    CASE atttypid
584
         WHEN 21 /*int2*/ THEN 16
585
         WHEN 23 /*int4*/ THEN 32
586
         WHEN 20 /*int8*/ THEN 64
587
         WHEN 1700 /*numeric*/ THEN
588
              CASE WHEN atttypmod = -1
589
               THEN null
590
               ELSE ((atttypmod - 4) >> 16) & 65535
591
               END
592
         WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/
593
         WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/
594
         ELSE null
595
      END   AS numeric_precision,
596
      CASE
597
        WHEN atttypid IN (21, 23, 20) THEN 0
598
        WHEN atttypid IN (1700) THEN
599
        CASE
600
            WHEN atttypmod = -1 THEN null
601
            ELSE (atttypmod - 4) & 65535
602
        END
603
           ELSE null
604
      END AS numeric_scale,
605
    CAST(
606
             information_schema._pg_char_max_length(information_schema._pg_truetypid(a, t), information_schema._pg_truetypmod(a, t))
607
             AS numeric
608
    ) AS size,
609
    a.attnum = any (ct.conkey) as is_pkey,
610
    COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
611
FROM
612
    pg_class c
613
    LEFT JOIN pg_attribute a ON a.attrelid = c.oid
614
    LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
615
    LEFT JOIN pg_type t ON a.atttypid = t.oid
616
    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
617
    LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
618
    LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
619
    LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
620
WHERE
621
    a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped
622 98
    AND c.relname = {$tableName}
623 98
    AND d.nspname = {$schemaName}
624
ORDER BY
625
    a.attnum;
626
SQL;
627 98
        $columns = $this->getDb()->createCommand($sql)->queryAll();
628 98
        if (empty($columns)) {
629 18
            return false;
630
        }
631 91
        foreach ($columns as $column) {
632 91
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
633
                $column = \array_change_key_case($column, CASE_LOWER);
634
            }
635 91
            $column = $this->loadColumnSchema($column);
636 91
            $table->columns($column->getName(), $column);
637 91
            if ($column->isPrimaryKey()) {
638 62
                $table->primaryKey($column->getName());
639 62
                if ($table->getSequenceName() === null) {
640 62
                    $table->sequenceName($column->getSequenceName());
641
                }
642 62
                $column->defaultValue(null);
643 90
            } elseif ($column->getDefaultValue()) {
644 54
                if ($column->getType() === 'timestamp' && $column->getDefaultValue() === 'now()') {
645 27
                    $column->defaultValue(new Expression($column->getDefaultValue()));
646 54
                } elseif ($column->getType() === 'boolean') {
647 51
                    $column->defaultValue(($column->getDefaultValue() === 'true'));
648 30
                } elseif (\preg_match("/^B'(.*?)'::/", $column->getDefaultValue(), $matches)) {
649 27
                    $column->defaultValue(\bindec($matches[1]));
650 30
                } elseif (\strncasecmp($column->getDbType(), 'bit', 3) === 0 || \strncasecmp($column->getDbType(), 'varbit', 6) === 0) {
651
                    $column->defaultValue(\bindec(\trim($column->getDefaultValue(), 'B\'')));
652 30
                } elseif (\preg_match("/^'(.*?)'::/", $column->getDefaultValue(), $matches)) {
653 28
                    $column->defaultValue($column->phpTypecast($matches[1]));
654 29
                } elseif (\preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $column->getDefaultValue(), $matches)) {
655 29
                    if ($matches[2] === 'NULL') {
656 4
                        $column->defaultValue(null);
657
                    } else {
658 29
                        $column->defaultValue($column->phpTypecast($matches[2]));
659
                    }
660
                } else {
661
                    $column->defaultValue($column->phpTypecast($column->getDefaultValue()));
662
                }
663
            }
664
        }
665
666 91
        return true;
667
    }
668
669
    /**
670
     * Loads the column information into a {@see ColumnSchema} object.
671
     *
672
     * @param array $info column information
673
     *
674
     * @return ColumnSchema the column schema object
675
     */
676 91
    protected function loadColumnSchema(array $info): ColumnSchema
677
    {
678
        /** @var ColumnSchema $column */
679 91
        $column = $this->createColumnSchema();
680 91
        $column->allowNull($info['is_nullable']);
681 91
        $column->autoIncrement($info['is_autoinc']);
682 91
        $column->comment($info['column_comment']);
683 91
        $column->dbType($info['data_type']);
684 91
        $column->defaultValue($info['column_default']);
685 91
        $column->enumValues(($info['enum_values'] !== null)
686 91
            ? \explode(',', \str_replace(["''"], ["'"], $info['enum_values'])) : null);
687 91
        $column->unsigned(false); // has no meaning in PG
688 91
        $column->primaryKey((bool) $info['is_pkey']);
689 91
        $column->name($info['column_name']);
690 91
        $column->precision($info['numeric_precision']);
691 91
        $column->scale($info['numeric_scale']);
692 91
        $column->size($info['size'] === null ? null : (int) $info['size']);
693 91
        $column->dimension((int) $info['dimension']);
694
695
        /**
696
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
697
         * default value
698
         */
699
700 91
        $defaultValue = $column->getDefaultValue();
701 91
        if (isset($defaultValue) && \preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1) {
702 59
            $column->sequenceName(\preg_replace(
703 59
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
704 59
                '',
705 59
                $defaultValue
706
            ));
707 91
        } elseif (isset($info['sequence_name'])) {
708
            $column->sequenceName($this->resolveTableName($info['sequence_name'])->getFullName());
709
        }
710
711 91
        if (isset($this->typeMap[$column->getDbType()])) {
712 91
            $column->type($this->typeMap[$column->getDbType()]);
713
        } else {
714
            $column->type(self::TYPE_STRING);
715
        }
716 91
        $column->phpType($this->getColumnPhpType($column));
717
718 91
        return $column;
719
    }
720
721
    /**
722
     * Executes the INSERT command, returning primary key values.
723
     *
724
     * @param string $table the table that new rows will be inserted into.
725
     * @param array $columns the column data (name => value) to be inserted into the table.
726
     *
727
     * @throws Exception
728
     * @throws InvalidArgumentException
729
     * @throws InvalidConfigException
730
     * @throws NotSupportedException
731
     *
732
     * @return array|false primary key values or false if the command fails.
733
     */
734
    public function insert(string $table, array $columns)
735
    {
736
        $params = [];
737
        $sql = $this->getDb()->getQueryBuilder()->insert($table, $columns, $params);
738
        $returnColumns = $this->getTableSchema($table)->getPrimaryKey();
739
        if (!empty($returnColumns)) {
740
            $returning = [];
741
            foreach ((array) $returnColumns as $name) {
742
                $returning[] = $this->quoteColumnName($name);
743
            }
744
            $sql .= ' RETURNING ' . \implode(', ', $returning);
745
        }
746
747
        $command = $this->getDb()->createCommand($sql, $params);
748
        $command->prepare(false);
749
        $result = $command->queryOne();
750
751
        return !$command->getPdoStatement()->rowCount() ? false : $result;
752
    }
753
754
    /**
755
     * Loads multiple types of constraints and returns the specified ones.
756
     *
757
     * @param string $tableName table name.
758
     * @param string $returnType return type:
759
     * - primaryKey
760
     * - foreignKeys
761
     * - uniques
762
     * - checks
763
     *
764
     * @throws Exception
765
     * @throws InvalidArgumentException
766
     * @throws InvalidConfigException
767
     *
768
     * @return mixed constraints.
769
     */
770 61
    private function loadTableConstraints(string $tableName, string $returnType)
771
    {
772 61
        static $sql = <<<'SQL'
773
SELECT
774
    "c"."conname" AS "name",
775
    "a"."attname" AS "column_name",
776
    "c"."contype" AS "type",
777
    "ftcns"."nspname" AS "foreign_table_schema",
778
    "ftc"."relname" AS "foreign_table_name",
779
    "fa"."attname" AS "foreign_column_name",
780
    "c"."confupdtype" AS "on_update",
781
    "c"."confdeltype" AS "on_delete",
782
    pg_get_constraintdef("c"."oid") AS "check_expr"
783
FROM "pg_class" AS "tc"
784
INNER JOIN "pg_namespace" AS "tcns"
785
    ON "tcns"."oid" = "tc"."relnamespace"
786
INNER JOIN "pg_constraint" AS "c"
787
    ON "c"."conrelid" = "tc"."oid"
788
INNER JOIN "pg_attribute" AS "a"
789
    ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
790
LEFT JOIN "pg_class" AS "ftc"
791
    ON "ftc"."oid" = "c"."confrelid"
792
LEFT JOIN "pg_namespace" AS "ftcns"
793
    ON "ftcns"."oid" = "ftc"."relnamespace"
794
LEFT JOIN "pg_attribute" "fa"
795
    ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
796
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
797
ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
798
SQL;
799 61
        static $actionTypes = [
800
            'a' => 'NO ACTION',
801
            'r' => 'RESTRICT',
802
            'c' => 'CASCADE',
803
            'n' => 'SET NULL',
804
            'd' => 'SET DEFAULT',
805
        ];
806
807 61
        $resolvedName = $this->resolveTableName($tableName);
808 61
        $constraints = $this->getDb()->createCommand($sql, [
809 61
            ':schemaName' => $resolvedName->getSchemaName(),
810 61
            ':tableName' => $resolvedName->getName(),
811 61
        ])->queryAll();
812 61
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
813 61
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
814
        $result = [
815 61
            'primaryKey' => null,
816
            'foreignKeys' => [],
817
            'uniques' => [],
818
            'checks' => [],
819
        ];
820 61
        foreach ($constraints as $type => $names) {
821 61
            foreach ($names as $name => $constraint) {
822 61
                switch ($type) {
823 61
                    case 'p':
824 46
                        $ct = (new Constraint())
825 46
                            ->name($name)
826 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
827
828 46
                        $result['primaryKey'] = $ct;
829 46
                        break;
830 59
                    case 'f':
831 13
                        $fk = (new ForeignKeyConstraint())
832 13
                            ->name($name)
833 13
                            ->columnNames(\array_values(
834 13
                                \array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
835
                            ))
836 13
                            ->foreignColumnNames($constraint[0]['foreign_table_schema'])
837 13
                            ->foreignTableName($constraint[0]['foreign_table_name'])
838 13
                            ->foreignColumnNames(\array_values(
839 13
                                \array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
840
                            ))
841 13
                            ->onDelete($actionTypes[$constraint[0]['on_delete']] ?? null)
842 13
                            ->onUpdate($actionTypes[$constraint[0]['on_update']] ?? null);
843
844 13
                        $result['foreignKeys'][] = $fk;
845 13
                        break;
846 47
                    case 'u':
847 46
                        $ct = (new Constraint())
848 46
                            ->name($name)
849 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
850
851 46
                        $result['uniques'][] = $ct;
852 46
                        break;
853 10
                    case 'c':
854 10
                        $ck = (new CheckConstraint())
855 10
                            ->name($name)
856 10
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
857 10
                            ->expression($constraint[0]['check_expr']);
858
859 10
                        $result['checks'][] = $ck;
860 10
                        break;
861
                }
862
            }
863
        }
864 61
        foreach ($result as $type => $data) {
865 61
            $this->setTableMetadata($tableName, $type, $data);
866
        }
867
868 61
        return $result[$returnType];
869
    }
870
871
    /**
872
     * Creates a column schema for the database.
873
     *
874
     * This method may be overridden by child classes to create a DBMS-specific column schema.
875
     *
876
     * @return ColumnSchema column schema instance.
877
     */
878 91
    protected function createColumnSchema(): ColumnSchema
879
    {
880 91
        return new ColumnSchema();
881
    }
882
}
883