Completed
Push — master ( be658f...d6bd62 )
by Carsten
68:33 queued 62:00
created

Schema::resolveTableNames()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0987

Importance

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