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