Passed
Push — tests-better-coverage ( b00e66...9c1c22 )
by Michael
23:49
created

getPortableNamespaceDefinition()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
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\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':
376 338
            case 'int2':
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