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