Completed
Push — 2.11.x ( eeed37...b3131f )
by Grégoire
13:08 queued 11s
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 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 strlen;
25
use function strpos;
26
use function strtolower;
27
use function trim;
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 650
    public function getSchemaNames()
43
    {
44 650
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
45
46 650
        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 650
    public function getSchemaSearchPaths()
57
    {
58 650
        $params = $this->_conn->getParams();
59 650
        $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 650
        if (isset($params['user'])) {
62 650
            $schema = str_replace('"$user"', $params['user'], $schema);
63
        }
64
65 650
        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 650
    public function getExistingSchemaSearchPaths()
76
    {
77 650
        if ($this->existingSchemaPaths === null) {
78 650
            $this->determineExistingSchemaSearchPaths();
79
        }
80
81 650
        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 650
    public function determineExistingSchemaSearchPaths()
92
    {
93 650
        $names = $this->getSchemaNames();
94 650
        $paths = $this->getSchemaSearchPaths();
95
96
        $this->existingSchemaPaths = array_filter($paths, static function ($v) use ($names) {
97 650
            return in_array($v, $names);
98 650
        });
99 650
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104 650
    public function dropDatabase($database)
105
    {
106
        try {
107 650
            parent::dropDatabase($database);
108 650
        } 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 650
            if ($exception->getSQLState() !== '55006') {
114 650
                throw $exception;
115
            }
116
117 376
            assert($this->_platform instanceof PostgreSqlPlatform);
118
119 376
            $this->_execSql(
120
                [
121 376
                    $this->_platform->getDisallowDatabaseConnectionsSQL($database),
122 376
                    $this->_platform->getCloseActiveDatabaseConnectionsSQL($database),
123
                ]
124
            );
125
126 376
            parent::dropDatabase($database);
127
        }
128 376
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 512
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
134
    {
135 512
        $onUpdate       = null;
136 512
        $onDelete       = null;
137 512
        $localColumns   = [];
138 512
        $foreignColumns = [];
139 512
        $foreignTable   = null;
140
141 512
        if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
142 505
            $onUpdate = $match[1];
143
        }
144 512
        if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
145 491
            $onDelete = $match[1];
146
        }
147
148 512
        if (preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values)) {
149
            // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
150
            // the idea to trim them here.
151 512
            $localColumns   = array_map('trim', explode(',', $values[1]));
152 512
            $foreignColumns = array_map('trim', explode(',', $values[3]));
153 512
            $foreignTable   = $values[2];
154
        }
155
156 512
        return new ForeignKeyConstraint(
157 512
            $localColumns,
158
            $foreignTable,
159
            $foreignColumns,
160 512
            $tableForeignKey['conname'],
161 512
            ['onUpdate' => $onUpdate, 'onDelete' => $onDelete]
162
        );
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168
    protected function _getPortableTriggerDefinition($trigger)
169
    {
170
        return $trigger['trigger_name'];
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176 257
    protected function _getPortableViewDefinition($view)
177
    {
178 257
        return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']);
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184
    protected function _getPortableUserDefinition($user)
185
    {
186
        return [
187
            'user' => $user['usename'],
188
            'password' => $user['passwd'],
189
        ];
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 650
    protected function _getPortableTableDefinition($table)
196
    {
197 650
        $schemas     = $this->getExistingSchemaSearchPaths();
198 650
        $firstSchema = array_shift($schemas);
199
200 650
        if ($table['schema_name'] === $firstSchema) {
201 650
            return $table['table_name'];
202
        }
203
204 512
        return $table['schema_name'] . '.' . $table['table_name'];
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     *
210
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
211
     */
212 601
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
213
    {
214 601
        $buffer = [];
215 601
        foreach ($tableIndexes as $row) {
216 540
            $colNumbers    = array_map('intval', explode(' ', $row['indkey']));
217 540
            $columnNameSql = sprintf(
218
                'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
219 540
                $row['indrelid'],
220 540
                implode(' ,', $colNumbers)
221
            );
222
223 540
            $stmt         = $this->_conn->executeQuery($columnNameSql);
224 540
            $indexColumns = $stmt->fetchAll();
225
226
            // required for getting the order of the columns right.
227 540
            foreach ($colNumbers as $colNum) {
228 540
                foreach ($indexColumns as $colRow) {
229 540
                    if ($colNum !== $colRow['attnum']) {
230 442
                        continue;
231
                    }
232
233 540
                    $buffer[] = [
234 540
                        'key_name' => $row['relname'],
235 540
                        'column_name' => trim($colRow['attname']),
236 540
                        'non_unique' => ! $row['indisunique'],
237 540
                        'primary' => $row['indisprimary'],
238 540
                        'where' => $row['where'],
239
                    ];
240
                }
241
            }
242
        }
243
244 601
        return parent::_getPortableTableIndexesList($buffer, $tableName);
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250 376
    protected function _getPortableDatabaseDefinition($database)
251
    {
252 376
        return $database['datname'];
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258 369
    protected function _getPortableSequencesList($sequences)
259
    {
260 369
        $sequenceDefinitions = [];
261
262 369
        foreach ($sequences as $sequence) {
263 369
            if ($sequence['schemaname'] !== 'public') {
264 369
                $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
265
            } else {
266 369
                $sequenceName = $sequence['relname'];
267
            }
268
269 369
            $sequenceDefinitions[$sequenceName] = $sequence;
270
        }
271
272 369
        $list = [];
273
274 369
        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
275 369
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
276
        }
277
278 369
        return $list;
279
    }
280
281
    /**
282
     * {@inheritdoc}
283
     */
284 348
    protected function getPortableNamespaceDefinition(array $namespace)
285
    {
286 348
        return $namespace['nspname'];
287
    }
288
289
    /**
290
     * {@inheritdoc}
291
     */
292 369
    protected function _getPortableSequenceDefinition($sequence)
293
    {
294 369
        if ($sequence['schemaname'] !== 'public') {
295 369
            $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
296
        } else {
297 369
            $sequenceName = $sequence['relname'];
298
        }
299
300 369
        if (! isset($sequence['increment_by'], $sequence['min_value'])) {
301
            /** @var string[] $data */
302 265
            $data = $this->_conn->fetchAssoc('SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName));
303
304 265
            $sequence += $data;
305
        }
306
307 369
        return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
308
    }
309
310
    /**
311
     * {@inheritdoc}
312
     */
313 601
    protected function _getPortableTableColumnDefinition($tableColumn)
314
    {
315 601
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
316
317 601
        if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
318
            // get length from varchar definition
319 587
            $length                = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']);
320 587
            $tableColumn['length'] = $length;
321
        }
322
323 601
        $matches = [];
324
325 601
        $autoincrement = false;
326 601
        if (preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) {
327 533
            $tableColumn['sequence'] = $matches[1];
328 533
            $tableColumn['default']  = null;
329 533
            $autoincrement           = true;
330
        }
331
332 601
        if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches)) {
333 587
            $tableColumn['default'] = $matches[1];
334 601
        } elseif (preg_match('/^NULL::/', $tableColumn['default'])) {
335 587
            $tableColumn['default'] = null;
336
        }
337
338 601
        $length = $tableColumn['length'] ?? null;
339 601
        if ($length === '-1' && isset($tableColumn['atttypmod'])) {
340
            $length = $tableColumn['atttypmod'] - 4;
341
        }
342 601
        if ((int) $length <= 0) {
343 601
            $length = null;
344
        }
345 601
        $fixed = null;
346
347 601
        if (! isset($tableColumn['name'])) {
348 601
            $tableColumn['name'] = '';
349
        }
350
351 601
        $precision = null;
352 601
        $scale     = null;
353 601
        $jsonb     = null;
354
355 601
        $dbType = strtolower($tableColumn['type']);
356 601
        if (strlen($tableColumn['domain_type']) && ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])) {
357 540
            $dbType                       = strtolower($tableColumn['domain_type']);
358 540
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
359
        }
360
361 601
        $type                   = $this->_platform->getDoctrineTypeMapping($dbType);
362 601
        $type                   = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
363 601
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
364
365
        switch ($dbType) {
366 601
            case 'smallint':
367 601
            case 'int2':
368 425
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
369 425
                $length                 = null;
370 425
                break;
371 601
            case 'int':
372 601
            case 'int4':
373 594
            case 'integer':
374 601
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
375 601
                $length                 = null;
376 601
                break;
377 594
            case 'bigint':
378 594
            case 'int8':
379 425
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
380 425
                $length                 = null;
381 425
                break;
382 594
            case 'bool':
383 587
            case 'boolean':
384 594
                if ($tableColumn['default'] === 'true') {
385
                    $tableColumn['default'] = true;
386
                }
387
388 594
                if ($tableColumn['default'] === 'false') {
389 594
                    $tableColumn['default'] = false;
390
                }
391
392 594
                $length = null;
393 594
                break;
394 587
            case 'text':
395 587
            case '_varchar':
396 587
            case 'varchar':
397 587
                $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']);
398 587
                $fixed                  = false;
399 587
                break;
400 540
            case 'interval':
401
                $fixed = false;
402
                break;
403 540
            case 'char':
404 540
            case 'bpchar':
405 449
                $fixed = true;
406 449
                break;
407 540
            case 'float':
408 540
            case 'float4':
409 540
            case 'float8':
410 540
            case 'double':
411 540
            case 'double precision':
412 540
            case 'real':
413 540
            case 'decimal':
414 540
            case 'money':
415 540
            case 'numeric':
416 540
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
417
418 540
                if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) {
419 540
                    $precision = $match[1];
420 540
                    $scale     = $match[2];
421 540
                    $length    = null;
422
                }
423 540
                break;
424 540
            case 'year':
425
                $length = null;
426
                break;
427
428
            // PostgreSQL 9.4+ only
429 540
            case 'jsonb':
430 314
                $jsonb = true;
431 314
                break;
432
        }
433
434 601
        if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) {
435 587
            $tableColumn['default'] = $match[1];
436
        }
437
438
        $options = [
439 601
            'length'        => $length,
440 601
            'notnull'       => (bool) $tableColumn['isnotnull'],
441 601
            'default'       => $tableColumn['default'],
442 601
            'precision'     => $precision,
443 601
            'scale'         => $scale,
444 601
            'fixed'         => $fixed,
445
            'unsigned'      => false,
446 601
            'autoincrement' => $autoincrement,
447 601
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
448 222
                ? $tableColumn['comment']
449
                : null,
450
        ];
451
452 601
        $column = new Column($tableColumn['field'], Type::getType($type), $options);
453
454 601
        if (isset($tableColumn['collation']) && ! empty($tableColumn['collation'])) {
455
            $column->setPlatformOption('collation', $tableColumn['collation']);
456
        }
457
458 601
        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

458
        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...
459 340
            $column->setPlatformOption('jsonb', $jsonb);
460
        }
461
462 601
        return $column;
463
    }
464
465
    /**
466
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
467
     *
468
     * @param mixed $defaultValue
469
     *
470
     * @return mixed
471
     */
472 601
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
473
    {
474 601
        if (strpos($defaultValue, '(') === 0) {
475 182
            return trim($defaultValue, '()');
476
        }
477
478 601
        return $defaultValue;
479
    }
480
481
    /**
482
     * Parses a default value expression as given by PostgreSQL
483
     */
484 587
    private function parseDefaultExpression(?string $default) : ?string
485
    {
486 587
        if ($default === null) {
487 587
            return $default;
488
        }
489
490 587
        return str_replace("''", "'", $default);
491
    }
492
493 601
    public function listTableDetails($tableName) : Table
494
    {
495 601
        $table = parent::listTableDetails($tableName);
496
497
        /** @var PostgreSqlPlatform $platform */
498 601
        $platform = $this->_platform;
499 601
        $sql      = $platform->getListTableMetadataSQL($tableName);
500
501 601
        $tableOptions = $this->_conn->fetchAssoc($sql);
502
503 601
        if ($tableOptions !== false) {
504 601
            $table->addOption('comment', $tableOptions['table_comment']);
505
        }
506
507 601
        return $table;
508
    }
509
}
510