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