fixVersion94NegativeNumericDefaultValue()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 2
nop 1
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Schema;
6
7
use Doctrine\DBAL\Exception\DriverException;
8
use Doctrine\DBAL\FetchMode;
9
use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
10
use Doctrine\DBAL\Types\Type;
11
use Doctrine\DBAL\Types\Types;
12
use function array_change_key_case;
13
use function array_filter;
14
use function array_keys;
15
use function array_map;
16
use function array_shift;
17
use function assert;
18
use function explode;
19
use function implode;
20
use function in_array;
21
use function is_string;
22
use function preg_match;
23
use function sprintf;
24
use function str_replace;
25
use function strpos;
26
use function strtolower;
27
use function trim;
28
use const CASE_LOWER;
29
30
/**
31
 * PostgreSQL Schema Manager.
32
 */
33
class PostgreSqlSchemaManager extends AbstractSchemaManager
34
{
35
    /** @var array<int, string> */
36
    private $existingSchemaPaths;
37
38
    /**
39
     * Gets all the existing schema names.
40
     *
41
     * @return array<int, string>
42
     */
43 10
    public function getSchemaNames() : array
44
    {
45 10
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
46
47 10
        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 array<int, string>
56
     */
57 85
    public function getSchemaSearchPaths() : array
58
    {
59 85
        $params = $this->_conn->getParams();
60 85
        $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 85
        if (isset($params['user'])) {
63 85
            $schema = str_replace('"$user"', $params['user'], $schema);
64
        }
65
66 85
        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 array<int, string>
75
     */
76 70
    public function getExistingSchemaSearchPaths() : array
77
    {
78 70
        if ($this->existingSchemaPaths === null) {
79 5
            $this->determineExistingSchemaSearchPaths();
80
        }
81
82 70
        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 5
    public function determineExistingSchemaSearchPaths() : void
91
    {
92 5
        $names = $this->getSchemaNames();
93 5
        $paths = $this->getSchemaSearchPaths();
94
95
        $this->existingSchemaPaths = array_filter($paths, static function ($v) use ($names) : bool {
96 5
            return in_array($v, $names, true);
97 5
        });
98 5
    }
99
100 10
    public function dropDatabase(string $database) : void
101
    {
102
        try {
103 10
            parent::dropDatabase($database);
104 10
        } catch (DriverException $exception) {
105
            // If we have a SQLSTATE 55006, the drop database operation failed
106
            // because of active connections on the database.
107
            // To force dropping the database, we first have to close all active connections
108
            // on that database and issue the drop database operation again.
109 10
            if ($exception->getSQLState() !== '55006') {
110 10
                throw $exception;
111
            }
112
113
            assert($this->_platform instanceof PostgreSQL94Platform);
114
115
            $this->_execSql(
116
                [
117
                    $this->_platform->getDisallowDatabaseConnectionsSQL($database),
118
                    $this->_platform->getCloseActiveDatabaseConnectionsSQL($database),
119
                ]
120
            );
121
122
            parent::dropDatabase($database);
123
        }
124 5
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129 85
    protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey) : ForeignKeyConstraint
130
    {
131 85
        $onUpdate       = null;
132 85
        $onDelete       = null;
133 85
        $localColumns   = [];
134 85
        $foreignColumns = [];
135 85
        $foreignTable   = null;
136
137 85
        if (preg_match(
138
            '(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))',
139 85
            $tableForeignKey['condef'],
140 85
            $match
141 85
        ) === 1) {
142 20
            $onUpdate = $match[1];
143
        }
144
145 85
        if (preg_match(
146
            '(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))',
147 85
            $tableForeignKey['condef'],
148 85
            $match
149 85
        ) === 1) {
150 20
            $onDelete = $match[1];
151
        }
152
153 85
        if (preg_match(
154
            '/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/',
155 85
            $tableForeignKey['condef'],
156 85
            $values
157 85
        ) === 1) {
158
            // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
159
            // the idea to trim them here.
160 85
            $localColumns   = array_map('trim', explode(',', $values[1]));
161 85
            $foreignColumns = array_map('trim', explode(',', $values[3]));
162 85
            $foreignTable   = $values[2];
163
        }
164
165 85
        return new ForeignKeyConstraint(
166 85
            $localColumns,
167
            $foreignTable,
168
            $foreignColumns,
169 85
            $tableForeignKey['conname'],
170 85
            ['onUpdate' => $onUpdate, 'onDelete' => $onDelete]
171
        );
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 5
    protected function _getPortableViewDefinition(array $view) : View
178
    {
179 5
        return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']);
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    protected function _getPortableUserDefinition(array $user) : array
186
    {
187
        return [
188
            'user' => $user['usename'],
189
            'password' => $user['passwd'],
190
        ];
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196 70
    protected function _getPortableTableDefinition(array $table) : string
197
    {
198 70
        $schemas     = $this->getExistingSchemaSearchPaths();
199 70
        $firstSchema = array_shift($schemas);
200
201 70
        if ($table['schema_name'] === $firstSchema) {
202 70
            return $table['table_name'];
203
        }
204
205 30
        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 345
    protected function _getPortableTableIndexesList(array $tableIndexRows, string $tableName) : array
214
    {
215 345
        $buffer = [];
216 345
        foreach ($tableIndexRows as $row) {
217 135
            $colNumbers    = array_map('intval', explode(' ', $row['indkey']));
218 135
            $columnNameSql = sprintf(
219
                'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
220 135
                $row['indrelid'],
221 135
                implode(' ,', $colNumbers)
222
            );
223
224 135
            $stmt         = $this->_conn->executeQuery($columnNameSql);
225 135
            $indexColumns = $stmt->fetchAll();
226
227
            // required for getting the order of the columns right.
228 135
            foreach ($colNumbers as $colNum) {
229 135
                foreach ($indexColumns as $colRow) {
230 135
                    if ($colNum !== $colRow['attnum']) {
231 35
                        continue;
232
                    }
233
234 135
                    $buffer[] = [
235 135
                        'key_name' => $row['relname'],
236 135
                        'column_name' => trim($colRow['attname']),
237 135
                        'non_unique' => ! $row['indisunique'],
238 135
                        'primary' => $row['indisprimary'],
239 135
                        'where' => $row['where'],
240
                    ];
241
                }
242
            }
243
        }
244
245 345
        return parent::_getPortableTableIndexesList($buffer, $tableName);
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 10
    protected function _getPortableDatabaseDefinition(array $database) : string
252
    {
253 10
        return $database['datname'];
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 30
    protected function _getPortableSequencesList(array $sequences) : array
260
    {
261 30
        $sequenceDefinitions = [];
262
263 30
        foreach ($sequences as $sequence) {
264 30
            if ($sequence['schemaname'] !== 'public') {
265 30
                $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
266
            } else {
267 30
                $sequenceName = $sequence['relname'];
268
            }
269
270 30
            $sequenceDefinitions[$sequenceName] = $sequence;
271
        }
272
273 30
        $list = [];
274
275 30
        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
276 30
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
277
        }
278
279 30
        return $list;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285 10
    protected function getPortableNamespaceDefinition(array $namespace) : string
286
    {
287 10
        return $namespace['nspname'];
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293 30
    protected function _getPortableSequenceDefinition(array $sequence) : Sequence
294
    {
295 30
        if ($sequence['schemaname'] !== 'public') {
296 30
            $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
297
        } else {
298 30
            $sequenceName = $sequence['relname'];
299
        }
300
301 30
        if (! isset($sequence['increment_by'], $sequence['min_value'])) {
302
            /** @var string[] $data */
303 18
            $data = $this->_conn->fetchAssoc('SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName));
304
305 18
            $sequence += $data;
306
        }
307
308 30
        return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314 380
    protected function _getPortableTableColumnDefinition(array $tableColumn) : Column
315
    {
316 380
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
317
318 380
        $length = null;
319
320 380
        if (in_array(strtolower($tableColumn['type']), ['varchar', 'bpchar'], true)
321 380
            && preg_match('/\((\d*)\)/', $tableColumn['complete_type'], $matches) === 1) {
322 195
            $length = (int) $matches[1];
323
        }
324
325 380
        $matches = [];
326
327 380
        $autoincrement = false;
328 380
        if ($tableColumn['default'] !== null && preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches) === 1) {
329 85
            $tableColumn['sequence'] = $matches[1];
330 85
            $tableColumn['default']  = null;
331 85
            $autoincrement           = true;
332
        }
333
334 380
        if ($tableColumn['default'] !== null) {
335 150
            if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches) === 1) {
336 135
                $tableColumn['default'] = $matches[1];
337 141
            } elseif (preg_match('/^NULL::/', $tableColumn['default']) === 1) {
338 115
                $tableColumn['default'] = null;
339
            }
340
        }
341
342 380
        if ($length === -1 && isset($tableColumn['atttypmod'])) {
343
            $length = $tableColumn['atttypmod'] - 4;
344
        }
345
346 380
        if ((int) $length <= 0) {
347 375
            $length = null;
348
        }
349
350 380
        $fixed = false;
351
352 380
        if (! isset($tableColumn['name'])) {
353 380
            $tableColumn['name'] = '';
354
        }
355
356 380
        $precision = null;
357 380
        $scale     = 0;
358 380
        $jsonb     = null;
359
360 380
        $dbType = strtolower($tableColumn['type']);
361 380
        if ($tableColumn['domain_type'] !== null
362 380
            && $tableColumn['domain_type'] !== ''
363 380
            && ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])
364
        ) {
365 5
            $dbType                       = strtolower($tableColumn['domain_type']);
366 5
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
367
        }
368
369 380
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'])
370 380
            ?? $this->_platform->getDoctrineTypeMapping($dbType);
371
372
        switch ($dbType) {
373 380
            case 'smallint':
374 380
            case 'int2':
375 15
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
376 15
                $length                 = null;
377 15
                break;
378
379 380
            case 'int':
380 380
            case 'int4':
381 275
            case 'integer':
382 350
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
383 350
                $length                 = null;
384 350
                break;
385
386 275
            case 'bigint':
387 275
            case 'int8':
388 35
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
389 35
                $length                 = null;
390 35
                break;
391
392 255
            case 'bool':
393 245
            case 'boolean':
394 25
                if ($tableColumn['default'] === 'true') {
395
                    $tableColumn['default'] = true;
396
                }
397
398 25
                if ($tableColumn['default'] === 'false') {
399 25
                    $tableColumn['default'] = false;
400
                }
401
402 25
                $length = null;
403 25
                break;
404
405 245
            case 'text':
406 245
            case '_varchar':
407 245
            case 'varchar':
408 210
                $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']);
409 210
                break;
410
411 70
            case 'char':
412 70
            case 'bpchar':
413 25
                $fixed = true;
414 25
                break;
415
416 60
            case 'float':
417 60
            case 'float4':
418 60
            case 'float8':
419 60
            case 'double':
420 60
            case 'double precision':
421 60
            case 'real':
422 60
            case 'decimal':
423 60
            case 'money':
424 60
            case 'numeric':
425 35
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
426
427 35
                if (preg_match(
428
                    '([A-Za-z]+\(([0-9]+)\,([0-9]+)\))',
429 35
                    $tableColumn['complete_type'],
430 35
                    $match
431 35
                ) === 1) {
432 35
                    $precision = (int) $match[1];
433 35
                    $scale     = (int) $match[2];
434 35
                    $length    = null;
435
                }
436
437 35
                break;
438
439 55
            case 'year':
440
                $length = null;
441
                break;
442
443
            // PostgreSQL 9.4+ only
444 55
            case 'jsonb':
445 15
                $jsonb = true;
446 15
                break;
447
        }
448
449 380
        if (is_string($tableColumn['default']) && preg_match(
450
            "('([^']+)'::)",
451 140
            $tableColumn['default'],
452 140
            $match
453 380
        ) === 1) {
454 100
            $tableColumn['default'] = $match[1];
455
        }
456
457
        $options = [
458 380
            'length'        => $length,
459 380
            'notnull'       => (bool) $tableColumn['isnotnull'],
460 380
            'default'       => $tableColumn['default'],
461 380
            'precision'     => $precision,
462 380
            'scale'         => $scale,
463 380
            'fixed'         => $fixed,
464
            'unsigned'      => false,
465 380
            'autoincrement' => $autoincrement,
466
        ];
467
468 380
        if (isset($tableColumn['comment'])) {
469 80
            $options['comment'] = $tableColumn['comment'];
470
        }
471
472 380
        $column = new Column($tableColumn['field'], Type::getType($type), $options);
473
474 380
        if (isset($tableColumn['collation']) && ! empty($tableColumn['collation'])) {
475
            $column->setPlatformOption('collation', $tableColumn['collation']);
476
        }
477
478 380
        if ($column->getType()->getName() === Types::JSON) {
479 20
            $column->setPlatformOption('jsonb', $jsonb);
480
        }
481
482 380
        return $column;
483
    }
484
485
    /**
486
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
487
     *
488
     * @param mixed $defaultValue
489
     *
490
     * @return mixed
491
     */
492 360
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
493
    {
494 360
        if ($defaultValue !== null && strpos($defaultValue, '(') === 0) {
495 3
            return trim($defaultValue, '()');
496
        }
497
498 359
        return $defaultValue;
499
    }
500
501
    /**
502
     * Parses a default value expression as given by PostgreSQL
503
     */
504 210
    private function parseDefaultExpression(?string $default) : ?string
505
    {
506 210
        if ($default === null) {
507 200
            return $default;
508
        }
509
510 130
        return str_replace("''", "'", $default);
511
    }
512
513 320
    public function listTableDetails(string $tableName) : Table
514
    {
515 320
        $table = parent::listTableDetails($tableName);
516
517 320
        $platform = $this->_platform;
518 320
        assert($platform instanceof PostgreSQL94Platform);
519 320
        $sql = $platform->getListTableMetadataSQL($tableName);
520
521 320
        $tableOptions = $this->_conn->fetchAssoc($sql);
522
523 320
        if ($tableOptions !== false) {
524 320
            $table->addOption('comment', $tableOptions['table_comment']);
525
        }
526
527 320
        return $table;
528
    }
529
}
530