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