Failed Conditions
Pull Request — master (#3530)
by Jonathan
45:38 queued 31:30
created

PostgreSqlSchemaManager   F

Complexity

Total Complexity 84

Size/Duplication

Total Lines 459
Duplicated Lines 0 %

Test Coverage

Coverage 82.81%

Importance

Changes 0
Metric Value
wmc 84
eloc 212
dl 0
loc 459
ccs 183
cts 221
cp 0.8281
rs 2
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A _getPortableDatabaseDefinition() 0 3 1
A _getPortableSequenceDefinition() 0 16 3
A _getPortableSequencesList() 0 21 4
A getPortableNamespaceDefinition() 0 3 1
A getSchemaNames() 0 5 1
A getExistingSchemaSearchPaths() 0 7 2
A determineExistingSchemaSearchPaths() 0 7 1
A _getPortableViewDefinition() 0 3 1
A getSchemaSearchPaths() 0 10 2
A _getPortableUserDefinition() 0 5 1
A dropDatabase() 0 23 3
A _getPortableTriggerDefinition() 0 3 1
A _getPortableTableDefinition() 0 10 2
A _getPortableTableForeignKeyDefinition() 0 29 4
A _getPortableTableIndexesList() 0 33 5
A parseDefaultExpression() 0 7 2
A fixVersion94NegativeNumericDefaultValue() 0 7 2
F _getPortableTableColumnDefinition() 0 150 48

How to fix   Complexity   

Complex Class

Complex classes like PostgreSqlSchemaManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PostgreSqlSchemaManager, and based on these observations, apply Extract Interface, too.

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