Completed
Pull Request — 2.11.x (#3968)
by Sergei
57:31 queued 54:01
created

_getPortableTableIndexesList()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5

Importance

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