Completed
Pull Request — develop (#3524)
by Jonathan
95:41 queued 92:25
created

_getPortableDatabaseDefinition()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

457
        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...
458 510
            $column->setPlatformOption('jsonb', $jsonb);
459
        }
460
461 718
        return $column;
462
    }
463
464
    /**
465
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
466
     *
467
     * @param mixed $defaultValue
468
     *
469
     * @return mixed
470
     */
471 718
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
472
    {
473 718
        if ($defaultValue !== null && strpos($defaultValue, '(') === 0) {
474 180
            return trim($defaultValue, '()');
475
        }
476
477 718
        return $defaultValue;
478
    }
479
}
480