Passed
Pull Request — master (#38)
by Wilmer
23:17 queued 08:17
created

Schema::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 1
cts 1
cp 1
crap 1
rs 10
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
    /** @psalm-var Connection $db */
129
    private ConnectionInterface $db;
130
131
    public function __construct(ConnectionInterface $db)
132
    {
133
        $this->db = $db;
134
135 76
        parent::__construct($db);
136
    }
137 76
138
    /**
139 76
     * Resolves the table name and schema name (if any).
140
     *
141 76
     * @param string $name the table name.
142 5
     *
143 5
     * @return TableSchema with resolved table, schema, etc. names.
144
     *
145 71
     * {@see TableSchema}
146 71
     */
147
    protected function resolveTableName(string $name): TableSchema
148
    {
149 76
        $resolvedName = new TableSchema();
150
151 76
        $parts = explode('.', str_replace('"', '', $name));
152 76
153
        if (isset($parts[1])) {
154
            $resolvedName->schemaName($parts[0]);
155 76
            $resolvedName->name($parts[1]);
156
        } else {
157
            $resolvedName->schemaName($this->defaultSchema);
158
            $resolvedName->name($name);
159
        }
160
161
        $resolvedName->fullName(
162
            (
163
                $resolvedName->getSchemaName() !== $this->defaultSchema ? $resolvedName->getSchemaName() . '.' : ''
164
            ) . $resolvedName->getName()
165
        );
166
167
        return $resolvedName;
168
    }
169
170 2
    /**
171
     * Returns all schema names in the database, including the default one but not system schemas.
172 2
     *
173
     * This method should be overridden by child classes in order to support this feature because the default
174
     * implementation simply throws an exception.
175
     *
176
     * @throws Exception|InvalidConfigException|Throwable
177
     *
178
     * @return array all schema names in the database, except system schemas.
179 2
     */
180
    protected function findSchemaNames(): array
181
    {
182
        static $sql = <<<'SQL'
183
SELECT "ns"."nspname"
184
FROM "pg_namespace" AS "ns"
185
WHERE "ns"."nspname" != 'information_schema' AND "ns"."nspname" NOT LIKE 'pg_%'
186
ORDER BY "ns"."nspname" ASC
187
SQL;
188
189
        return $this->db->createCommand($sql)->queryColumn();
190
    }
191
192
    /**
193
     * Returns all table names in the database.
194
     *
195
     * This method should be overridden by child classes in order to support this feature because the default
196 5
     * implementation simply throws an exception.
197
     *
198 5
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
199 5
     *
200
     * @throws Exception|InvalidConfigException|Throwable
201
     *
202
     * @return array all table names in the database. The names have NO schema name prefix.
203 5
     */
204
    protected function findTableNames(string $schema = ''): array
205
    {
206
        if ($schema === '') {
207
            $schema = $this->defaultSchema;
208
        }
209
210 5
        $sql = <<<'SQL'
211
SELECT c.relname AS table_name
212
FROM pg_class c
213
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
214
WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f', 'p')
215
ORDER BY c.relname
216
SQL;
217
218
        return $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn();
219
    }
220
221
    /**
222
     * Loads the metadata for the specified table.
223
     *
224
     * @param string $name table name.
225 99
     *
226
     * @throws Exception|InvalidConfigException
227 99
     *
228
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
229 99
     */
230
    protected function loadTableSchema(string $name): ?TableSchema
231 99
    {
232 93
        $table = new TableSchema();
233 93
234
        $this->resolveTableNames($table, $name);
235
236 16
        if ($this->findColumns($table)) {
237
            $this->findConstraints($table);
238
            return $table;
239
        }
240
241
        return null;
242
    }
243
244
    /**
245
     * Loads a primary key for the given table.
246
     *
247
     * @param string $tableName table name.
248
     *
249
     * @throws Exception|InvalidConfigException
250 31
     *
251
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
252 31
     */
253
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
254
    {
255
        return $this->loadTableConstraints($tableName, 'primaryKey');
256
    }
257
258
    /**
259
     * Loads all foreign keys for the given table.
260
     *
261
     * @param string $tableName table name.
262
     *
263
     * @throws Exception|InvalidConfigException
264
     *
265
     * @return ForeignKeyConstraint[] foreign keys for the given table.
266 4
     */
267
    protected function loadTableForeignKeys(string $tableName): array
268 4
    {
269
        return $this->loadTableConstraints($tableName, 'foreignKeys');
270
    }
271
272
    /**
273
     * Loads all indexes for the given table.
274
     *
275
     * @param string $tableName table name.
276
     *
277
     * @throws Exception|InvalidConfigException|Throwable
278
     *
279
     * @return IndexConstraint[] indexes for the given table.
280
     */
281
    protected function loadTableIndexes(string $tableName): array
282 28
    {
283
        static $sql = <<<'SQL'
284 28
SELECT
285
    "ic"."relname" AS "name",
286
    "ia"."attname" AS "column_name",
287
    "i"."indisunique" AS "index_is_unique",
288
    "i"."indisprimary" AS "index_is_primary"
289
FROM "pg_class" AS "tc"
290
INNER JOIN "pg_namespace" AS "tcns"
291
    ON "tcns"."oid" = "tc"."relnamespace"
292
INNER JOIN "pg_index" AS "i"
293
    ON "i"."indrelid" = "tc"."oid"
294
INNER JOIN "pg_class" AS "ic"
295
    ON "ic"."oid" = "i"."indexrelid"
296
INNER JOIN "pg_attribute" AS "ia"
297
    ON "ia"."attrelid" = "i"."indrelid" AND "ia"."attnum" = ANY ("i"."indkey")
298
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
299
ORDER BY "ia"."attnum" ASC
300
SQL;
301
302
        $resolvedName = $this->resolveTableName($tableName);
303 28
304
        $indexes = $this->db->createCommand($sql, [
305 28
            ':schemaName' => $resolvedName->getSchemaName(),
306 28
            ':tableName' => $resolvedName->getName(),
307 28
        ])->queryAll();
308 28
309
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
310 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
311 28
        $result = [];
312 28
313
        foreach ($indexes as $name => $index) {
314 28
            $ic = (new IndexConstraint())
315 25
                ->name($name)
316 25
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
317 25
                ->primary((bool) $index[0]['index_is_primary'])
318 25
                ->unique((bool) $index[0]['index_is_unique']);
319 25
320
            $result[] = $ic;
321 25
        }
322
323
        return $result;
324 28
    }
325
326
    /**
327
     * Loads all unique constraints for the given table.
328
     *
329
     * @param string $tableName table name.
330
     *
331
     * @throws Exception|InvalidConfigException
332
     *
333
     * @return Constraint[] unique constraints for the given table.
334
     */
335
    protected function loadTableUniques(string $tableName): array
336
    {
337
        return $this->loadTableConstraints($tableName, 'uniques');
338 13
    }
339
340 13
    /**
341
     * Loads all check constraints for the given table.
342
     *
343
     * @param string $tableName table name.
344
     *
345
     * @throws Exception|InvalidConfigException
346
     *
347
     * @return CheckConstraint[] check constraints for the given table.
348
     */
349
    protected function loadTableChecks(string $tableName): array
350
    {
351
        return $this->loadTableConstraints($tableName, 'checks');
352
    }
353
354 13
    /**
355
     * Loads all default value constraints for the given table.
356 13
     *
357
     * @param string $tableName table name.
358
     *
359
     * @throws NotSupportedException
360
     *
361
     * @return DefaultValueConstraint[] default value constraints for the given table.
362
     */
363
    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

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