Passed
Pull Request — master (#3135)
by Michael
17:53
created

PostgreSqlSchemaManager   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 437
Duplicated Lines 0 %

Test Coverage

Coverage 94.69%

Importance

Changes 0
Metric Value
wmc 82
dl 0
loc 437
ccs 196
cts 207
cp 0.9469
rs 1.5789
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A _getPortableUserDefinition() 0 5 1
A _getPortableTriggerDefinition() 0 3 1
A getSchemaNames() 0 5 1
A getExistingSchemaSearchPaths() 0 7 2
A _getPortableViewDefinition() 0 3 1
A getSchemaSearchPaths() 0 10 2
A dropDatabase() 0 21 3
B _getPortableTableForeignKeyDefinition() 0 23 4
A _getPortableTableDefinition() 0 10 2
A _getPortableDatabaseDefinition() 0 3 1
A _getPortableSequenceDefinition() 0 14 3
A _getPortableSequencesList() 0 21 4
A fixVersion94NegativeNumericDefaultValue() 0 7 2
A getPortableNamespaceDefinition() 0 3 1
B _getPortableTableIndexesList() 0 29 5
F _getPortableTableColumnDefinition() 0 151 48
A determineExistingSchemaSearchPaths() 0 7 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
 * 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\Types\Type;
24
use const CASE_LOWER;
25
use function array_change_key_case;
26
use function array_filter;
27
use function array_keys;
28
use function array_map;
29
use function array_shift;
30
use function explode;
31
use function in_array;
32
use function join;
33
use function preg_match;
34
use function preg_replace;
35
use function str_replace;
36
use function stripos;
37
use function strlen;
38
use function strpos;
39
use function strtolower;
40
use function trim;
41
42
/**
43
 * PostgreSQL Schema Manager.
44
 *
45
 * @author Konsta Vesterinen <[email protected]>
46
 * @author Lukas Smith <[email protected]> (PEAR MDB2 library)
47
 * @author Benjamin Eberlei <[email protected]>
48
 * @since  2.0
49
 */
50
class PostgreSqlSchemaManager extends AbstractSchemaManager
51
{
52
    /**
53
     * @var array
54
     */
55
    private $existingSchemaPaths;
56
57
    /**
58
     * Gets all the existing schema names.
59
     *
60
     * @return array
61
     */
62 14
    public function getSchemaNames()
63
    {
64 14
        $rows = $this->_conn->fetchAll("SELECT nspname as schema_name FROM pg_namespace WHERE nspname !~ '^pg_.*' and nspname != 'information_schema'");
65
66
        return array_map(function ($v) { return $v['schema_name']; }, $rows);
67
    }
68
69
    /**
70
     * Returns an array of schema search paths.
71
     *
72
     * This is a PostgreSQL only function.
73
     *
74
     * @return array
75
     */
76 119
    public function getSchemaSearchPaths()
77
    {
78 119
        $params = $this->_conn->getParams();
79 119
        $schema = explode(",", $this->_conn->fetchColumn('SHOW search_path'));
80
81 119
        if (isset($params['user'])) {
82 119
            $schema = str_replace('"$user"', $params['user'], $schema);
83
        }
84
85 119
        return array_map('trim', $schema);
86
    }
87
88
    /**
89
     * Gets names of all existing schemas in the current users search path.
90
     *
91
     * This is a PostgreSQL only function.
92
     *
93
     * @return array
94
     */
95 603
    public function getExistingSchemaSearchPaths()
96
    {
97 603
        if ($this->existingSchemaPaths === null) {
98 7
            $this->determineExistingSchemaSearchPaths();
99
        }
100
101 603
        return $this->existingSchemaPaths;
102
    }
103
104
    /**
105
     * Sets or resets the order of the existing schemas in the current search path of the user.
106
     *
107
     * This is a PostgreSQL only function.
108
     *
109
     * @return void
110
     */
111 7
    public function determineExistingSchemaSearchPaths()
112
    {
113 7
        $names = $this->getSchemaNames();
114 7
        $paths = $this->getSchemaSearchPaths();
115
116
        $this->existingSchemaPaths = array_filter($paths, function ($v) use ($names) {
117 7
            return in_array($v, $names);
118 7
        });
119 7
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 14
    public function dropDatabase($database)
125
    {
126
        try {
127 14
            parent::dropDatabase($database);
128 14
        } catch (DriverException $exception) {
129
            // If we have a SQLSTATE 55006, the drop database operation failed
130
            // because of active connections on the database.
131
            // To force dropping the database, we first have to close all active connections
132
            // on that database and issue the drop database operation again.
133 14
            if ($exception->getSQLState() !== '55006') {
134 14
                throw $exception;
135
            }
136
137 7
            $this->_execSql(
138
                [
139 7
                    $this->_platform->getDisallowDatabaseConnectionsSQL($database),
0 ignored issues
show
Bug introduced by
The method getDisallowDatabaseConnectionsSQL() does not exist on Doctrine\DBAL\Platforms\AbstractPlatform. It seems like you code against a sub-type of Doctrine\DBAL\Platforms\AbstractPlatform such as Doctrine\DBAL\Platforms\PostgreSqlPlatform. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

139
                    $this->_platform->/** @scrutinizer ignore-call */ 
140
                                      getDisallowDatabaseConnectionsSQL($database),
Loading history...
140 7
                    $this->_platform->getCloseActiveDatabaseConnectionsSQL($database),
0 ignored issues
show
Bug introduced by
The method getCloseActiveDatabaseConnectionsSQL() does not exist on Doctrine\DBAL\Platforms\AbstractPlatform. It seems like you code against a sub-type of Doctrine\DBAL\Platforms\AbstractPlatform such as Doctrine\DBAL\Platforms\PostgreSqlPlatform. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

140
                    $this->_platform->/** @scrutinizer ignore-call */ 
141
                                      getCloseActiveDatabaseConnectionsSQL($database),
Loading history...
141
                ]
142
            );
143
144 7
            parent::dropDatabase($database);
145
        }
146 7
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 98
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
152
    {
153 98
        $onUpdate = null;
154 98
        $onDelete = null;
155
156 98
        if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
157 28
            $onUpdate = $match[1];
158
        }
159 98
        if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
160 28
            $onDelete = $match[1];
161
        }
162
163 98
        if (preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values)) {
164
            // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
165
            // the idea to trim them here.
166 98
            $localColumns = array_map('trim', explode(",", $values[1]));
167 98
            $foreignColumns = array_map('trim', explode(",", $values[3]));
168 98
            $foreignTable = $values[2];
169
        }
170
171 98
        return new ForeignKeyConstraint(
172 98
            $localColumns, $foreignTable, $foreignColumns, $tableForeignKey['conname'],
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $foreignTable does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $foreignColumns does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $localColumns does not seem to be defined for all execution paths leading up to this point.
Loading history...
173 98
            ['onUpdate' => $onUpdate, 'onDelete' => $onDelete]
174
        );
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180
    protected function _getPortableTriggerDefinition($trigger)
181
    {
182
        return $trigger['trigger_name'];
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188 7
    protected function _getPortableViewDefinition($view)
189
    {
190 7
        return new View($view['schemaname'].'.'.$view['viewname'], $view['definition']);
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196
    protected function _getPortableUserDefinition($user)
197
    {
198
        return [
199
            'user' => $user['usename'],
200
            'password' => $user['passwd']
201
        ];
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     */
207 603
    protected function _getPortableTableDefinition($table)
208
    {
209 603
        $schemas = $this->getExistingSchemaSearchPaths();
210 603
        $firstSchema = array_shift($schemas);
211
212 603
        if ($table['schema_name'] == $firstSchema) {
213 603
            return $table['table_name'];
214
        }
215
216 505
        return $table['schema_name'] . "." . $table['table_name'];
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     *
222
     * @license New BSD License
223
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
224
     */
225 315
    protected function _getPortableTableIndexesList($tableIndexes, $tableName=null)
226
    {
227 315
        $buffer = [];
228 315
        foreach ($tableIndexes as $row) {
229 175
            $colNumbers = explode(' ', $row['indkey']);
230 175
            $colNumbersSql = 'IN (' . join(' ,', $colNumbers) . ' )';
231
            $columnNameSql = "SELECT attnum, attname FROM pg_attribute
232 175
                WHERE attrelid={$row['indrelid']} AND attnum $colNumbersSql ORDER BY attnum ASC;";
233
234 175
            $stmt = $this->_conn->executeQuery($columnNameSql);
235 175
            $indexColumns = $stmt->fetchAll();
236
237
            // required for getting the order of the columns right.
238 175
            foreach ($colNumbers as $colNum) {
239 175
                foreach ($indexColumns as $colRow) {
240 175
                    if ($colNum == $colRow['attnum']) {
241 175
                        $buffer[] = [
242 175
                            'key_name' => $row['relname'],
243 175
                            'column_name' => trim($colRow['attname']),
244 175
                            'non_unique' => !$row['indisunique'],
245 175
                            'primary' => $row['indisprimary'],
246 175
                            'where' => $row['where'],
247
                        ];
248
                    }
249
                }
250
            }
251
        }
252
253 315
        return parent::_getPortableTableIndexesList($buffer, $tableName);
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 14
    protected function _getPortableDatabaseDefinition($database)
260
    {
261 14
        return $database['datname'];
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267 42
    protected function _getPortableSequencesList($sequences)
268
    {
269 42
        $sequenceDefinitions = [];
270
271 42
        foreach ($sequences as $sequence) {
272 42
            if ($sequence['schemaname'] != 'public') {
273 42
                $sequenceName = $sequence['schemaname'] . "." . $sequence['relname'];
274
            } else {
275 42
                $sequenceName = $sequence['relname'];
276
            }
277
278 42
            $sequenceDefinitions[$sequenceName] = $sequence;
279
        }
280
281 42
        $list = [];
282
283 42
        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
284 42
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
285
        }
286
287 42
        return $list;
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293 14
    protected function getPortableNamespaceDefinition(array $namespace)
294
    {
295 14
        return $namespace['nspname'];
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301 42
    protected function _getPortableSequenceDefinition($sequence)
302
    {
303 42
        if ($sequence['schemaname'] !== 'public') {
304 42
            $sequenceName = $sequence['schemaname'] . "." . $sequence['relname'];
305
        } else {
306 42
            $sequenceName = $sequence['relname'];
307
        }
308
309 42
        if ( ! isset($sequence['increment_by'], $sequence['min_value'])) {
310 36
            $data      = $this->_conn->fetchAssoc('SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName));
311 36
            $sequence += $data;
312
        }
313
314 42
        return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
315
    }
316
317
    /**
318
     * {@inheritdoc}
319
     */
320 393
    protected function _getPortableTableColumnDefinition($tableColumn)
321
    {
322 393
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
323
324 393
        if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
325
            // get length from varchar definition
326 119
            $length = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']);
327 119
            $tableColumn['length'] = $length;
328
        }
329
330 393
        $matches = [];
331
332 393
        $autoincrement = false;
333 393
        if (preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) {
334 112
            $tableColumn['sequence'] = $matches[1];
335 112
            $tableColumn['default'] = null;
336 112
            $autoincrement = true;
337
        }
338
339 393
        if (preg_match("/^['(](.*)[')]::.*$/", $tableColumn['default'], $matches)) {
340 70
            $tableColumn['default'] = $matches[1];
341
        }
342
343 393
        if (stripos($tableColumn['default'], 'NULL') === 0) {
344 42
            $tableColumn['default'] = null;
345
        }
346
347 393
        $length = $tableColumn['length'] ?? null;
348 393
        if ($length == '-1' && isset($tableColumn['atttypmod'])) {
349
            $length = $tableColumn['atttypmod'] - 4;
350
        }
351 393
        if ((int) $length <= 0) {
352 386
            $length = null;
353
        }
354 393
        $fixed = null;
355
356 393
        if (!isset($tableColumn['name'])) {
357 393
            $tableColumn['name'] = '';
358
        }
359
360 393
        $precision = null;
361 393
        $scale = null;
362 393
        $jsonb = null;
363
364 393
        $dbType = strtolower($tableColumn['type']);
365 393
        if (strlen($tableColumn['domain_type']) && !$this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])) {
366 7
            $dbType = strtolower($tableColumn['domain_type']);
367 7
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
368
        }
369
370 393
        $type = $this->_platform->getDoctrineTypeMapping($dbType);
371 393
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
372 393
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
373
374
        switch ($dbType) {
375 393
            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...
376 393
            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...
377 21
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
378 21
                $length = null;
379 21
                break;
380 393
            case 'int':
381 393
            case 'int4':
382 225
            case 'integer':
383 329
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
384 329
                $length = null;
385 329
                break;
386 225
            case 'bigint':
387 225
            case 'int8':
388 49
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
389 49
                $length = null;
390 49
                break;
391 197
            case 'bool':
392 190
            case 'boolean':
393 28
                if ($tableColumn['default'] === 'true') {
394
                    $tableColumn['default'] = true;
395
                }
396
397 28
                if ($tableColumn['default'] === 'false') {
398 28
                    $tableColumn['default'] = false;
399
                }
400
401 28
                $length = null;
402 28
                break;
403 190
            case 'text':
404 49
                $fixed = false;
405 49
                break;
406 183
            case 'varchar':
407 134
            case 'interval':
408 134
            case '_varchar':
409 98
                $fixed = false;
410 98
                break;
411 134
            case 'char':
412 134
            case 'bpchar':
413 42
                $fixed = true;
414 42
                break;
415 113
            case 'float':
416 113
            case 'float4':
417 113
            case 'float8':
418 113
            case 'double':
419 113
            case 'double precision':
420 113
            case 'real':
421 113
            case 'decimal':
422 113
            case 'money':
423 113
            case 'numeric':
424 49
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
425
426 49
                if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) {
427 49
                    $precision = $match[1];
428 49
                    $scale = $match[2];
429 49
                    $length = null;
430
                }
431 49
                break;
432 106
            case 'year':
433
                $length = null;
434
                break;
435
436
            // PostgreSQL 9.4+ only
437 106
            case 'jsonb':
438 16
                $jsonb = true;
439 16
                break;
440
        }
441
442 393
        if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) {
443
            $tableColumn['default'] = $match[1];
444
        }
445
446
        $options = [
447 393
            'length'        => $length,
448 393
            'notnull'       => (bool) $tableColumn['isnotnull'],
449 393
            'default'       => $tableColumn['default'],
450 393
            'precision'     => $precision,
451 393
            'scale'         => $scale,
452 393
            'fixed'         => $fixed,
453
            'unsigned'      => false,
454 393
            'autoincrement' => $autoincrement,
455 393
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
456 98
                ? $tableColumn['comment']
457
                : null,
458
        ];
459
460 393
        $column = new Column($tableColumn['field'], Type::getType($type), $options);
461
462 393
        if (isset($tableColumn['collation']) && !empty($tableColumn['collation'])) {
463
            $column->setPlatformOption('collation', $tableColumn['collation']);
464
        }
465
466 393
        if (in_array($column->getType()->getName(), [Type::JSON_ARRAY, Type::JSON], true)) {
467 51
            $column->setPlatformOption('jsonb', $jsonb);
468
        }
469
470 393
        return $column;
471
    }
472
473
    /**
474
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
475
     *
476
     * @param mixed $defaultValue
477
     *
478
     * @return mixed
479
     */
480 343
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
481
    {
482 343
        if (strpos($defaultValue, '(') === 0) {
483 12
            return trim($defaultValue, '()');
484
        }
485
486 343
        return $defaultValue;
487
    }
488
}
489