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