Completed
Push — master ( 49564a...c92cef )
by Dmitry
30s
created

Schema::loadColumnSchema()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 24
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4.0023

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 18
cts 19
cp 0.9474
rs 8.6845
c 0
b 0
f 0
cc 4
eloc 20
nc 8
nop 1
crap 4.0023
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db\pgsql;
9
10
use yii\db\Expression;
11
use yii\db\TableSchema;
12
use yii\db\ColumnSchema;
13
use yii\db\ViewFinderTrait;
14
15
/**
16
 * Schema is the class for retrieving metadata from a PostgreSQL database
17
 * (version 9.x and above).
18
 *
19
 * @property string[] $viewNames All view names in the database. This property is read-only.
20
 *
21
 * @author Gevik Babakhani <[email protected]>
22
 * @since 2.0
23
 */
24
class Schema extends \yii\db\Schema
25
{
26
    use ViewFinderTrait;
27
28
    /**
29
     * @var string the default schema used for the current session.
30
     */
31
    public $defaultSchema = 'public';
32
    /**
33
     * @var array mapping from physical column types (keys) to abstract
34
     * column types (values)
35
     * @see http://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE
36
     */
37
    public $typeMap = [
38
        'bit' => self::TYPE_INTEGER,
39
        'bit varying' => self::TYPE_INTEGER,
40
        'varbit' => self::TYPE_INTEGER,
41
42
        'bool' => self::TYPE_BOOLEAN,
43
        'boolean' => self::TYPE_BOOLEAN,
44
45
        'box' => self::TYPE_STRING,
46
        'circle' => self::TYPE_STRING,
47
        'point' => self::TYPE_STRING,
48
        'line' => self::TYPE_STRING,
49
        'lseg' => self::TYPE_STRING,
50
        'polygon' => self::TYPE_STRING,
51
        'path' => self::TYPE_STRING,
52
53
        'character' => self::TYPE_CHAR,
54
        'char' => self::TYPE_CHAR,
55
        'bpchar' => self::TYPE_CHAR,
56
        'character varying' => self::TYPE_STRING,
57
        'varchar' => self::TYPE_STRING,
58
        'text' => self::TYPE_TEXT,
59
60
        'bytea' => self::TYPE_BINARY,
61
62
        'cidr' => self::TYPE_STRING,
63
        'inet' => self::TYPE_STRING,
64
        'macaddr' => self::TYPE_STRING,
65
66
        'real' => self::TYPE_FLOAT,
67
        'float4' => self::TYPE_FLOAT,
68
        'double precision' => self::TYPE_DOUBLE,
69
        'float8' => self::TYPE_DOUBLE,
70
        'decimal' => self::TYPE_DECIMAL,
71
        'numeric' => self::TYPE_DECIMAL,
72
73
        'money' => self::TYPE_MONEY,
74
75
        'smallint' => self::TYPE_SMALLINT,
76
        'int2' => self::TYPE_SMALLINT,
77
        'int4' => self::TYPE_INTEGER,
78
        'int' => self::TYPE_INTEGER,
79
        'integer' => self::TYPE_INTEGER,
80
        'bigint' => self::TYPE_BIGINT,
81
        'int8' => self::TYPE_BIGINT,
82
        'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal!
83
84
        'smallserial' => self::TYPE_SMALLINT,
85
        'serial2' => self::TYPE_SMALLINT,
86
        'serial4' => self::TYPE_INTEGER,
87
        'serial' => self::TYPE_INTEGER,
88
        'bigserial' => self::TYPE_BIGINT,
89
        'serial8' => self::TYPE_BIGINT,
90
        'pg_lsn' => self::TYPE_BIGINT,
91
92
        'date' => self::TYPE_DATE,
93
        'interval' => self::TYPE_STRING,
94
        'time without time zone' => self::TYPE_TIME,
95
        'time' => self::TYPE_TIME,
96
        'time with time zone' => self::TYPE_TIME,
97
        'timetz' => self::TYPE_TIME,
98
        'timestamp without time zone' => self::TYPE_TIMESTAMP,
99
        'timestamp' => self::TYPE_TIMESTAMP,
100
        'timestamp with time zone' => self::TYPE_TIMESTAMP,
101
        'timestamptz' => self::TYPE_TIMESTAMP,
102
        'abstime' => self::TYPE_TIMESTAMP,
103
104
        'tsquery' => self::TYPE_STRING,
105
        'tsvector' => self::TYPE_STRING,
106
        'txid_snapshot' => self::TYPE_STRING,
107
108
        'unknown' => self::TYPE_STRING,
109
110
        'uuid' => self::TYPE_STRING,
111
        'json' => self::TYPE_STRING,
112
        'jsonb' => self::TYPE_STRING,
113
        'xml' => self::TYPE_STRING,
114
    ];
115
116
117
    /**
118
     * Creates a query builder for the PostgreSQL database.
119
     * @return QueryBuilder query builder instance
120
     */
121 157
    public function createQueryBuilder()
122
    {
123 157
        return new QueryBuilder($this->db);
124
    }
125
126
    /**
127
     * Resolves the table name and schema name (if any).
128
     * @param TableSchema $table the table metadata object
129
     * @param string $name the table name
130
     */
131 149
    protected function resolveTableNames($table, $name)
132
    {
133 149
        $parts = explode('.', str_replace('"', '', $name));
134
135 149
        if (isset($parts[1])) {
136
            $table->schemaName = $parts[0];
137
            $table->name = $parts[1];
138
        } else {
139 149
            $table->schemaName = $this->defaultSchema;
140 149
            $table->name = $name;
141
        }
142
143 149
        $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name;
144 149
    }
145
146
    /**
147
     * Quotes a table name for use in a query.
148
     * A simple table name has no schema prefix.
149
     * @param string $name table name
150
     * @return string the properly quoted table name
151
     */
152 195
    public function quoteSimpleTableName($name)
153
    {
154 195
        return strpos($name, '"') !== false ? $name : '"' . $name . '"';
155
    }
156
157
    /**
158
     * Loads the metadata for the specified table.
159
     * @param string $name table name
160
     * @return TableSchema|null driver dependent table metadata. Null if the table does not exist.
161
     */
162 149
    public function loadTableSchema($name)
163
    {
164 149
        $table = new TableSchema();
165 149
        $this->resolveTableNames($table, $name);
166 149
        if ($this->findColumns($table)) {
167 146
            $this->findConstraints($table);
168 146
            return $table;
169
        } else {
170 9
            return null;
171
        }
172
    }
173
174
    /**
175
     * Returns all schema names in the database, including the default one but not system schemas.
176
     * This method should be overridden by child classes in order to support this feature
177
     * because the default implementation simply throws an exception.
178
     * @return array all schema names in the database, except system schemas
179
     * @since 2.0.4
180
     */
181 1
    protected function findSchemaNames()
182
    {
183
        $sql = <<<SQL
184
SELECT ns.nspname AS schema_name
185
FROM pg_namespace ns
186
WHERE ns.nspname != 'information_schema' AND ns.nspname NOT LIKE 'pg_%'
187 1
ORDER BY ns.nspname
188
SQL;
189 1
        return $this->db->createCommand($sql)->queryColumn();
190
    }
191
192
    /**
193
     * Returns all table names in the database.
194
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
195
     * @return array all table names in the database. The names have NO schema name prefix.
196
     */
197 7
    protected function findTableNames($schema = '')
198
    {
199 7
        if ($schema === '') {
200 5
            $schema = $this->defaultSchema;
201
        }
202
        $sql = <<<SQL
203
SELECT c.relname AS table_name
204
FROM pg_class c
205
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
206
WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f')
207 7
ORDER BY c.relname
208
SQL;
209 7
        return $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn();
210
    }
211
212
    /**
213
     * @inheritdoc
214
     */
215 2
    protected function findViewNames($schema = '')
216
    {
217 2
        if ($schema === '') {
218
            $schema = $this->defaultSchema;
219
        }
220
        $sql = <<<SQL
221
SELECT c.relname AS table_name
222
FROM pg_class c
223
INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
224
WHERE ns.nspname = :schemaName AND c.relkind = 'v'
225 2
ORDER BY c.relname
226
SQL;
227 2
        return $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn();
228
    }
229
230
    /**
231
     * Collects the foreign key column details for the given table.
232
     * @param TableSchema $table the table metadata
233
     */
234 146
    protected function findConstraints($table)
235
    {
236 146
        $tableName = $this->quoteValue($table->name);
237 146
        $tableSchema = $this->quoteValue($table->schemaName);
238
239
        //We need to extract the constraints de hard way since:
240
        //http://www.postgresql.org/message-id/[email protected]
241
242
        $sql = <<<SQL
243
select
244
    ct.conname as constraint_name,
245
    a.attname as column_name,
246
    fc.relname as foreign_table_name,
247
    fns.nspname as foreign_table_schema,
248
    fa.attname as foreign_column_name
249
from
250
    (SELECT ct.conname, ct.conrelid, ct.confrelid, ct.conkey, ct.contype, ct.confkey, generate_subscripts(ct.conkey, 1) AS s
251
       FROM pg_constraint ct
252
    ) AS ct
253
    inner join pg_class c on c.oid=ct.conrelid
254
    inner join pg_namespace ns on c.relnamespace=ns.oid
255
    inner join pg_attribute a on a.attrelid=ct.conrelid and a.attnum = ct.conkey[ct.s]
256
    left join pg_class fc on fc.oid=ct.confrelid
257
    left join pg_namespace fns on fc.relnamespace=fns.oid
258
    left join pg_attribute fa on fa.attrelid=ct.confrelid and fa.attnum = ct.confkey[ct.s]
259
where
260
    ct.contype='f'
261 146
    and c.relname={$tableName}
262 146
    and ns.nspname={$tableSchema}
263
order by
264 146
    fns.nspname, fc.relname, a.attnum
265
SQL;
266
267 146
        $constraints = [];
268 146
        foreach ($this->db->createCommand($sql)->queryAll() as $constraint) {
269 56
            if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
270
                $constraint = array_change_key_case($constraint, CASE_LOWER);
271
            }
272 56
            if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
273
                $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
274
            } else {
275 56
                $foreignTable = $constraint['foreign_table_name'];
276
            }
277 56
            $name = $constraint['constraint_name'];
278 56
            if (!isset($constraints[$name])) {
279 56
                $constraints[$name] = [
280 56
                    'tableName' => $foreignTable,
281
                    'columns' => [],
282
                ];
283
            }
284 56
            $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name'];
285
        }
286 146
        foreach ($constraints as $name => $constraint) {
287 56
            $table->foreignKeys[$name] = array_merge([$constraint['tableName']], $constraint['columns']);
288
        }
289 146
    }
290
291
    /**
292
     * Gets information about given table unique indexes.
293
     * @param TableSchema $table the table metadata
294
     * @return array with index and column names
295
     */
296 1
    protected function getUniqueIndexInformation($table)
297
    {
298
        $sql = <<<SQL
299
SELECT
300
    i.relname as indexname,
301
    pg_get_indexdef(idx.indexrelid, k + 1, TRUE) AS columnname
302
FROM (
303
  SELECT *, generate_subscripts(indkey, 1) AS k
304
  FROM pg_index
305
) idx
306
INNER JOIN pg_class i ON i.oid = idx.indexrelid
307
INNER JOIN pg_class c ON c.oid = idx.indrelid
308
INNER JOIN pg_namespace ns ON c.relnamespace = ns.oid
309
WHERE idx.indisprimary = FALSE AND idx.indisunique = TRUE
310
AND c.relname = :tableName AND ns.nspname = :schemaName
311 1
ORDER BY i.relname, k
312
SQL;
313
314 1
        return $this->db->createCommand($sql, [
315 1
            ':schemaName' => $table->schemaName,
316 1
            ':tableName' => $table->name,
317 1
        ])->queryAll();
318
    }
319
320
    /**
321
     * Returns all unique indexes for the given table.
322
     * Each array element is of the following structure:
323
     *
324
     * ```php
325
     * [
326
     *     'IndexName1' => ['col1' [, ...]],
327
     *     'IndexName2' => ['col2' [, ...]],
328
     * ]
329
     * ```
330
     *
331
     * @param TableSchema $table the table metadata
332
     * @return array all unique indexes for the given table.
333
     */
334 1
    public function findUniqueIndexes($table)
335
    {
336 1
        $uniqueIndexes = [];
337
338 1
        $rows = $this->getUniqueIndexInformation($table);
339 1
        foreach ($rows as $row) {
340 1
            if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
341
                $row = array_change_key_case($row, CASE_LOWER);
342
            }
343 1
            $column = $row['columnname'];
344 1
            if (!empty($column) && $column[0] === '"') {
345
                // postgres will quote names that are not lowercase-only
346
                // https://github.com/yiisoft/yii2/issues/10613
347 1
                $column = substr($column, 1, -1);
348
            }
349 1
            $uniqueIndexes[$row['indexname']][] = $column;
350
        }
351
352 1
        return $uniqueIndexes;
353
    }
354
355
    /**
356
     * Collects the metadata of table columns.
357
     * @param TableSchema $table the table metadata
358
     * @return bool whether the table exists in the database
359
     */
360 149
    protected function findColumns($table)
361
    {
362 149
        $tableName = $this->db->quoteValue($table->name);
363 149
        $schemaName = $this->db->quoteValue($table->schemaName);
364
        $sql = <<<SQL
365
SELECT
366
    d.nspname AS table_schema,
367
    c.relname AS table_name,
368
    a.attname AS column_name,
369
    t.typname AS data_type,
370
    a.attlen AS character_maximum_length,
371
    pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
372
    a.atttypmod AS modifier,
373
    a.attnotnull = false AS is_nullable,
374
    CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
375
    coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) AS is_autoinc,
376
    array_to_string((select array_agg(enumlabel) from pg_enum where enumtypid=a.atttypid)::varchar[],',') as enum_values,
377
    CASE atttypid
378
         WHEN 21 /*int2*/ THEN 16
379
         WHEN 23 /*int4*/ THEN 32
380
         WHEN 20 /*int8*/ THEN 64
381
         WHEN 1700 /*numeric*/ THEN
382
              CASE WHEN atttypmod = -1
383
               THEN null
384
               ELSE ((atttypmod - 4) >> 16) & 65535
385
               END
386
         WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/
387
         WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/
388
         ELSE null
389
      END   AS numeric_precision,
390
      CASE
391
        WHEN atttypid IN (21, 23, 20) THEN 0
392
        WHEN atttypid IN (1700) THEN
393
        CASE
394
            WHEN atttypmod = -1 THEN null
395
            ELSE (atttypmod - 4) & 65535
396
        END
397
           ELSE null
398
      END AS numeric_scale,
399
    CAST(
400
             information_schema._pg_char_max_length(information_schema._pg_truetypid(a, t), information_schema._pg_truetypmod(a, t))
401
             AS numeric
402
    ) AS size,
403
    a.attnum = any (ct.conkey) as is_pkey
404
FROM
405
    pg_class c
406
    LEFT JOIN pg_attribute a ON a.attrelid = c.oid
407
    LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
408
    LEFT JOIN pg_type t ON a.atttypid = t.oid
409
    LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
410
    LEFT join pg_constraint ct on ct.conrelid=c.oid and ct.contype='p'
411
WHERE
412
    a.attnum > 0 and t.typname != ''
413 149
    and c.relname = {$tableName}
414 149
    and d.nspname = {$schemaName}
415
ORDER BY
416 149
    a.attnum;
417
SQL;
418
419 149
        $columns = $this->db->createCommand($sql)->queryAll();
420 149
        if (empty($columns)) {
421 9
            return false;
422
        }
423 146
        foreach ($columns as $column) {
424 146
            if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
425
                $column = array_change_key_case($column, CASE_LOWER);
426
            }
427 146
            $column = $this->loadColumnSchema($column);
428 146
            $table->columns[$column->name] = $column;
429 146
            if ($column->isPrimaryKey) {
430 125
                $table->primaryKey[] = $column->name;
431 125
                if ($table->sequenceName === null && preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $column->defaultValue) === 1) {
432 116
                    $table->sequenceName = preg_replace(['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'], '', $column->defaultValue);
0 ignored issues
show
Documentation Bug introduced by
It seems like preg_replace(array('/nex... $column->defaultValue) can also be of type array<integer,string>. However, the property $sequenceName is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
433
                }
434 125
                $column->defaultValue = null;
435 146
            } elseif ($column->defaultValue) {
436 92
                if ($column->type === 'timestamp' && $column->defaultValue === 'now()') {
437 21
                    $column->defaultValue = new Expression($column->defaultValue);
438 92
                } elseif ($column->type === 'boolean') {
439 87
                        $column->defaultValue = ($column->defaultValue === 'true');
440 27
                } elseif (stripos($column->dbType, 'bit') === 0 || stripos($column->dbType, 'varbit') === 0) {
441 21
                    $column->defaultValue = bindec(trim($column->defaultValue, 'B\''));
442 27
                } elseif (preg_match("/^'(.*?)'::/", $column->defaultValue, $matches)) {
443 21
                    $column->defaultValue = $matches[1];
444 27
                } elseif (preg_match('/^(?:\()?(.*?)(?(1)\))(?:::.+)?$/', $column->defaultValue, $matches)) {
445 5
                    if ($matches[1] === 'NULL') {
446
                        $column->defaultValue = null;
447
                    } else {
448 5
                        $column->defaultValue = $column->phpTypecast($matches[1]);
449
                    }
450
                } else {
451 25
                    $column->defaultValue = $column->phpTypecast($column->defaultValue);
452
                }
453
            }
454
        }
455
456 146
        return true;
457
    }
458
459
    /**
460
     * Loads the column information into a [[ColumnSchema]] object.
461
     * @param array $info column information
462
     * @return ColumnSchema the column schema object
463
     */
464 146
    protected function loadColumnSchema($info)
465
    {
466 146
        $column = $this->createColumnSchema();
467 146
        $column->allowNull = $info['is_nullable'];
468 146
        $column->autoIncrement = $info['is_autoinc'];
469 146
        $column->comment = $info['column_comment'];
470 146
        $column->dbType = $info['data_type'];
471 146
        $column->defaultValue = $info['column_default'];
472 146
        $column->enumValues = ($info['enum_values'] !== null) ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like $info['enum_values'] !==...'enum_values'])) : null can be null. However, the property $enumValues is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
473 146
        $column->unsigned = false; // has no meaning in PG
474 146
        $column->isPrimaryKey = $info['is_pkey'];
475 146
        $column->name = $info['column_name'];
476 146
        $column->precision = $info['numeric_precision'];
477 146
        $column->scale = $info['numeric_scale'];
478 146
        $column->size = $info['size'] === null ? null : (int) $info['size'];
479 146
        if (isset($this->typeMap[$column->dbType])) {
480 146
            $column->type = $this->typeMap[$column->dbType];
481
        } else {
482
            $column->type = self::TYPE_STRING;
483
        }
484 146
        $column->phpType = $this->getColumnPhpType($column);
485
486 146
        return $column;
487
    }
488
489
    /**
490
     * @inheritdoc
491
     */
492 27
    public function insert($table, $columns)
493
    {
494 27
        $params = [];
495 27
        $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params);
496 27
        $returnColumns = $this->getTableSchema($table)->primaryKey;
497 27
        if (!empty($returnColumns)) {
498 19
            $returning = [];
499 19
            foreach ((array) $returnColumns as $name) {
500 19
                $returning[] = $this->quoteColumnName($name);
501
            }
502 19
            $sql .= ' RETURNING ' . implode(', ', $returning);
503
        }
504
505 27
        $command = $this->db->createCommand($sql, $params);
506 27
        $command->prepare(false);
507 27
        $result = $command->queryOne();
508
509 27
        return !$command->pdoStatement->rowCount() ? false : $result;
510
    }
511
}
512