Completed
Pull Request — master (#3512)
by David
64:39 queued 61:17
created

PostgreSqlSchemaManager   F

Complexity

Total Complexity 83

Size/Duplication

Total Lines 463
Duplicated Lines 0 %

Test Coverage

Coverage 82.3%

Importance

Changes 0
Metric Value
wmc 83
eloc 214
dl 0
loc 463
rs 2
c 0
b 0
f 0
ccs 186
cts 226
cp 0.823

18 Methods

Rating   Name   Duplication   Size   Complexity  
A _getPortableTriggerDefinition() 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 _getPortableTableDefinition() 0 10 2
A _getPortableDatabaseDefinition() 0 3 1
A _getPortableSequenceDefinition() 0 16 3
A _getPortableSequencesList() 0 21 4
A fixVersion94NegativeNumericDefaultValue() 0 7 2
A getPortableNamespaceDefinition() 0 3 1
A _getPortableTableForeignKeyDefinition() 0 29 4
A _getPortableTableIndexesList() 0 33 5
F _getPortableTableColumnDefinition() 0 151 48
A listTableDetails() 0 13 1

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 stripos;
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 624
    public function getSchemaNames()
44
    {
45 624
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
46
47 624
        return $statement->fetchAll(FetchMode::COLUMN);
48
    }
49
50
    /**
51
     * Returns an array of schema search paths.
52
     *
53
     * This is a PostgreSQL only function.
54
     *
55
     * @return string[]
56
     */
57 624
    public function getSchemaSearchPaths()
58
    {
59 624
        $params = $this->_conn->getParams();
60 624
        $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

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

460
        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...
461 355
            $column->setPlatformOption('jsonb', $jsonb);
462
        }
463
464 575
        return $column;
465
    }
466
467
    /**
468
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
469
     *
470
     * @param mixed $defaultValue
471
     *
472
     * @return mixed
473
     */
474 575
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
475
    {
476 575
        if (strpos($defaultValue, '(') === 0) {
477 188
            return trim($defaultValue, '()');
478
        }
479
480 575
        return $defaultValue;
481
    }
482
483 575
    public function listTableDetails($tableName) : Table
484
    {
485 575
        $table = parent::listTableDetails($tableName);
486
487
        /** @var PostgreSqlPlatform $platform */
488 575
        $platform = $this->_platform;
489 575
        $sql      = $platform->getListTableMetadataSQL($tableName);
490
491 575
        $tableOptions = $this->_conn->fetchAssoc($sql);
492
493 575
        $table->addOption('comment', $tableOptions['table_comment']);
494
495 575
        return $table;
496
    }
497
}
498