Passed
Pull Request — master (#3025)
by Michael
19:27 queued 26s
created

PostgreSqlSchemaManager   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 444
Duplicated Lines 0 %

Test Coverage

Coverage 94.34%

Importance

Changes 0
Metric Value
wmc 82
dl 0
loc 444
rs 2
c 0
b 0
f 0
ccs 200
cts 212
cp 0.9434

17 Methods

Rating   Name   Duplication   Size   Complexity  
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 _getPortableTableForeignKeyDefinition() 0 26 4
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 _getPortableTableIndexesList() 0 29 5
F _getPortableTableColumnDefinition() 0 151 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
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL\Schema;
21
22
use Doctrine\DBAL\Exception\DriverException;
23
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
24
use Doctrine\DBAL\Types\Type;
25
use const CASE_LOWER;
26
use function array_change_key_case;
27
use function array_filter;
28
use function array_keys;
29
use function array_map;
30
use function array_shift;
31
use function assert;
32
use function explode;
33
use function in_array;
34
use function join;
35
use function preg_match;
36
use function preg_replace;
37
use function str_replace;
38
use function stripos;
39
use function strlen;
40
use function strpos;
41
use function strtolower;
42
use function trim;
43
44
/**
45
 * PostgreSQL Schema Manager.
46
 *
47
 * @author Konsta Vesterinen <[email protected]>
48
 * @author Lukas Smith <[email protected]> (PEAR MDB2 library)
49
 * @author Benjamin Eberlei <[email protected]>
50
 * @since  2.0
51
 */
52
class PostgreSqlSchemaManager extends AbstractSchemaManager
53
{
54
    /**
55
     * @var array
56
     */
57
    private $existingSchemaPaths;
58
59
    /**
60
     * Gets all the existing schema names.
61
     *
62
     * @return array
63
     */
64 12
    public function getSchemaNames()
65
    {
66 12
        $rows = $this->_conn->fetchAll("SELECT nspname as schema_name FROM pg_namespace WHERE nspname !~ '^pg_.*' and nspname != 'information_schema'");
67
68
        return array_map(function ($v) { return $v['schema_name']; }, $rows);
69
    }
70
71
    /**
72
     * Returns an array of schema search paths.
73
     *
74
     * This is a PostgreSQL only function.
75
     *
76
     * @return array
77
     */
78 102
    public function getSchemaSearchPaths()
79
    {
80 102
        $params = $this->_conn->getParams();
81 102
        $schema = explode(",", $this->_conn->fetchColumn('SHOW search_path'));
82
83 102
        if (isset($params['user'])) {
84 102
            $schema = str_replace('"$user"', $params['user'], $schema);
85
        }
86
87 102
        return array_map('trim', $schema);
88
    }
89
90
    /**
91
     * Gets names of all existing schemas in the current users search path.
92
     *
93
     * This is a PostgreSQL only function.
94
     *
95
     * @return array
96
     */
97 524
    public function getExistingSchemaSearchPaths()
98
    {
99 524
        if ($this->existingSchemaPaths === null) {
100 6
            $this->determineExistingSchemaSearchPaths();
101
        }
102
103 524
        return $this->existingSchemaPaths;
104
    }
105
106
    /**
107
     * Sets or resets the order of the existing schemas in the current search path of the user.
108
     *
109
     * This is a PostgreSQL only function.
110
     *
111
     * @return void
112
     */
113 6
    public function determineExistingSchemaSearchPaths()
114
    {
115 6
        $names = $this->getSchemaNames();
116 6
        $paths = $this->getSchemaSearchPaths();
117
118
        $this->existingSchemaPaths = array_filter($paths, function ($v) use ($names) {
119 6
            return in_array($v, $names);
120 6
        });
121 6
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126 12
    public function dropDatabase($database)
127
    {
128
        try {
129 12
            parent::dropDatabase($database);
130 12
        } catch (DriverException $exception) {
131
            // If we have a SQLSTATE 55006, the drop database operation failed
132
            // because of active connections on the database.
133
            // To force dropping the database, we first have to close all active connections
134
            // on that database and issue the drop database operation again.
135 12
            if ($exception->getSQLState() !== '55006') {
136 12
                throw $exception;
137
            }
138
139 6
            assert($this->_platform instanceof PostgreSqlPlatform);
140
141 6
            $this->_execSql(
142
                [
143 6
                    $this->_platform->getDisallowDatabaseConnectionsSQL($database),
144 6
                    $this->_platform->getCloseActiveDatabaseConnectionsSQL($database),
145
                ]
146
            );
147
148 6
            parent::dropDatabase($database);
149
        }
150 6
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155 84
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
156
    {
157 84
        $onUpdate       = null;
158 84
        $onDelete       = null;
159 84
        $localColumns   = null;
160 84
        $foreignColumns = null;
161 84
        $foreignTable   = null;
162
163 84
        if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
164 24
            $onUpdate = $match[1];
165
        }
166 84
        if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
167 24
            $onDelete = $match[1];
168
        }
169
170 84
        if (preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values)) {
171
            // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
172
            // the idea to trim them here.
173 84
            $localColumns = array_map('trim', explode(",", $values[1]));
174 84
            $foreignColumns = array_map('trim', explode(",", $values[3]));
175 84
            $foreignTable = $values[2];
176
        }
177
178 84
        return new ForeignKeyConstraint(
179 84
            $localColumns, $foreignTable, $foreignColumns, $tableForeignKey['conname'],
180 84
            ['onUpdate' => $onUpdate, 'onDelete' => $onDelete]
181
        );
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187
    protected function _getPortableTriggerDefinition($trigger)
188
    {
189
        return $trigger['trigger_name'];
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 6
    protected function _getPortableViewDefinition($view)
196
    {
197 6
        return new View($view['schemaname'].'.'.$view['viewname'], $view['definition']);
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203
    protected function _getPortableUserDefinition($user)
204
    {
205
        return [
206
            'user' => $user['usename'],
207
            'password' => $user['passwd']
208
        ];
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214 524
    protected function _getPortableTableDefinition($table)
215
    {
216 524
        $schemas = $this->getExistingSchemaSearchPaths();
217 524
        $firstSchema = array_shift($schemas);
218
219 524
        if ($table['schema_name'] == $firstSchema) {
220 524
            return $table['table_name'];
221
        }
222
223 440
        return $table['schema_name'] . "." . $table['table_name'];
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     *
229
     * @license New BSD License
230
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
231
     */
232 270
    protected function _getPortableTableIndexesList($tableIndexes, $tableName=null)
233
    {
234 270
        $buffer = [];
235 270
        foreach ($tableIndexes as $row) {
236 150
            $colNumbers = explode(' ', $row['indkey']);
237 150
            $colNumbersSql = 'IN (' . join(' ,', $colNumbers) . ' )';
238
            $columnNameSql = "SELECT attnum, attname FROM pg_attribute
239 150
                WHERE attrelid={$row['indrelid']} AND attnum $colNumbersSql ORDER BY attnum ASC;";
240
241 150
            $stmt = $this->_conn->executeQuery($columnNameSql);
242 150
            $indexColumns = $stmt->fetchAll();
243
244
            // required for getting the order of the columns right.
245 150
            foreach ($colNumbers as $colNum) {
246 150
                foreach ($indexColumns as $colRow) {
247 150
                    if ($colNum == $colRow['attnum']) {
248 150
                        $buffer[] = [
249 150
                            'key_name' => $row['relname'],
250 150
                            'column_name' => trim($colRow['attname']),
251 150
                            'non_unique' => !$row['indisunique'],
252 150
                            'primary' => $row['indisprimary'],
253 150
                            'where' => $row['where'],
254
                        ];
255
                    }
256
                }
257
            }
258
        }
259
260 270
        return parent::_getPortableTableIndexesList($buffer, $tableName);
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     */
266 12
    protected function _getPortableDatabaseDefinition($database)
267
    {
268 12
        return $database['datname'];
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274 36
    protected function _getPortableSequencesList($sequences)
275
    {
276 36
        $sequenceDefinitions = [];
277
278 36
        foreach ($sequences as $sequence) {
279 36
            if ($sequence['schemaname'] != 'public') {
280 36
                $sequenceName = $sequence['schemaname'] . "." . $sequence['relname'];
281
            } else {
282 36
                $sequenceName = $sequence['relname'];
283
            }
284
285 36
            $sequenceDefinitions[$sequenceName] = $sequence;
286
        }
287
288 36
        $list = [];
289
290 36
        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
291 36
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
292
        }
293
294 36
        return $list;
295
    }
296
297
    /**
298
     * {@inheritdoc}
299
     */
300 12
    protected function getPortableNamespaceDefinition(array $namespace)
301
    {
302 12
        return $namespace['nspname'];
303
    }
304
305
    /**
306
     * {@inheritdoc}
307
     */
308 36
    protected function _getPortableSequenceDefinition($sequence)
309
    {
310 36
        if ($sequence['schemaname'] !== 'public') {
311 36
            $sequenceName = $sequence['schemaname'] . "." . $sequence['relname'];
312
        } else {
313 36
            $sequenceName = $sequence['relname'];
314
        }
315
316 36
        if ( ! isset($sequence['increment_by'], $sequence['min_value'])) {
317
            /** @var string[] $data */
318 30
            $data      = $this->_conn->fetchAssoc('SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName));
319
320 30
            $sequence += $data;
321
        }
322
323 36
        return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329 338
    protected function _getPortableTableColumnDefinition($tableColumn)
330
    {
331 338
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
332
333 338
        if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
334
            // get length from varchar definition
335 102
            $length = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']);
336 102
            $tableColumn['length'] = $length;
337
        }
338
339 338
        $matches = [];
340
341 338
        $autoincrement = false;
342 338
        if (preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) {
343 96
            $tableColumn['sequence'] = $matches[1];
344 96
            $tableColumn['default'] = null;
345 96
            $autoincrement = true;
346
        }
347
348 338
        if (preg_match("/^['(](.*)[')]::.*$/", $tableColumn['default'], $matches)) {
349 60
            $tableColumn['default'] = $matches[1];
350
        }
351
352 338
        if (stripos($tableColumn['default'], 'NULL') === 0) {
353 36
            $tableColumn['default'] = null;
354
        }
355
356 338
        $length = $tableColumn['length'] ?? null;
357 338
        if ($length == '-1' && isset($tableColumn['atttypmod'])) {
358
            $length = $tableColumn['atttypmod'] - 4;
359
        }
360 338
        if ((int) $length <= 0) {
361 332
            $length = null;
362
        }
363 338
        $fixed = null;
364
365 338
        if (!isset($tableColumn['name'])) {
366 338
            $tableColumn['name'] = '';
367
        }
368
369 338
        $precision = null;
370 338
        $scale = null;
371 338
        $jsonb = null;
372
373 338
        $dbType = strtolower($tableColumn['type']);
374 338
        if (strlen($tableColumn['domain_type']) && !$this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])) {
375 6
            $dbType = strtolower($tableColumn['domain_type']);
376 6
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
377
        }
378
379 338
        $type = $this->_platform->getDoctrineTypeMapping($dbType);
380 338
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
381 338
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
382
383
        switch ($dbType) {
384 338
            case 'smallint':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
385 338
            case 'int2':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
386 18
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
387 18
                $length = null;
388 18
                break;
389 338
            case 'int':
390 338
            case 'int4':
391 194
            case 'integer':
392 282
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
393 282
                $length = null;
394 282
                break;
395 194
            case 'bigint':
396 194
            case 'int8':
397 42
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
398 42
                $length = null;
399 42
                break;
400 170
            case 'bool':
401 164
            case 'boolean':
402 24
                if ($tableColumn['default'] === 'true') {
403
                    $tableColumn['default'] = true;
404
                }
405
406 24
                if ($tableColumn['default'] === 'false') {
407 24
                    $tableColumn['default'] = false;
408
                }
409
410 24
                $length = null;
411 24
                break;
412 164
            case 'text':
413 42
                $fixed = false;
414 42
                break;
415 158
            case 'varchar':
416 110
            case 'interval':
417 110
            case '_varchar':
418 90
                $fixed = false;
419 90
                break;
420 110
            case 'char':
421 110
            case 'bpchar':
422 30
                $fixed = true;
423 30
                break;
424 98
            case 'float':
425 98
            case 'float4':
426 98
            case 'float8':
427 98
            case 'double':
428 98
            case 'double precision':
429 98
            case 'real':
430 98
            case 'decimal':
431 98
            case 'money':
432 98
            case 'numeric':
433 42
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
434
435 42
                if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) {
436 42
                    $precision = $match[1];
437 42
                    $scale = $match[2];
438 42
                    $length = null;
439
                }
440 42
                break;
441 92
            case 'year':
442
                $length = null;
443
                break;
444
445
            // PostgreSQL 9.4+ only
446 92
            case 'jsonb':
447 16
                $jsonb = true;
448 16
                break;
449
        }
450
451 338
        if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) {
452
            $tableColumn['default'] = $match[1];
453
        }
454
455
        $options = [
456 338
            'length'        => $length,
457 338
            'notnull'       => (bool) $tableColumn['isnotnull'],
458 338
            'default'       => $tableColumn['default'],
459 338
            'precision'     => $precision,
460 338
            'scale'         => $scale,
461 338
            'fixed'         => $fixed,
462
            'unsigned'      => false,
463 338
            'autoincrement' => $autoincrement,
464 338
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
465 84
                ? $tableColumn['comment']
466
                : null,
467
        ];
468
469 338
        $column = new Column($tableColumn['field'], Type::getType($type), $options);
470
471 338
        if (isset($tableColumn['collation']) && !empty($tableColumn['collation'])) {
472
            $column->setPlatformOption('collation', $tableColumn['collation']);
473
        }
474
475 338
        if (in_array($column->getType()->getName(), [Type::JSON_ARRAY, Type::JSON], true)) {
476 46
            $column->setPlatformOption('jsonb', $jsonb);
477
        }
478
479 338
        return $column;
480
    }
481
482
    /**
483
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
484
     *
485
     * @param mixed $defaultValue
486
     *
487
     * @return mixed
488
     */
489 294
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
490
    {
491 294
        if (strpos($defaultValue, '(') === 0) {
492 9
            return trim($defaultValue, '()');
493
        }
494
495 294
        return $defaultValue;
496
    }
497
}
498