Passed
Push — allow-sf4-console ( abb17e...64a32a )
by Michael
252:50 queued 248:44
created

PostgreSqlSchemaManager   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 444
Duplicated Lines 0 %

Test Coverage

Coverage 94.81%

Importance

Changes 0
Metric Value
wmc 82
dl 0
loc 444
ccs 201
cts 212
cp 0.9481
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':
386 338
            case 'int2':
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