Passed
Push — master ( 769a3d...1d6fef )
by Dmitriy
07:25
created

Schema::findColumns()   C

Complexity

Conditions 16
Paths 48

Size

Total Lines 127
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 16.1922

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 16
eloc 58
c 1
b 0
f 0
nc 48
nop 1
dl 0
loc 127
ccs 40
cts 44
cp 0.9091
crap 16.1922
rs 5.5666

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;
6
7
use JsonException;
8
use PDO;
9
use Throwable;
10
use Yiisoft\Arrays\ArrayHelper;
11
use Yiisoft\Db\Connection\ConnectionInterface;
12
use Yiisoft\Db\Constraint\CheckConstraint;
13
use Yiisoft\Db\Constraint\Constraint;
14
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
15
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
16
use Yiisoft\Db\Constraint\DefaultValueConstraint;
17
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
18
use Yiisoft\Db\Constraint\IndexConstraint;
19
use Yiisoft\Db\Exception\Exception;
20
use Yiisoft\Db\Exception\InvalidConfigException;
21
use Yiisoft\Db\Exception\NotSupportedException;
22
use Yiisoft\Db\Expression\Expression;
23
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
24
use Yiisoft\Db\Schema\Schema as AbstractSchema;
25
use Yiisoft\Db\View\ViewFinderTrait;
26
27
use function array_change_key_case;
28
use function array_merge;
29
use function array_unique;
30
use function array_values;
31
use function bindec;
32
use function explode;
33
use function implode;
34
use function preg_match;
35
use function preg_replace;
36
use function str_replace;
37
use function substr;
38
39
final class Schema extends AbstractSchema implements ConstraintFinderInterface
40
{
41
    use ViewFinderTrait;
42
    use ConstraintFinderTrait;
43
44
    public const TYPE_JSONB = 'jsonb';
45
46
    /**
47
     * @var array mapping from physical column types (keys) to abstract column types (values).
48
     *
49
     * {@see http://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE}
50
     */
51
    private array $typeMap = [
52
        'bit' => self::TYPE_INTEGER,
53
        'bit varying' => self::TYPE_INTEGER,
54
        'varbit' => self::TYPE_INTEGER,
55
        'bool' => self::TYPE_BOOLEAN,
56
        'boolean' => self::TYPE_BOOLEAN,
57
        'box' => self::TYPE_STRING,
58
        'circle' => self::TYPE_STRING,
59
        'point' => self::TYPE_STRING,
60
        'line' => self::TYPE_STRING,
61
        'lseg' => self::TYPE_STRING,
62
        'polygon' => self::TYPE_STRING,
63
        'path' => self::TYPE_STRING,
64
        'character' => self::TYPE_CHAR,
65
        'char' => self::TYPE_CHAR,
66
        'bpchar' => self::TYPE_CHAR,
67
        'character varying' => self::TYPE_STRING,
68
        'varchar' => self::TYPE_STRING,
69
        'text' => self::TYPE_TEXT,
70
        'bytea' => self::TYPE_BINARY,
71
        'cidr' => self::TYPE_STRING,
72
        'inet' => self::TYPE_STRING,
73
        'macaddr' => self::TYPE_STRING,
74
        'real' => self::TYPE_FLOAT,
75
        'float4' => self::TYPE_FLOAT,
76
        'double precision' => self::TYPE_DOUBLE,
77
        'float8' => self::TYPE_DOUBLE,
78
        'decimal' => self::TYPE_DECIMAL,
79
        'numeric' => self::TYPE_DECIMAL,
80
        'money' => self::TYPE_MONEY,
81
        'smallint' => self::TYPE_SMALLINT,
82
        'int2' => self::TYPE_SMALLINT,
83
        'int4' => self::TYPE_INTEGER,
84
        'int' => self::TYPE_INTEGER,
85
        'integer' => self::TYPE_INTEGER,
86
        'bigint' => self::TYPE_BIGINT,
87
        'int8' => self::TYPE_BIGINT,
88
        'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal!
89
        'smallserial' => self::TYPE_SMALLINT,
90
        'serial2' => self::TYPE_SMALLINT,
91
        'serial4' => self::TYPE_INTEGER,
92
        'serial' => self::TYPE_INTEGER,
93
        'bigserial' => self::TYPE_BIGINT,
94
        'serial8' => self::TYPE_BIGINT,
95
        'pg_lsn' => self::TYPE_BIGINT,
96
        'date' => self::TYPE_DATE,
97
        'interval' => self::TYPE_STRING,
98
        'time without time zone' => self::TYPE_TIME,
99
        'time' => self::TYPE_TIME,
100
        'time with time zone' => self::TYPE_TIME,
101
        'timetz' => self::TYPE_TIME,
102
        'timestamp without time zone' => self::TYPE_TIMESTAMP,
103
        'timestamp' => self::TYPE_TIMESTAMP,
104
        'timestamp with time zone' => self::TYPE_TIMESTAMP,
105
        'timestamptz' => self::TYPE_TIMESTAMP,
106
        'abstime' => self::TYPE_TIMESTAMP,
107
        'tsquery' => self::TYPE_STRING,
108
        'tsvector' => self::TYPE_STRING,
109
        'txid_snapshot' => self::TYPE_STRING,
110
        'unknown' => self::TYPE_STRING,
111
        'uuid' => self::TYPE_STRING,
112
        'json' => self::TYPE_JSON,
113
        'jsonb' => self::TYPE_JSON,
114
        'xml' => self::TYPE_STRING,
115
    ];
116
117
    /**
118
     * @var string|null the default schema used for the current session.
119
     */
120
    protected ?string $defaultSchema = 'public';
121
122
    /**
123
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
124
     * case starting and ending characters are different.
125
     */
126
    protected $tableQuoteCharacter = '"';
127
128
    /**
129
     * Resolves the table name and schema name (if any).
130
     *
131
     * @param string $name the table name.
132
     *
133
     * @return TableSchema with resolved table, schema, etc. names.
134
     *
135
     * {@see TableSchema}
136
     */
137 71
    protected function resolveTableName(string $name): TableSchema
138
    {
139 71
        $resolvedName = new TableSchema();
140
141 71
        $parts = explode('.', str_replace('"', '', $name));
142
143 71
        if (isset($parts[1])) {
144
            $resolvedName->schemaName($parts[0]);
145
            $resolvedName->name($parts[1]);
146
        } else {
147 71
            $resolvedName->schemaName($this->defaultSchema);
148 71
            $resolvedName->name($name);
149
        }
150
151 71
        $resolvedName->fullName(
152
            (
153 71
                $resolvedName->getSchemaName() !== $this->defaultSchema ? $resolvedName->getSchemaName() . '.' : ''
154 71
            ) . $resolvedName->getName()
155
        );
156
157 71
        return $resolvedName;
158
    }
159
160
    /**
161
     * Returns all schema names in the database, including the default one but not system schemas.
162
     *
163
     * This method should be overridden by child classes in order to support this feature because the default
164
     * implementation simply throws an exception.
165
     *
166
     * @throws Exception|InvalidConfigException|Throwable
167
     *
168
     * @return array all schema names in the database, except system schemas.
169
     */
170 2
    protected function findSchemaNames(): array
171
    {
172 2
        static $sql = <<<'SQL'
173
SELECT "ns"."nspname"
174
FROM "pg_namespace" AS "ns"
175
WHERE "ns"."nspname" != 'information_schema' AND "ns"."nspname" NOT LIKE 'pg_%'
176
ORDER BY "ns"."nspname" ASC
177
SQL;
178
179 2
        return $this->getDb()->createCommand($sql)->queryColumn();
180
    }
181
182
    /**
183
     * Returns all table names in the database.
184
     *
185
     * This method should be overridden by child classes in order to support this feature because the default
186
     * implementation simply throws an exception.
187
     *
188
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
189
     *
190
     * @throws Exception|InvalidConfigException|Throwable
191
     *
192
     * @return array all table names in the database. The names have NO schema name prefix.
193
     */
194 5
    protected function findTableNames(string $schema = ''): array
195
    {
196 5
        if ($schema === '') {
197 5
            $schema = $this->defaultSchema;
198
        }
199
200
        $sql = <<<'SQL'
201 5
SELECT c.relname AS table_name
202
FROM pg_class c
203
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
204
WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f', 'p')
205
ORDER BY c.relname
206
SQL;
207
208 5
        return $this->getDb()->createCommand($sql, [':schemaName' => $schema])->queryColumn();
209
    }
210
211
    /**
212
     * Loads the metadata for the specified table.
213
     *
214
     * @param string $name table name.
215
     *
216
     * @throws Exception|InvalidConfigException
217
     *
218
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
219
     */
220 97
    protected function loadTableSchema(string $name): ?TableSchema
221
    {
222 97
        $table = new TableSchema();
223
224 97
        $this->resolveTableNames($table, $name);
225
226 97
        if ($this->findColumns($table)) {
227 91
            $this->findConstraints($table);
228 91
            return $table;
229
        }
230
231 16
        return null;
232
    }
233
234
    /**
235
     * Loads a primary key for the given table.
236
     *
237
     * @param string $tableName table name.
238
     *
239
     * @throws Exception|InvalidConfigException
240
     *
241
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
242
     */
243 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
244
    {
245 31
        return $this->loadTableConstraints($tableName, 'primaryKey');
246
    }
247
248
    /**
249
     * Loads all foreign keys for the given table.
250
     *
251
     * @param string $tableName table name.
252
     *
253
     * @throws Exception|InvalidConfigException
254
     *
255
     * @return ForeignKeyConstraint[] foreign keys for the given table.
256
     */
257 4
    protected function loadTableForeignKeys(string $tableName): array
258
    {
259 4
        return $this->loadTableConstraints($tableName, 'foreignKeys');
260
    }
261
262
    /**
263
     * Loads all indexes for the given table.
264
     *
265
     * @param string $tableName table name.
266
     *
267
     * @throws Exception|InvalidConfigException|Throwable
268
     *
269
     * @return IndexConstraint[] indexes for the given table.
270
     */
271 28
    protected function loadTableIndexes(string $tableName): array
272
    {
273 28
        static $sql = <<<'SQL'
274
SELECT
275
    "ic"."relname" AS "name",
276
    "ia"."attname" AS "column_name",
277
    "i"."indisunique" AS "index_is_unique",
278
    "i"."indisprimary" AS "index_is_primary"
279
FROM "pg_class" AS "tc"
280
INNER JOIN "pg_namespace" AS "tcns"
281
    ON "tcns"."oid" = "tc"."relnamespace"
282
INNER JOIN "pg_index" AS "i"
283
    ON "i"."indrelid" = "tc"."oid"
284
INNER JOIN "pg_class" AS "ic"
285
    ON "ic"."oid" = "i"."indexrelid"
286
INNER JOIN "pg_attribute" AS "ia"
287
    ON "ia"."attrelid" = "i"."indrelid" AND "ia"."attnum" = ANY ("i"."indkey")
288
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
289
ORDER BY "ia"."attnum" ASC
290
SQL;
291
292 28
        $resolvedName = $this->resolveTableName($tableName);
293
294 28
        $indexes = $this->getDb()->createCommand($sql, [
295 28
            ':schemaName' => $resolvedName->getSchemaName(),
296 28
            ':tableName' => $resolvedName->getName(),
297 28
        ])->queryAll();
298
299 28
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
300 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
301 28
        $result = [];
302
303 28
        foreach ($indexes as $name => $index) {
304 25
            $ic = (new IndexConstraint())
305 25
                ->name($name)
306 25
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
307 25
                ->primary((bool) $index[0]['index_is_primary'])
308 25
                ->unique((bool) $index[0]['index_is_unique']);
309
310 25
            $result[] = $ic;
311
        }
312
313 28
        return $result;
314
    }
315
316
    /**
317
     * Loads all unique constraints for the given table.
318
     *
319
     * @param string $tableName table name.
320
     *
321
     * @throws Exception|InvalidConfigException
322
     *
323
     * @return Constraint[] unique constraints for the given table.
324
     */
325 13
    protected function loadTableUniques(string $tableName): array
326
    {
327 13
        return $this->loadTableConstraints($tableName, 'uniques');
328
    }
329
330
    /**
331
     * Loads all check constraints for the given table.
332
     *
333
     * @param string $tableName table name.
334
     *
335
     * @throws Exception|InvalidConfigException
336
     *
337
     * @return CheckConstraint[] check constraints for the given table.
338
     */
339 13
    protected function loadTableChecks(string $tableName): array
340
    {
341 13
        return $this->loadTableConstraints($tableName, 'checks');
342
    }
343
344
    /**
345
     * Loads all default value constraints for the given table.
346
     *
347
     * @param string $tableName table name.
348
     *
349
     * @throws NotSupportedException
350
     *
351
     * @return DefaultValueConstraint[] default value constraints for the given table.
352
     */
353 12
    protected function loadTableDefaultValues(string $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

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