Completed
Pull Request — master (#3512)
by David
23:44
created

_getPortableSequenceDefinition()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 16
ccs 8
cts 8
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 4
nop 1
crap 3
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 const CASE_LOWER;
11
use function array_change_key_case;
12
use function array_filter;
13
use function array_keys;
14
use function array_map;
15
use function array_shift;
16
use function assert;
17
use function explode;
18
use function implode;
19
use function in_array;
20
use function preg_match;
21
use function preg_replace;
22
use function sprintf;
23
use function str_replace;
24
use function stripos;
25
use function strlen;
26
use function strpos;
27
use function strtolower;
28
use function trim;
29
30
/**
31
 * PostgreSQL Schema Manager.
32
 */
33
class PostgreSqlSchemaManager extends AbstractSchemaManager
34
{
35
    /** @var string[] */
36
    private $existingSchemaPaths;
37
38
    /**
39
     * Gets all the existing schema names.
40
     *
41
     * @return string[]
42
     */
43 621
    public function getSchemaNames()
44
    {
45 621
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
46
47 621
        return $statement->fetchAll(FetchMode::COLUMN);
48
    }
49
50
    /**
51
     * Returns an array of schema search paths.
52
     *
53
     * This is a PostgreSQL only function.
54
     *
55
     * @return string[]
56
     */
57 621
    public function getSchemaSearchPaths()
58
    {
59 621
        $params = $this->_conn->getParams();
60 621
        $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

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

460
        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...
461 352
            $column->setPlatformOption('jsonb', $jsonb);
462
        }
463
464 572
        return $column;
465
    }
466
467
    /**
468
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
469
     *
470
     * @param mixed $defaultValue
471
     *
472
     * @return mixed
473
     */
474 572
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
475
    {
476 572
        if (strpos($defaultValue, '(') === 0) {
477 186
            return trim($defaultValue, '()');
478
        }
479
480 572
        return $defaultValue;
481
    }
482
483 572
    public function listTableDetails($tableName)
484
    {
485 572
        $table = parent::listTableDetails($tableName);
486
487
        /** @var PostgreSqlPlatform $platform */
488 572
        $platform = $this->_platform;
489 572
        $sql      = $platform->getListTableMetadataSQL($tableName);
490
491 572
        $tableOptions = $this->_conn->fetchAssoc($sql);
492
493 572
        $table->addOption('comment', $tableOptions['table_comment']);
494
495 572
        return $table;
496
    }
497
}
498