Completed
Pull Request — develop (#3533)
by
unknown
16:31 queued 01:26
created

_getPortableTableForeignKeyDefinition()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4.016

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 29
ccs 18
cts 20
cp 0.9
rs 9.6333
c 0
b 0
f 0
cc 4
nc 8
nop 1
crap 4.016
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 521
    public function getSchemaNames()
45
    {
46 521
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
47
48 521
        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 521
    public function getSchemaSearchPaths()
59
    {
60 521
        $params = $this->_conn->getParams();
61 521
        $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 521
        if (isset($params['user'])) {
64 521
            $schema = str_replace('"$user"', $params['user'], $schema);
65
        }
66
67 521
        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 521
    public function getExistingSchemaSearchPaths()
78
    {
79 521
        if ($this->existingSchemaPaths === null) {
80 521
            $this->determineExistingSchemaSearchPaths();
81
        }
82
83 521
        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 521
    public function determineExistingSchemaSearchPaths()
94
    {
95 521
        $names = $this->getSchemaNames();
96 521
        $paths = $this->getSchemaSearchPaths();
97
98
        $this->existingSchemaPaths = array_filter($paths, static function ($v) use ($names) {
99 521
            return in_array($v, $names);
100 521
        });
101 521
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106 521
    public function dropDatabase($database)
107
    {
108
        try {
109 521
            parent::dropDatabase($database);
110 521
        } 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 521
            if ($exception->getSQLState() !== '55006') {
116 521
                throw $exception;
117
            }
118
119 319
            assert($this->_platform instanceof PostgreSqlPlatform);
120
121 319
            $this->_execSql(
122
                [
123 319
                    $this->_platform->getDisallowDatabaseConnectionsSQL($database),
124 319
                    $this->_platform->getCloseActiveDatabaseConnectionsSQL($database),
125
                ]
126
            );
127
128 319
            parent::dropDatabase($database);
129
        }
130 319
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135 431
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
136
    {
137 431
        $onUpdate       = null;
138 431
        $onDelete       = null;
139 431
        $localColumns   = [];
140 431
        $foreignColumns = [];
141 431
        $foreignTable   = null;
142
143 431
        if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
144 425
            $onUpdate = $match[1];
145
        }
146 431
        if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
147 413
            $onDelete = $match[1];
148
        }
149
150 431
        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 431
            $localColumns   = array_map('trim', explode(',', $values[1]));
154 431
            $foreignColumns = array_map('trim', explode(',', $values[3]));
155 431
            $foreignTable   = $values[2];
156
        }
157
158 431
        return new ForeignKeyConstraint(
159 431
            $localColumns,
160
            $foreignTable,
161
            $foreignColumns,
162 431
            $tableForeignKey['conname'],
163 431
            ['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 217
    protected function _getPortableViewDefinition($view)
179
    {
180 217
        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 521
    protected function _getPortableTableDefinition($table)
198
    {
199 521
        $schemas     = $this->getExistingSchemaSearchPaths();
200 521
        $firstSchema = array_shift($schemas);
201
202 521
        if ($table['schema_name'] === $firstSchema) {
203 521
            return $table['table_name'];
204
        }
205
206 431
        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 479
    protected function _getPortableTableIndexesList(array $tableIndexRows, string $tableName) : array
215
    {
216 479
        $buffer = [];
217 479
        foreach ($tableIndexRows as $row) {
218 455
            $colNumbers    = array_map('intval', explode(' ', $row['indkey']));
219 455
            $columnNameSql = sprintf(
220
                'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
221 455
                $row['indrelid'],
222 455
                implode(' ,', $colNumbers)
223
            );
224
225 455
            $stmt         = $this->_conn->executeQuery($columnNameSql);
226 455
            $indexColumns = $stmt->fetchAll();
227
228
            // required for getting the order of the columns right.
229 455
            foreach ($colNumbers as $colNum) {
230 455
                foreach ($indexColumns as $colRow) {
231 455
                    if ($colNum !== $colRow['attnum']) {
232 377
                        continue;
233
                    }
234
235 455
                    $buffer[] = [
236 455
                        'key_name' => $row['relname'],
237 455
                        'column_name' => trim($colRow['attname']),
238 455
                        'non_unique' => ! $row['indisunique'],
239 455
                        'primary' => $row['indisprimary'],
240 455
                        'where' => $row['where'],
241
                    ];
242
                }
243
            }
244
        }
245
246 479
        return parent::_getPortableTableIndexesList($buffer, $tableName);
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252 319
    protected function _getPortableDatabaseDefinition($database)
253
    {
254 319
        return $database['datname'];
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260 313
    protected function _getPortableSequencesList($sequences)
261
    {
262 313
        $sequenceDefinitions = [];
263
264 313
        foreach ($sequences as $sequence) {
265 313
            if ($sequence['schemaname'] !== 'public') {
266 313
                $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
267
            } else {
268 313
                $sequenceName = $sequence['relname'];
269
            }
270
271 313
            $sequenceDefinitions[$sequenceName] = $sequence;
272
        }
273
274 313
        $list = [];
275
276 313
        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
277 313
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
278
        }
279
280 313
        return $list;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286 295
    protected function getPortableNamespaceDefinition(array $namespace)
287
    {
288 295
        return $namespace['nspname'];
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     */
294 313
    protected function _getPortableSequenceDefinition($sequence)
295
    {
296 313
        if ($sequence['schemaname'] !== 'public') {
297 313
            $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
298
        } else {
299 313
            $sequenceName = $sequence['relname'];
300
        }
301
302 313
        if (! isset($sequence['increment_by'], $sequence['min_value'])) {
303
            /** @var string[] $data */
304 208
            $data = $this->_conn->fetchAssoc('SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName));
305
306 208
            $sequence += $data;
307
        }
308
309 313
        return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
310
    }
311
312
    /**
313
     * {@inheritdoc}
314
     */
315 479
    protected function _getPortableTableColumnDefinition($tableColumn)
316
    {
317 479
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
318
319 479
        $length = null;
320
321 479
        if (in_array(strtolower($tableColumn['type']), ['varchar', 'bpchar'], true)
322 479
            && preg_match('/\((\d*)\)/', $tableColumn['complete_type'], $matches)) {
323 407
            $length = (int) $matches[1];
324
        }
325
326 479
        $matches = [];
327
328 479
        $autoincrement = false;
329 479
        if ($tableColumn['default'] !== null && preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) {
330 449
            $tableColumn['sequence'] = $matches[1];
331 449
            $tableColumn['default']  = null;
332 449
            $autoincrement           = true;
333
        }
334
335 479
        if ($tableColumn['default'] !== null && preg_match("/^['(](.*)[')]::.*$/", $tableColumn['default'], $matches)) {
336 407
            $tableColumn['default'] = $matches[1];
337
        }
338
339 479
        if ($tableColumn['default'] !== null && stripos($tableColumn['default'], 'NULL') === 0) {
340 383
            $tableColumn['default'] = null;
341
        }
342
343 479
        if ($length === -1 && isset($tableColumn['atttypmod'])) {
344
            $length = $tableColumn['atttypmod'] - 4;
345
        }
346
347 479
        if ((int) $length <= 0) {
348 479
            $length = null;
349
        }
350
351 479
        $fixed = false;
352
353 479
        if (! isset($tableColumn['name'])) {
354 479
            $tableColumn['name'] = '';
355
        }
356
357 479
        $precision = null;
358 479
        $scale     = 0;
359 479
        $jsonb     = null;
360
361 479
        $dbType = strtolower($tableColumn['type']);
362 479
        if ($tableColumn['domain_type'] !== null
363 479
            && strlen($tableColumn['domain_type'])
364 479
            && ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])
365
        ) {
366 455
            $dbType                       = strtolower($tableColumn['domain_type']);
367 455
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
368
        }
369
370 479
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'])
371 479
            ?? $this->_platform->getDoctrineTypeMapping($dbType);
372
373 479
        switch ($dbType) {
374
            case 'smallint':
375
            case 'int2':
376 361
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
377 361
                $length                 = null;
378 361
                break;
379
            case 'int':
380
            case 'int4':
381
            case 'integer':
382 479
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
383 479
                $length                 = null;
384 479
                break;
385
            case 'bigint':
386
            case 'int8':
387 361
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
388 361
                $length                 = null;
389 361
                break;
390
            case 'bool':
391
            case 'boolean':
392 473
                if ($tableColumn['default'] === 'true') {
393
                    $tableColumn['default'] = true;
394
                }
395
396 473
                if ($tableColumn['default'] === 'false') {
397 473
                    $tableColumn['default'] = false;
398
                }
399
400 473
                $length = null;
401 473
                break;
402
            case 'char':
403
            case 'bpchar':
404 383
                $fixed = true;
405 383
                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 455
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
416
417 455
                if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) {
418 455
                    $precision = (int) $match[1];
419 455
                    $scale     = (int) $match[2];
420 455
                    $length    = null;
421
                }
422 455
                break;
423
            case 'year':
424
                $length = null;
425
                break;
426
427
            // PostgreSQL 9.4+ only
428
            case 'jsonb':
429 310
                $jsonb = true;
430 310
                break;
431
        }
432
433 479
        if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) {
434
            $tableColumn['default'] = $match[1];
435
        }
436
437
        $options = [
438 479
            'length'        => $length,
439 479
            'notnull'       => (bool) $tableColumn['isnotnull'],
440 479
            'default'       => $tableColumn['default'],
441 479
            'precision'     => $precision,
442 479
            'scale'         => $scale,
443 479
            'fixed'         => $fixed,
444
            'unsigned'      => false,
445 479
            'autoincrement' => $autoincrement,
446 479
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
447 187
                ? $tableColumn['comment']
448
                : null,
449
        ];
450
451 479
        $column = new Column($tableColumn['field'], Type::getType($type), $options);
452
453 479
        if (isset($tableColumn['collation']) && ! empty($tableColumn['collation'])) {
454
            $column->setPlatformOption('collation', $tableColumn['collation']);
455
        }
456
457 479
        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 325
            $column->setPlatformOption('jsonb', $jsonb);
459
        }
460
461 479
        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 479
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
472
    {
473 479
        if ($defaultValue !== null && strpos($defaultValue, '(') === 0) {
474 121
            return trim($defaultValue, '()');
475
        }
476
477 479
        return $defaultValue;
478
    }
479
}
480