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