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

Schema::findColumns()   C

Complexity

Conditions 17
Paths 48

Size

Total Lines 112
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 17.289

Importance

Changes 0
Metric Value
cc 17
eloc 53
c 0
b 0
f 0
nc 48
nop 1
dl 0
loc 112
ccs 36
cts 40
cp 0.9
crap 17.289
rs 5.2166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Pgsql\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