Passed
Push — master ( 7d724e...2a9aa3 )
by Alexander
06:49
created

Schema   F

Complexity

Total Complexity 70

Size/Duplication

Total Lines 916
Duplicated Lines 0 %

Test Coverage

Coverage 82.93%

Importance

Changes 0
Metric Value
eloc 417
dl 0
loc 916
ccs 204
cts 246
cp 0.8293
rs 2.8
c 0
b 0
f 0
wmc 70

22 Methods

Rating   Name   Duplication   Size   Complexity  
B findConstraints() 0 62 6
C findColumns() 0 127 16
A loadTableForeignKeys() 0 3 1
A createColumnSchema() 0 3 1
B loadColumnSchema() 0 47 7
A loadTableUniques() 0 3 1
A findUniqueIndexes() 0 24 5
B loadTableConstraints() 0 102 8
A getUniqueIndexInformation() 0 22 1
A findSchemaNames() 0 10 1
A resolveTableName() 0 21 3
A loadTableIndexes() 0 43 2
A createColumnSchemaBuilder() 0 3 1
A loadTableSchema() 0 12 2
A loadTableChecks() 0 3 1
A resolveTableNames() 0 14 3
A loadTablePrimaryKey() 0 3 1
A loadTableDefaultValues() 0 3 1
A createQueryBuilder() 0 3 1
A findTableNames() 0 15 2
A findViewNames() 0 15 2
A insert() 0 19 4

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

368
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ $tableName): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
369
    {
370 12
        throw new NotSupportedException('PostgreSQL does not support default value constraints.');
371
    }
372
373
    /**
374
     * Creates a query builder for the PostgreSQL database.
375
     *
376
     * @return QueryBuilder query builder instance
377
     */
378 69
    public function createQueryBuilder(): QueryBuilder
379
    {
380 69
        return new QueryBuilder($this->getDb());
0 ignored issues
show
Bug introduced by
It seems like $this->getDb() can also be of type null; however, parameter $db of Yiisoft\Db\Pgsql\QueryBuilder::__construct() does only seem to accept Yiisoft\Db\Connection\ConnectionInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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