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