Passed
Pull Request — 2.11.x (#3971)
by Grégoire
03:18
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.0026

Importance

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