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