Completed
Pull Request — 2.10.x (#3887)
by
unknown
13:09
created

PostgreSqlPlatform::getSequenceNextValSQL()   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 1
crap 1
1
<?php
2
3
namespace Doctrine\DBAL\Platforms;
4
5
use Doctrine\DBAL\Schema\Column;
6
use Doctrine\DBAL\Schema\ColumnDiff;
7
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
8
use Doctrine\DBAL\Schema\Identifier;
9
use Doctrine\DBAL\Schema\Index;
10
use Doctrine\DBAL\Schema\Sequence;
11
use Doctrine\DBAL\Schema\TableDiff;
12
use Doctrine\DBAL\Types\BigIntType;
13
use Doctrine\DBAL\Types\BinaryType;
14
use Doctrine\DBAL\Types\BlobType;
15
use Doctrine\DBAL\Types\IntegerType;
16
use Doctrine\DBAL\Types\Type;
17
use UnexpectedValueException;
18
use function array_diff;
19
use function array_merge;
20
use function array_unique;
21
use function array_values;
22
use function count;
23
use function explode;
24
use function func_get_args;
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
 * PostgreSqlPlatform.
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
     * @param bool $flag
73
     */
74 10922
    public function setUseBooleanTrueFalseStrings($flag)
75
    {
76 10922
        $this->useBooleanTrueFalseStrings = (bool) $flag;
77 10922
    }
78
79
    /**
80
     * {@inheritDoc}
81
     */
82 12759
    public function getSubstringExpression($value, $from, $length = null)
83
    {
84 12759
        if ($length === null) {
85 12759
            return 'SUBSTRING(' . $value . ' FROM ' . $from . ')';
86
        }
87
88 11372
        return 'SUBSTRING(' . $value . ' FROM ' . $from . ' FOR ' . $length . ')';
89
    }
90
91
    /**
92
     * {@inheritDoc}
93
     */
94
    public function getNowExpression()
95
    {
96
        return 'LOCALTIMESTAMP(0)';
97
    }
98
99
    /**
100
     * {@inheritDoc}
101
     */
102 11372
    public function getRegexpExpression()
103
    {
104 11372
        return 'SIMILAR TO';
105
    }
106
107
    /**
108
     * {@inheritDoc}
109
     */
110 4845
    public function getLocateExpression($str, $substr, $startPos = false)
111
    {
112 4845
        if ($startPos !== false) {
113 4845
            $str = $this->getSubstringExpression($str, $startPos);
114
115 4845
            return 'CASE WHEN (POSITION(' . $substr . ' IN ' . $str . ') = 0) THEN 0 ELSE (POSITION(' . $substr . ' IN ' . $str . ') + ' . ($startPos-1) . ') END';
116
        }
117
118 4845
        return 'POSITION(' . $substr . ' IN ' . $str . ')';
119
    }
120
121
    /**
122
     * {@inheritDoc}
123
     */
124 11372
    public function getConcatExpression()
125
    {
126 11372
        return sprintf('CONCAT(%s)', implode(', ', func_get_args()));
127
    }
128
129
    /**
130
     * {@inheritdoc}
131
     */
132 4852
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
133
    {
134 4852
        if ($unit === DateIntervalUnit::QUARTER) {
135 4852
            $interval *= 3;
136 4852
            $unit      = DateIntervalUnit::MONTH;
137
        }
138
139 4852
        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
140
    }
141
142
    /**
143
     * {@inheritDoc}
144
     */
145 4656
    public function getDateDiffExpression($date1, $date2)
146
    {
147 4656
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
148
    }
149
150
    /**
151
     * {@inheritDoc}
152
     */
153 4291
    public function supportsSequences()
154
    {
155 4291
        return true;
156
    }
157
158
    /**
159
     * {@inheritDoc}
160
     */
161 4260
    public function supportsSchemas()
162
    {
163 4260
        return true;
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169 4085
    public function getDefaultSchemaName()
170
    {
171 4085
        return 'public';
172
    }
173
174
    /**
175
     * {@inheritDoc}
176
     */
177 4172
    public function supportsIdentityColumns()
178
    {
179 4172
        return true;
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185 11900
    public function supportsPartialIndexes()
186
    {
187 11900
        return true;
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193 10918
    public function usesSequenceEmulatedIdentityColumns()
194
    {
195 10918
        return true;
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 11840
    public function getIdentitySequenceName($tableName, $columnName)
202
    {
203 11840
        return $tableName . '_' . $columnName . '_seq';
204
    }
205
206
    /**
207
     * {@inheritDoc}
208
     */
209 12932
    public function supportsCommentOnStatement()
210
    {
211 12932
        return true;
212
    }
213
214
    /**
215
     * {@inheritDoc}
216
     */
217 10
    public function prefersSequences()
218
    {
219 10
        return true;
220
    }
221
222
    /**
223
     * {@inheritDoc}
224
     */
225 13282
    public function hasNativeGuidType()
226
    {
227 13282
        return true;
228
    }
229
230
    /**
231
     * {@inheritDoc}
232
     */
233 4288
    public function getListDatabasesSQL()
234
    {
235 4288
        return 'SELECT datname FROM pg_database';
236
    }
237
238
    /**
239
     * {@inheritDoc}
240
     */
241 4260
    public function getListNamespacesSQL()
242
    {
243 4260
        return "SELECT schema_name AS nspname
244
                FROM   information_schema.schemata
245
                WHERE  schema_name NOT LIKE 'pg\_%'
246
                AND    schema_name != 'information_schema'";
247
    }
248
249
    /**
250
     * {@inheritDoc}
251
     */
252 3057
    public function getListSequencesSQL($database)
253
    {
254 3057
        return "SELECT sequence_name AS relname,
255
                       sequence_schema AS schemaname
256
                FROM   information_schema.sequences
257
                WHERE  sequence_schema NOT LIKE 'pg\_%'
258
                AND    sequence_schema != 'information_schema'";
259
    }
260
261
    /**
262
     * {@inheritDoc}
263
     */
264 4712
    public function getListTablesSQL()
265
    {
266 4712
        return "SELECT quote_ident(table_name) AS table_name,
267
                       table_schema AS schema_name
268
                FROM   information_schema.tables
269
                WHERE  table_schema NOT LIKE 'pg\_%'
270
                AND    table_schema != 'information_schema'
271
                AND    table_name != 'geometry_columns'
272
                AND    table_name != 'spatial_ref_sys'
273
                AND    table_type != 'VIEW'";
274
    }
275
276
    /**
277
     * {@inheritDoc}
278
     */
279 4169
    public function getListViewsSQL($database)
280
    {
281 4169
        return 'SELECT quote_ident(table_name) AS viewname,
282
                       table_schema AS schemaname,
283
                       view_definition AS definition
284
                FROM   information_schema.views
285
                WHERE  view_definition IS NOT NULL';
286
    }
287
288
    /**
289
     * {@inheritDoc}
290
     */
291 11754
    public function getListTableForeignKeysSQL($table, $database = null)
0 ignored issues
show
Unused Code introduced by
The parameter $database is not used and could be removed. ( Ignorable by Annotation )

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

291
    public function getListTableForeignKeysSQL($table, /** @scrutinizer ignore-unused */ $database = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
292
    {
293
        return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef
294
                  FROM pg_catalog.pg_constraint r
295
                  WHERE r.conrelid =
296
                  (
297
                      SELECT c.oid
298
                      FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n
299 11754
                      WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
300
                  )
301
                  AND r.contype = 'f'";
302
    }
303
304
    /**
305
     * {@inheritDoc}
306
     */
307 4361
    public function getCreateViewSQL($name, $sql)
308
    {
309 4361
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
310
    }
311
312
    /**
313
     * {@inheritDoc}
314
     */
315 4361
    public function getDropViewSQL($name)
316
    {
317 4361
        return 'DROP VIEW ' . $name;
318
    }
319
320
    /**
321
     * {@inheritDoc}
322
     */
323 10360
    public function getListTableConstraintsSQL($table)
324
    {
325 10360
        $table = new Identifier($table);
326 10360
        $table = $this->quoteStringLiteral($table->getName());
327
328 10360
        return sprintf(
329
            <<<'SQL'
330 10
SELECT
331
    quote_ident(relname) as relname
332
FROM
333
    pg_class
334
WHERE oid IN (
335
    SELECT indexrelid
336
    FROM pg_index, pg_class
337
    WHERE pg_class.relname = %s
338
        AND pg_class.oid = pg_index.indrelid
339
        AND (indisunique = 't' OR indisprimary = 't')
340
    )
341
SQL
342
            ,
343 10360
            $table
344
        );
345
    }
346
347
    /**
348
     * {@inheritDoc}
349
     *
350
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
351
     */
352 11706
    public function getListTableIndexesSQL($table, $currentDatabase = null)
353
    {
354
        return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary,
355
                       pg_index.indkey, pg_index.indrelid,
356
                       pg_get_expr(indpred, indrelid) AS where
357
                 FROM pg_class, pg_index
358
                 WHERE oid IN (
359
                    SELECT indexrelid
360
                    FROM pg_index si, pg_class sc, pg_namespace sn
361 11706
                    WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . ' AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid
362
                 ) AND pg_index.indexrelid = oid';
363
    }
364
365
    /**
366
     * @param string $table
367
     * @param string $classAlias
368
     * @param string $namespaceAlias
369
     *
370
     * @return string
371
     */
372 11794
    private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n')
373
    {
374 11794
        $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
375 11794
        if (strpos($table, '.') !== false) {
376 11670
            [$schema, $table] = explode('.', $table);
377 11670
            $schema           = $this->quoteStringLiteral($schema);
378
        } else {
379 11764
            $schema = 'ANY(current_schemas(false))';
380
        }
381
382 11794
        $table = new Identifier($table);
383 11794
        $table = $this->quoteStringLiteral($table->getName());
384
385 11794
        return $whereClause . sprintf(
386 60
            '%s.relname = %s AND %s.nspname = %s',
387 11794
            $classAlias,
388 11794
            $table,
389 11794
            $namespaceAlias,
390 11794
            $schema
391
        );
392
    }
393
394
    /**
395
     * {@inheritDoc}
396
     */
397 11674
    public function getListTableColumnsSQL($table, $database = null)
398
    {
399
        return "SELECT
400
                    a.attnum,
401
                    quote_ident(a.attname) AS field,
402
                    t.typname AS type,
403
                    format_type(a.atttypid, a.atttypmod) AS complete_type,
404
                    (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
405
                    (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
406
                      pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
407
                    a.attnotnull AS isnotnull,
408
                    (SELECT 't'
409
                     FROM pg_index
410
                     WHERE c.oid = pg_index.indrelid
411
                        AND pg_index.indkey[0] = a.attnum
412
                        AND pg_index.indisprimary = 't'
413
                    ) AS pri,
414
                    (SELECT pg_get_expr(adbin, adrelid)
415
                     FROM pg_attrdef
416
                     WHERE c.oid = pg_attrdef.adrelid
417
                        AND pg_attrdef.adnum=a.attnum
418
                    ) AS default,
419
                    (SELECT pg_description.description
420
                        FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
421
                    ) AS comment
422
                    FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
423 11674
                    WHERE " . $this->getTableWhereClause($table, 'c', 'n') . '
424
                        AND a.attnum > 0
425
                        AND a.attrelid = c.oid
426
                        AND a.atttypid = t.oid
427
                        AND n.oid = c.relnamespace
428
                    ORDER BY a.attnum';
429
    }
430
431
    /**
432
     * {@inheritDoc}
433
     */
434 12804
    public function getCreateDatabaseSQL($name)
435
    {
436 12804
        return 'CREATE DATABASE ' . $name;
437
    }
438
439
    /**
440
     * Returns the SQL statement for disallowing new connections on the given database.
441
     *
442
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
443
     *
444
     * @param string $database The name of the database to disallow new connections for.
445
     *
446
     * @return string
447
     */
448 11546
    public function getDisallowDatabaseConnectionsSQL($database)
449
    {
450 11546
        return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = " . $this->quoteStringLiteral($database);
451
    }
452
453
    /**
454
     * Returns the SQL statement for closing currently active connections on the given database.
455
     *
456
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
457
     *
458
     * @param string $database The name of the database to close currently active connections for.
459
     *
460
     * @return string
461
     */
462 7989
    public function getCloseActiveDatabaseConnectionsSQL($database)
463
    {
464
        return 'SELECT pg_terminate_backend(procpid) FROM pg_stat_activity WHERE datname = '
465 7989
            . $this->quoteStringLiteral($database);
466
    }
467
468
    /**
469
     * {@inheritDoc}
470
     */
471 12794
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
472
    {
473 12794
        $query = '';
474
475 12794
        if ($foreignKey->hasOption('match')) {
476 11395
            $query .= ' MATCH ' . $foreignKey->getOption('match');
477
        }
478
479 12794
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
480
481 12794
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
482 11395
            $query .= ' DEFERRABLE';
483
        } else {
484 12794
            $query .= ' NOT DEFERRABLE';
485
        }
486
487 12794
        if (($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false)
488 12794
            || ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false)
489
        ) {
490 11395
            $query .= ' INITIALLY DEFERRED';
491
        } else {
492 12794
            $query .= ' INITIALLY IMMEDIATE';
493
        }
494
495 12794
        return $query;
496
    }
497
498
    /**
499
     * {@inheritDoc}
500
     */
501 12068
    public function getAlterTableSQL(TableDiff $diff)
502
    {
503 12068
        $sql         = [];
504 12068
        $commentsSQL = [];
505 12068
        $columnSql   = [];
506
507 12068
        foreach ($diff->addedColumns as $column) {
508 11135
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
509
                continue;
510
            }
511
512 11135
            $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
513 11135
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
514
515 11135
            $comment = $this->getColumnComment($column);
516
517 11135
            if ($comment === null || $comment === '') {
518 11125
                continue;
519
            }
520
521 9854
            $commentsSQL[] = $this->getCommentOnColumnSQL(
522 9854
                $diff->getName($this)->getQuotedName($this),
523 9854
                $column->getQuotedName($this),
524
                $comment
525
            );
526
        }
527
528 12068
        foreach ($diff->removedColumns as $column) {
529 11647
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
530
                continue;
531
            }
532
533 11647
            $query = 'DROP ' . $column->getQuotedName($this);
534 11647
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
535
        }
536
537 12068
        foreach ($diff->changedColumns as $columnDiff) {
538
            /** @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
539 11988
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
540
                continue;
541
            }
542
543 11988
            if ($this->isUnchangedBinaryColumn($columnDiff)) {
544 10567
                continue;
545
            }
546
547 11978
            $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
548 11978
            $column        = $columnDiff->column;
549
550 11978
            if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('precision') || $columnDiff->hasChanged('scale') || $columnDiff->hasChanged('fixed')) {
551 11792
                $type = $column->getType();
552
553
                // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
554 11792
                $columnDefinition                  = $column->toArray();
555 11792
                $columnDefinition['autoincrement'] = false;
556
557
                // here was a server version check before, but DBAL API does not support this anymore.
558 11792
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this);
559 11792
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
560
            }
561
562 11978
            if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
563 11423
                $defaultClause = $column->getDefault() === null
564 11392
                    ? ' DROP DEFAULT'
565 11423
                    : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
566 11423
                $query         = 'ALTER ' . $oldColumnName . $defaultClause;
567 11423
                $sql[]         = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
568
            }
569
570 11978
            if ($columnDiff->hasChanged('notnull')) {
571 9956
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
572 9956
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
573
            }
574
575 11978
            if ($columnDiff->hasChanged('autoincrement')) {
576 4438
                if ($column->getAutoincrement()) {
577
                    // add autoincrement
578 4438
                    $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);
579
580 4438
                    $sql[] = 'CREATE SEQUENCE ' . $seqName;
581 4438
                    $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' . $diff->getName($this)->getQuotedName($this) . '))';
582 4438
                    $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
583 4438
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
584
                } else {
585
                    // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
586 4431
                    $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT';
587 4431
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
588
                }
589
            }
590
591 11978
            $newComment = $this->getColumnComment($column);
592 11978
            $oldComment = $this->getOldColumnComment($columnDiff);
593
594 11978
            if ($columnDiff->hasChanged('comment') || ($columnDiff->fromColumn !== null && $oldComment !== $newComment)) {
595 11460
                $commentsSQL[] = $this->getCommentOnColumnSQL(
596 11460
                    $diff->getName($this)->getQuotedName($this),
597 11460
                    $column->getQuotedName($this),
598
                    $newComment
599
                );
600
            }
601
602 11978
            if (! $columnDiff->hasChanged('length')) {
603 11968
                continue;
604
            }
605
606 9417
            $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $column->getType()->getSQLDeclaration($column->toArray(), $this);
607 9417
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
608
        }
609
610 12068
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
611 11068
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
612
                continue;
613
            }
614
615 11068
            $oldColumnName = new Identifier($oldColumnName);
616
617 11068
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
618 11068
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
619
        }
620
621 12068
        $tableSql = [];
622
623 12068
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
624 12068
            $sql = array_merge($sql, $commentsSQL);
625
626 12068
            $newName = $diff->getNewName();
627
628 12068
            if ($newName !== false) {
629 9956
                $sql[] = sprintf(
630 20
                    'ALTER TABLE %s RENAME TO %s',
631 9956
                    $diff->getName($this)->getQuotedName($this),
632 9956
                    $newName->getQuotedName($this)
633
                );
634
            }
635
636 12068
            $sql = array_merge(
637 12068
                $this->getPreAlterTableIndexForeignKeySQL($diff),
638 12068
                $sql,
639 12068
                $this->getPostAlterTableIndexForeignKeySQL($diff)
640
            );
641
        }
642
643 12068
        return array_merge($sql, $tableSql, $columnSql);
644
    }
645
646
    /**
647
     * Checks whether a given column diff is a logically unchanged binary type column.
648
     *
649
     * Used to determine whether a column alteration for a binary type column can be skipped.
650
     * Doctrine's {@link \Doctrine\DBAL\Types\BinaryType} and {@link \Doctrine\DBAL\Types\BlobType}
651
     * are mapped to the same database column type on this platform as this platform
652
     * does not have a native VARBINARY/BINARY column type. Therefore the {@link \Doctrine\DBAL\Schema\Comparator}
653
     * might detect differences for binary type columns which do not have to be propagated
654
     * to database as there actually is no difference at database level.
655
     *
656
     * @param ColumnDiff $columnDiff The column diff to check against.
657
     *
658
     * @return bool True if the given column diff is an unchanged binary type column, false otherwise.
659
     */
660 11988
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff)
661
    {
662 11988
        $columnType = $columnDiff->column->getType();
663
664 11988
        if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
665 11978
            return false;
666
        }
667
668 10567
        $fromColumn = $columnDiff->fromColumn instanceof Column ? $columnDiff->fromColumn : null;
669
670 10567
        if ($fromColumn) {
671 10567
            $fromColumnType = $fromColumn->getType();
672
673 10567
            if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
674
                return false;
675
            }
676
677 10567
            return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
678
        }
679
680
        if ($columnDiff->hasChanged('type')) {
681
            return false;
682
        }
683
684
        return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0;
685
    }
686
687
    /**
688
     * {@inheritdoc}
689
     */
690 10825
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
691
    {
692 10825
        if (strpos($tableName, '.') !== false) {
693 9404
            [$schema]     = explode('.', $tableName);
694 9404
            $oldIndexName = $schema . '.' . $oldIndexName;
695
        }
696
697 10825
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
698
    }
699
700
    /**
701
     * {@inheritdoc}
702
     */
703 11702
    public function getCommentOnColumnSQL($tableName, $columnName, $comment)
704
    {
705 11702
        $tableName  = new Identifier($tableName);
706 11702
        $columnName = new Identifier($columnName);
707 11702
        $comment    = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);
708
709 11702
        return sprintf(
710 90
            'COMMENT ON COLUMN %s.%s IS %s',
711 11702
            $tableName->getQuotedName($this),
712 11702
            $columnName->getQuotedName($this),
713 11702
            $comment
714
        );
715
    }
716
717
    /**
718
     * {@inheritDoc}
719
     */
720 12013
    public function getCreateSequenceSQL(Sequence $sequence)
721
    {
722 12013
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
723 12013
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
724 12013
            ' MINVALUE ' . $sequence->getInitialValue() .
725 12013
            ' START ' . $sequence->getInitialValue() .
726 12013
            $this->getSequenceCacheSQL($sequence);
727
    }
728
729
    /**
730
     * {@inheritDoc}
731
     */
732
    public function getAlterSequenceSQL(Sequence $sequence)
733
    {
734
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
735
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
736
            $this->getSequenceCacheSQL($sequence);
737
    }
738
739
    /**
740
     * Cache definition for sequences
741
     *
742
     * @return string
743
     */
744 12013
    private function getSequenceCacheSQL(Sequence $sequence)
745
    {
746 12013
        if ($sequence->getCache() > 1) {
747 10613
            return ' CACHE ' . $sequence->getCache();
748
        }
749
750 12003
        return '';
751
    }
752
753
    /**
754
     * {@inheritDoc}
755
     */
756 12003
    public function getDropSequenceSQL($sequence)
757
    {
758 12003
        if ($sequence instanceof Sequence) {
759
            $sequence = $sequence->getQuotedName($this);
760
        }
761
762 12003
        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
763
    }
764
765
    /**
766
     * {@inheritDoc}
767
     */
768 11726
    public function getCreateSchemaSQL($schemaName)
769
    {
770 11726
        return 'CREATE SCHEMA ' . $schemaName;
771
    }
772
773
    /**
774
     * {@inheritDoc}
775
     */
776 11602
    public function getDropForeignKeySQL($foreignKey, $table)
777
    {
778 11602
        return $this->getDropConstraintSQL($foreignKey, $table);
779
    }
780
781
    /**
782
     * {@inheritDoc}
783
     */
784 12922
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
785
    {
786 12922
        $queryFields = $this->getColumnDeclarationListSQL($columns);
787
788 12922
        if (isset($options['primary']) && ! empty($options['primary'])) {
789 11970
            $keyColumns   = array_unique(array_values($options['primary']));
790 11970
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
791
        }
792
793 12922
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
794
795 12922
        $sql = [$query];
796
797 12922
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
798 11846
            foreach ($options['indexes'] as $index) {
799 11846
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
800
            }
801
        }
802
803 12922
        if (isset($options['foreignKeys'])) {
804 11636
            foreach ((array) $options['foreignKeys'] as $definition) {
805 11538
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
806
            }
807
        }
808
809 12922
        return $sql;
810
    }
811
812
    /**
813
     * Converts a single boolean value.
814
     *
815
     * First converts the value to its native PHP boolean type
816
     * and passes it to the given callback function to be reconverted
817
     * into any custom representation.
818
     *
819
     * @param mixed    $value    The value to convert.
820
     * @param callable $callback The callback function to use for converting the real boolean value.
821
     *
822
     * @return mixed
823
     *
824
     * @throws UnexpectedValueException
825
     */
826 12511
    private function convertSingleBooleanValue($value, $callback)
827
    {
828 12511
        if ($value === null) {
829 11350
            return $callback(null);
830
        }
831
832 12491
        if (is_bool($value) || is_numeric($value)) {
833 12381
            return $callback((bool) $value);
834
        }
835
836 11534
        if (! is_string($value)) {
837
            return $callback(true);
838
        }
839
840
        /**
841
         * Better safe than sorry: http://php.net/in_array#106319
842
         */
843 11534
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
844 11426
            return $callback(false);
845
        }
846
847 11100
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
848 11090
            return $callback(true);
849
        }
850
851 10751
        throw new UnexpectedValueException("Unrecognized boolean literal '${value}'");
852
    }
853
854
    /**
855
     * Converts one or multiple boolean values.
856
     *
857
     * First converts the value(s) to their native PHP boolean type
858
     * and passes them to the given callback function to be reconverted
859
     * into any custom representation.
860
     *
861
     * @param mixed    $item     The value(s) to convert.
862
     * @param callable $callback The callback function to use for converting the real boolean value(s).
863
     *
864
     * @return mixed
865
     */
866 12511
    private function doConvertBooleans($item, $callback)
867
    {
868 12511
        if (is_array($item)) {
869
            foreach ($item as $key => $value) {
870
                $item[$key] = $this->convertSingleBooleanValue($value, $callback);
871
            }
872
873
            return $item;
874
        }
875
876 12511
        return $this->convertSingleBooleanValue($item, $callback);
877
    }
878
879
    /**
880
     * {@inheritDoc}
881
     *
882
     * Postgres wants boolean values converted to the strings 'true'/'false'.
883
     */
884 12371
    public function convertBooleans($item)
885
    {
886 12371
        if (! $this->useBooleanTrueFalseStrings) {
887 10922
            return parent::convertBooleans($item);
888
        }
889
890 12351
        return $this->doConvertBooleans(
891 12351
            $item,
892
            static function ($boolean) {
893 12351
                if ($boolean === null) {
894 10935
                    return 'NULL';
895
                }
896
897 12341
                return $boolean === true ? 'true' : 'false';
898 12351
            }
899
        );
900
    }
901
902
    /**
903
     * {@inheritDoc}
904
     */
905 11482
    public function convertBooleansToDatabaseValue($item)
906
    {
907 11482
        if (! $this->useBooleanTrueFalseStrings) {
908 10843
            return parent::convertBooleansToDatabaseValue($item);
909
        }
910
911 11472
        return $this->doConvertBooleans(
912 11472
            $item,
913
            static function ($boolean) {
914 11462
                return $boolean === null ? null : (int) $boolean;
915 11472
            }
916
        );
917
    }
918
919
    /**
920
     * {@inheritDoc}
921
     */
922 11379
    public function convertFromBoolean($item)
923
    {
924 11379
        if (in_array(strtolower($item), $this->booleanLiterals['false'], true)) {
925 10847
            return false;
926
        }
927
928 11319
        return parent::convertFromBoolean($item);
929
    }
930
931
    /**
932
     * {@inheritDoc}
933
     */
934 11250
    public function getSequenceNextValSQL($sequenceName)
935
    {
936 11250
        return "SELECT NEXTVAL('" . $sequenceName . "')";
937
    }
938
939
    /**
940
     * {@inheritDoc}
941
     */
942 11349
    public function getSetTransactionIsolationSQL($level)
943
    {
944
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
945 11349
            . $this->_getTransactionIsolationLevelSQL($level);
946
    }
947
948
    /**
949
     * {@inheritDoc}
950
     */
951 11417
    public function getBooleanTypeDeclarationSQL(array $field)
952
    {
953 11417
        return 'BOOLEAN';
954
    }
955
956
    /**
957
     * {@inheritDoc}
958
     */
959 12888
    public function getIntegerTypeDeclarationSQL(array $field)
960
    {
961 12888
        if (! empty($field['autoincrement'])) {
962 12667
            return 'SERIAL';
963
        }
964
965 12736
        return 'INT';
966
    }
967
968
    /**
969
     * {@inheritDoc}
970
     */
971 12181
    public function getBigIntTypeDeclarationSQL(array $field)
972
    {
973 12181
        if (! empty($field['autoincrement'])) {
974 12167
            return 'BIGSERIAL';
975
        }
976
977 4337
        return 'BIGINT';
978
    }
979
980
    /**
981
     * {@inheritDoc}
982
     */
983 12279
    public function getSmallIntTypeDeclarationSQL(array $field)
984
    {
985 12279
        return 'SMALLINT';
986
    }
987
988
    /**
989
     * {@inheritDoc}
990
     */
991 10521
    public function getGuidTypeDeclarationSQL(array $field)
992
    {
993 10521
        return 'UUID';
994
    }
995
996
    /**
997
     * {@inheritDoc}
998
     */
999 12149
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1000
    {
1001 12149
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
1002
    }
1003
1004
    /**
1005
     * {@inheritDoc}
1006
     */
1007 3709
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
1008
    {
1009 3709
        return 'TIMESTAMP(0) WITH TIME ZONE';
1010
    }
1011
1012
    /**
1013
     * {@inheritDoc}
1014
     */
1015 4642
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1016
    {
1017 4642
        return 'DATE';
1018
    }
1019
1020
    /**
1021
     * {@inheritDoc}
1022
     */
1023 4635
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1024
    {
1025 4635
        return 'TIME(0) WITHOUT TIME ZONE';
1026
    }
1027
1028
    /**
1029
     * {@inheritDoc}
1030
     *
1031
     * @deprecated Use application-generated UUIDs instead
1032
     */
1033
    public function getGuidExpression()
1034
    {
1035
        return 'UUID_GENERATE_V4()';
1036
    }
1037
1038
    /**
1039
     * {@inheritDoc}
1040
     */
1041
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1042
    {
1043
        return '';
1044
    }
1045
1046
    /**
1047
     * {@inheritDoc}
1048
     */
1049 12711
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1050
    {
1051 12711
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
1052 12711
            : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
1053
    }
1054
1055
    /**
1056
     * {@inheritdoc}
1057
     */
1058 11752
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1059
    {
1060 11752
        return 'BYTEA';
1061
    }
1062
1063
    /**
1064
     * {@inheritDoc}
1065
     */
1066 11768
    public function getClobTypeDeclarationSQL(array $field)
1067
    {
1068 11768
        return 'TEXT';
1069
    }
1070
1071
    /**
1072
     * {@inheritDoc}
1073
     */
1074 12020
    public function getName()
1075
    {
1076 12020
        return 'postgresql';
1077
    }
1078
1079
    /**
1080
     * {@inheritDoc}
1081
     *
1082
     * PostgreSQL returns all column names in SQL result sets in lowercase.
1083
     */
1084
    public function getSQLResultCasing($column)
1085
    {
1086
        return strtolower($column);
1087
    }
1088
1089
    /**
1090
     * {@inheritDoc}
1091
     */
1092 3626
    public function getDateTimeTzFormatString()
1093
    {
1094 3626
        return 'Y-m-d H:i:sO';
1095
    }
1096
1097
    /**
1098
     * {@inheritDoc}
1099
     */
1100 3500
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
1101
    {
1102 3500
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
1103
    }
1104
1105
    /**
1106
     * {@inheritDoc}
1107
     */
1108 11485
    public function getTruncateTableSQL($tableName, $cascade = false)
1109
    {
1110 11485
        $tableIdentifier = new Identifier($tableName);
1111 11485
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
1112
1113 11485
        if ($cascade) {
1114
            $sql .= ' CASCADE';
1115
        }
1116
1117 11485
        return $sql;
1118
    }
1119
1120
    /**
1121
     * {@inheritDoc}
1122
     */
1123
    public function getReadLockSQL()
1124
    {
1125
        return 'FOR SHARE';
1126
    }
1127
1128
    /**
1129
     * {@inheritDoc}
1130
     */
1131 12510
    protected function initializeDoctrineTypeMappings()
1132
    {
1133 12510
        $this->doctrineTypeMapping = [
1134
            'smallint'      => 'smallint',
1135
            'int2'          => 'smallint',
1136
            'serial'        => 'integer',
1137
            'serial4'       => 'integer',
1138
            'int'           => 'integer',
1139
            'int4'          => 'integer',
1140
            'integer'       => 'integer',
1141
            'bigserial'     => 'bigint',
1142
            'serial8'       => 'bigint',
1143
            'bigint'        => 'bigint',
1144
            'int8'          => 'bigint',
1145
            'bool'          => 'boolean',
1146
            'boolean'       => 'boolean',
1147
            'text'          => 'text',
1148
            'tsvector'      => 'text',
1149
            'varchar'       => 'string',
1150
            'interval'      => 'string',
1151
            '_varchar'      => 'string',
1152
            'char'          => 'string',
1153
            'bpchar'        => 'string',
1154
            'inet'          => 'string',
1155
            'date'          => 'date',
1156
            'datetime'      => 'datetime',
1157
            'timestamp'     => 'datetime',
1158
            'timestamptz'   => 'datetimetz',
1159
            'time'          => 'time',
1160
            'timetz'        => 'time',
1161
            'float'         => 'float',
1162
            'float4'        => 'float',
1163
            'float8'        => 'float',
1164
            'double'        => 'float',
1165
            'double precision' => 'float',
1166
            'real'          => 'float',
1167
            'decimal'       => 'decimal',
1168
            'money'         => 'decimal',
1169
            'numeric'       => 'decimal',
1170
            'year'          => 'date',
1171
            'uuid'          => 'guid',
1172
            'bytea'         => 'blob',
1173
        ];
1174 12510
    }
1175
1176
    /**
1177
     * {@inheritDoc}
1178
     */
1179 12711
    public function getVarcharMaxLength()
1180
    {
1181 12711
        return 65535;
1182
    }
1183
1184
    /**
1185
     * {@inheritdoc}
1186
     */
1187 11762
    public function getBinaryMaxLength()
1188
    {
1189 11762
        return 0;
1190
    }
1191
1192
    /**
1193
     * {@inheritdoc}
1194
     */
1195 11762
    public function getBinaryDefaultLength()
1196
    {
1197 11762
        return 0;
1198
    }
1199
1200
    /**
1201
     * {@inheritDoc}
1202
     */
1203 2452
    protected function getReservedKeywordsClass()
1204
    {
1205 2452
        return Keywords\PostgreSQLKeywords::class;
1206
    }
1207
1208
    /**
1209
     * {@inheritDoc}
1210
     */
1211 12292
    public function getBlobTypeDeclarationSQL(array $field)
1212
    {
1213 12292
        return 'BYTEA';
1214
    }
1215
1216
    /**
1217
     * {@inheritdoc}
1218
     */
1219 13022
    public function getDefaultValueDeclarationSQL($field)
1220
    {
1221 13022
        if ($this->isSerialField($field)) {
1222 12697
            return '';
1223
        }
1224
1225 12104
        return parent::getDefaultValueDeclarationSQL($field);
1226
    }
1227
1228
    /**
1229
     * @param mixed[] $field
1230
     */
1231 13022
    private function isSerialField(array $field) : bool
1232
    {
1233 13022
        return isset($field['type'], $field['autoincrement'])
1234 13022
            && $field['autoincrement'] === true
1235 13022
            && $this->isNumericType($field['type']);
1236
    }
1237
1238
    /**
1239
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
1240
     */
1241 11968
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
1242
    {
1243 11968
        if (! $columnDiff->fromColumn) {
1244 10745
            return $columnDiff->hasChanged('type');
1245
        }
1246
1247 11774
        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
1248 11774
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());
1249
1250
        // default should not be changed when switching between numeric types and the default comes from a sequence
1251 11774
        return $columnDiff->hasChanged('type')
1252 11774
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
1253
    }
1254
1255 12737
    private function isNumericType(Type $type) : bool
1256
    {
1257 12737
        return $type instanceof IntegerType || $type instanceof BigIntType;
1258
    }
1259
1260 11978
    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
1261
    {
1262 11978
        return $columnDiff->fromColumn ? $this->getColumnComment($columnDiff->fromColumn) : null;
1263
    }
1264
1265 4502
    public function getListTableMetadataSQL(string $table, ?string $schema = null) : string
1266
    {
1267 4502
        if ($schema !== null) {
1268
            $table = $schema . '.' . $table;
1269
        }
1270
1271 4502
        return sprintf(
1272
            <<<'SQL'
1273
SELECT obj_description(%s::regclass) AS table_comment;
1274
SQL
1275
            ,
1276 4502
            $this->quoteStringLiteral($table)
1277
        );
1278
    }
1279
}
1280