Completed
Push — 2.10.x ( 61a6b9...f20ba1 )
by Grégoire
13:37 queued 11s
created

_getPortableSequencesList()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 21
ccs 11
cts 11
cp 1
rs 9.9
c 0
b 0
f 0
cc 4
nc 6
nop 1
crap 4
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 659
    public function getSchemaNames()
43
    {
44 659
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
45
46 659
        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 659
    public function getSchemaSearchPaths()
57
    {
58 659
        $params = $this->_conn->getParams();
59 659
        $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 659
        if (isset($params['user'])) {
62 659
            $schema = str_replace('"$user"', $params['user'], $schema);
63
        }
64
65 659
        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 659
    public function getExistingSchemaSearchPaths()
76
    {
77 659
        if ($this->existingSchemaPaths === null) {
78 659
            $this->determineExistingSchemaSearchPaths();
79
        }
80
81 659
        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 659
    public function determineExistingSchemaSearchPaths()
92
    {
93 659
        $names = $this->getSchemaNames();
94 659
        $paths = $this->getSchemaSearchPaths();
95
96
        $this->existingSchemaPaths = array_filter($paths, static function ($v) use ($names) {
97 659
            return in_array($v, $names);
98 659
        });
99 659
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104 659
    public function dropDatabase($database)
105
    {
106
        try {
107 659
            parent::dropDatabase($database);
108 659
        } 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 659
            if ($exception->getSQLState() !== '55006') {
114 659
                throw $exception;
115
            }
116
117 388
            assert($this->_platform instanceof PostgreSqlPlatform);
118
119 388
            $this->_execSql(
120
                [
121 388
                    $this->_platform->getDisallowDatabaseConnectionsSQL($database),
122 388
                    $this->_platform->getCloseActiveDatabaseConnectionsSQL($database),
123
                ]
124
            );
125
126 388
            parent::dropDatabase($database);
127
        }
128 388
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 573
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
134
    {
135 573
        $onUpdate       = null;
136 573
        $onDelete       = null;
137 573
        $localColumns   = [];
138 573
        $foreignColumns = [];
139 573
        $foreignTable   = null;
140
141 573
        if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
142 517
            $onUpdate = $match[1];
143
        }
144
145 573
        if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
146 503
            $onDelete = $match[1];
147
        }
148
149 573
        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 573
            $localColumns   = array_map('trim', explode(',', $values[1]));
153 573
            $foreignColumns = array_map('trim', explode(',', $values[3]));
154 573
            $foreignTable   = $values[2];
155
        }
156
157 573
        return new ForeignKeyConstraint(
158 573
            $localColumns,
159
            $foreignTable,
160
            $foreignColumns,
161 573
            $tableForeignKey['conname'],
162 573
            ['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 269
    protected function _getPortableViewDefinition($view)
178
    {
179 269
        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 659
    protected function _getPortableTableDefinition($table)
197
    {
198 659
        $schemas     = $this->getExistingSchemaSearchPaths();
199 659
        $firstSchema = array_shift($schemas);
200
201 659
        if ($table['schema_name'] === $firstSchema) {
202 659
            return $table['table_name'];
203
        }
204
205 524
        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 610
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
214
    {
215 610
        $buffer = [];
216 610
        foreach ($tableIndexes as $row) {
217 552
            $colNumbers    = array_map('intval', explode(' ', $row['indkey']));
218 552
            $columnNameSql = sprintf(
219
                'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
220 552
                $row['indrelid'],
221 552
                implode(' ,', $colNumbers)
222
            );
223
224 552
            $stmt         = $this->_conn->executeQuery($columnNameSql);
225 552
            $indexColumns = $stmt->fetchAll();
226
227
            // required for getting the order of the columns right.
228 552
            foreach ($colNumbers as $colNum) {
229 552
                foreach ($indexColumns as $colRow) {
230 552
                    if ($colNum !== $colRow['attnum']) {
231 454
                        continue;
232
                    }
233
234 552
                    $buffer[] = [
235 552
                        'key_name' => $row['relname'],
236 552
                        'column_name' => trim($colRow['attname']),
237 552
                        'non_unique' => ! $row['indisunique'],
238 552
                        'primary' => $row['indisprimary'],
239 552
                        'where' => $row['where'],
240
                    ];
241
                }
242
            }
243
        }
244
245 610
        return parent::_getPortableTableIndexesList($buffer, $tableName);
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 388
    protected function _getPortableDatabaseDefinition($database)
252
    {
253 388
        return $database['datname'];
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 381
    protected function _getPortableSequencesList($sequences)
260
    {
261 381
        $sequenceDefinitions = [];
262
263 381
        foreach ($sequences as $sequence) {
264 381
            if ($sequence['schemaname'] !== 'public') {
265 381
                $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
266
            } else {
267 381
                $sequenceName = $sequence['relname'];
268
            }
269
270 381
            $sequenceDefinitions[$sequenceName] = $sequence;
271
        }
272
273 381
        $list = [];
274
275 381
        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
276 381
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
277
        }
278
279 381
        return $list;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285 360
    protected function getPortableNamespaceDefinition(array $namespace)
286
    {
287 360
        return $namespace['nspname'];
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293 381
    protected function _getPortableSequenceDefinition($sequence)
294
    {
295 381
        if ($sequence['schemaname'] !== 'public') {
296 381
            $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
297
        } else {
298 381
            $sequenceName = $sequence['relname'];
299
        }
300
301 381
        if (! isset($sequence['increment_by'], $sequence['min_value'])) {
302
            /** @var string[] $data */
303 273
            $data = $this->_conn->fetchAssoc('SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName));
304
305 273
            $sequence += $data;
306
        }
307
308 381
        return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314 610
    protected function _getPortableTableColumnDefinition($tableColumn)
315
    {
316 610
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
317
318 610
        if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
319
            // get length from varchar definition
320 596
            $length                = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']);
321 596
            $tableColumn['length'] = $length;
322
        }
323
324 610
        $matches = [];
325
326 610
        $autoincrement = false;
327 610
        if (preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) {
328 545
            $tableColumn['sequence'] = $matches[1];
329 545
            $tableColumn['default']  = null;
330 545
            $autoincrement           = true;
331
        }
332
333 610
        if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches)) {
334 596
            $tableColumn['default'] = $matches[1];
335 610
        } elseif (preg_match('/^NULL::/', $tableColumn['default'])) {
336 596
            $tableColumn['default'] = null;
337
        }
338
339 610
        $length = $tableColumn['length'] ?? null;
340 610
        if ($length === '-1' && isset($tableColumn['atttypmod'])) {
341
            $length = $tableColumn['atttypmod'] - 4;
342
        }
343
344 610
        if ((int) $length <= 0) {
345 610
            $length = null;
346
        }
347
348 610
        $fixed = null;
349
350 610
        if (! isset($tableColumn['name'])) {
351 610
            $tableColumn['name'] = '';
352
        }
353
354 610
        $precision = null;
355 610
        $scale     = null;
356 610
        $jsonb     = null;
357
358 610
        $dbType = strtolower($tableColumn['type']);
359 610
        if (strlen($tableColumn['domain_type']) && ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])) {
360 552
            $dbType                       = strtolower($tableColumn['domain_type']);
361 552
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
362
        }
363
364 610
        $type                   = $this->_platform->getDoctrineTypeMapping($dbType);
365 610
        $type                   = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
366 610
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
367
368
        switch ($dbType) {
369 610
            case 'smallint':
370 610
            case 'int2':
371 437
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
372 437
                $length                 = null;
373 437
                break;
374
375 610
            case 'int':
376 610
            case 'int4':
377 603
            case 'integer':
378 610
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
379 610
                $length                 = null;
380 610
                break;
381
382 603
            case 'bigint':
383 603
            case 'int8':
384 437
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
385 437
                $length                 = null;
386 437
                break;
387
388 603
            case 'bool':
389 596
            case 'boolean':
390 603
                if ($tableColumn['default'] === 'true') {
391
                    $tableColumn['default'] = true;
392
                }
393
394 603
                if ($tableColumn['default'] === 'false') {
395 603
                    $tableColumn['default'] = false;
396
                }
397
398 603
                $length = null;
399 603
                break;
400
401 596
            case 'text':
402 596
            case '_varchar':
403 596
            case 'varchar':
404 596
                $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']);
405 596
                $fixed                  = false;
406 596
                break;
407 552
            case 'interval':
408
                $fixed = false;
409
                break;
410
411 552
            case 'char':
412 552
            case 'bpchar':
413 461
                $fixed = true;
414 461
                break;
415
416 552
            case 'float':
417 552
            case 'float4':
418 552
            case 'float8':
419 552
            case 'double':
420 552
            case 'double precision':
421 552
            case 'real':
422 552
            case 'decimal':
423 552
            case 'money':
424 552
            case 'numeric':
425 552
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
426
427 552
                if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) {
428 552
                    $precision = $match[1];
429 552
                    $scale     = $match[2];
430 552
                    $length    = null;
431
                }
432
433 552
                break;
434
435 552
            case 'year':
436
                $length = null;
437
                break;
438
439
            // PostgreSQL 9.4+ only
440 552
            case 'jsonb':
441 322
                $jsonb = true;
442 322
                break;
443
        }
444
445 610
        if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) {
446 596
            $tableColumn['default'] = $match[1];
447
        }
448
449
        $options = [
450 610
            'length'        => $length,
451 610
            'notnull'       => (bool) $tableColumn['isnotnull'],
452 610
            'default'       => $tableColumn['default'],
453 610
            'precision'     => $precision,
454 610
            'scale'         => $scale,
455 610
            'fixed'         => $fixed,
456
            'unsigned'      => false,
457 610
            'autoincrement' => $autoincrement,
458 610
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
459 234
                ? $tableColumn['comment']
460
                : null,
461
        ];
462
463 610
        $column = new Column($tableColumn['field'], Type::getType($type), $options);
464
465 610
        if (isset($tableColumn['collation']) && ! empty($tableColumn['collation'])) {
466
            $column->setPlatformOption('collation', $tableColumn['collation']);
467
        }
468
469 610
        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 352
            $column->setPlatformOption('jsonb', $jsonb);
471
        }
472
473 610
        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 610
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
484
    {
485 610
        if (strpos($defaultValue, '(') === 0) {
486 188
            return trim($defaultValue, '()');
487
        }
488
489 610
        return $defaultValue;
490
    }
491
492
    /**
493
     * Parses a default value expression as given by PostgreSQL
494
     */
495 596
    private function parseDefaultExpression(?string $default) : ?string
496
    {
497 596
        if ($default === null) {
498 596
            return $default;
499
        }
500
501 596
        return str_replace("''", "'", $default);
502
    }
503
504
    /**
505
     * {@inheritdoc}
506
     */
507 610
    public function listTableDetails($tableName) : Table
508
    {
509 610
        $table = parent::listTableDetails($tableName);
510
511 610
        $platform = $this->_platform;
512 610
        assert($platform instanceof PostgreSqlPlatform);
513 610
        $sql = $platform->getListTableMetadataSQL($tableName);
514
515 610
        $tableOptions = $this->_conn->fetchAssoc($sql);
516
517 610
        if ($tableOptions !== false) {
518 610
            $table->addOption('comment', $tableOptions['table_comment']);
519
        }
520
521 610
        return $table;
522
    }
523
}
524