Completed
Push — refactor-db-tests ( 8ba032...86f17c )
by Carsten
10:02
created

Schema::findSchemaNames()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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