Passed
Pull Request — master (#3025)
by Michael
14:33
created

determineExistingSchemaSearchPaths()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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