Completed
Pull Request — master (#3512)
by David
61:26
created

PostgreSqlSchemaManager::listTableDetails()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 13
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
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 stripos;
25
use function strlen;
26
use function strpos;
27
use function strtolower;
28
use function trim;
29
30
/**
31
 * PostgreSQL Schema Manager.
32
 */
33
class PostgreSqlSchemaManager extends AbstractSchemaManager
34
{
35
    /** @var string[] */
36
    private $existingSchemaPaths;
37
38
    /**
39
     * Gets all the existing schema names.
40
     *
41
     * @return string[]
42
     */
43 609
    public function getSchemaNames()
44
    {
45 609
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
46
47 609
        return $statement->fetchAll(FetchMode::COLUMN);
48
    }
49
50
    /**
51
     * Returns an array of schema search paths.
52
     *
53
     * This is a PostgreSQL only function.
54
     *
55
     * @return string[]
56
     */
57 609
    public function getSchemaSearchPaths()
58
    {
59 609
        $params = $this->_conn->getParams();
60 609
        $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

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

460
        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...
461 340
            $column->setPlatformOption('jsonb', $jsonb);
462
        }
463
464 560
        return $column;
465
    }
466
467
    /**
468
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
469
     *
470
     * @param mixed $defaultValue
471
     *
472
     * @return mixed
473
     */
474 560
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
475
    {
476 560
        if (strpos($defaultValue, '(') === 0) {
477 181
            return trim($defaultValue, '()');
478
        }
479
480 560
        return $defaultValue;
481
    }
482
483
    public function listTableDetails($tableName)
484
    {
485
        $table = parent::listTableDetails($tableName);
486
487
        /** @var PostgreSqlPlatform $platform */
488
        $platform = $this->_platform;
489
        $sql      = $platform->getListTableMetadataSQL($tableName);
490
491
        $tableOptions = $this->_conn->fetchAssoc($sql);
492
493
        $table->addOption('comment', $tableOptions['table_comment']);
494
495
        return $table;
496
    }
497
}
498