Completed
Pull Request — master (#2960)
by Sergei
125:59 queued 60:57
created

PostgreSqlSchemaManager::parseDefaultExpression()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

458
        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...
459
            $column->setPlatformOption('jsonb', $jsonb);
460 561
        }
461 341
462
        return $column;
463
    }
464 561
465
    /**
466
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
467
     *
468
     * @param mixed $defaultValue
469
     *
470
     * @return mixed
471
     */
472
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
473
    {
474 561
        if (strpos($defaultValue, '(') === 0) {
475
            return trim($defaultValue, '()');
476 561
        }
477 182
478
        return $defaultValue;
479
    }
480 561
481
    /**
482
     * Parses a default value expression as given by PostgreSQL
483
     */
484
    private function parseDefaultExpression(?string $default) : ?string
485
    {
486
        if ($default === null) {
487
            return $default;
488
        }
489
490
        return str_replace("''", "'", $default);
491
    }
492
}
493