Passed
Pull Request — master (#24)
by Wilmer
17:04 queued 01:59
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
                ->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 25
296
            $result[] = $ic;
297 25
        }
298
299
        return $result;
300 28
    }
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
    protected function loadTableUniques(string $tableName): array
314 13
    {
315
        return $this->loadTableConstraints($tableName, 'uniques');
316 13
    }
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
    protected function loadTableChecks(string $tableName): array
330 13
    {
331
        return $this->loadTableConstraints($tableName, 'checks');
332 13
    }
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
    protected function loadTableDefaultValues($tableName): array
344 12
    {
345
        throw new NotSupportedException('PostgreSQL does not support default value constraints.');
346 12
    }
347
348
    /**
349
     * Creates a query builder for the PostgreSQL database.
350
     *
351
     * @return QueryBuilder query builder instance
352
     */
353
    public function createQueryBuilder(): QueryBuilder
354 68
    {
355
        return new QueryBuilder($this->getDb());
356 68
    }
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
    protected function resolveTableNames(TableSchema $table, string $name): void
365 98
    {
366
        $parts = \explode('.', \str_replace('"', '', $name));
367 98
368
        if (isset($parts[1])) {
369 98
            $table->schemaName($parts[0]);
370
            $table->name($parts[1]);
371
        } else {
372
            $table->schemaName($this->defaultSchema);
373 98
            $table->name($parts[0]);
374 98
        }
375
376
        $table->fullName($table->getSchemaName() !== $this->defaultSchema ? $table->getSchemaName() . '.'
377 98
            . $table->getName() : $table->getName());
378 98
    }
379 98
380
    protected function findViewNames(string $schema = '')
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
    protected function findConstraints(TableSchema $table)
405 91
    {
406
        $tableName = $this->quoteValue($table->getName());
407 91
        $tableSchema = $this->quoteValue($table->getSchemaName());
408 91
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
select
416 91
    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
    and c.relname={$tableName}
434 91
    and ns.nspname={$tableSchema}
435 91
order by
436
    fns.nspname, fc.relname, a.attnum
437
SQL;
438
439
        $constraints = [];
440 91
441
        foreach ($this->getDb()->createCommand($sql)->queryAll() as $constraint) {
442 91
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
443 9
                $constraint = \array_change_key_case($constraint, CASE_LOWER);
444
            }
445
            if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
446 9
                $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
447
            } else {
448
                $foreignTable = $constraint['foreign_table_name'];
449 9
            }
450
            $name = $constraint['constraint_name'];
451 9
            if (!isset($constraints[$name])) {
452 9
                $constraints[$name] = [
453 9
                    'tableName' => $foreignTable,
454 9
                    'columns' => [],
455
                ];
456
            }
457
            $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name'];
458 9
        }
459
460
        foreach ($constraints as $name => $constraint) {
461 91
            $table->foreignKey($name, \array_merge([$constraint['tableName']], $constraint['columns']));
462 9
        }
463
    }
464 91
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
    protected function getUniqueIndexInformation(TableSchema $table): array
477 1
    {
478
        $sql = <<<'SQL'
479
SELECT
480 1
    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
        return $this->getDb()->createCommand($sql, [
495 1
            ':schemaName' => $table->getSchemaName(),
496 1
            ':tableName' => $table->getName(),
497 1
        ])->queryAll();
498 1
    }
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
    public function findUniqueIndexes($table): array
521 1
    {
522
        $uniqueIndexes = [];
523 1
524
        foreach ($this->getUniqueIndexInformation($table) as $row) {
525 1
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
526 1
                $row = \array_change_key_case($row, CASE_LOWER);
527
            }
528
            $column = $row['columnname'];
529 1
            if (!empty($column) && $column[0] === '"') {
530 1
                /**
531
                 * postgres will quote names that are not lowercase-only.
532
                 *
533
                 * {@see https://github.com/yiisoft/yii2/issues/10613}
534
                 */
535
                $column = \substr($column, 1, -1);
536 1
            }
537
            $uniqueIndexes[$row['indexname']][] = $column;
538 1
        }
539
540
        return $uniqueIndexes;
541 1
    }
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
    protected function findColumns(TableSchema $table): bool
556 98
    {
557
        $tableName = $this->getDb()->quoteValue($table->getName());
558 98
        $schemaName = $this->getDb()->quoteValue($table->getSchemaName());
559 98
560
        $orIdentity = '';
561 98
        if (\version_compare($this->getDb()->getServerVersion(), '12.0', '>=')) {
562 98
            $orIdentity = 'OR attidentity != \'\'';
563
        }
564
565
        $sql = <<<SQL
566
SELECT
567 98
    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
    coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) {$orIdentity} AS is_autoinc,
578 98
    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
    AND c.relname = {$tableName}
623 98
    AND d.nspname = {$schemaName}
624 98
ORDER BY
625
    a.attnum;
626
SQL;
627
        $columns = $this->getDb()->createCommand($sql)->queryAll();
628 98
        if (empty($columns)) {
629 98
            return false;
630 18
        }
631
        foreach ($columns as $column) {
632 91
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
633 91
                $column = \array_change_key_case($column, CASE_LOWER);
634
            }
635
            $column = $this->loadColumnSchema($column);
636 91
            $table->columns($column->getName(), $column);
637 91
            if ($column->isPrimaryKey()) {
638 91
                $table->primaryKey($column->getName());
639 62
                if ($table->getSequenceName() === null) {
640 62
                    $table->sequenceName($column->getSequenceName());
641 62
                }
642
                $column->defaultValue(null);
643 62
            } elseif ($column->getDefaultValue()) {
644 90
                if ($column->getType() === 'timestamp' && $column->getDefaultValue() === 'now()') {
645 54
                    $column->defaultValue(new Expression($column->getDefaultValue()));
646 27
                } elseif ($column->getType() === 'boolean') {
647 54
                    $column->defaultValue(($column->getDefaultValue() === 'true'));
648 51
                } elseif (\preg_match("/^B'(.*?)'::/", $column->getDefaultValue(), $matches)) {
649 30
                    $column->defaultValue(\bindec($matches[1]));
650 27
                } elseif (\strncasecmp($column->getDbType(), 'bit', 3) === 0 || \strncasecmp($column->getDbType(), 'varbit', 6) === 0) {
651 30
                    $column->defaultValue(\bindec(\trim($column->getDefaultValue(), 'B\'')));
652
                } elseif (\preg_match("/^'(.*?)'::/", $column->getDefaultValue(), $matches)) {
653 30
                    $column->defaultValue($column->phpTypecast($matches[1]));
654 28
                } elseif (\preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $column->getDefaultValue(), $matches)) {
655 29
                    if ($matches[2] === 'NULL') {
656 29
                        $column->defaultValue(null);
657 4
                    } else {
658
                        $column->defaultValue($column->phpTypecast($matches[2]));
659 29
                    }
660
                } else {
661
                    $column->defaultValue($column->phpTypecast($column->getDefaultValue()));
662
                }
663
            }
664
        }
665
666
        return true;
667 91
    }
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
    protected function loadColumnSchema(array $info): ColumnSchema
677 91
    {
678
        /** @var ColumnSchema $column */
679
        $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 91
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
        $defaultValue = $column->getDefaultValue();
701 91
        if (isset($defaultValue) && \preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1) {
702 91
            $column->sequenceName(\preg_replace(
703 59
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
704 59
                '',
705 59
                $defaultValue
706 59
            ));
707
        } elseif (isset($info['sequence_name'])) {
708 91
            $column->sequenceName($this->resolveTableName($info['sequence_name'])->getFullName());
709
        }
710
711
        if (isset($this->typeMap[$column->getDbType()])) {
712 91
            $column->type($this->typeMap[$column->getDbType()]);
713 91
        } else {
714
            $column->type(self::TYPE_STRING);
715
        }
716
        $column->phpType($this->getColumnPhpType($column));
717 91
718
        return $column;
719 91
    }
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
    private function loadTableConstraints(string $tableName, string $returnType)
771 61
    {
772
        static $sql = <<<'SQL'
773 61
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
        static $actionTypes = [
800 61
            'a' => 'NO ACTION',
801
            'r' => 'RESTRICT',
802
            'c' => 'CASCADE',
803
            'n' => 'SET NULL',
804
            'd' => 'SET DEFAULT',
805
        ];
806
807
        $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 61
        $result = [
815
            'primaryKey' => null,
816 61
            'foreignKeys' => [],
817
            'uniques' => [],
818
            'checks' => [],
819
        ];
820
        foreach ($constraints as $type => $names) {
821 61
            foreach ($names as $name => $constraint) {
822 61
                switch ($type) {
823 61
                    case 'p':
824 61
                        $ct = (new Constraint())
825 46
                            ->name($name)
826 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
827 46
828
                        $result['primaryKey'] = $ct;
829 46
                        break;
830 46
                    case 'f':
831 59
                        $fk = (new ForeignKeyConstraint())
832 13
                            ->name($name)
833 13
                            ->columnNames(\array_values(
834 13
                                \array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
835 13
                            ))
836
                            ->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 13
                            ))
841
                            ->onDelete($actionTypes[$constraint[0]['on_delete']] ?? null)
842 13
                            ->onUpdate($actionTypes[$constraint[0]['on_update']] ?? null);
843 13
844
                        $result['foreignKeys'][] = $fk;
845 13
                        break;
846 13
                    case 'u':
847 47
                        $ct = (new Constraint())
848 46
                            ->name($name)
849 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
850 46
851
                        $result['uniques'][] = $ct;
852 46
                        break;
853 46
                    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 10
859
                        $result['checks'][] = $ck;
860 10
                        break;
861 10
                }
862
            }
863
        }
864
        foreach ($result as $type => $data) {
865 61
            $this->setTableMetadata($tableName, $type, $data);
866 61
        }
867
868
        return $result[$returnType];
869 61
    }
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
    protected function createColumnSchema(): ColumnSchema
879 91
    {
880
        return new ColumnSchema();
881 91
    }
882
}
883