Failed Conditions
Pull Request — develop (#3348)
by Sergei
126:17 queued 61:12
created

PostgreSqlPlatform::getNowExpression()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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