Passed
Pull Request — master (#3092)
by Michael
13:56
created

_getPortableTableIndexesList()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 18
cts 18
cp 1
rs 8.439
c 0
b 0
f 0
cc 5
eloc 18
nc 5
nop 2
crap 5
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 12
    public function getSchemaNames()
63
    {
64 12
        $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 102
    public function getSchemaSearchPaths()
77
    {
78 102
        $params = $this->_conn->getParams();
79 102
        $schema = explode(",", $this->_conn->fetchColumn('SHOW search_path'));
80
81 102
        if (isset($params['user'])) {
82 102
            $schema = str_replace('"$user"', $params['user'], $schema);
83
        }
84
85 102
        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 512
    public function getExistingSchemaSearchPaths()
96
    {
97 512
        if ($this->existingSchemaPaths === null) {
98 6
            $this->determineExistingSchemaSearchPaths();
99
        }
100
101 512
        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 6
    public function determineExistingSchemaSearchPaths()
112
    {
113 6
        $names = $this->getSchemaNames();
114 6
        $paths = $this->getSchemaSearchPaths();
115
116 6
        $this->existingSchemaPaths = array_filter($paths, function ($v) use ($names) {
117 6
            return in_array($v, $names);
118 6
        });
119 6
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 12
    public function dropDatabase($database)
125
    {
126
        try {
127 12
            parent::dropDatabase($database);
128 12
        } 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 12
            if ($exception->getSQLState() !== '55006') {
134 12
                throw $exception;
135
            }
136
137 6
            $this->_execSql(
138
                [
139 6
                    $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 6
                    $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 6
            parent::dropDatabase($database);
145
        }
146 6
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 84
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
152
    {
153 84
        $onUpdate = null;
154 84
        $onDelete = null;
155
156 84
        if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
157 24
            $onUpdate = $match[1];
158
        }
159 84
        if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
160 24
            $onDelete = $match[1];
161
        }
162
163 84
        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 84
            $localColumns = array_map('trim', explode(",", $values[1]));
167 84
            $foreignColumns = array_map('trim', explode(",", $values[3]));
168 84
            $foreignTable = $values[2];
169
        }
170
171 84
        return new ForeignKeyConstraint(
172 84
            $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 84
            ['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 6
    protected function _getPortableViewDefinition($view)
189
    {
190 6
        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 512
    protected function _getPortableTableDefinition($table)
208
    {
209 512
        $schemas = $this->getExistingSchemaSearchPaths();
210 512
        $firstSchema = array_shift($schemas);
211
212 512
        if ($table['schema_name'] == $firstSchema) {
213 512
            return $table['table_name'];
214
        }
215
216 428
        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 270
    protected function _getPortableTableIndexesList($tableIndexes, $tableName=null)
226
    {
227 270
        $buffer = [];
228 270
        foreach ($tableIndexes as $row) {
229 150
            $colNumbers = explode(' ', $row['indkey']);
230 150
            $colNumbersSql = 'IN (' . join(' ,', $colNumbers) . ' )';
231
            $columnNameSql = "SELECT attnum, attname FROM pg_attribute
232 150
                WHERE attrelid={$row['indrelid']} AND attnum $colNumbersSql ORDER BY attnum ASC;";
233
234 150
            $stmt = $this->_conn->executeQuery($columnNameSql);
235 150
            $indexColumns = $stmt->fetchAll();
236
237
            // required for getting the order of the columns right.
238 150
            foreach ($colNumbers as $colNum) {
239 150
                foreach ($indexColumns as $colRow) {
240 150
                    if ($colNum == $colRow['attnum']) {
241 150
                        $buffer[] = [
242 150
                            'key_name' => $row['relname'],
243 150
                            'column_name' => trim($colRow['attname']),
244 150
                            'non_unique' => !$row['indisunique'],
245 150
                            'primary' => $row['indisprimary'],
246 150
                            'where' => $row['where'],
247
                        ];
248
                    }
249
                }
250
            }
251
        }
252
253 270
        return parent::_getPortableTableIndexesList($buffer, $tableName);
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 12
    protected function _getPortableDatabaseDefinition($database)
260
    {
261 12
        return $database['datname'];
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267 30
    protected function _getPortableSequencesList($sequences)
268
    {
269 30
        $sequenceDefinitions = [];
270
271 30
        foreach ($sequences as $sequence) {
272 30
            if ($sequence['schemaname'] != 'public') {
273 30
                $sequenceName = $sequence['schemaname'] . "." . $sequence['relname'];
274
            } else {
275 30
                $sequenceName = $sequence['relname'];
276
            }
277
278 30
            $sequenceDefinitions[$sequenceName] = $sequence;
279
        }
280
281 30
        $list = [];
282
283 30
        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
284 30
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
285
        }
286
287 30
        return $list;
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293 12
    protected function getPortableNamespaceDefinition(array $namespace)
294
    {
295 12
        return $namespace['nspname'];
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301 30
    protected function _getPortableSequenceDefinition($sequence)
302
    {
303 30
        if ($sequence['schemaname'] !== 'public') {
304 30
            $sequenceName = $sequence['schemaname'] . "." . $sequence['relname'];
305
        } else {
306 30
            $sequenceName = $sequence['relname'];
307
        }
308
309 30
        if ( ! isset($sequence['increment_by'], $sequence['min_value'])) {
310 25
            $data      = $this->_conn->fetchAssoc('SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName));
311 25
            $sequence += $data;
312
        }
313
314 30
        return new Sequence($sequenceName, $sequence['increment_by'], $sequence['min_value']);
315
    }
316
317
    /**
318
     * {@inheritdoc}
319
     */
320 338
    protected function _getPortableTableColumnDefinition($tableColumn)
321
    {
322 338
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
323
324 338
        if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
325
            // get length from varchar definition
326 102
            $length = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']);
327 102
            $tableColumn['length'] = $length;
328
        }
329
330 338
        $matches = [];
331
332 338
        $autoincrement = false;
333 338
        if (preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) {
334 96
            $tableColumn['sequence'] = $matches[1];
335 96
            $tableColumn['default'] = null;
336 96
            $autoincrement = true;
337
        }
338
339 338
        if (preg_match("/^['(](.*)[')]::.*$/", $tableColumn['default'], $matches)) {
340 60
            $tableColumn['default'] = $matches[1];
341
        }
342
343 338
        if (stripos($tableColumn['default'], 'NULL') === 0) {
344 36
            $tableColumn['default'] = null;
345
        }
346
347 338
        $length = $tableColumn['length'] ?? null;
348 338
        if ($length == '-1' && isset($tableColumn['atttypmod'])) {
349
            $length = $tableColumn['atttypmod'] - 4;
350
        }
351 338
        if ((int) $length <= 0) {
352 332
            $length = null;
353
        }
354 338
        $fixed = null;
355
356 338
        if (!isset($tableColumn['name'])) {
357 338
            $tableColumn['name'] = '';
358
        }
359
360 338
        $precision = null;
361 338
        $scale = null;
362 338
        $jsonb = null;
363
364 338
        $dbType = strtolower($tableColumn['type']);
365 338
        if (strlen($tableColumn['domain_type']) && !$this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])) {
366 6
            $dbType = strtolower($tableColumn['domain_type']);
367 6
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
368
        }
369
370 338
        $type = $this->_platform->getDoctrineTypeMapping($dbType);
371 338
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
372 338
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
373
374
        switch ($dbType) {
375 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...
376 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...
377 18
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
378 18
                $length = null;
379 18
                break;
380 338
            case 'int':
381 338
            case 'int4':
382 194
            case 'integer':
383 282
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
384 282
                $length = null;
385 282
                break;
386 194
            case 'bigint':
387 194
            case 'int8':
388 42
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
389 42
                $length = null;
390 42
                break;
391 170
            case 'bool':
392 164
            case 'boolean':
393 24
                if ($tableColumn['default'] === 'true') {
394
                    $tableColumn['default'] = true;
395
                }
396
397 24
                if ($tableColumn['default'] === 'false') {
398 24
                    $tableColumn['default'] = false;
399
                }
400
401 24
                $length = null;
402 24
                break;
403 164
            case 'text':
404 42
                $fixed = false;
405 42
                break;
406 158
            case 'varchar':
407 116
            case 'interval':
408 116
            case '_varchar':
409 84
                $fixed = false;
410 84
                break;
411 116
            case 'char':
412 116
            case 'bpchar':
413 36
                $fixed = true;
414 36
                break;
415 98
            case 'float':
416 98
            case 'float4':
417 98
            case 'float8':
418 98
            case 'double':
419 98
            case 'double precision':
420 98
            case 'real':
421 98
            case 'decimal':
422 98
            case 'money':
423 98
            case 'numeric':
424 42
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
425
426 42
                if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) {
427 42
                    $precision = $match[1];
428 42
                    $scale = $match[2];
429 42
                    $length = null;
430
                }
431 42
                break;
432 92
            case 'year':
433
                $length = null;
434
                break;
435
436
            // PostgreSQL 9.4+ only
437 92
            case 'jsonb':
438 16
                $jsonb = true;
439 16
                break;
440
        }
441
442 338
        if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) {
443
            $tableColumn['default'] = $match[1];
444
        }
445
446
        $options = [
447 338
            'length'        => $length,
448 338
            'notnull'       => (bool) $tableColumn['isnotnull'],
449 338
            'default'       => $tableColumn['default'],
450 338
            'precision'     => $precision,
451 338
            'scale'         => $scale,
452 338
            'fixed'         => $fixed,
453
            'unsigned'      => false,
454 338
            'autoincrement' => $autoincrement,
455 338
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
456 84
                ? $tableColumn['comment']
457
                : null,
458
        ];
459
460 338
        $column = new Column($tableColumn['field'], Type::getType($type), $options);
461
462 338
        if (isset($tableColumn['collation']) && !empty($tableColumn['collation'])) {
463
            $column->setPlatformOption('collation', $tableColumn['collation']);
464
        }
465
466 338
        if (in_array($column->getType()->getName(), [Type::JSON_ARRAY, Type::JSON], true)) {
467 46
            $column->setPlatformOption('jsonb', $jsonb);
468
        }
469
470 338
        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 294
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
481
    {
482 294
        if (strpos($defaultValue, '(') === 0) {
483 9
            return trim($defaultValue, '()');
484
        }
485
486 294
        return $defaultValue;
487
    }
488
}
489