Failed Conditions
Pull Request — develop (#3525)
by Jonathan
12:46
created

_getPortableTableIndexesList()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5.0026

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 33
ccs 20
cts 21
cp 0.9524
rs 9.2888
c 0
b 0
f 0
cc 5
nc 5
nop 2
crap 5.0026
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Schema;
6
7
use Doctrine\DBAL\Exception\DriverException;
8
use Doctrine\DBAL\FetchMode;
9
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
10
use Doctrine\DBAL\Types\Type;
11
use Doctrine\DBAL\Types\Types;
12
use const CASE_LOWER;
13
use function array_change_key_case;
14
use function array_filter;
15
use function array_keys;
16
use function array_map;
17
use function array_shift;
18
use function assert;
19
use function explode;
20
use function implode;
21
use function in_array;
22
use function preg_match;
23
use function sprintf;
24
use function str_replace;
25
use function stripos;
26
use function strlen;
27
use function strpos;
28
use function strtolower;
29
use function trim;
30
31
/**
32
 * PostgreSQL Schema Manager.
33
 */
34
class PostgreSqlSchemaManager extends AbstractSchemaManager
35
{
36
    /** @var string[] */
37
    private $existingSchemaPaths;
38
39
    /**
40
     * Gets all the existing schema names.
41
     *
42
     * @return string[]
43
     */
44 347
    public function getSchemaNames()
45
    {
46 347
        $statement = $this->_conn->executeQuery("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'");
47
48 347
        return $statement->fetchAll(FetchMode::COLUMN);
49
    }
50
51
    /**
52
     * Returns an array of schema search paths.
53
     *
54
     * This is a PostgreSQL only function.
55
     *
56
     * @return string[]
57
     */
58 347
    public function getSchemaSearchPaths()
59
    {
60 347
        $params = $this->_conn->getParams();
61 347
        $schema = explode(',', $this->_conn->fetchColumn('SHOW search_path'));
0 ignored issues
show
Bug introduced by
It seems like $this->_conn->fetchColumn('SHOW search_path') can also be of type false; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

61
        $schema = explode(',', /** @scrutinizer ignore-type */ $this->_conn->fetchColumn('SHOW search_path'));
Loading history...
62
63 347
        if (isset($params['user'])) {
64 347
            $schema = str_replace('"$user"', $params['user'], $schema);
65
        }
66
67 347
        return array_map('trim', $schema);
68
    }
69
70
    /**
71
     * Gets names of all existing schemas in the current users search path.
72
     *
73
     * This is a PostgreSQL only function.
74
     *
75
     * @return string[]
76
     */
77 347
    public function getExistingSchemaSearchPaths()
78
    {
79 347
        if ($this->existingSchemaPaths === null) {
80 347
            $this->determineExistingSchemaSearchPaths();
81
        }
82
83 347
        return $this->existingSchemaPaths;
84
    }
85
86
    /**
87
     * Sets or resets the order of the existing schemas in the current search path of the user.
88
     *
89
     * This is a PostgreSQL only function.
90
     *
91
     * @return void
92
     */
93 347
    public function determineExistingSchemaSearchPaths()
94
    {
95 347
        $names = $this->getSchemaNames();
96 347
        $paths = $this->getSchemaSearchPaths();
97
98
        $this->existingSchemaPaths = array_filter($paths, static function ($v) use ($names) {
99 347
            return in_array($v, $names);
100 347
        });
101 347
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106 347
    public function dropDatabase($database)
107
    {
108
        try {
109 347
            parent::dropDatabase($database);
110 347
        } catch (DriverException $exception) {
111
            // If we have a SQLSTATE 55006, the drop database operation failed
112
            // because of active connections on the database.
113
            // To force dropping the database, we first have to close all active connections
114
            // on that database and issue the drop database operation again.
115 347
            if ($exception->getSQLState() !== '55006') {
116 347
                throw $exception;
117
            }
118
119 213
            assert($this->_platform instanceof PostgreSqlPlatform);
120
121 213
            $this->_execSql(
122
                [
123 213
                    $this->_platform->getDisallowDatabaseConnectionsSQL($database),
124 213
                    $this->_platform->getCloseActiveDatabaseConnectionsSQL($database),
125
                ]
126
            );
127
128 213
            parent::dropDatabase($database);
129
        }
130 213
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135 287
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
136
    {
137 287
        $onUpdate       = null;
138 287
        $onDelete       = null;
139 287
        $localColumns   = [];
140 287
        $foreignColumns = [];
141 287
        $foreignTable   = null;
142
143 287
        if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
144 283
            $onUpdate = $match[1];
145
        }
146 287
        if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
147 275
            $onDelete = $match[1];
148
        }
149
150 287
        if (preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values)) {
151
            // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
152
            // the idea to trim them here.
153 287
            $localColumns   = array_map('trim', explode(',', $values[1]));
154 287
            $foreignColumns = array_map('trim', explode(',', $values[3]));
155 287
            $foreignTable   = $values[2];
156
        }
157
158 287
        return new ForeignKeyConstraint(
159 287
            $localColumns,
160
            $foreignTable,
161
            $foreignColumns,
162 287
            $tableForeignKey['conname'],
163 287
            ['onUpdate' => $onUpdate, 'onDelete' => $onDelete]
164
        );
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170
    protected function _getPortableTriggerDefinition($trigger)
171
    {
172
        return $trigger['trigger_name'];
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178 145
    protected function _getPortableViewDefinition($view)
179
    {
180 145
        return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']);
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186
    protected function _getPortableUserDefinition($user)
187
    {
188
        return [
189
            'user' => $user['usename'],
190
            'password' => $user['passwd'],
191
        ];
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197 347
    protected function _getPortableTableDefinition($table)
198
    {
199 347
        $schemas     = $this->getExistingSchemaSearchPaths();
200 347
        $firstSchema = array_shift($schemas);
201
202 347
        if ($table['schema_name'] === $firstSchema) {
203 347
            return $table['table_name'];
204
        }
205
206 287
        return $table['schema_name'] . '.' . $table['table_name'];
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     *
212
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
213
     */
214 319
    protected function _getPortableTableIndexesList(array $tableIndexRows, string $tableName) : array
215
    {
216 319
        $buffer = [];
217 319
        foreach ($tableIndexRows as $row) {
218 303
            $colNumbers    = array_map('intval', explode(' ', $row['indkey']));
219 303
            $columnNameSql = sprintf(
220
                'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
221 303
                $row['indrelid'],
222 303
                implode(' ,', $colNumbers)
223
            );
224
225 303
            $stmt         = $this->_conn->executeQuery($columnNameSql);
226 303
            $indexColumns = $stmt->fetchAll();
227
228
            // required for getting the order of the columns right.
229 303
            foreach ($colNumbers as $colNum) {
230 303
                foreach ($indexColumns as $colRow) {
231 303
                    if ($colNum !== $colRow['attnum']) {
232 251
                        continue;
233
                    }
234
235 303
                    $buffer[] = [
236 303
                        'key_name' => $row['relname'],
237 303
                        'column_name' => trim($colRow['attname']),
238 303
                        'non_unique' => ! $row['indisunique'],
239 303
                        'primary' => $row['indisprimary'],
240 303
                        'where' => $row['where'],
241
                    ];
242
                }
243
            }
244
        }
245
246 319
        return parent::_getPortableTableIndexesList($buffer, $tableName);
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252 213
    protected function _getPortableDatabaseDefinition($database)
253
    {
254 213
        return $database['datname'];
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260 209
    protected function _getPortableSequencesList($sequences)
261
    {
262 209
        $sequenceDefinitions = [];
263
264 209
        foreach ($sequences as $sequence) {
265 209
            if ($sequence['schemaname'] !== 'public') {
266 209
                $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
267
            } else {
268 209
                $sequenceName = $sequence['relname'];
269
            }
270
271 209
            $sequenceDefinitions[$sequenceName] = $sequence;
272
        }
273
274 209
        $list = [];
275
276 209
        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
277 209
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
278
        }
279
280 209
        return $list;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286 197
    protected function getPortableNamespaceDefinition(array $namespace)
287
    {
288 197
        return $namespace['nspname'];
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     */
294 209
    protected function _getPortableSequenceDefinition($sequence)
295
    {
296 209
        if ($sequence['schemaname'] !== 'public') {
297 209
            $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
298
        } else {
299 209
            $sequenceName = $sequence['relname'];
300
        }
301
302 209
        if (! isset($sequence['increment_by'], $sequence['min_value'])) {
303
            /** @var string[] $data */
304 209
            $data = $this->_conn->fetchAssoc('SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName));
305
306 209
            $sequence += $data;
307
        }
308
309 209
        return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
310
    }
311
312
    /**
313
     * {@inheritdoc}
314
     */
315 319
    protected function _getPortableTableColumnDefinition($tableColumn)
316
    {
317 319
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
318
319 319
        $length = null;
320
321 319
        if (in_array(strtolower($tableColumn['type']), ['varchar', 'bpchar'], true)
322 319
            && preg_match('/\((\d*)\)/', $tableColumn['complete_type'], $matches)) {
323 271
            $length = (int) $matches[1];
324
        }
325
326 319
        $matches = [];
327
328 319
        $autoincrement = false;
329 319
        if ($tableColumn['default'] !== null && preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) {
330 299
            $tableColumn['sequence'] = $matches[1];
331 299
            $tableColumn['default']  = null;
332 299
            $autoincrement           = true;
333
        }
334
335 319
        if ($tableColumn['default'] !== null && preg_match("/^['(](.*)[')]::.*$/", $tableColumn['default'], $matches)) {
336 271
            $tableColumn['default'] = $matches[1];
337
        }
338
339 319
        if ($tableColumn['default'] !== null && stripos($tableColumn['default'], 'NULL') === 0) {
340 255
            $tableColumn['default'] = null;
341
        }
342
343 319
        if ($length === -1 && isset($tableColumn['atttypmod'])) {
344
            $length = $tableColumn['atttypmod'] - 4;
345
        }
346
347 319
        if ((int) $length <= 0) {
348 319
            $length = null;
349
        }
350
351 319
        $fixed = false;
352
353 319
        if (! isset($tableColumn['name'])) {
354 319
            $tableColumn['name'] = '';
355
        }
356
357 319
        $precision = null;
358 319
        $scale     = 0;
359 319
        $jsonb     = null;
360
361 319
        $dbType = strtolower($tableColumn['type']);
362 319
        if ($tableColumn['domain_type'] !== null
363 319
            && strlen($tableColumn['domain_type'])
364 319
            && ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])
365
        ) {
366 303
            $dbType                       = strtolower($tableColumn['domain_type']);
367 303
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
368
        }
369
370 319
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'])
371 319
            ?? $this->_platform->getDoctrineTypeMapping($dbType);
372
373 319
        switch ($dbType) {
374
            case 'smallint':
375
            case 'int2':
376 241
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
377 241
                $length                 = null;
378 241
                break;
379
            case 'int':
380
            case 'int4':
381
            case 'integer':
382 319
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
383 319
                $length                 = null;
384 319
                break;
385
            case 'bigint':
386
            case 'int8':
387 241
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
388 241
                $length                 = null;
389 241
                break;
390
            case 'bool':
391
            case 'boolean':
392 315
                if ($tableColumn['default'] === 'true') {
393
                    $tableColumn['default'] = true;
394
                }
395
396 315
                if ($tableColumn['default'] === 'false') {
397 315
                    $tableColumn['default'] = false;
398
                }
399
400 315
                $length = null;
401 315
                break;
402
            case 'char':
403
            case 'bpchar':
404 255
                $fixed = true;
405 255
                break;
406
            case 'float':
407
            case 'float4':
408
            case 'float8':
409
            case 'double':
410
            case 'double precision':
411
            case 'real':
412
            case 'decimal':
413
            case 'money':
414
            case 'numeric':
415 303
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
416
417 303
                if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) {
418 303
                    $precision = (int) $match[1];
419 303
                    $scale     = (int) $match[2];
420 303
                    $length    = null;
421
                }
422 303
                break;
423
            case 'year':
424
                $length = null;
425
                break;
426
427
            // PostgreSQL 9.4+ only
428
            case 'jsonb':
429 186
                $jsonb = true;
430 186
                break;
431
        }
432
433 319
        if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) {
434
            $tableColumn['default'] = $match[1];
435
        }
436
437
        $options = [
438 319
            'length'        => $length,
439 319
            'notnull'       => (bool) $tableColumn['isnotnull'],
440 319
            'default'       => $tableColumn['default'],
441 319
            'precision'     => $precision,
442 319
            'scale'         => $scale,
443 319
            'fixed'         => $fixed,
444
            'unsigned'      => false,
445 319
            'autoincrement' => $autoincrement,
446 319
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
447 125
                ? $tableColumn['comment']
448
                : null,
449
        ];
450
451 319
        $column = new Column($tableColumn['field'], Type::getType($type), $options);
452
453 319
        if (isset($tableColumn['collation']) && ! empty($tableColumn['collation'])) {
454
            $column->setPlatformOption('collation', $tableColumn['collation']);
455
        }
456
457 319
        if (in_array($column->getType()->getName(), [Types::JSON_ARRAY, Types::JSON], true)) {
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Types::JSON_ARRAY has been deprecated: json_array type is deprecated, use {@see DefaultTypes::JSON} instead. ( Ignorable by Annotation )

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

457
        if (in_array($column->getType()->getName(), [/** @scrutinizer ignore-deprecated */ Types::JSON_ARRAY, Types::JSON], true)) {

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
458 201
            $column->setPlatformOption('jsonb', $jsonb);
459
        }
460
461 319
        return $column;
462
    }
463
464
    /**
465
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
466
     *
467
     * @param mixed $defaultValue
468
     *
469
     * @return mixed
470
     */
471 319
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
472
    {
473 319
        if ($defaultValue !== null && strpos($defaultValue, '(') === 0) {
474 121
            return trim($defaultValue, '()');
475
        }
476
477 319
        return $defaultValue;
478
    }
479
}
480