Completed
Push — 2.11.x ( 0a2e2f...a8544c )
by Grégoire
23s queued 16s
created

_getPortableTableIndexesList()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5.0026

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 33
ccs 20
cts 21
cp 0.9524
rs 9.2888
c 0
b 0
f 0
cc 5
nc 5
nop 2
crap 5.0026
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 function array_change_key_case;
11
use function array_filter;
12
use function array_keys;
13
use function array_map;
14
use function array_shift;
15
use function assert;
16
use function explode;
17
use function implode;
18
use function in_array;
19
use function preg_match;
20
use function preg_replace;
21
use function sprintf;
22
use function str_replace;
23
use function strlen;
24
use function strpos;
25
use function strtolower;
26
use function trim;
27
use const CASE_LOWER;
28
29
/**
30
 * PostgreSQL Schema Manager.
31
 */
32
class PostgreSqlSchemaManager extends AbstractSchemaManager
33
{
34
    /** @var string[] */
35
    private $existingSchemaPaths;
36
37
    /**
38
     * Gets all the existing schema names.
39
     *
40
     * @return string[]
41
     */
42 665
    public function getSchemaNames()
43
    {
44 665
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
45
46 665
        return $statement->fetchAll(FetchMode::COLUMN);
47
    }
48
49
    /**
50
     * Returns an array of schema search paths.
51
     *
52
     * This is a PostgreSQL only function.
53
     *
54
     * @return string[]
55
     */
56 665
    public function getSchemaSearchPaths()
57
    {
58 665
        $params = $this->_conn->getParams();
59 665
        $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

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

469
        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...
470 351
            $column->setPlatformOption('jsonb', $jsonb);
471
        }
472
473 616
        return $column;
474
    }
475
476
    /**
477
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
478
     *
479
     * @param mixed $defaultValue
480
     *
481
     * @return mixed
482
     */
483 616
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
484
    {
485 616
        if (strpos($defaultValue, '(') === 0) {
486 187
            return trim($defaultValue, '()');
487
        }
488
489 616
        return $defaultValue;
490
    }
491
492
    /**
493
     * Parses a default value expression as given by PostgreSQL
494
     */
495 602
    private function parseDefaultExpression(?string $default) : ?string
496
    {
497 602
        if ($default === null) {
498 602
            return $default;
499
        }
500
501 602
        return str_replace("''", "'", $default);
502
    }
503
504
    /**
505
     * {@inheritdoc}
506
     */
507 616
    public function listTableDetails($tableName) : Table
508
    {
509 616
        $table = parent::listTableDetails($tableName);
510
511 616
        $platform = $this->_platform;
512 616
        assert($platform instanceof PostgreSqlPlatform);
513 616
        $sql = $platform->getListTableMetadataSQL($tableName);
514
515 616
        $tableOptions = $this->_conn->fetchAssoc($sql);
516
517 616
        if ($tableOptions !== false) {
518 616
            $table->addOption('comment', $tableOptions['table_comment']);
519
        }
520
521 616
        return $table;
522
    }
523
}
524