Completed
Pull Request — develop (#3570)
by Jonathan
155:39 queued 152:58
created

PostgreSqlPlatform::getListTableConstraintsSQL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 21
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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 6532
    public function setUseBooleanTrueFalseStrings(bool $flag) : void
73
    {
74 6532
        $this->useBooleanTrueFalseStrings = $flag;
75 6532
    }
76
77
    /**
78
     * {@inheritDoc}
79
     */
80
    public function getNowExpression() : string
81
    {
82
        return 'LOCALTIMESTAMP(0)';
83
    }
84
85
    /**
86
     * {@inheritDoc}
87
     */
88 6992
    public function getRegexpExpression() : string
89
    {
90 6992
        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 2520
    public function supportsSequences() : bool
132
    {
133 2520
        return true;
134
    }
135
136
    /**
137
     * {@inheritDoc}
138
     */
139 2502
    public function supportsSchemas() : bool
140
    {
141 2502
        return true;
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     */
147 2352
    public function getDefaultSchemaName() : string
148
    {
149 2352
        return 'public';
150
    }
151
152
    /**
153
     * {@inheritDoc}
154
     */
155 2418
    public function supportsIdentityColumns() : bool
156
    {
157 2418
        return true;
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     */
163 8248
    public function supportsPartialIndexes() : bool
164
    {
165 8248
        return true;
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171 6525
    public function usesSequenceEmulatedIdentityColumns() : bool
172
    {
173 6525
        return true;
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179 7269
    public function getIdentitySequenceName(string $tableName, string $columnName) : string
180
    {
181 7269
        return $tableName . '_' . $columnName . '_seq';
182
    }
183
184
    /**
185
     * {@inheritDoc}
186
     */
187 8359
    public function supportsCommentOnStatement() : bool
188
    {
189 8359
        return true;
190
    }
191
192
    /**
193
     * {@inheritDoc}
194
     */
195
    public function prefersSequences() : bool
196
    {
197
        return true;
198
    }
199
200
    /**
201
     * {@inheritDoc}
202
     */
203 8359
    public function hasNativeGuidType() : bool
204
    {
205 8359
        return true;
206
    }
207
208
    /**
209
     * {@inheritDoc}
210
     */
211 2526
    public function getListDatabasesSQL() : string
212
    {
213 2526
        return 'SELECT datname FROM pg_database';
214
    }
215
216
    /**
217
     * {@inheritDoc}
218
     */
219 2502
    public function getListNamespacesSQL() : string
220
    {
221 2502
        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 1680
    public function getListSequencesSQL(string $database) : string
231
    {
232 1680
        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 2424
    public function getListViewsSQL(string $database) : string
258
    {
259 2424
        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 7150
    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 7150
                      WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
278
                  )
279
                  AND r.contype = 'f'";
280
    }
281
282
    /**
283
     * {@inheritDoc}
284
     */
285 2585
    public function getCreateViewSQL(string $name, string $sql) : string
286
    {
287 2585
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
288
    }
289
290
    /**
291
     * {@inheritDoc}
292
     */
293 2585
    public function getDropViewSQL(string $name) : string
294
    {
295 2585
        return 'DROP VIEW ' . $name;
296
    }
297
298
    /**
299
     * {@inheritDoc}
300
     */
301 5980
    public function getListTableConstraintsSQL(string $table) : string
302
    {
303 5980
        $table = new Identifier($table);
304 5980
        $table = $this->quoteStringLiteral($table->getName());
305
306 5980
        return sprintf(
307
            <<<'SQL'
308
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 5980
            $table
322
        );
323
    }
324
325
    /**
326
     * {@inheritDoc}
327
     *
328
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
329
     */
330 7099
    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 7099
                    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 7150
    private function getTableWhereClause(string $table, string $classAlias = 'c', string $namespaceAlias = 'n') : string
344
    {
345 7150
        $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
346 7150
        if (strpos($table, '.') !== false) {
347 7070
            [$schema, $table] = explode('.', $table);
348 7070
            $schema           = $this->quoteStringLiteral($schema);
349
        } else {
350 7150
            $schema = "ANY(string_to_array((select replace(replace(setting,'\"\$user\"',user),' ','') from pg_catalog.pg_settings where name = 'search_path'),','))";
351
        }
352
353 7150
        $table = new Identifier($table);
354 7150
        $table = $this->quoteStringLiteral($table->getName());
355
356 7150
        return $whereClause . sprintf(
357
            '%s.relname = %s AND %s.nspname = %s',
358 7150
            $classAlias,
359 7150
            $table,
360 7150
            $namespaceAlias,
361 7150
            $schema
362
        );
363
    }
364
365
    /**
366
     * {@inheritDoc}
367
     */
368 7065
    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 7065
                    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 8376
    public function getCreateDatabaseSQL(string $database) : string
407
    {
408 8376
        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 6997
    public function getDisallowDatabaseConnectionsSQL(string $database) : string
419
    {
420 6997
        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 7728
    public function getCloseActiveDatabaseConnectionsSQL(string $database) : string
431
    {
432
        return 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '
433 7728
            . $this->quoteStringLiteral($database);
434
    }
435
436
    /**
437
     * {@inheritDoc}
438
     */
439 8163
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) : string
440
    {
441 8163
        $query = '';
442
443 8163
        if ($foreignKey->hasOption('match')) {
444 7015
            $query .= ' MATCH ' . $foreignKey->getOption('match');
445
        }
446
447 8163
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
448
449 8163
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
450 7015
            $query .= ' DEFERRABLE';
451
        } else {
452 8163
            $query .= ' NOT DEFERRABLE';
453
        }
454
455 8163
        if ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) {
456 7015
            $query .= ' INITIALLY DEFERRED';
457
        } else {
458 8163
            $query .= ' INITIALLY IMMEDIATE';
459
        }
460
461 8163
        return $query;
462
    }
463
464
    /**
465
     * {@inheritDoc}
466
     */
467 7320
    public function getAlterTableSQL(TableDiff $diff) : array
468
    {
469 7320
        $sql         = [];
470 7320
        $commentsSQL = [];
471 7320
        $columnSql   = [];
472
473 7320
        foreach ($diff->addedColumns as $column) {
474 6533
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
475
                continue;
476
            }
477
478 6533
            $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
479 6533
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
480
481 6533
            $comment = $this->getColumnComment($column);
482
483 6533
            if ($comment === null || $comment === '') {
484 6533
                continue;
485
            }
486
487 5451
            $commentsSQL[] = $this->getCommentOnColumnSQL(
488 5451
                $diff->getName($this)->getQuotedName($this),
489 5451
                $column->getQuotedName($this),
490
                $comment
491
            );
492
        }
493
494 7320
        foreach ($diff->removedColumns as $column) {
495 7094
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
496
                continue;
497
            }
498
499 7094
            $query = 'DROP ' . $column->getQuotedName($this);
500 7094
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
501
        }
502
503 7320
        foreach ($diff->changedColumns as $columnDiff) {
504
            /** @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
505 7320
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
506
                continue;
507
            }
508
509 7320
            if ($this->isUnchangedBinaryColumn($columnDiff)) {
510 6187
                continue;
511
            }
512
513 7320
            $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
514 7320
            $column        = $columnDiff->column;
515
516 7320
            if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('precision') || $columnDiff->hasChanged('scale') || $columnDiff->hasChanged('fixed')) {
517 7213
                $type = $column->getType();
518
519
                // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
520 7213
                $columnDefinition                  = $column->toArray();
521 7213
                $columnDefinition['autoincrement'] = false;
522
523
                // here was a server version check before, but DBAL API does not support this anymore.
524 7213
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this);
525 7213
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
526
            }
527
528 7320
            if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
529 6881
                $defaultClause = $column->getDefault() === null
530 6863
                    ? ' DROP DEFAULT'
531 6881
                    : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
532 6881
                $query         = 'ALTER ' . $oldColumnName . $defaultClause;
533 6881
                $sql[]         = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
534
            }
535
536 7320
            if ($columnDiff->hasChanged('notnull')) {
537 5543
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
538 5543
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
539
            }
540
541 7320
            if ($columnDiff->hasChanged('autoincrement')) {
542 2645
                if ($column->getAutoincrement()) {
543
                    // add autoincrement
544 2645
                    $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);
545
546 2645
                    $sql[] = 'CREATE SEQUENCE ' . $seqName;
547 2645
                    $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' . $diff->getName($this)->getQuotedName($this) . '))';
548 2645
                    $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
549 2645
                    $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 2639
                    $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT';
553 2639
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
554
                }
555
            }
556
557 7320
            $newComment = $this->getColumnComment($column);
558 7320
            $oldComment = $this->getOldColumnComment($columnDiff);
559
560 7320
            if ($columnDiff->hasChanged('comment') || ($columnDiff->fromColumn !== null && $oldComment !== $newComment)) {
561 6916
                $commentsSQL[] = $this->getCommentOnColumnSQL(
562 6916
                    $diff->getName($this)->getQuotedName($this),
563 6916
                    $column->getQuotedName($this),
564
                    $newComment
565
                );
566
            }
567
568 7320
            if (! $columnDiff->hasChanged('length')) {
569 7320
                continue;
570
            }
571
572 5037
            $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $column->getType()->getSQLDeclaration($column->toArray(), $this);
573 5037
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
574
        }
575
576 7320
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
577 6469
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
578
                continue;
579
            }
580
581 6469
            $oldColumnName = new Identifier($oldColumnName);
582
583 6469
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
584 6469
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
585
        }
586
587 7320
        $tableSql = [];
588
589 7320
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
590 7320
            $sql = array_merge($sql, $commentsSQL);
591
592 7320
            $newName = $diff->getNewName();
593
594 7320
            if ($newName !== null) {
595 5543
                $sql[] = sprintf(
596
                    'ALTER TABLE %s RENAME TO %s',
597 5543
                    $diff->getName($this)->getQuotedName($this),
598 5543
                    $newName->getQuotedName($this)
599
                );
600
            }
601
602 7320
            $sql = array_merge(
603 7320
                $this->getPreAlterTableIndexForeignKeySQL($diff),
604 7320
                $sql,
605 7320
                $this->getPostAlterTableIndexForeignKeySQL($diff)
606
            );
607
        }
608
609 7320
        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 7320
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff) : bool
627
    {
628 7320
        $columnType = $columnDiff->column->getType();
629
630 7320
        if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
631 7320
            return false;
632
        }
633
634 6187
        $fromColumn = $columnDiff->fromColumn;
635
636 6187
        if ($fromColumn !== null) {
637 6187
            $fromColumnType = $fromColumn->getType();
638
639 6187
            if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
640
                return false;
641
            }
642
643 6187
            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 6210
    protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName) : array
657
    {
658 6210
        if (strpos($tableName, '.') !== false) {
659 5014
            [$schema]     = explode('.', $tableName);
660 5014
            $oldIndexName = $schema . '.' . $oldIndexName;
661
        }
662
663 6210
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
664
    }
665
666
    /**
667
     * {@inheritdoc}
668
     */
669 6950
    public function getCommentOnColumnSQL(string $tableName, string $columnName, ?string $comment) : string
670
    {
671 6950
        $tableName  = new Identifier($tableName);
672 6950
        $columnName = new Identifier($columnName);
673 6950
        $comment    = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);
674
675 6950
        return sprintf(
676
            'COMMENT ON COLUMN %s.%s IS %s',
677 6950
            $tableName->getQuotedName($this),
678 6950
            $columnName->getQuotedName($this),
679 6950
            $comment
680
        );
681
    }
682
683
    /**
684
     * {@inheritDoc}
685
     */
686 7484
    public function getCreateSequenceSQL(Sequence $sequence) : string
687
    {
688 7484
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
689 7484
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
690 7484
            ' MINVALUE ' . $sequence->getInitialValue() .
691 7484
            ' START ' . $sequence->getInitialValue() .
692 7484
            $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 7484
    private function getSequenceCacheSQL(Sequence $sequence) : string
709
    {
710 7484
        if ($sequence->getCache() > 1) {
711 6233
            return ' CACHE ' . $sequence->getCache();
712
        }
713
714 7484
        return '';
715
    }
716
717
    /**
718
     * {@inheritDoc}
719
     */
720 7484
    public function getDropSequenceSQL($sequence) : string
721
    {
722 7484
        if ($sequence instanceof Sequence) {
723
            $sequence = $sequence->getQuotedName($this);
724
        }
725
726 7484
        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
727
    }
728
729
    /**
730
     * {@inheritDoc}
731
     */
732 7194
    public function getCreateSchemaSQL(string $schemaName) : string
733
    {
734 7194
        return 'CREATE SCHEMA ' . $schemaName;
735
    }
736
737
    /**
738
     * {@inheritDoc}
739
     */
740 7064
    public function getDropForeignKeySQL($foreignKey, $table) : string
741
    {
742 7064
        return $this->getDropConstraintSQL($foreignKey, $table);
743
    }
744
745
    /**
746
     * {@inheritDoc}
747
     */
748 8359
    protected function _getCreateTableSQL(string $tableName, array $columns, array $options = []) : array
749
    {
750 8359
        $queryFields = $this->getColumnDeclarationListSQL($columns);
751
752 8359
        if (isset($options['primary']) && ! empty($options['primary'])) {
753 7441
            $keyColumns   = array_unique(array_values($options['primary']));
754 7441
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
755
        }
756
757 8359
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
758
759 8359
        $sql = [$query];
760
761 8359
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
762 7160
            foreach ($options['indexes'] as $index) {
763 7160
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
764
            }
765
        }
766
767 8359
        if (isset($options['foreignKeys'])) {
768 7118
            foreach ((array) $options['foreignKeys'] as $definition) {
769 6854
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
770
            }
771
        }
772
773 8359
        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 7637
    private function convertSingleBooleanValue($value, callable $callback)
791
    {
792 7637
        if ($value === null) {
793 6922
            return $callback(null);
794
        }
795
796 7637
        if (is_bool($value) || is_numeric($value)) {
797 7637
            return $callback((bool) $value);
798
        }
799
800 7019
        if (! is_string($value)) {
801
            return $callback(true);
802
        }
803
804
        /**
805
         * Better safe than sorry: http://php.net/in_array#106319
806
         */
807 7019
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
808 6968
            return $callback(false);
809
        }
810
811 6670
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
812 6670
            return $callback(true);
813
        }
814
815 6371
        throw new UnexpectedValueException(sprintf(
816
            'Unrecognized boolean literal, %s given.',
817 6371
            $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 7637
    private function doConvertBooleans($item, callable $callback)
834
    {
835 7637
        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 7637
        return $this->convertSingleBooleanValue($item, $callback);
844
    }
845
846
    /**
847
     * {@inheritDoc}
848
     *
849
     * Postgres wants boolean values converted to the strings 'true'/'false'.
850
     */
851 7637
    public function convertBooleans($item)
852
    {
853 7637
        if (! $this->useBooleanTrueFalseStrings) {
854 6532
            return parent::convertBooleans($item);
855
        }
856
857 7637
        return $this->doConvertBooleans(
858 7637
            $item,
859
            static function ($boolean) : string {
860 7637
                if ($boolean === null) {
861 6555
                    return 'NULL';
862
                }
863
864 7637
                return $boolean === true ? 'true' : 'false';
865 7637
            }
866
        );
867
    }
868
869
    /**
870
     * {@inheritDoc}
871
     */
872 6900
    public function convertBooleansToDatabaseValue($item)
873
    {
874 6900
        if (! $this->useBooleanTrueFalseStrings) {
875 6463
            return parent::convertBooleansToDatabaseValue($item);
876
        }
877
878 6900
        return $this->doConvertBooleans(
879 6900
            $item,
880
            static function ($boolean) : ?int {
881 6900
                return $boolean === null ? null : (int) $boolean;
882 6900
            }
883
        );
884
    }
885
886
    /**
887
     * {@inheritDoc}
888
     */
889 6819
    public function convertFromBoolean($item) : ?bool
890
    {
891 6819
        if (in_array($item, $this->booleanLiterals['false'], true)) {
892 6417
            return false;
893
        }
894
895 6819
        return parent::convertFromBoolean($item);
896
    }
897
898
    /**
899
     * {@inheritDoc}
900
     */
901 6872
    public function getSequenceNextValSQL(string $sequenceName) : string
902
    {
903 6872
        return "SELECT NEXTVAL('" . $sequenceName . "')";
904
    }
905
906
    /**
907
     * {@inheritDoc}
908
     */
909 6969
    public function getSetTransactionIsolationSQL(int $level) : string
910
    {
911
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
912 6969
            . $this->_getTransactionIsolationLevelSQL($level);
913
    }
914
915
    /**
916
     * {@inheritDoc}
917
     */
918 6787
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
919
    {
920 6787
        return 'BOOLEAN';
921
    }
922
923
    /**
924
     * {@inheritDoc}
925
     */
926 8359
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
927
    {
928 8359
        if (! empty($columnDef['autoincrement'])) {
929 8029
            return 'SERIAL';
930
        }
931
932 8240
        return 'INT';
933
    }
934
935
    /**
936
     * {@inheritDoc}
937
     */
938 7651
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
939
    {
940 7651
        if (! empty($columnDef['autoincrement'])) {
941 7639
            return 'BIGSERIAL';
942
        }
943
944 2568
        return 'BIGINT';
945
    }
946
947
    /**
948
     * {@inheritDoc}
949
     */
950 7787
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
951
    {
952 7787
        if (! empty($columnDef['autoincrement'])) {
953 7061
            return 'SMALLSERIAL';
954
        }
955
956 7787
        return 'SMALLINT';
957
    }
958
959
    /**
960
     * {@inheritDoc}
961
     */
962 6141
    public function getGuidTypeDeclarationSQL(array $field) : string
963
    {
964 6141
        return 'UUID';
965
    }
966
967
    /**
968
     * {@inheritDoc}
969
     */
970 7699
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
971
    {
972 7699
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
973
    }
974
975
    /**
976
     * {@inheritDoc}
977
     */
978 2059
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) : string
979
    {
980 2059
        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 8175
    protected function getVarcharTypeDeclarationSQLSnippet(int $length, bool $fixed) : string
1011
    {
1012 8175
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
1013 8175
            : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
1014
    }
1015
1016
    /**
1017
     * {@inheritdoc}
1018
     */
1019 7187
    protected function getBinaryTypeDeclarationSQLSnippet(int $length, bool $fixed) : string
1020
    {
1021 7187
        return 'BYTEA';
1022
    }
1023
1024
    /**
1025
     * {@inheritDoc}
1026
     */
1027 7254
    public function getClobTypeDeclarationSQL(array $field) : string
1028
    {
1029 7254
        return 'TEXT';
1030
    }
1031
1032
    /**
1033
     * {@inheritDoc}
1034
     */
1035 7524
    public function getName() : string
1036
    {
1037 7524
        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 1992
    public function getDateTimeTzFormatString() : string
1054
    {
1055 1992
        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 6819
    public function getTruncateTableSQL(string $tableName, bool $cascade = false) : string
1070
    {
1071 6819
        $tableIdentifier = new Identifier($tableName);
1072 6819
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
1073
1074 6819
        if ($cascade) {
1075
            $sql .= ' CASCADE';
1076
        }
1077
1078 6819
        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 7983
    protected function initializeDoctrineTypeMappings() : void
1093
    {
1094 7983
        $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 7983
    }
1137
1138
    /**
1139
     * {@inheritDoc}
1140
     */
1141 8175
    public function getVarcharMaxLength() : int
1142
    {
1143 8175
        return 65535;
1144
    }
1145
1146
    /**
1147
     * {@inheritdoc}
1148
     */
1149
    public function getBinaryMaxLength() : int
1150
    {
1151
        return 0;
1152
    }
1153
1154
    /**
1155
     * {@inheritdoc}
1156
     */
1157 7187
    public function getBinaryDefaultLength() : int
1158
    {
1159 7187
        return 0;
1160
    }
1161
1162
    /**
1163
     * {@inheritdoc}
1164
     */
1165 8478
    public function hasNativeJsonType() : bool
1166
    {
1167 8478
        return true;
1168
    }
1169
1170
    /**
1171
     * {@inheritDoc}
1172
     */
1173 2740
    protected function getReservedKeywordsClass() : string
1174
    {
1175 2740
        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 8359
    public function getDefaultValueDeclarationSQL(array $field) : string
1190
    {
1191 8359
        if ($this->isSerialField($field)) {
1192 8029
            return '';
1193
        }
1194
1195 7441
        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 7107
    public function getColumnCollationDeclarationSQL(string $collation) : string
1210
    {
1211 7107
        return 'COLLATE ' . $this->quoteSingleIdentifier($collation);
1212
    }
1213
1214
    /**
1215
     * {@inheritdoc}
1216
     */
1217 2631
    public function getJsonTypeDeclarationSQL(array $field) : string
1218
    {
1219 2631
        return 'JSON';
1220
    }
1221
1222
    /**
1223
     * @param mixed[] $field
1224
     */
1225 8359
    private function isSerialField(array $field) : bool
1226
    {
1227 8359
        return isset($field['type'], $field['autoincrement'])
1228 8359
            && $field['autoincrement'] === true
1229 8359
            && $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 7320
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
1236
    {
1237 7320
        if ($columnDiff->fromColumn === null) {
1238 6325
            return $columnDiff->hasChanged('type');
1239
        }
1240
1241 7167
        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
1242 7167
        $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 7167
        return $columnDiff->hasChanged('type')
1246 7167
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
1247
    }
1248
1249 8029
    private function isNumericType(Type $type) : bool
1250
    {
1251 8029
        return $type instanceof IntegerType || $type instanceof BigIntType;
1252
    }
1253
1254 7320
    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
1255
    {
1256 7320
        if ($columnDiff->fromColumn === null) {
1257 6325
            return null;
1258
        }
1259
1260 7167
        return $this->getColumnComment($columnDiff->fromColumn);
1261
    }
1262
}
1263