PostgreSQL94Platform   F
last analyzed

Complexity

Total Complexity 164

Size/Duplication

Total Lines 1080
Duplicated Lines 0 %

Test Coverage

Coverage 91.73%

Importance

Changes 0
Metric Value
eloc 362
dl 0
loc 1080
ccs 344
cts 375
cp 0.9173
rs 2
c 0
b 0
f 0
wmc 164

83 Methods

Rating   Name   Duplication   Size   Complexity  
A getListDatabasesSQL() 0 3 1
A getDefaultValueDeclarationSQL() 0 7 2
A getVarcharTypeDeclarationSQLSnippet() 0 9 2
A getDefaultSchemaName() 0 3 1
A getCloseActiveDatabaseConnectionsSQL() 0 4 1
A getDateDiffExpression() 0 3 1
A getCurrentDatabaseExpression() 0 3 1
A getListTableColumnsSQL() 0 28 1
F getAlterTableSQL() 0 142 28
A getSequenceCacheSQL() 0 7 2
A getBinaryTypeDeclarationSQLSnippet() 0 3 1
A getClobTypeDeclarationSQL() 0 3 1
A getGuidTypeDeclarationSQL() 0 3 1
A getListNamespacesSQL() 0 3 1
A isSerialField() 0 5 3
A getOldColumnComment() 0 7 2
A getCreateSequenceSQL() 0 7 1
A getBigIntTypeDeclarationSQL() 0 7 2
A supportsSequences() 0 3 1
A getTruncateTableSQL() 0 10 2
A getColumnCollationDeclarationSQL() 0 3 1
A getDropSequenceSQL() 0 7 2
A getListViewsSQL() 0 3 1
A getReservedKeywordsClass() 0 3 1
A hasNativeJsonType() 0 3 1
A getDateArithmeticIntervalExpression() 0 8 2
A getSetTransactionIsolationSQL() 0 4 1
A getIntegerTypeDeclarationSQL() 0 7 2
A getSequenceNextValSQL() 0 3 1
A getJsonTypeDeclarationSQL() 0 7 2
A getCreateViewSQL() 0 3 1
A getBlobTypeDeclarationSQL() 0 3 1
A getListTableForeignKeysSQL() 0 9 1
A supportsSchemas() 0 3 1
A doConvertBooleans() 0 11 3
B isUnchangedBinaryColumn() 0 25 7
A getLocateExpression() 0 9 2
A getAdvancedForeignKeyOptionsSQL() 0 23 6
A getBooleanTypeDeclarationSQL() 0 3 1
A getCreateDatabaseSQL() 0 3 1
A getDateTimeTzTypeDeclarationSQL() 0 3 1
A convertBooleans() 0 14 4
A supportsIdentityColumns() 0 3 1
A getTimeTypeDeclarationSQL() 0 3 1
A setUseBooleanTrueFalseStrings() 0 3 1
B convertSingleBooleanValue() 0 28 7
A getSmallIntTypeDeclarationSQL() 0 7 2
A getAlterSequenceSQL() 0 5 1
A getDateTimeTypeDeclarationSQL() 0 3 1
A supportsPartialIndexes() 0 3 1
A getRenameIndexSQL() 0 8 2
A getName() 0 3 1
A supportsColumnCollation() 0 3 1
A convertFromBoolean() 0 7 2
A getDisallowDatabaseConnectionsSQL() 0 3 1
A isNumericType() 0 3 2
A getListSequencesSQL() 0 3 1
A prefersSequences() 0 3 1
A getListTableIndexesSQL() 0 10 1
A _getCommonIntegerTypeDeclarationSQL() 0 3 1
A getListTableConstraintsSQL() 0 21 1
A supportsCommentOnStatement() 0 3 1
A getCreateSchemaSQL() 0 3 1
A getListTablesSQL() 0 3 1
A getReadLockSQL() 0 3 1
A getDropViewSQL() 0 3 1
A getEmptyIdentityInsertSQL() 0 3 1
A getDateTypeDeclarationSQL() 0 3 1
A getTableWhereClause() 0 19 2
A getVarbinaryTypeDeclarationSQLSnippet() 0 3 1
A typeChangeBreaksDefaultValue() 0 12 5
A getDateTimeTzFormatString() 0 3 1
A getRegexpExpression() 0 3 1
A hasNativeGuidType() 0 3 1
A getIdentitySequenceName() 0 3 1
A getSQLResultCasing() 0 3 1
A getDropForeignKeySQL() 0 3 1
A initializeDoctrineTypeMappings() 0 44 1
A getNowExpression() 0 3 1
A usesSequenceEmulatedIdentityColumns() 0 3 1
B _getCreateTableSQL() 0 26 8
A getListTableMetadataSQL() 0 12 2
A convertBooleansToDatabaseValue() 0 10 3

How to fix   Complexity   

Complex Class

Complex classes like PostgreSQL94Platform 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 PostgreSQL94Platform, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Platforms;
6
7
use Doctrine\DBAL\Schema\ColumnDiff;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Doctrine\DBAL\Schema\Identifier;
10
use Doctrine\DBAL\Schema\Index;
11
use Doctrine\DBAL\Schema\Sequence;
12
use Doctrine\DBAL\Schema\TableDiff;
13
use Doctrine\DBAL\Types\BigIntType;
14
use Doctrine\DBAL\Types\BinaryType;
15
use Doctrine\DBAL\Types\BlobType;
16
use Doctrine\DBAL\Types\IntegerType;
17
use Doctrine\DBAL\Types\Type;
18
use UnexpectedValueException;
19
use function array_diff;
20
use function array_merge;
21
use function array_unique;
22
use function array_values;
23
use function count;
24
use function explode;
25
use function implode;
26
use function in_array;
27
use function is_array;
28
use function is_bool;
29
use function is_numeric;
30
use function is_string;
31
use function sprintf;
32
use function strpos;
33
use function strtolower;
34
use function trim;
35
36
/**
37
 * Provides the behavior, features and SQL dialect of the PostgreSQL 9.4+ database platform.
38
 */
39
class PostgreSQL94Platform extends AbstractPlatform
40
{
41
    /** @var bool */
42
    private $useBooleanTrueFalseStrings = true;
43
44
    /** @var string[][] PostgreSQL booleans literals */
45
    private $booleanLiterals = [
46
        'true' => [
47
            't',
48
            'true',
49
            'y',
50
            'yes',
51
            'on',
52
            '1',
53
        ],
54
        'false' => [
55
            'f',
56
            'false',
57
            'n',
58
            'no',
59
            'off',
60
            '0',
61
        ],
62
    ];
63
64
    /**
65
     * PostgreSQL has different behavior with some drivers
66
     * with regard to how booleans have to be handled.
67
     *
68
     * Enables use of 'true'/'false' or otherwise 1 and 0 instead.
69
     */
70 88
    public function setUseBooleanTrueFalseStrings(bool $flag) : void
71
    {
72 88
        $this->useBooleanTrueFalseStrings = $flag;
73 88
    }
74
75
    public function getNowExpression() : string
76
    {
77
        return 'LOCALTIMESTAMP(0)';
78
    }
79
80 44
    public function getRegexpExpression() : string
81
    {
82 44
        return 'SIMILAR TO';
83
    }
84
85 5
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
86
    {
87 5
        if ($start !== null) {
88 5
            $string = $this->getSubstringExpression($string, $start);
89
90 5
            return 'CASE WHEN (POSITION(' . $substring . ' IN ' . $string . ') = 0) THEN 0 ELSE (POSITION(' . $substring . ' IN ' . $string . ') + ' . $start . ' - 1) END';
91
        }
92
93 5
        return sprintf('POSITION(%s IN %s)', $substring, $string);
94
    }
95
96 240
    protected function getDateArithmeticIntervalExpression(string $date, string $operator, string $interval, string $unit) : string
97
    {
98 240
        if ($unit === DateIntervalUnit::QUARTER) {
99 30
            $interval = $this->multiplyInterval($interval, 3);
100 30
            $unit     = DateIntervalUnit::MONTH;
101
        }
102
103 240
        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
104
    }
105
106 15
    public function getDateDiffExpression(string $date1, string $date2) : string
107
    {
108 15
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
109
    }
110
111 470
    public function getCurrentDatabaseExpression() : string
112
    {
113 470
        return 'CURRENT_DATABASE()';
114
    }
115
116 84
    public function supportsSequences() : bool
117
    {
118 84
        return true;
119
    }
120
121 25
    public function supportsSchemas() : bool
122
    {
123 25
        return true;
124
    }
125
126 5
    public function getDefaultSchemaName() : string
127
    {
128 5
        return 'public';
129
    }
130
131 59
    public function supportsIdentityColumns() : bool
132
    {
133 59
        return true;
134
    }
135
136 457
    public function supportsPartialIndexes() : bool
137
    {
138 457
        return true;
139
    }
140
141 49
    public function usesSequenceEmulatedIdentityColumns() : bool
142
    {
143 49
        return true;
144
    }
145
146 54
    public function getIdentitySequenceName(string $tableName, string $columnName) : string
147
    {
148 54
        return $tableName . '_' . $columnName . '_seq';
149
    }
150
151 1494
    public function supportsCommentOnStatement() : bool
152
    {
153 1494
        return true;
154
    }
155
156 44
    public function prefersSequences() : bool
157
    {
158 44
        return true;
159
    }
160
161 2240
    public function hasNativeGuidType() : bool
162
    {
163 2240
        return true;
164
    }
165
166 10
    public function getListDatabasesSQL() : string
167
    {
168 10
        return 'SELECT datname FROM pg_database';
169
    }
170
171 10
    public function getListNamespacesSQL() : string
172
    {
173 10
        return "SELECT schema_name AS nspname
174
                FROM   information_schema.schemata
175
                WHERE  schema_name NOT LIKE 'pg\_%'
176
                AND    schema_name != 'information_schema'";
177
    }
178
179 18
    public function getListSequencesSQL(string $database) : string
180
    {
181 18
        return "SELECT sequence_name AS relname,
182
                       sequence_schema AS schemaname
183
                FROM   information_schema.sequences
184
                WHERE  sequence_schema NOT LIKE 'pg\_%'
185
                AND    sequence_schema != 'information_schema'";
186
    }
187
188 70
    public function getListTablesSQL() : string
189
    {
190 70
        return "SELECT quote_ident(table_name) AS table_name,
191
                       table_schema AS schema_name
192
                FROM   information_schema.tables
193
                WHERE  table_schema NOT LIKE 'pg\_%'
194
                AND    table_schema != 'information_schema'
195
                AND    table_name != 'geometry_columns'
196
                AND    table_name != 'spatial_ref_sys'
197
                AND    table_type != 'VIEW'";
198
    }
199
200 5
    public function getListViewsSQL(string $database) : string
201
    {
202 5
        return 'SELECT quote_ident(table_name) AS viewname,
203
                       table_schema AS schemaname,
204
                       view_definition AS definition
205
                FROM   information_schema.views
206
                WHERE  view_definition IS NOT NULL';
207
    }
208
209 428
    public function getListTableForeignKeysSQL(string $table, ?string $database = null) : string
210
    {
211
        return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef
212
                  FROM pg_catalog.pg_constraint r
213
                  WHERE r.conrelid =
214
                  (
215
                      SELECT c.oid
216
                      FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n
217 428
                      WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
218
                  )
219
                  AND r.contype = 'f'";
220
    }
221
222 10
    public function getCreateViewSQL(string $name, string $sql) : string
223
    {
224 10
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
225
    }
226
227 10
    public function getDropViewSQL(string $name) : string
228
    {
229 10
        return 'DROP VIEW ' . $name;
230
    }
231
232 44
    public function getListTableConstraintsSQL(string $table) : string
233
    {
234 44
        $table = new Identifier($table);
235 44
        $table = $this->quoteStringLiteral($table->getName());
236
237 44
        return sprintf(
238
            <<<'SQL'
239 2
SELECT
240
    quote_ident(relname) as relname
241
FROM
242
    pg_class
243
WHERE oid IN (
244
    SELECT indexrelid
245
    FROM pg_index, pg_class
246
    WHERE pg_class.relname = %s
247
        AND pg_class.oid = pg_index.indrelid
248
        AND (indisunique = 't' OR indisprimary = 't')
249
    )
250
SQL
251
            ,
252 44
            $table
253
        );
254
    }
255
256
    /**
257
     * {@inheritDoc}
258
     *
259
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
260
     */
261 433
    public function getListTableIndexesSQL(string $table, ?string $currentDatabase = null) : string
262
    {
263
        return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary,
264
                       pg_index.indkey, pg_index.indrelid,
265
                       pg_get_expr(indpred, indrelid) AS where
266
                 FROM pg_class, pg_index
267
                 WHERE oid IN (
268
                    SELECT indexrelid
269
                    FROM pg_index si, pg_class sc, pg_namespace sn
270 433
                    WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . ' AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid
271
                 ) AND pg_index.indexrelid = oid';
272
    }
273
274 679
    private function getTableWhereClause(string $table, string $classAlias = 'c', string $namespaceAlias = 'n') : string
275
    {
276 679
        $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
277 679
        if (strpos($table, '.') !== false) {
278 157
            [$schema, $table] = explode('.', $table);
279 157
            $schema           = $this->quoteStringLiteral($schema);
280
        } else {
281 542
            $schema = 'ANY(current_schemas(false))';
282
        }
283
284 679
        $table = new Identifier($table);
285 679
        $table = $this->quoteStringLiteral($table->getName());
286
287 679
        return $whereClause . sprintf(
288 12
            '%s.relname = %s AND %s.nspname = %s',
289 679
            $classAlias,
290 679
            $table,
291 679
            $namespaceAlias,
292 679
            $schema
293
        );
294
    }
295
296 468
    public function getListTableColumnsSQL(string $table, ?string $database = null) : string
297
    {
298
        return "SELECT
299
                    a.attnum,
300
                    quote_ident(a.attname) AS field,
301
                    t.typname AS type,
302
                    format_type(a.atttypid, a.atttypmod) AS complete_type,
303
                    (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation,
304
                    (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
305
                    (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
306
                      pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
307
                    a.attnotnull AS isnotnull,
308
                    (SELECT 't'
309
                     FROM pg_index
310
                     WHERE c.oid = pg_index.indrelid
311
                        AND pg_index.indkey[0] = a.attnum
312
                        AND pg_index.indisprimary = 't'
313
                    ) AS pri,
314
                    (SELECT pg_get_expr(adbin, adrelid)
315
                     FROM pg_attrdef
316
                     WHERE c.oid = pg_attrdef.adrelid
317
                        AND pg_attrdef.adnum=a.attnum
318
                    ) AS default,
319
                    (SELECT pg_description.description
320
                        FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
321
                    ) AS comment
322
                    FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
323 468
                    WHERE " . $this->getTableWhereClause($table, 'c', 'n') . '
324
                        AND a.attnum > 0
325
                        AND a.attrelid = c.oid
326
                        AND a.atttypid = t.oid
327
                        AND n.oid = c.relnamespace
328
                    ORDER BY a.attnum';
329
    }
330
331 54
    public function getCreateDatabaseSQL(string $database) : string
332
    {
333 54
        return 'CREATE DATABASE ' . $database;
334
    }
335
336
    /**
337
     * Returns the SQL statement for disallowing new connections on the given database.
338
     *
339
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
340
     *
341
     * @param string $database The name of the database to disallow new connections for.
342
     */
343 44
    public function getDisallowDatabaseConnectionsSQL(string $database) : string
344
    {
345 44
        return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = " . $this->quoteStringLiteral($database);
346
    }
347
348
    /**
349
     * Returns the SQL statement for closing currently active connections on the given database.
350
     *
351
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
352
     *
353
     * @param string $database The name of the database to close currently active connections for.
354
     */
355 88
    public function getCloseActiveDatabaseConnectionsSQL(string $database) : string
356
    {
357
        return 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '
358 88
            . $this->quoteStringLiteral($database);
359
    }
360
361 266
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) : string
362
    {
363 266
        $query = '';
364
365 266
        if ($foreignKey->hasOption('match')) {
366 44
            $query .= ' MATCH ' . $foreignKey->getOption('match');
367
        }
368
369 266
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
370
371 266
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
372 44
            $query .= ' DEFERRABLE';
373
        } else {
374 266
            $query .= ' NOT DEFERRABLE';
375
        }
376
377 266
        if ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) {
378 44
            $query .= ' INITIALLY DEFERRED';
379
        } else {
380 266
            $query .= ' INITIALLY IMMEDIATE';
381
        }
382
383 266
        return $query;
384
    }
385
386
    /**
387
     * {@inheritDoc}
388
     */
389 906
    public function getAlterTableSQL(TableDiff $diff) : array
390
    {
391 906
        $sql         = [];
392 906
        $commentsSQL = [];
393 906
        $columnSql   = [];
394
395 906
        foreach ($diff->addedColumns as $column) {
396 181
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
397
                continue;
398
            }
399
400 181
            $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
401 181
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
402
403 181
            $comment = $this->getColumnComment($column);
404
405 181
            if ($comment === '') {
406 137
                continue;
407
            }
408
409 44
            $commentsSQL[] = $this->getCommentOnColumnSQL(
410 44
                $diff->getName($this)->getQuotedName($this),
411 44
                $column->getQuotedName($this),
412
                $comment
413
            );
414
        }
415
416 906
        foreach ($diff->removedColumns as $column) {
417 181
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
418
                continue;
419
            }
420
421 181
            $query = 'DROP ' . $column->getQuotedName($this);
422 181
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
423
        }
424
425 906
        foreach ($diff->changedColumns as $columnDiff) {
426 539
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
427
                continue;
428
            }
429
430 539
            if ($this->isUnchangedBinaryColumn($columnDiff)) {
431 44
                continue;
432
            }
433
434 495
            $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
435 495
            $column        = $columnDiff->column;
436
437 495
            if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('precision') || $columnDiff->hasChanged('scale') || $columnDiff->hasChanged('fixed')) {
438 235
                $type = $column->getType();
439
440
                // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
441 235
                $columnDefinition                  = $column->toArray();
442 235
                $columnDefinition['autoincrement'] = false;
443
444
                // here was a server version check before, but DBAL API does not support this anymore.
445 235
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this);
446 235
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
447
            }
448
449 495
            if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
450 142
                $defaultClause = $column->getDefault() === null
451 93
                    ? ' DROP DEFAULT'
452 142
                    : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
453 142
                $query         = 'ALTER ' . $oldColumnName . $defaultClause;
454 142
                $sql[]         = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
455
            }
456
457 495
            if ($columnDiff->hasChanged('notnull')) {
458 88
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
459 88
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
460
            }
461
462 495
            if ($columnDiff->hasChanged('autoincrement')) {
463 10
                if ($column->getAutoincrement()) {
464
                    // add autoincrement
465 5
                    $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);
466
467 5
                    $sql[] = 'CREATE SEQUENCE ' . $seqName;
468 5
                    $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' . $diff->getName($this)->getQuotedName($this) . '))';
469 5
                    $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
470 5
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
471
                } else {
472
                    // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
473 5
                    $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT';
474 5
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
475
                }
476
            }
477
478 495
            $newComment = $this->getColumnComment($column);
479 495
            $oldComment = $this->getOldColumnComment($columnDiff);
480
481 495
            if ($columnDiff->hasChanged('comment') || ($columnDiff->fromColumn !== null && $oldComment !== $newComment)) {
482 157
                $commentsSQL[] = $this->getCommentOnColumnSQL(
483 157
                    $diff->getName($this)->getQuotedName($this),
484 157
                    $column->getQuotedName($this),
485
                    $newComment
486
                );
487
            }
488
489 495
            if (! $columnDiff->hasChanged('length')) {
490 451
                continue;
491
            }
492
493 44
            $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $column->getType()->getSQLDeclaration($column->toArray(), $this);
494 44
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
495
        }
496
497 906
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
498 181
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
499
                continue;
500
            }
501
502 181
            $oldColumnName = new Identifier($oldColumnName);
503
504 181
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
505 181
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
506
        }
507
508 906
        $tableSql = [];
509
510 906
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
511 906
            $sql = array_merge($sql, $commentsSQL);
512
513 906
            $newName = $diff->getNewName();
514
515 906
            if ($newName !== null) {
516 88
                $sql[] = sprintf(
517 4
                    'ALTER TABLE %s RENAME TO %s',
518 88
                    $diff->getName($this)->getQuotedName($this),
519 88
                    $newName->getQuotedName($this)
520
                );
521
            }
522
523 906
            $sql = array_merge(
524 906
                $this->getPreAlterTableIndexForeignKeySQL($diff),
525 906
                $sql,
526 906
                $this->getPostAlterTableIndexForeignKeySQL($diff)
527
            );
528
        }
529
530 906
        return array_merge($sql, $tableSql, $columnSql);
531
    }
532
533
    /**
534
     * Checks whether a given column diff is a logically unchanged binary type column.
535
     *
536
     * Used to determine whether a column alteration for a binary type column can be skipped.
537
     * Doctrine's {@link \Doctrine\DBAL\Types\BinaryType} and {@link \Doctrine\DBAL\Types\BlobType}
538
     * are mapped to the same database column type on this platform as this platform
539
     * does not have a native VARBINARY/BINARY column type. Therefore the {@link \Doctrine\DBAL\Schema\Comparator}
540
     * might detect differences for binary type columns which do not have to be propagated
541
     * to database as there actually is no difference at database level.
542
     *
543
     * @param ColumnDiff $columnDiff The column diff to check against.
544
     *
545
     * @return bool True if the given column diff is an unchanged binary type column, false otherwise.
546
     */
547 539
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff) : bool
548
    {
549 539
        $columnType = $columnDiff->column->getType();
550
551 539
        if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
552 495
            return false;
553
        }
554
555 44
        $fromColumn = $columnDiff->fromColumn;
556
557 44
        if ($fromColumn !== null) {
558 44
            $fromColumnType = $fromColumn->getType();
559
560 44
            if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
561
                return false;
562
            }
563
564 44
            return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
565
        }
566
567
        if ($columnDiff->hasChanged('type')) {
568
            return false;
569
        }
570
571
        return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0;
572
    }
573
574
    /**
575
     * {@inheritdoc}
576
     */
577 230
    protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName) : array
578
    {
579 230
        if (strpos($tableName, '.') !== false) {
580 88
            [$schema]     = explode('.', $tableName);
581 88
            $oldIndexName = $schema . '.' . $oldIndexName;
582
        }
583
584 230
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
585
    }
586
587 113
    public function getCreateSequenceSQL(Sequence $sequence) : string
588
    {
589 113
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
590 113
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
591 113
            ' MINVALUE ' . $sequence->getInitialValue() .
592 113
            ' START ' . $sequence->getInitialValue() .
593 113
            $this->getSequenceCacheSQL($sequence);
594
    }
595
596
    public function getAlterSequenceSQL(Sequence $sequence) : string
597
    {
598
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
599
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
600
            $this->getSequenceCacheSQL($sequence);
601
    }
602
603
    /**
604
     * Cache definition for sequences
605
     */
606 113
    private function getSequenceCacheSQL(Sequence $sequence) : string
607
    {
608 113
        if ($sequence->getCache() > 1) {
609 44
            return ' CACHE ' . $sequence->getCache();
610
        }
611
612 69
        return '';
613
    }
614
615
    /**
616
     * {@inheritDoc}
617
     */
618 54
    public function getDropSequenceSQL($sequence) : string
619
    {
620 54
        if ($sequence instanceof Sequence) {
621
            $sequence = $sequence->getQuotedName($this);
622
        }
623
624 54
        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
625
    }
626
627 54
    public function getCreateSchemaSQL(string $schemaName) : string
628
    {
629 54
        return 'CREATE SCHEMA ' . $schemaName;
630
    }
631
632
    /**
633
     * {@inheritDoc}
634
     */
635 137
    public function getDropForeignKeySQL($foreignKey, $table) : string
636
    {
637 137
        return $this->getDropConstraintSQL($foreignKey, $table);
638
    }
639
640
    /**
641
     * {@inheritDoc}
642
     */
643 1450
    protected function _getCreateTableSQL(string $tableName, array $columns, array $options = []) : array
644
    {
645 1450
        $queryFields = $this->getColumnDeclarationListSQL($columns);
646
647 1450
        if (isset($options['primary']) && ! empty($options['primary'])) {
648 686
            $keyColumns   = array_unique(array_values($options['primary']));
649 686
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
650
        }
651
652 1450
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
653
654 1450
        $sql = [$query];
655
656 1450
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
657 232
            foreach ($options['indexes'] as $index) {
658 232
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
659
            }
660
        }
661
662 1450
        if (isset($options['foreignKeys'])) {
663 789
            foreach ((array) $options['foreignKeys'] as $definition) {
664 109
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
665
            }
666
        }
667
668 1450
        return $sql;
669
    }
670
671
    /**
672
     * Converts a single boolean value.
673
     *
674
     * First converts the value to its native PHP boolean type
675
     * and passes it to the given callback function to be reconverted
676
     * into any custom representation.
677
     *
678
     * @param mixed    $value    The value to convert.
679
     * @param callable $callback The callback function to use for converting the real boolean value.
680
     *
681
     * @return mixed
682
     *
683
     * @throws UnexpectedValueException
684
     */
685 1199
    private function convertSingleBooleanValue($value, callable $callback)
686
    {
687 1199
        if ($value === null) {
688 10
            return $callback(null);
689
        }
690
691 1189
        if (is_bool($value) || is_numeric($value)) {
692 700
            return $callback((bool) $value);
693
        }
694
695 489
        if (! is_string($value)) {
696
            return $callback(true);
697
        }
698
699
        /**
700
         * Better safe than sorry: http://php.net/in_array#106319
701
         */
702 489
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
703 225
            return $callback(false);
704
        }
705
706 264
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
707 220
            return $callback(true);
708
        }
709
710 44
        throw new UnexpectedValueException(sprintf(
711 2
            'Unrecognized boolean literal, %s given.',
712 44
            $value
713
        ));
714
    }
715
716
    /**
717
     * Converts one or multiple boolean values.
718
     *
719
     * First converts the value(s) to their native PHP boolean type
720
     * and passes them to the given callback function to be reconverted
721
     * into any custom representation.
722
     *
723
     * @param mixed    $item     The value(s) to convert.
724
     * @param callable $callback The callback function to use for converting the real boolean value(s).
725
     *
726
     * @return mixed
727
     */
728 1199
    private function doConvertBooleans($item, callable $callback)
729
    {
730 1199
        if (is_array($item)) {
731
            foreach ($item as $key => $value) {
732
                $item[$key] = $this->convertSingleBooleanValue($value, $callback);
733
            }
734
735
            return $item;
736
        }
737
738 1199
        return $this->convertSingleBooleanValue($item, $callback);
739
    }
740
741
    /**
742
     * {@inheritDoc}
743
     *
744
     * Postgres wants boolean values converted to the strings 'true'/'false'.
745
     */
746 670
    public function convertBooleans($item)
747
    {
748 670
        if (! $this->useBooleanTrueFalseStrings) {
749 88
            return parent::convertBooleans($item);
750
        }
751
752 582
        return $this->doConvertBooleans(
753 582
            $item,
754
            static function ($boolean) : string {
755 582
                if ($boolean === null) {
756
                    return 'NULL';
757
                }
758
759 582
                return $boolean === true ? 'true' : 'false';
760 582
            }
761
        );
762
    }
763
764
    /**
765
     * {@inheritDoc}
766
     */
767 661
    public function convertBooleansToDatabaseValue($item)
768
    {
769 661
        if (! $this->useBooleanTrueFalseStrings) {
770 44
            return parent::convertBooleansToDatabaseValue($item);
771
        }
772
773 617
        return $this->doConvertBooleans(
774 617
            $item,
775
            static function ($boolean) : ?int {
776 573
                return $boolean === null ? null : (int) $boolean;
777 617
            }
778
        );
779
    }
780
781
    /**
782
     * {@inheritDoc}
783
     */
784 538
    public function convertFromBoolean($item) : ?bool
785
    {
786 538
        if (in_array($item, $this->booleanLiterals['false'], true)) {
787 264
            return false;
788
        }
789
790 274
        return parent::convertFromBoolean($item);
791
    }
792
793 49
    public function getSequenceNextValSQL(string $sequenceName) : string
794
    {
795 49
        return "SELECT NEXTVAL('" . $sequenceName . "')";
796
    }
797
798 44
    public function getSetTransactionIsolationSQL(int $level) : string
799
    {
800
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
801 44
            . $this->_getTransactionIsolationLevelSQL($level);
802
    }
803
804
    /**
805
     * {@inheritDoc}
806
     */
807 129
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
808
    {
809 129
        return 'BOOLEAN';
810
    }
811
812
    /**
813
     * {@inheritDoc}
814
     */
815 1214
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
816
    {
817 1214
        if (! empty($columnDef['autoincrement'])) {
818 360
            return 'SERIAL';
819
        }
820
821 998
        return 'INT';
822
    }
823
824
    /**
825
     * {@inheritDoc}
826
     */
827 188
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
828
    {
829 188
        if (! empty($columnDef['autoincrement'])) {
830 103
            return 'BIGSERIAL';
831
        }
832
833 85
        return 'BIGINT';
834
    }
835
836
    /**
837
     * {@inheritDoc}
838
     */
839 54
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
840
    {
841 54
        if (! empty($columnDef['autoincrement'])) {
842 44
            return 'SMALLSERIAL';
843
        }
844
845 54
        return 'SMALLINT';
846
    }
847
848
    /**
849
     * {@inheritDoc}
850
     */
851 44
    public function getGuidTypeDeclarationSQL(array $column) : string
852
    {
853 44
        return 'UUID';
854
    }
855
856
    /**
857
     * {@inheritDoc}
858
     */
859 159
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
860
    {
861 159
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
862
    }
863
864
    /**
865
     * {@inheritDoc}
866
     */
867 75
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) : string
868
    {
869 75
        return 'TIMESTAMP(0) WITH TIME ZONE';
870
    }
871
872
    /**
873
     * {@inheritDoc}
874
     */
875 95
    public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string
876
    {
877 95
        return 'DATE';
878
    }
879
880
    /**
881
     * {@inheritDoc}
882
     */
883 95
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string
884
    {
885 95
        return 'TIME(0) WITHOUT TIME ZONE';
886
    }
887
888
    /**
889
     * {@inheritDoc}
890
     */
891
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : string
892
    {
893
        return '';
894
    }
895
896 967
    protected function getVarcharTypeDeclarationSQLSnippet(?int $length) : string
897
    {
898 967
        $sql = 'VARCHAR';
899
900 967
        if ($length !== null) {
901 854
            $sql .= sprintf('(%d)', $length);
902
        }
903
904 967
        return $sql;
905
    }
906
907 98
    protected function getBinaryTypeDeclarationSQLSnippet(?int $length) : string
908
    {
909 98
        return 'BYTEA';
910
    }
911
912 98
    protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length) : string
913
    {
914 98
        return 'BYTEA';
915
    }
916
917
    /**
918
     * {@inheritDoc}
919
     */
920 264
    public function getClobTypeDeclarationSQL(array $field) : string
921
    {
922 264
        return 'TEXT';
923
    }
924
925 483
    public function getName() : string
926
    {
927 483
        return 'postgresql';
928
    }
929
930
    /**
931
     * {@inheritDoc}
932
     *
933
     * PostgreSQL returns all column names in SQL result sets in lowercase.
934
     */
935
    public function getSQLResultCasing(string $column) : string
936
    {
937
        return strtolower($column);
938
    }
939
940 5
    public function getDateTimeTzFormatString() : string
941
    {
942 5
        return 'Y-m-d H:i:sO';
943
    }
944
945 5
    public function getEmptyIdentityInsertSQL(string $tableName, string $identifierColumnName) : string
946
    {
947 5
        return 'INSERT INTO ' . $tableName . ' (' . $identifierColumnName . ') VALUES (DEFAULT)';
948
    }
949
950 89
    public function getTruncateTableSQL(string $tableName, bool $cascade = false) : string
951
    {
952 89
        $tableIdentifier = new Identifier($tableName);
953 89
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
954
955 89
        if ($cascade) {
956
            $sql .= ' CASCADE';
957
        }
958
959 89
        return $sql;
960
    }
961
962
    public function getReadLockSQL() : string
963
    {
964
        return 'FOR SHARE';
965
    }
966
967 269
    protected function initializeDoctrineTypeMappings() : void
968
    {
969 269
        $this->doctrineTypeMapping = [
970
            'bigint'           => 'bigint',
971
            'bigserial'        => 'bigint',
972
            'bool'             => 'boolean',
973
            'boolean'          => 'boolean',
974
            'bpchar'           => 'string',
975
            'bytea'            => 'blob',
976
            'char'             => 'string',
977
            'date'             => 'date',
978
            'datetime'         => 'datetime',
979
            'decimal'          => 'decimal',
980
            'double'           => 'float',
981
            'double precision' => 'float',
982
            'float'            => 'float',
983
            'float4'           => 'float',
984
            'float8'           => 'float',
985
            'inet'             => 'string',
986
            'int'              => 'integer',
987
            'int2'             => 'smallint',
988
            'int4'             => 'integer',
989
            'int8'             => 'bigint',
990
            'integer'          => 'integer',
991
            'interval'         => 'string',
992
            'json'             => 'json',
993
            'jsonb'            => 'json',
994
            'money'            => 'decimal',
995
            'numeric'          => 'decimal',
996
            'serial'           => 'integer',
997
            'serial4'          => 'integer',
998
            'serial8'          => 'bigint',
999
            'real'             => 'float',
1000
            'smallint'         => 'smallint',
1001
            'text'             => 'text',
1002
            'time'             => 'time',
1003
            'timestamp'        => 'datetime',
1004
            'timestamptz'      => 'datetimetz',
1005
            'timetz'           => 'time',
1006
            'tsvector'         => 'text',
1007
            'uuid'             => 'guid',
1008
            'varchar'          => 'string',
1009
            'year'             => 'date',
1010
            '_varchar'         => 'string',
1011
        ];
1012 269
    }
1013
1014 2289
    public function hasNativeJsonType() : bool
1015
    {
1016 2289
        return true;
1017
    }
1018
1019 1146
    protected function getReservedKeywordsClass() : string
1020
    {
1021 1146
        return Keywords\PostgreSQL94Keywords::class;
1022
    }
1023
1024
    /**
1025
     * {@inheritDoc}
1026
     */
1027 40
    public function getBlobTypeDeclarationSQL(array $field) : string
1028
    {
1029 40
        return 'BYTEA';
1030
    }
1031
1032
    /**
1033
     * {@inheritdoc}
1034
     */
1035 1890
    public function getDefaultValueDeclarationSQL(array $field) : string
1036
    {
1037 1890
        if ($this->isSerialField($field)) {
1038 507
            return '';
1039
        }
1040
1041 1532
        return parent::getDefaultValueDeclarationSQL($field);
1042
    }
1043
1044
    public function supportsColumnCollation() : bool
1045
    {
1046
        return true;
1047
    }
1048
1049 44
    public function getColumnCollationDeclarationSQL(string $collation) : string
1050
    {
1051 44
        return 'COLLATE ' . $this->quoteSingleIdentifier($collation);
1052
    }
1053
1054
    /**
1055
     * {@inheritdoc}
1056
     */
1057 124
    public function getJsonTypeDeclarationSQL(array $field) : string
1058
    {
1059 124
        if (! empty($field['jsonb'])) {
1060 49
            return 'JSONB';
1061
        }
1062
1063 119
        return 'JSON';
1064
    }
1065
1066
    /**
1067
     * @param mixed[] $field
1068
     */
1069 1890
    private function isSerialField(array $field) : bool
1070
    {
1071 1890
        return isset($field['type'], $field['autoincrement'])
1072 1890
            && $field['autoincrement'] === true
1073 1890
            && $this->isNumericType($field['type']);
1074
    }
1075
1076
    /**
1077
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
1078
     */
1079 446
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
1080
    {
1081 446
        if ($columnDiff->fromColumn === null) {
1082 220
            return $columnDiff->hasChanged('type');
1083
        }
1084
1085 226
        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
1086 226
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());
1087
1088
        // default should not be changed when switching between numeric types and the default comes from a sequence
1089 226
        return $columnDiff->hasChanged('type')
1090 226
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
1091
    }
1092
1093 718
    private function isNumericType(Type $type) : bool
1094
    {
1095 718
        return $type instanceof IntegerType || $type instanceof BigIntType;
1096
    }
1097
1098 495
    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
1099
    {
1100 495
        if ($columnDiff->fromColumn === null) {
1101 264
            return null;
1102
        }
1103
1104 231
        return $this->getColumnComment($columnDiff->fromColumn);
1105
    }
1106
1107 320
    public function getListTableMetadataSQL(string $table, ?string $schema = null) : string
1108
    {
1109 320
        if ($schema !== null) {
1110
            $table = $schema . '.' . $table;
1111
        }
1112
1113 320
        return sprintf(
1114
            <<<'SQL'
1115
SELECT obj_description(%s::regclass) AS table_comment;
1116
SQL
1117
            ,
1118 320
            $this->quoteStringLiteral($table)
1119
        );
1120
    }
1121
}
1122