Failed Conditions
Pull Request — master (#3512)
by David
17:16
created

PostgreSqlSchemaManager::listTableDetails()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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

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 348
            $column->setPlatformOption('jsonb', $jsonb);
460
        }
461
462 640
        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 640
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
473
    {
474 640
        if (strpos($defaultValue, '(') === 0) {
475 186
            return trim($defaultValue, '()');
476
        }
477
478 640
        return $defaultValue;
479
    }
480
481
    /**
482
     * Parses a default value expression as given by PostgreSQL
483
     */
484 626
    private function parseDefaultExpression(?string $default) : ?string
485
    {
486 626
        if ($default === null) {
487 626
            return $default;
488
        }
489
490 626
        return str_replace("''", "'", $default);
491
    }
492
493 640
    public function listTableDetails($tableName) : Table
494
    {
495 640
        $table = parent::listTableDetails($tableName);
496
497
        /** @var PostgreSqlPlatform $platform */
498 640
        $platform = $this->_platform;
499 640
        $sql      = $platform->getListTableMetadataSQL($tableName);
500
501 640
        $tableOptions = $this->_conn->fetchAssoc($sql);
502
503 640
        $table->addOption('comment', $tableOptions['table_comment']);
504
505 640
        return $table;
506
    }
507
}
508