Failed Conditions
Pull Request — develop (#3348)
by Sergei
38:24 queued 35:29
created

PostgreSqlPlatform::getCommentOnColumnSQL()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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