Completed
Push — master ( 402bbc...29f179 )
by Marco
39:33 queued 36:00
created

PostgreSqlSchemaManager   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 444
Duplicated Lines 0 %

Test Coverage

Coverage 94.37%

Importance

Changes 0
Metric Value
wmc 82
dl 0
loc 444
ccs 201
cts 213
cp 0.9437
rs 2
c 0
b 0
f 0

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