Failed Conditions
Pull Request — master (#3543)
by Andreas
15:32
created

_getPortableTableIndexesList()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 5.0023

Importance

Changes 0
Metric Value
eloc 21
dl 0
loc 34
ccs 21
cts 22
cp 0.9545
rs 9.2728
c 0
b 0
f 0
cc 5
nc 5
nop 2
crap 5.0023
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 is_array;
21
use function preg_match;
22
use function preg_replace;
23
use function sprintf;
24
use function str_replace;
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 713
    public function getSchemaNames()
44
    {
45 713
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
46
47 713
        $schemaNames = $statement->fetchAll(FetchMode::COLUMN);
48 713
        assert(is_array($schemaNames));
49
50 713
        return $schemaNames;
51
    }
52
53
    /**
54
     * Returns an array of schema search paths.
55
     *
56
     * This is a PostgreSQL only function.
57
     *
58
     * @return string[]
59
     */
60 713
    public function getSchemaSearchPaths()
61
    {
62 713
        $params = $this->_conn->getParams();
63 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

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

463
        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...
464 345
            $column->setPlatformOption('jsonb', $jsonb);
465
        }
466
467 664
        return $column;
468
    }
469
470
    /**
471
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
472
     *
473
     * @param mixed $defaultValue
474
     *
475
     * @return mixed
476
     */
477 664
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
478
    {
479 664
        if (strpos($defaultValue, '(') === 0) {
480 181
            return trim($defaultValue, '()');
481
        }
482
483 664
        return $defaultValue;
484
    }
485
486
    /**
487
     * Parses a default value expression as given by PostgreSQL
488
     */
489 650
    private function parseDefaultExpression(?string $default) : ?string
490
    {
491 650
        if ($default === null) {
492 650
            return $default;
493
        }
494
495 650
        return str_replace("''", "'", $default);
496
    }
497
}
498