Passed
Pull Request — master (#19)
by Wilmer
12:28
created

Schema::loadTableConstraints()   B

Complexity

Conditions 8
Paths 14

Size

Total Lines 99
Code Lines 83

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 83
c 0
b 0
f 0
nc 14
nop 2
dl 0
loc 99
rs 7.1264

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\Schema;
6
7
use Yiisoft\Db\Constraint\CheckConstraint;
8
use Yiisoft\Db\Constraint\Constraint;
9
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
10
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
11
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
12
use Yiisoft\Db\Constraint\IndexConstraint;
13
use Yiisoft\Db\Exception\NotSupportedException;
14
use Yiisoft\Db\Expression\Expression;
15
use Yiisoft\Db\Pgsql\Query\QueryBuilder;
16
use Yiisoft\Db\Schema\Schema as AbstractSchema;
17
use Yiisoft\Db\View\ViewFinderTrait;
18
use Yiisoft\Arrays\ArrayHelper;
19
20
/**
21
 * Schema is the class for retrieving metadata from a Postgres SQL database
22
 * (version 9.x and above).
23
 */
24
class Schema extends AbstractSchema implements ConstraintFinderInterface
25
{
26
    use ViewFinderTrait;
27
    use ConstraintFinderTrait;
28
29
    public const TYPE_JSONB = 'jsonb';
30
31
    /**
32
     * @var string the default schema used for the current session.
33
     */
34
    protected ?string $defaultSchema = 'public';
35
    protected string $tableQuoteCharacter = '"';
36
37
    /**
38
     * @var array mapping from physical column types (keys) to abstract column types (values).
39
     *
40
     * {@see http://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE}
41
     */
42
    private array $typeMap = [
43
        'bit' => self::TYPE_INTEGER,
44
        'bit varying' => self::TYPE_INTEGER,
45
        'varbit' => self::TYPE_INTEGER,
46
        'bool' => self::TYPE_BOOLEAN,
47
        'boolean' => self::TYPE_BOOLEAN,
48
        'box' => self::TYPE_STRING,
49
        'circle' => self::TYPE_STRING,
50
        'point' => self::TYPE_STRING,
51
        'line' => self::TYPE_STRING,
52
        'lseg' => self::TYPE_STRING,
53
        'polygon' => self::TYPE_STRING,
54
        'path' => self::TYPE_STRING,
55
        'character' => self::TYPE_CHAR,
56
        'char' => self::TYPE_CHAR,
57
        'bpchar' => self::TYPE_CHAR,
58
        'character varying' => self::TYPE_STRING,
59
        'varchar' => self::TYPE_STRING,
60
        'text' => self::TYPE_TEXT,
61
        'bytea' => self::TYPE_BINARY,
62
        'cidr' => self::TYPE_STRING,
63
        'inet' => self::TYPE_STRING,
64
        'macaddr' => self::TYPE_STRING,
65
        'real' => self::TYPE_FLOAT,
66
        'float4' => self::TYPE_FLOAT,
67
        'double precision' => self::TYPE_DOUBLE,
68
        'float8' => self::TYPE_DOUBLE,
69
        'decimal' => self::TYPE_DECIMAL,
70
        'numeric' => self::TYPE_DECIMAL,
71
        'money' => self::TYPE_MONEY,
72
        'smallint' => self::TYPE_SMALLINT,
73
        'int2' => self::TYPE_SMALLINT,
74
        'int4' => self::TYPE_INTEGER,
75
        'int' => self::TYPE_INTEGER,
76
        'integer' => self::TYPE_INTEGER,
77
        'bigint' => self::TYPE_BIGINT,
78
        'int8' => self::TYPE_BIGINT,
79
        'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal!
80
        'smallserial' => self::TYPE_SMALLINT,
81
        'serial2' => self::TYPE_SMALLINT,
82
        'serial4' => self::TYPE_INTEGER,
83
        'serial' => self::TYPE_INTEGER,
84
        'bigserial' => self::TYPE_BIGINT,
85
        'serial8' => self::TYPE_BIGINT,
86
        'pg_lsn' => self::TYPE_BIGINT,
87
        'date' => self::TYPE_DATE,
88
        'interval' => self::TYPE_STRING,
89
        'time without time zone' => self::TYPE_TIME,
90
        'time' => self::TYPE_TIME,
91
        'time with time zone' => self::TYPE_TIME,
92
        'timetz' => self::TYPE_TIME,
93
        'timestamp without time zone' => self::TYPE_TIMESTAMP,
94
        'timestamp' => self::TYPE_TIMESTAMP,
95
        'timestamp with time zone' => self::TYPE_TIMESTAMP,
96
        'timestamptz' => self::TYPE_TIMESTAMP,
97
        'abstime' => self::TYPE_TIMESTAMP,
98
        'tsquery' => self::TYPE_STRING,
99
        'tsvector' => self::TYPE_STRING,
100
        'txid_snapshot' => self::TYPE_STRING,
101
        'unknown' => self::TYPE_STRING,
102
        'uuid' => self::TYPE_STRING,
103
        'json' => self::TYPE_JSON,
104
        'jsonb' => self::TYPE_JSON,
105
        'xml' => self::TYPE_STRING,
106
    ];
107
108
    protected function resolveTableName($name)
109
    {
110
        $resolvedName = new TableSchema();
111
        $parts = \explode('.', \str_replace('"', '', $name));
112
113
        if (isset($parts[1])) {
114
            $resolvedName->schemaName($parts[0]);
115
            $resolvedName->name($parts[1]);
116
        } else {
117
            $resolvedName->schemaName($this->defaultSchema);
118
            $resolvedName->name($name);
119
        }
120
        $resolvedName->fullName(($resolvedName->getSchemaName() !== $this->defaultSchema ? $resolvedName->getSchemaName() . '.' : '')
121
            . $resolvedName->getName());
122
        return $resolvedName;
123
    }
124
125
    protected function findSchemaNames()
126
    {
127
        static $sql = <<<'SQL'
128
SELECT "ns"."nspname"
129
FROM "pg_namespace" AS "ns"
130
WHERE "ns"."nspname" != 'information_schema' AND "ns"."nspname" NOT LIKE 'pg_%'
131
ORDER BY "ns"."nspname" ASC
132
SQL;
133
134
        return $this->getDb()->createCommand($sql)->queryColumn();
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    protected function findTableNames($schema = '')
141
    {
142
        if ($schema === '') {
143
            $schema = $this->defaultSchema;
144
        }
145
        $sql = <<<'SQL'
146
SELECT c.relname AS table_name
147
FROM pg_class c
148
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
149
WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f', 'p')
150
ORDER BY c.relname
151
SQL;
152
        return $this->getDb()->createCommand($sql, [':schemaName' => $schema])->queryColumn();
153
    }
154
155
    protected function loadTableSchema(string $name): ?TableSchema
156
    {
157
        $table = new TableSchema();
158
        $this->resolveTableNames($table, $name);
159
160
        if ($this->findColumns($table)) {
161
            $this->findConstraints($table);
162
            return $table;
163
        }
164
165
        return null;
166
    }
167
168
    protected function loadTablePrimaryKey($tableName)
169
    {
170
        return $this->loadTableConstraints($tableName, 'primaryKey');
171
    }
172
173
    protected function loadTableForeignKeys($tableName)
174
    {
175
        return $this->loadTableConstraints($tableName, 'foreignKeys');
176
    }
177
178
    protected function loadTableIndexes($tableName)
179
    {
180
        static $sql = <<<'SQL'
181
SELECT
182
    "ic"."relname" AS "name",
183
    "ia"."attname" AS "column_name",
184
    "i"."indisunique" AS "index_is_unique",
185
    "i"."indisprimary" AS "index_is_primary"
186
FROM "pg_class" AS "tc"
187
INNER JOIN "pg_namespace" AS "tcns"
188
    ON "tcns"."oid" = "tc"."relnamespace"
189
INNER JOIN "pg_index" AS "i"
190
    ON "i"."indrelid" = "tc"."oid"
191
INNER JOIN "pg_class" AS "ic"
192
    ON "ic"."oid" = "i"."indexrelid"
193
INNER JOIN "pg_attribute" AS "ia"
194
    ON "ia"."attrelid" = "i"."indrelid" AND "ia"."attnum" = ANY ("i"."indkey")
195
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
196
ORDER BY "ia"."attnum" ASC
197
SQL;
198
199
        $resolvedName = $this->resolveTableName($tableName);
200
201
        $indexes = $this->getDb()->createCommand($sql, [
202
            ':schemaName' => $resolvedName->getSchemaName(),
203
            ':tableName' => $resolvedName->getName(),
204
        ])->queryAll();
205
206
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
207
        $indexes = ArrayHelper::index($indexes, null, 'name');
208
        $result = [];
209
210
        foreach ($indexes as $name => $index) {
211
            $ic = new IndexConstraint();
212
213
            $ic->setName($name);
214
            $ic->setColumnNames(ArrayHelper::getColumn($index, 'column_name'));
215
            $ic->setIsPrimary((bool) $index[0]['index_is_primary']);
216
            $ic->setIsUnique((bool) $index[0]['index_is_unique']);
217
218
            $result[] = $ic;
219
        }
220
221
        return $result;
222
    }
223
224
    protected function loadTableUniques($tableName)
225
    {
226
        return $this->loadTableConstraints($tableName, 'uniques');
227
    }
228
229
    protected function loadTableChecks($tableName)
230
    {
231
        return $this->loadTableConstraints($tableName, 'checks');
232
    }
233
234
    protected function loadTableDefaultValues($tableName)
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

234
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ $tableName)

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...
235
    {
236
        throw new NotSupportedException('PostgreSQL does not support default value constraints.');
237
    }
238
239
    /**
240
     * Creates a query builder for the PostgreSQL database.
241
     *
242
     * @return QueryBuilder query builder instance
243
     */
244
    public function createQueryBuilder()
245
    {
246
        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\Query\QueryBuilder::__construct() does only seem to accept Yiisoft\Db\Connection\Connection, 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

246
        return new QueryBuilder(/** @scrutinizer ignore-type */ $this->getDb());
Loading history...
247
    }
248
249
    /**
250
     * Resolves the table name and schema name (if any).
251
     * @param TableSchema $table the table metadata object.
252
     *
253
     * @param string $name the table name
254
     */
255
    protected function resolveTableNames($table, $name)
256
    {
257
        $parts = \explode('.', \str_replace('"', '', $name));
258
259
        if (isset($parts[1])) {
260
            $table->schemaName($parts[0]);
261
            $table->name($parts[1]);
262
        } else {
263
            $table->schemaName($this->defaultSchema);
264
            $table->name($parts[0]);
265
        }
266
267
        $table->fullName($table->getSchemaName() !== $this->defaultSchema ? $table->getSchemaName() . '.'
268
            . $table->name : $table->getName());
0 ignored issues
show
Bug introduced by
The property name is declared private in Yiisoft\Db\Schema\TableSchema and cannot be accessed from this context.
Loading history...
269
    }
270
271
    /**
272
     * {@inheritdoc]
273
     */
274
    protected function findViewNames($schema = '')
275
    {
276
        if ($schema === '') {
277
            $schema = $this->defaultSchema;
278
        }
279
        $sql = <<<'SQL'
280
SELECT c.relname AS table_name
281
FROM pg_class c
282
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
283
WHERE ns.nspname = :schemaName AND (c.relkind = 'v' OR c.relkind = 'm')
284
ORDER BY c.relname
285
SQL;
286
        return $this->getDb()->createCommand($sql, [':schemaName' => $schema])->queryColumn();
287
    }
288
289
    /**
290
     * Collects the foreign key column details for the given table.
291
     *
292
     * @param TableSchema $table the table metadata
293
     */
294
    protected function findConstraints($table)
295
    {
296
        $tableName = $this->quoteValue($table->getName());
297
        $tableSchema = $this->quoteValue($table->getSchemaName());
298
299
        //We need to extract the constraints de hard way since:
300
        //http://www.postgresql.org/message-id/[email protected]
301
302
        $sql = <<<SQL
303
select
304
    ct.conname as constraint_name,
305
    a.attname as column_name,
306
    fc.relname as foreign_table_name,
307
    fns.nspname as foreign_table_schema,
308
    fa.attname as foreign_column_name
309
from
310
    (SELECT ct.conname, ct.conrelid, ct.confrelid, ct.conkey, ct.contype, ct.confkey, generate_subscripts(ct.conkey, 1) AS s
311
       FROM pg_constraint ct
312
    ) AS ct
313
    inner join pg_class c on c.oid=ct.conrelid
314
    inner join pg_namespace ns on c.relnamespace=ns.oid
315
    inner join pg_attribute a on a.attrelid=ct.conrelid and a.attnum = ct.conkey[ct.s]
316
    left join pg_class fc on fc.oid=ct.confrelid
317
    left join pg_namespace fns on fc.relnamespace=fns.oid
318
    left join pg_attribute fa on fa.attrelid=ct.confrelid and fa.attnum = ct.confkey[ct.s]
319
where
320
    ct.contype='f'
321
    and c.relname={$tableName}
322
    and ns.nspname={$tableSchema}
323
order by
324
    fns.nspname, fc.relname, a.attnum
325
SQL;
326
327
        $constraints = [];
328
329
        foreach ($this->getDb()->createCommand($sql)->queryAll() as $constraint) {
330
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
331
                $constraint = \array_change_key_case($constraint, CASE_LOWER);
332
            }
333
            if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
334
                $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
335
            } else {
336
                $foreignTable = $constraint['foreign_table_name'];
337
            }
338
            $name = $constraint['constraint_name'];
339
            if (!isset($constraints[$name])) {
340
                $constraints[$name] = [
341
                    'tableName' => $foreignTable,
342
                    'columns' => [],
343
                ];
344
            }
345
            $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name'];
346
        }
347
348
        foreach ($constraints as $name => $constraint) {
349
            $table->foreignKey($name, \array_merge([$constraint['tableName']], $constraint['columns']));
350
        }
351
    }
352
353
    /**
354
     * Gets information about given table unique indexes.
355
     *
356
     * @param TableSchema $table the table metadata
357
     *
358
     * @return array with index and column names
359
     */
360
    protected function getUniqueIndexInformation($table)
361
    {
362
        $sql = <<<'SQL'
363
SELECT
364
    i.relname as indexname,
365
    pg_get_indexdef(idx.indexrelid, k + 1, TRUE) AS columnname
366
FROM (
367
  SELECT *, generate_subscripts(indkey, 1) AS k
368
  FROM pg_index
369
) idx
370
INNER JOIN pg_class i ON i.oid = idx.indexrelid
371
INNER JOIN pg_class c ON c.oid = idx.indrelid
372
INNER JOIN pg_namespace ns ON c.relnamespace = ns.oid
373
WHERE idx.indisprimary = FALSE AND idx.indisunique = TRUE
374
AND c.relname = :tableName AND ns.nspname = :schemaName
375
ORDER BY i.relname, k
376
SQL;
377
378
        return $this->getDb()->createCommand($sql, [
379
            ':schemaName' => $table->getSchemaName(),
380
            ':tableName' => $table->getName(),
381
        ])->queryAll();
382
    }
383
384
    /**
385
     * Returns all unique indexes for the given table.
386
     *
387
     * Each array element is of the following structure:
388
     *
389
     * ```php
390
     * [
391
     *     'IndexName1' => ['col1' [, ...]],
392
     *     'IndexName2' => ['col2' [, ...]],
393
     * ]
394
     * ```
395
     *
396
     * @param TableSchema $table the table metadata
397
     * @return array all unique indexes for the given table.
398
     */
399
    public function findUniqueIndexes($table)
400
    {
401
        $uniqueIndexes = [];
402
403
        foreach ($this->getUniqueIndexInformation($table) as $row) {
404
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
405
                $row = \array_change_key_case($row, CASE_LOWER);
406
            }
407
            $column = $row['columnname'];
408
            if (!empty($column) && $column[0] === '"') {
409
                /**
410
                 * postgres will quote names that are not lowercase-only
411
                 * https://github.com/yiisoft/yii2/issues/10613
412
                 */
413
                $column = \substr($column, 1, -1);
414
            }
415
            $uniqueIndexes[$row['indexname']][] = $column;
416
        }
417
418
        return $uniqueIndexes;
419
    }
420
421
    /**
422
     * Collects the metadata of table columns.
423
     *
424
     * @param TableSchema $table the table metadata
425
     *
426
     * @return bool whether the table exists in the database
427
     */
428
    protected function findColumns($table): bool
429
    {
430
        $tableName = $this->getDb()->quoteValue($table->getName());
431
        $schemaName = $this->getDb()->quoteValue($table->getSchemaName());
432
433
        $orIdentity = '';
434
        if (\version_compare($this->getDb()->getServerVersion(), '12.0', '>=')) {
435
            $orIdentity = 'OR attidentity != \'\'';
436
        }
437
438
        $sql = <<<SQL
439
SELECT
440
    d.nspname AS table_schema,
441
    c.relname AS table_name,
442
    a.attname AS column_name,
443
    COALESCE(td.typname, tb.typname, t.typname) AS data_type,
444
    COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
445
    a.attlen AS character_maximum_length,
446
    pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
447
    a.atttypmod AS modifier,
448
    a.attnotnull = false AS is_nullable,
449
    CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
450
    coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) {$orIdentity} AS is_autoinc,
451
    pg_get_serial_sequence(quote_ident(d.nspname) || '.' || quote_ident(c.relname), a.attname) AS sequence_name,
452
    CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
453
        THEN array_to_string((SELECT array_agg(enumlabel) FROM pg_enum WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid))::varchar[], ',')
454
        ELSE NULL
455
    END AS enum_values,
456
    CASE atttypid
457
         WHEN 21 /*int2*/ THEN 16
458
         WHEN 23 /*int4*/ THEN 32
459
         WHEN 20 /*int8*/ THEN 64
460
         WHEN 1700 /*numeric*/ THEN
461
              CASE WHEN atttypmod = -1
462
               THEN null
463
               ELSE ((atttypmod - 4) >> 16) & 65535
464
               END
465
         WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/
466
         WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/
467
         ELSE null
468
      END   AS numeric_precision,
469
      CASE
470
        WHEN atttypid IN (21, 23, 20) THEN 0
471
        WHEN atttypid IN (1700) THEN
472
        CASE
473
            WHEN atttypmod = -1 THEN null
474
            ELSE (atttypmod - 4) & 65535
475
        END
476
           ELSE null
477
      END AS numeric_scale,
478
    CAST(
479
             information_schema._pg_char_max_length(information_schema._pg_truetypid(a, t), information_schema._pg_truetypmod(a, t))
480
             AS numeric
481
    ) AS size,
482
    a.attnum = any (ct.conkey) as is_pkey,
483
    COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
484
FROM
485
    pg_class c
486
    LEFT JOIN pg_attribute a ON a.attrelid = c.oid
487
    LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
488
    LEFT JOIN pg_type t ON a.atttypid = t.oid
489
    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
490
    LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
491
    LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
492
    LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
493
WHERE
494
    a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped
495
    AND c.relname = {$tableName}
496
    AND d.nspname = {$schemaName}
497
ORDER BY
498
    a.attnum;
499
SQL;
500
        $columns = $this->getDb()->createCommand($sql)->queryAll();
501
        if (empty($columns)) {
502
            return false;
503
        }
504
        foreach ($columns as $column) {
505
            if ($this->getDb()->getSlavePdo()->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
506
                $column = \array_change_key_case($column, CASE_LOWER);
507
            }
508
            $column = $this->loadColumnSchema($column);
509
            $table->columns($column->getName(), $column);
510
            if ($column->getIsPrimaryKey()) {
511
                $table->primaryKey($column->getName());
512
                if ($table->getSequenceName() === null) {
513
                    $table->sequenceName($column->sequenceName);
514
                }
515
                $column->defaultValue(null);
516
            } elseif ($column->getDefaultValue()) {
517
                if ($column->getType() === 'timestamp' && $column->getDefaultValue() === 'now()') {
518
                    $column->defaultValue(new Expression($column->getDefaultValue()));
519
                } elseif ($column->getType() === 'boolean') {
520
                    $column->defaultValue(($column->getDefaultValue() === 'true'));
521
                } elseif (\preg_match("/^B'(.*?)'::/", $column->getDefaultValue(), $matches)) {
522
                    $column->defaultValue(\bindec($matches[1]));
523
                } elseif (\strncasecmp($column->getDbType(), 'bit', 3) === 0 || \strncasecmp($column->getDbType(), 'varbit', 6) === 0) {
524
                    $column->defaultValue(\bindec(\trim($column->getDefaultValue(), 'B\'')));
525
                } elseif (\preg_match("/^'(.*?)'::/", $column->getDefaultValue(), $matches)) {
526
                    $column->defaultValue($column->phpTypecast($matches[1]));
527
                } elseif (\preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $column->getDefaultValue(), $matches)) {
528
                    if ($matches[2] === 'NULL') {
529
                        $column->defaultValue(null);
530
                    } else {
531
                        $column->defaultValue($column->phpTypecast($matches[2]));
532
                    }
533
                } else {
534
                    $column->defaultValue($column->phpTypecast($column->defaultValue));
0 ignored issues
show
Bug introduced by
The property defaultValue is declared private in Yiisoft\Db\Schema\ColumnSchema and cannot be accessed from this context.
Loading history...
535
                }
536
            }
537
        }
538
539
        return true;
540
    }
541
542
    /**
543
     * Loads the column information into a {@see ColumnSchema} object.
544
     *
545
     * @param array $info column information
546
     *
547
     * @return ColumnSchema the column schema object
548
     */
549
    protected function loadColumnSchema($info): ColumnSchema
550
    {
551
        /** @var ColumnSchema $column */
552
        $column = $this->createColumnSchema();
553
        $column->allowNull($info['is_nullable']);
554
        $column->autoIncrement($info['is_autoinc']);
555
        $column->comment($info['column_comment']);
556
        $column->dbType($info['data_type']);
557
        $column->defaultValue($info['column_default']);
558
        $column->enumValues(($info['enum_values'] !== null)
559
            ? \explode(',', \str_replace(["''"], ["'"], $info['enum_values'])) : null);
560
        $column->unsigned(false); // has no meaning in PG
561
        $column->isPrimaryKey((bool) $info['is_pkey']);
562
        $column->name($info['column_name']);
563
        $column->precision($info['numeric_precision']);
564
        $column->scale($info['numeric_scale']);
565
        $column->size($info['size'] === null ? null : (int) $info['size']);
566
        $column->dimension = (int) $info['dimension'];
567
568
        /**
569
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
570
         * default value
571
         */
572
573
        $defaultValue = $column->getDefaultValue();
574
        if (isset($defaultValue) && \preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1) {
575
            $column->sequenceName = \preg_replace(
576
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
577
                '',
578
                $defaultValue
579
            );
580
        } elseif (isset($info['sequence_name'])) {
581
            $column->sequenceName = $this->resolveTableName($info['sequence_name'])->getFullName();
582
        }
583
584
        if (isset($this->typeMap[$column->getDbType()])) {
585
            $column->type($this->typeMap[$column->getDbType()]);
586
        } else {
587
            $column->type(self::TYPE_STRING);
588
        }
589
        $column->phpType($this->getColumnPhpType($column));
590
591
        return $column;
592
    }
593
594
    /**
595
     * {@inheritdoc}
596
     */
597
    public function insert($table, $columns)
598
    {
599
        $params = [];
600
        $sql = $this->getDb()->getQueryBuilder()->insert($table, $columns, $params);
601
        $returnColumns = $this->getTableSchema($table)->getPrimaryKeys();
0 ignored issues
show
Bug introduced by
The method getPrimaryKeys() does not exist on Yiisoft\Db\Schema\TableSchema. Did you maybe mean getPrimaryKey()? ( Ignorable by Annotation )

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

601
        $returnColumns = $this->getTableSchema($table)->/** @scrutinizer ignore-call */ getPrimaryKeys();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
602
        if (!empty($returnColumns)) {
603
            $returning = [];
604
            foreach ((array) $returnColumns as $name) {
605
                $returning[] = $this->quoteColumnName($name);
606
            }
607
            $sql .= ' RETURNING ' . \implode(', ', $returning);
608
        }
609
610
        $command = $this->getDb()->createCommand($sql, $params);
611
        $command->prepare(false);
612
        $result = $command->queryOne();
613
614
        return !$command->getPdoStatement()->rowCount() ? false : $result;
615
    }
616
617
    /**
618
     * Loads multiple types of constraints and returns the specified ones.
619
     *
620
     * @param string $tableName table name.
621
     * @param string $returnType return type:
622
     * - primaryKey
623
     * - foreignKeys
624
     * - uniques
625
     * - checks
626
     *
627
     * @return mixed constraints.
628
     */
629
    private function loadTableConstraints($tableName, $returnType)
630
    {
631
        static $sql = <<<'SQL'
632
SELECT
633
    "c"."conname" AS "name",
634
    "a"."attname" AS "column_name",
635
    "c"."contype" AS "type",
636
    "ftcns"."nspname" AS "foreign_table_schema",
637
    "ftc"."relname" AS "foreign_table_name",
638
    "fa"."attname" AS "foreign_column_name",
639
    "c"."confupdtype" AS "on_update",
640
    "c"."confdeltype" AS "on_delete",
641
    pg_get_constraintdef("c"."oid") AS "check_expr"
642
FROM "pg_class" AS "tc"
643
INNER JOIN "pg_namespace" AS "tcns"
644
    ON "tcns"."oid" = "tc"."relnamespace"
645
INNER JOIN "pg_constraint" AS "c"
646
    ON "c"."conrelid" = "tc"."oid"
647
INNER JOIN "pg_attribute" AS "a"
648
    ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
649
LEFT JOIN "pg_class" AS "ftc"
650
    ON "ftc"."oid" = "c"."confrelid"
651
LEFT JOIN "pg_namespace" AS "ftcns"
652
    ON "ftcns"."oid" = "ftc"."relnamespace"
653
LEFT JOIN "pg_attribute" "fa"
654
    ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
655
WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
656
ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
657
SQL;
658
        static $actionTypes = [
659
            'a' => 'NO ACTION',
660
            'r' => 'RESTRICT',
661
            'c' => 'CASCADE',
662
            'n' => 'SET NULL',
663
            'd' => 'SET DEFAULT',
664
        ];
665
666
        $resolvedName = $this->resolveTableName($tableName);
667
        $constraints = $this->getDb()->createCommand($sql, [
668
            ':schemaName' => $resolvedName->getSchemaName(),
669
            ':tableName' => $resolvedName->getName(),
670
        ])->queryAll();
671
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
672
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
673
        $result = [
674
            'primaryKey' => null,
675
            'foreignKeys' => [],
676
            'uniques' => [],
677
            'checks' => [],
678
        ];
679
        foreach ($constraints as $type => $names) {
680
            foreach ($names as $name => $constraint) {
681
                switch ($type) {
682
                    case 'p':
683
                        $ct = new Constraint();
684
                        $ct->setName($name);
685
                        $ct->setColumnNames(ArrayHelper::getColumn($constraint, 'column_name'));
686
687
                        $result['primaryKey'] = $ct;
688
                        break;
689
                    case 'f':
690
                        $fk = new ForeignKeyConstraint();
691
                        $fk->setName($name);
692
                        $fk->setColumnNames(\array_values(
693
                            \array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
694
                        ));
695
                        $fk->setForeignColumnNames($constraint[0]['foreign_table_schema']);
696
                        $fk->setForeignTableName($constraint[0]['foreign_table_name']);
697
                        $fk->setForeignColumnNames(\array_values(
698
                            \array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
699
                        ));
700
                        $fk->setOnDelete($actionTypes[$constraint[0]['on_delete']] ?? null);
701
                        $fk->setOnUpdate($actionTypes[$constraint[0]['on_update']] ?? null);
702
703
                        $result['foreignKeys'][] = $fk;
704
                        break;
705
                    case 'u':
706
                        $ct = new Constraint();
707
                        $ct->setName($name);
708
                        $ct->setColumnNames(ArrayHelper::getColumn($constraint, 'column_name'));
709
710
                        $result['uniques'][] = $ct;
711
                        break;
712
                    case 'c':
713
                        $ck = new CheckConstraint();
714
                        $ck->setName($name);
715
                        $ck->setColumnNames(ArrayHelper::getColumn($constraint, 'column_name'));
716
                        $ck->setExpression($constraint[0]['check_expr']);
717
718
                        $result['checks'][] = $ck;
719
                        break;
720
                }
721
            }
722
        }
723
        foreach ($result as $type => $data) {
724
            $this->setTableMetadata($tableName, $type, $data);
725
        }
726
727
        return $result[$returnType];
728
    }
729
730
    /**
731
     * Creates a column schema for the database.
732
     *
733
     * This method may be overridden by child classes to create a DBMS-specific column schema.
734
     *
735
     * @return ColumnSchema column schema instance.
736
     */
737
    protected function createColumnSchema(): ColumnSchema
738
    {
739
        return new ColumnSchema();
740
    }
741
}
742