Failed Conditions
Pull Request — 2.10.x (#3997)
by
unknown
66:03 queued 62:30
created

_getPortableTableIndexesList()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 33
ccs 20
cts 20
cp 1
rs 9.2888
c 0
b 0
f 0
cc 5
nc 5
nop 2
crap 5
1
<?php
2
3
namespace Doctrine\DBAL\Schema;
4
5
use Doctrine\DBAL\Exception\DriverException;
6
use Doctrine\DBAL\FetchMode;
7
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
8
use Doctrine\DBAL\Types\Type;
9
use Doctrine\DBAL\Types\Types;
10
use function array_change_key_case;
11
use function array_filter;
12
use function array_keys;
13
use function array_map;
14
use function array_shift;
15
use function assert;
16
use function explode;
17
use function implode;
18
use function in_array;
19
use function preg_match;
20
use function preg_replace;
21
use function sprintf;
22
use function str_replace;
23
use function strlen;
24
use function strpos;
25
use function strtolower;
26
use function trim;
27
use const CASE_LOWER;
28
29
/**
30
 * PostgreSQL Schema Manager.
31
 */
32
class PostgreSqlSchemaManager extends AbstractSchemaManager
33
{
34
    /** @var string[] */
35
    private $existingSchemaPaths;
36
37
    /**
38
     * Gets all the existing schema names.
39
     *
40
     * @return string[]
41
     */
42 660
    public function getSchemaNames()
43
    {
44 660
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
45
46 660
        return $statement->fetchAll(FetchMode::COLUMN);
47
    }
48
49
    /**
50
     * Returns an array of schema search paths.
51
     *
52
     * This is a PostgreSQL only function.
53
     *
54
     * @return string[]
55
     */
56 660
    public function getSchemaSearchPaths()
57
    {
58 660
        $params = $this->_conn->getParams();
59 660
        $schema = explode(',', $this->_conn->fetchColumn('SHOW search_path'));
0 ignored issues
show
Bug introduced by
It seems like $this->_conn->fetchColumn('SHOW search_path') can also be of type false; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

59
        $schema = explode(',', /** @scrutinizer ignore-type */ $this->_conn->fetchColumn('SHOW search_path'));
Loading history...
60
61 660
        if (isset($params['user'])) {
62 660
            $schema = str_replace('"$user"', $params['user'], $schema);
63
        }
64
65 660
        return array_map('trim', $schema);
66
    }
67
68
    /**
69
     * Gets names of all existing schemas in the current users search path.
70
     *
71
     * This is a PostgreSQL only function.
72
     *
73
     * @return string[]
74
     */
75 660
    public function getExistingSchemaSearchPaths()
76
    {
77 660
        if ($this->existingSchemaPaths === null) {
78 660
            $this->determineExistingSchemaSearchPaths();
79
        }
80
81 660
        return $this->existingSchemaPaths;
82
    }
83
84
    /**
85
     * Sets or resets the order of the existing schemas in the current search path of the user.
86
     *
87
     * This is a PostgreSQL only function.
88
     *
89
     * @return void
90
     */
91 660
    public function determineExistingSchemaSearchPaths()
92
    {
93 660
        $names = $this->getSchemaNames();
94 660
        $paths = $this->getSchemaSearchPaths();
95
96
        $this->existingSchemaPaths = array_filter($paths, static function ($v) use ($names) {
97 660
            return in_array($v, $names);
98 660
        });
99 660
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104 660
    public function dropDatabase($database)
105
    {
106
        try {
107 660
            parent::dropDatabase($database);
108 660
        } catch (DriverException $exception) {
109
            // If we have a SQLSTATE 55006, the drop database operation failed
110
            // because of active connections on the database.
111
            // To force dropping the database, we first have to close all active connections
112
            // on that database and issue the drop database operation again.
113 660
            if ($exception->getSQLState() !== '55006') {
114 660
                throw $exception;
115
            }
116
117 385
            assert($this->_platform instanceof PostgreSqlPlatform);
118
119 385
            $this->_execSql(
120
                [
121 385
                    $this->_platform->getDisallowDatabaseConnectionsSQL($database),
122 385
                    $this->_platform->getCloseActiveDatabaseConnectionsSQL($database),
123
                ]
124
            );
125
126 385
            parent::dropDatabase($database);
127
        }
128 385
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 521
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
134
    {
135 521
        $onUpdate       = null;
136 521
        $onDelete       = null;
137 521
        $localColumns   = [];
138 521
        $foreignColumns = [];
139 521
        $foreignTable   = null;
140
141 521
        if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
142 514
            $onUpdate = $match[1];
143
        }
144
145 521
        if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
146 500
            $onDelete = $match[1];
147
        }
148
149 521
        if (preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values)) {
150
            // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
151
            // the idea to trim them here.
152 521
            $localColumns   = array_map('trim', explode(',', $values[1]));
153 521
            $foreignColumns = array_map('trim', explode(',', $values[3]));
154 521
            $foreignTable   = $values[2];
155
        }
156
157 521
        return new ForeignKeyConstraint(
158 521
            $localColumns,
159
            $foreignTable,
160
            $foreignColumns,
161 521
            $tableForeignKey['conname'],
162 521
            ['onUpdate' => $onUpdate, 'onDelete' => $onDelete]
163
        );
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169
    protected function _getPortableTriggerDefinition($trigger)
170
    {
171
        return $trigger['trigger_name'];
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 266
    protected function _getPortableViewDefinition($view)
178
    {
179 266
        return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']);
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    protected function _getPortableUserDefinition($user)
186
    {
187
        return [
188
            'user' => $user['usename'],
189
            'password' => $user['passwd'],
190
        ];
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196 660
    protected function _getPortableTableDefinition($table)
197
    {
198 660
        $schemas     = $this->getExistingSchemaSearchPaths();
199 660
        $firstSchema = array_shift($schemas);
200
201 660
        if ($table['schema_name'] === $firstSchema) {
202 660
            return $table['table_name'];
203
        }
204
205 521
        return $table['schema_name'] . '.' . $table['table_name'];
206
    }
207
208
    /**
209
     * {@inheritdoc}
210
     *
211
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
212
     */
213 611
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
214
    {
215 611
        $buffer = [];
216 611
        foreach ($tableIndexes as $row) {
217 549
            $colNumbers    = array_map('intval', explode(' ', $row['indkey']));
218 549
            $columnNameSql = sprintf(
219
                'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
220 549
                $row['indrelid'],
221 549
                implode(' ,', $colNumbers)
222
            );
223
224 549
            $stmt         = $this->_conn->executeQuery($columnNameSql);
225 549
            $indexColumns = $stmt->fetchAll();
226
227
            // required for getting the order of the columns right.
228 549
            foreach ($colNumbers as $colNum) {
229 549
                foreach ($indexColumns as $colRow) {
230 549
                    if ($colNum !== $colRow['attnum']) {
231 451
                        continue;
232
                    }
233
234 549
                    $buffer[] = [
235 549
                        'key_name' => $row['relname'],
236 549
                        'column_name' => trim($colRow['attname']),
237 549
                        'non_unique' => ! $row['indisunique'],
238 549
                        'primary' => $row['indisprimary'],
239 549
                        'where' => $row['where'],
240
                    ];
241
                }
242
            }
243
        }
244
245 611
        return parent::_getPortableTableIndexesList($buffer, $tableName);
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 385
    protected function _getPortableDatabaseDefinition($database)
252
    {
253 385
        return $database['datname'];
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 378
    protected function _getPortableSequencesList($sequences)
260
    {
261 378
        $sequenceDefinitions = [];
262
263 378
        foreach ($sequences as $sequence) {
264 378
            if ($sequence['schemaname'] !== 'public') {
265 378
                $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
266
            } else {
267 378
                $sequenceName = $sequence['relname'];
268
            }
269
270 378
            $sequenceDefinitions[$sequenceName] = $sequence;
271
        }
272
273 378
        $list = [];
274
275 378
        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
276 378
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
277
        }
278
279 378
        return $list;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285 357
    protected function getPortableNamespaceDefinition(array $namespace)
286
    {
287 357
        return $namespace['nspname'];
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293 378
    protected function _getPortableSequenceDefinition($sequence)
294
    {
295 378
        if ($sequence['schemaname'] !== 'public') {
296 378
            $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
297
        } else {
298 378
            $sequenceName = $sequence['relname'];
299
        }
300
301 378
        if (! isset($sequence['increment_by'], $sequence['min_value'])) {
302
            /** @var string[] $data */
303 270
            $data = $this->_conn->fetchAssoc('SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName));
304
305 270
            $sequence += $data;
306
        }
307
308 378
        return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314 611
    protected function _getPortableTableColumnDefinition($tableColumn)
315
    {
316 611
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
317
318 611
        if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
319
            // get length from varchar definition
320 597
            $length                = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']);
321 597
            $tableColumn['length'] = $length;
322
        }
323
324 611
        $matches = [];
325
326 611
        $autoincrement = false;
327 611
        if (preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) {
328 542
            $tableColumn['sequence'] = $matches[1];
329 542
            $tableColumn['default']  = null;
330 542
            $autoincrement           = true;
331
        }
332
333 611
        if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches)) {
334 597
            $tableColumn['default'] = $matches[1];
335 611
        } elseif (preg_match('/^NULL::/', $tableColumn['default'])) {
336 597
            $tableColumn['default'] = null;
337
        }
338
339 611
        $length = $tableColumn['length'] ?? null;
340 611
        if ($length === '-1' && isset($tableColumn['atttypmod'])) {
341
            $length = $tableColumn['atttypmod'] - 4;
342
        }
343
344 611
        if ((int) $length <= 0) {
345 611
            $length = null;
346
        }
347
348 611
        $fixed = null;
349
350 611
        if (! isset($tableColumn['name'])) {
351 611
            $tableColumn['name'] = '';
352
        }
353
354 611
        $precision = null;
355 611
        $scale     = null;
356 611
        $jsonb     = null;
357
358 611
        $dbType = strtolower($tableColumn['type']);
359 611
        if (strlen($tableColumn['domain_type']) && ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])) {
360 549
            $dbType                       = strtolower($tableColumn['domain_type']);
361 549
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
362
        }
363
364 611
        $type                   = $this->_platform->getDoctrineTypeMapping($dbType);
365 611
        $type                   = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
366 611
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
367
368
        switch ($dbType) {
369 611
            case 'smallint':
370 611
            case 'int2':
371 434
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
372 434
                $length                 = null;
373 434
                break;
374
375 611
            case 'int':
376 611
            case 'int4':
377 604
            case 'integer':
378 611
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
379 611
                $length                 = null;
380 611
                break;
381
382 604
            case 'bigint':
383 604
            case 'int8':
384 434
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
385 434
                $length                 = null;
386 434
                break;
387
388 604
            case 'bool':
389 597
            case 'boolean':
390 604
                if ($tableColumn['default'] === 'true') {
391
                    $tableColumn['default'] = true;
392
                }
393
394 604
                if ($tableColumn['default'] === 'false') {
395 604
                    $tableColumn['default'] = false;
396
                }
397
398 604
                $length = null;
399 604
                break;
400
401 597
            case 'text':
402 597
            case '_varchar':
403 597
            case 'varchar':
404 597
                $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']);
405 597
                $fixed                  = false;
406 597
                break;
407 549
            case 'interval':
408
                $fixed = false;
409
                break;
410
411 549
            case 'char':
412 549
            case 'bpchar':
413 458
                $fixed = true;
414 458
                break;
415
416 549
            case 'float':
417 549
            case 'float4':
418 549
            case 'float8':
419 549
            case 'double':
420 549
            case 'double precision':
421 549
            case 'real':
422 549
            case 'decimal':
423 549
            case 'money':
424 549
            case 'numeric':
425 549
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
426
427 549
                if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) {
428 549
                    $precision = $match[1];
429 549
                    $scale     = $match[2];
430 549
                    $length    = null;
431
                }
432
433 549
                break;
434
435 549
            case 'year':
436
                $length = null;
437
                break;
438
439
            // PostgreSQL 9.4+ only
440 549
            case 'jsonb':
441 320
                $jsonb = true;
442 320
                break;
443
        }
444
445 611
        if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) {
446 597
            $tableColumn['default'] = $match[1];
447
        }
448
449
        $options = [
450 611
            'length'        => $length,
451 611
            'notnull'       => (bool) $tableColumn['isnotnull'],
452 611
            'default'       => $tableColumn['default'],
453 611
            'precision'     => $precision,
454 611
            'scale'         => $scale,
455 611
            'fixed'         => $fixed,
456
            'unsigned'      => false,
457 611
            'autoincrement' => $autoincrement,
458 611
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
459 231
                ? $tableColumn['comment']
460
                : null,
461
        ];
462
463 611
        $column = new Column($tableColumn['field'], Type::getType($type), $options);
464
465 611
        if (isset($tableColumn['collation']) && ! empty($tableColumn['collation'])) {
466
            $column->setPlatformOption('collation', $tableColumn['collation']);
467
        }
468
469 611
        if (in_array($column->getType()->getName(), [Types::JSON_ARRAY, Types::JSON], true)) {
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Types::JSON_ARRAY has been deprecated: json_array type is deprecated, use {@see self::JSON} instead. ( Ignorable by Annotation )

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

469
        if (in_array($column->getType()->getName(), [/** @scrutinizer ignore-deprecated */ Types::JSON_ARRAY, Types::JSON], true)) {

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
470 349
            $column->setPlatformOption('jsonb', $jsonb);
471
        }
472
473 611
        return $column;
474
    }
475
476
    /**
477
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
478
     *
479
     * @param mixed $defaultValue
480
     *
481
     * @return mixed
482
     */
483 611
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
484
    {
485 611
        if (strpos($defaultValue, '(') === 0) {
486 186
            return trim($defaultValue, '()');
487
        }
488
489 611
        return $defaultValue;
490
    }
491
492
    /**
493
     * Parses a default value expression as given by PostgreSQL
494
     */
495 597
    private function parseDefaultExpression(?string $default) : ?string
496
    {
497 597
        if ($default === null) {
498 597
            return $default;
499
        }
500
501 597
        return str_replace("''", "'", $default);
502
    }
503
504
    /**
505
     * {@inheritdoc}
506
     */
507 611
    public function listTableDetails($tableName) : Table
508
    {
509 611
        $table = parent::listTableDetails($tableName);
510
511 611
        $platform = $this->_platform;
512 611
        assert($platform instanceof PostgreSqlPlatform);
513 611
        $sql = $platform->getListTableMetadataSQL($tableName);
514
515 611
        $tableOptions = $this->_conn->fetchAssoc($sql);
516
517 611
        if ($tableOptions !== false) {
518 611
            $table->addOption('comment', $tableOptions['table_comment']);
519
        }
520
521 611
        return $table;
522
    }
523
}
524