Failed Conditions
Push — 2.10.x ( db5afa...61a6b9 )
by Grégoire
27s queued 15s
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 function array_change_key_case;
11
use function array_filter;
12
use function array_keys;
13
use function array_map;
14
use function array_shift;
15
use function assert;
16
use function explode;
17
use function implode;
18
use function in_array;
19
use function preg_match;
20
use function preg_replace;
21
use function sprintf;
22
use function str_replace;
23
use function strlen;
24
use function strpos;
25
use function strtolower;
26
use function trim;
27
use const CASE_LOWER;
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 663
    public function getSchemaNames()
43
    {
44 663
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
45
46 663
        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 663
    public function getSchemaSearchPaths()
57
    {
58 663
        $params = $this->_conn->getParams();
59 663
        $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 663
        if (isset($params['user'])) {
62 663
            $schema = str_replace('"$user"', $params['user'], $schema);
63
        }
64
65 663
        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 663
    public function getExistingSchemaSearchPaths()
76
    {
77 663
        if ($this->existingSchemaPaths === null) {
78 663
            $this->determineExistingSchemaSearchPaths();
79
        }
80
81 663
        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 663
    public function determineExistingSchemaSearchPaths()
92
    {
93 663
        $names = $this->getSchemaNames();
94 663
        $paths = $this->getSchemaSearchPaths();
95
96
        $this->existingSchemaPaths = array_filter($paths, static function ($v) use ($names) {
97 663
            return in_array($v, $names);
98 663
        });
99 663
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104 663
    public function dropDatabase($database)
105
    {
106
        try {
107 663
            parent::dropDatabase($database);
108 663
        } 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 663
            if ($exception->getSQLState() !== '55006') {
114 663
                throw $exception;
115
            }
116
117 388
            assert($this->_platform instanceof PostgreSqlPlatform);
118
119 388
            $this->_execSql(
120
                [
121 388
                    $this->_platform->getDisallowDatabaseConnectionsSQL($database),
122 388
                    $this->_platform->getCloseActiveDatabaseConnectionsSQL($database),
123
                ]
124
            );
125
126 388
            parent::dropDatabase($database);
127
        }
128 388
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 524
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
134
    {
135 524
        $onUpdate       = null;
136 524
        $onDelete       = null;
137 524
        $localColumns   = [];
138 524
        $foreignColumns = [];
139 524
        $foreignTable   = null;
140
141 524
        if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
142 517
            $onUpdate = $match[1];
143
        }
144
145 524
        if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
146 503
            $onDelete = $match[1];
147
        }
148
149 524
        if (preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values)) {
150
            // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
151
            // the idea to trim them here.
152 524
            $localColumns   = array_map('trim', explode(',', $values[1]));
153 524
            $foreignColumns = array_map('trim', explode(',', $values[3]));
154 524
            $foreignTable   = $values[2];
155
        }
156
157 524
        return new ForeignKeyConstraint(
158 524
            $localColumns,
159
            $foreignTable,
160
            $foreignColumns,
161 524
            $tableForeignKey['conname'],
162 524
            ['onUpdate' => $onUpdate, 'onDelete' => $onDelete]
163
        );
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169
    protected function _getPortableTriggerDefinition($trigger)
170
    {
171
        return $trigger['trigger_name'];
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 269
    protected function _getPortableViewDefinition($view)
178
    {
179 269
        return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']);
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    protected function _getPortableUserDefinition($user)
186
    {
187
        return [
188
            'user' => $user['usename'],
189
            'password' => $user['passwd'],
190
        ];
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196 663
    protected function _getPortableTableDefinition($table)
197
    {
198 663
        $schemas     = $this->getExistingSchemaSearchPaths();
199 663
        $firstSchema = array_shift($schemas);
200
201 663
        if ($table['schema_name'] === $firstSchema) {
202 663
            return $table['table_name'];
203
        }
204
205 524
        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 614
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
214
    {
215 614
        $buffer = [];
216 614
        foreach ($tableIndexes as $row) {
217 552
            $colNumbers    = array_map('intval', explode(' ', $row['indkey']));
218 552
            $columnNameSql = sprintf(
219
                'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
220 552
                $row['indrelid'],
221 552
                implode(' ,', $colNumbers)
222
            );
223
224 552
            $stmt         = $this->_conn->executeQuery($columnNameSql);
225 552
            $indexColumns = $stmt->fetchAll();
226
227
            // required for getting the order of the columns right.
228 552
            foreach ($colNumbers as $colNum) {
229 552
                foreach ($indexColumns as $colRow) {
230 552
                    if ($colNum !== $colRow['attnum']) {
231 454
                        continue;
232
                    }
233
234 552
                    $buffer[] = [
235 552
                        'key_name' => $row['relname'],
236 552
                        'column_name' => trim($colRow['attname']),
237 552
                        'non_unique' => ! $row['indisunique'],
238 552
                        'primary' => $row['indisprimary'],
239 552
                        'where' => $row['where'],
240
                    ];
241
                }
242
            }
243
        }
244
245 614
        return parent::_getPortableTableIndexesList($buffer, $tableName);
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 388
    protected function _getPortableDatabaseDefinition($database)
252
    {
253 388
        return $database['datname'];
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 381
    protected function _getPortableSequencesList($sequences)
260
    {
261 381
        $sequenceDefinitions = [];
262
263 381
        foreach ($sequences as $sequence) {
264 381
            if ($sequence['schemaname'] !== 'public') {
265 381
                $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
266
            } else {
267 381
                $sequenceName = $sequence['relname'];
268
            }
269
270 381
            $sequenceDefinitions[$sequenceName] = $sequence;
271
        }
272
273 381
        $list = [];
274
275 381
        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
276 381
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
277
        }
278
279 381
        return $list;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285 360
    protected function getPortableNamespaceDefinition(array $namespace)
286
    {
287 360
        return $namespace['nspname'];
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293 381
    protected function _getPortableSequenceDefinition($sequence)
294
    {
295 381
        if ($sequence['schemaname'] !== 'public') {
296 381
            $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
297
        } else {
298 381
            $sequenceName = $sequence['relname'];
299
        }
300
301 381
        if (! isset($sequence['increment_by'], $sequence['min_value'])) {
302
            /** @var string[] $data */
303 273
            $data = $this->_conn->fetchAssoc('SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName));
304
305 273
            $sequence += $data;
306
        }
307
308 381
        return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314 614
    protected function _getPortableTableColumnDefinition($tableColumn)
315
    {
316 614
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
317
318 614
        if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
319
            // get length from varchar definition
320 600
            $length                = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']);
321 600
            $tableColumn['length'] = $length;
322
        }
323
324 614
        $matches = [];
325
326 614
        $autoincrement = false;
327 614
        if (preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) {
328 545
            $tableColumn['sequence'] = $matches[1];
329 545
            $tableColumn['default']  = null;
330 545
            $autoincrement           = true;
331
        }
332
333 614
        if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches)) {
334 600
            $tableColumn['default'] = $matches[1];
335 614
        } elseif (preg_match('/^NULL::/', $tableColumn['default'])) {
336 600
            $tableColumn['default'] = null;
337
        }
338
339 614
        $length = $tableColumn['length'] ?? null;
340 614
        if ($length === '-1' && isset($tableColumn['atttypmod'])) {
341
            $length = $tableColumn['atttypmod'] - 4;
342
        }
343
344 614
        if ((int) $length <= 0) {
345 614
            $length = null;
346
        }
347
348 614
        $fixed = null;
349
350 614
        if (! isset($tableColumn['name'])) {
351 614
            $tableColumn['name'] = '';
352
        }
353
354 614
        $precision = null;
355 614
        $scale     = null;
356 614
        $jsonb     = null;
357
358 614
        $dbType = strtolower($tableColumn['type']);
359 614
        if (strlen($tableColumn['domain_type']) && ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])) {
360 552
            $dbType                       = strtolower($tableColumn['domain_type']);
361 552
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
362
        }
363
364 614
        $type                   = $this->_platform->getDoctrineTypeMapping($dbType);
365 614
        $type                   = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
366 614
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
367
368
        switch ($dbType) {
369 614
            case 'smallint':
370 614
            case 'int2':
371 437
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
372 437
                $length                 = null;
373 437
                break;
374
375 614
            case 'int':
376 614
            case 'int4':
377 607
            case 'integer':
378 614
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
379 614
                $length                 = null;
380 614
                break;
381
382 607
            case 'bigint':
383 607
            case 'int8':
384 437
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
385 437
                $length                 = null;
386 437
                break;
387
388 607
            case 'bool':
389 600
            case 'boolean':
390 607
                if ($tableColumn['default'] === 'true') {
391
                    $tableColumn['default'] = true;
392
                }
393
394 607
                if ($tableColumn['default'] === 'false') {
395 607
                    $tableColumn['default'] = false;
396
                }
397
398 607
                $length = null;
399 607
                break;
400
401 600
            case 'text':
402 600
            case '_varchar':
403 600
            case 'varchar':
404 600
                $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']);
405 600
                $fixed                  = false;
406 600
                break;
407 552
            case 'interval':
408
                $fixed = false;
409
                break;
410
411 552
            case 'char':
412 552
            case 'bpchar':
413 461
                $fixed = true;
414 461
                break;
415
416 552
            case 'float':
417 552
            case 'float4':
418 552
            case 'float8':
419 552
            case 'double':
420 552
            case 'double precision':
421 552
            case 'real':
422 552
            case 'decimal':
423 552
            case 'money':
424 552
            case 'numeric':
425 552
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
426
427 552
                if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) {
428 552
                    $precision = $match[1];
429 552
                    $scale     = $match[2];
430 552
                    $length    = null;
431
                }
432
433 552
                break;
434
435 552
            case 'year':
436
                $length = null;
437
                break;
438
439
            // PostgreSQL 9.4+ only
440 552
            case 'jsonb':
441 323
                $jsonb = true;
442 323
                break;
443
        }
444
445 614
        if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) {
446 600
            $tableColumn['default'] = $match[1];
447
        }
448
449
        $options = [
450 614
            'length'        => $length,
451 614
            'notnull'       => (bool) $tableColumn['isnotnull'],
452 614
            'default'       => $tableColumn['default'],
453 614
            'precision'     => $precision,
454 614
            'scale'         => $scale,
455 614
            'fixed'         => $fixed,
456
            'unsigned'      => false,
457 614
            'autoincrement' => $autoincrement,
458 614
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
459 234
                ? $tableColumn['comment']
460
                : null,
461
        ];
462
463 614
        $column = new Column($tableColumn['field'], Type::getType($type), $options);
464
465 614
        if (isset($tableColumn['collation']) && ! empty($tableColumn['collation'])) {
466
            $column->setPlatformOption('collation', $tableColumn['collation']);
467
        }
468
469 614
        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

469
        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...
470 351
            $column->setPlatformOption('jsonb', $jsonb);
471
        }
472
473 614
        return $column;
474
    }
475
476
    /**
477
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
478
     *
479
     * @param mixed $defaultValue
480
     *
481
     * @return mixed
482
     */
483 614
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
484
    {
485 614
        if (strpos($defaultValue, '(') === 0) {
486 187
            return trim($defaultValue, '()');
487
        }
488
489 614
        return $defaultValue;
490
    }
491
492
    /**
493
     * Parses a default value expression as given by PostgreSQL
494
     */
495 600
    private function parseDefaultExpression(?string $default) : ?string
496
    {
497 600
        if ($default === null) {
498 600
            return $default;
499
        }
500
501 600
        return str_replace("''", "'", $default);
502
    }
503
504
    /**
505
     * {@inheritdoc}
506
     */
507 614
    public function listTableDetails($tableName) : Table
508
    {
509 614
        $table = parent::listTableDetails($tableName);
510
511 614
        $platform = $this->_platform;
512 614
        assert($platform instanceof PostgreSqlPlatform);
513 614
        $sql = $platform->getListTableMetadataSQL($tableName);
514
515 614
        $tableOptions = $this->_conn->fetchAssoc($sql);
516
517 614
        if ($tableOptions !== false) {
518 614
            $table->addOption('comment', $tableOptions['table_comment']);
519
        }
520
521 614
        return $table;
522
    }
523
}
524