Completed
Pull Request — develop (#3576)
by Jonathan
64:38 queued 61:53
created

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

1117
            'json'             => /** @scrutinizer ignore-deprecated */ Type::JSON,

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...
1118
            'money'            => 'decimal',
1119
            'numeric'          => 'decimal',
1120
            'serial'           => 'integer',
1121
            'serial4'          => 'integer',
1122
            'serial8'          => 'bigint',
1123
            'real'             => 'float',
1124
            'smallint'         => 'smallint',
1125
            'text'             => 'text',
1126
            'time'             => 'time',
1127
            'timestamp'        => 'datetime',
1128
            'timestamptz'      => 'datetimetz',
1129
            'timetz'           => 'time',
1130
            'tsvector'         => 'text',
1131
            'uuid'             => 'guid',
1132
            'varchar'          => 'string',
1133
            'year'             => 'date',
1134
            '_varchar'         => 'string',
1135
        ];
1136 8934
    }
1137
1138
    /**
1139
     * {@inheritDoc}
1140
     */
1141 9090
    public function getVarcharMaxLength() : int
1142
    {
1143 9090
        return 65535;
1144
    }
1145
1146
    /**
1147
     * {@inheritdoc}
1148
     */
1149 3
    public function getBinaryMaxLength() : int
1150
    {
1151 3
        return 0;
1152
    }
1153
1154
    /**
1155
     * {@inheritdoc}
1156
     */
1157 8004
    public function getBinaryDefaultLength() : int
1158
    {
1159 8004
        return 0;
1160
    }
1161
1162
    /**
1163
     * {@inheritdoc}
1164
     */
1165 9552
    public function hasNativeJsonType() : bool
1166
    {
1167 9552
        return true;
1168
    }
1169
1170
    /**
1171
     * {@inheritDoc}
1172
     */
1173 3091
    protected function getReservedKeywordsClass() : string
1174
    {
1175 3091
        return Keywords\PostgreSQLKeywords::class;
1176
    }
1177
1178
    /**
1179
     * {@inheritDoc}
1180
     */
1181 3242
    public function getBlobTypeDeclarationSQL(array $field) : string
1182
    {
1183 3242
        return 'BYTEA';
1184
    }
1185
1186
    /**
1187
     * {@inheritdoc}
1188
     */
1189 9334
    public function getDefaultValueDeclarationSQL(array $field) : string
1190
    {
1191 9334
        if ($this->isSerialField($field)) {
1192 8956
            return '';
1193
        }
1194
1195 8233
        return parent::getDefaultValueDeclarationSQL($field);
1196
    }
1197
1198
    /**
1199
     * {@inheritdoc}
1200
     */
1201
    public function supportsColumnCollation() : bool
1202
    {
1203
        return true;
1204
    }
1205
1206
    /**
1207
     * {@inheritdoc}
1208
     */
1209 8037
    public function getColumnCollationDeclarationSQL(string $collation) : string
1210
    {
1211 8037
        return 'COLLATE ' . $this->quoteSingleIdentifier($collation);
1212
    }
1213
1214
    /**
1215
     * {@inheritdoc}
1216
     */
1217 2945
    public function getJsonTypeDeclarationSQL(array $field) : string
1218
    {
1219 2945
        return 'JSON';
1220
    }
1221
1222
    /**
1223
     * @param mixed[] $field
1224
     */
1225 9334
    private function isSerialField(array $field) : bool
1226
    {
1227 9334
        return isset($field['type'], $field['autoincrement'])
1228 9334
            && $field['autoincrement'] === true
1229 9334
            && $this->isNumericType($field['type']);
1230
    }
1231
1232
    /**
1233
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
1234
     */
1235 8173
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
1236
    {
1237 8173
        if ($columnDiff->fromColumn === null) {
1238 7165
            return $columnDiff->hasChanged('type');
1239
        }
1240
1241 7978
        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
1242 7978
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());
1243
1244
        // default should not be changed when switching between numeric types and the default comes from a sequence
1245 7978
        return $columnDiff->hasChanged('type')
1246 7978
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
1247
    }
1248
1249 8968
    private function isNumericType(Type $type) : bool
1250
    {
1251 8968
        return $type instanceof IntegerType || $type instanceof BigIntType;
1252
    }
1253
1254 8176
    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
1255
    {
1256 8176
        if ($columnDiff->fromColumn === null) {
1257 7168
            return null;
1258
        }
1259
1260 7978
        return $this->getColumnComment($columnDiff->fromColumn);
1261
    }
1262
}
1263