Passed
Pull Request — master (#38)
by Wilmer
12:04
created

Schema::findConstraints()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 62
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6.0585

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 40
c 1
b 0
f 0
nc 18
nop 1
dl 0
loc 62
ccs 15
cts 17
cp 0.8824
crap 6.0585
rs 8.6577

How to fix   Long Method   

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
    /** @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