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

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 346
            $column->setPlatformOption('jsonb', $jsonb);
460
        }
461
462 664
        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 664
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
473
    {
474 664
        if (strpos($defaultValue, '(') === 0) {
475 183
            return trim($defaultValue, '()');
476
        }
477
478 664
        return $defaultValue;
479
    }
480
481
    /**
482
     * Parses a default value expression as given by PostgreSQL
483
     */
484 650
    private function parseDefaultExpression(?string $default) : ?string
485
    {
486 650
        if ($default === null) {
487 650
            return $default;
488
        }
489
490 650
        return str_replace("''", "'", $default);
491
    }
492
}
493