Completed
Pull Request — master (#3769)
by Benjamin
65:11 queued 11s
created

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

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

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

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1131
            'jsonb'            => Type::JSON,
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::JSON has been deprecated: Use {@see Types::JSON} instead. ( Ignorable by Annotation )

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

1131
            'jsonb'            => /** @scrutinizer ignore-deprecated */ Type::JSON,

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1132
            'money'            => 'decimal',
1133
            'numeric'          => 'decimal',
1134
            'serial'           => 'integer',
1135
            'serial4'          => 'integer',
1136
            'serial8'          => 'bigint',
1137
            'real'             => 'float',
1138
            'smallint'         => 'smallint',
1139
            'text'             => 'text',
1140
            'time'             => 'time',
1141
            'timestamp'        => 'datetime',
1142
            'timestamptz'      => 'datetimetz',
1143
            'timetz'           => 'time',
1144
            'tsvector'         => 'text',
1145
            'uuid'             => 'guid',
1146
            'varchar'          => 'string',
1147
            'year'             => 'date',
1148
            '_varchar'         => 'string',
1149
        ];
1150
    }
1151
1152
    /**
1153
     * {@inheritdoc}
1154
     */
1155
    public function hasNativeJsonType() : bool
1156
    {
1157
        return true;
1158
    }
1159
1160
    /**
1161
     * {@inheritDoc}
1162
     */
1163
    protected function getReservedKeywordsClass() : string
1164
    {
1165 13531
        return Keywords\PostgreSQLKeywords::class;
1166
    }
1167
1168
    /**
1169
     * {@inheritDoc}
1170 13704
     */
1171
    public function getBlobTypeDeclarationSQL(array $field) : string
1172 13704
    {
1173
        return 'BYTEA';
1174
    }
1175
1176
    /**
1177
     * {@inheritdoc}
1178 12675
     */
1179
    public function getDefaultValueDeclarationSQL(array $field) : string
1180 12675
    {
1181
        if ($this->isSerialField($field)) {
1182
            return '';
1183
        }
1184
1185
        return parent::getDefaultValueDeclarationSQL($field);
1186 12675
    }
1187
1188 12675
    /**
1189
     * {@inheritdoc}
1190
     */
1191
    public function supportsColumnCollation() : bool
1192
    {
1193
        return true;
1194 2656
    }
1195
1196 2656
    /**
1197
     * {@inheritdoc}
1198
     */
1199
    public function getColumnCollationDeclarationSQL(string $collation) : string
1200
    {
1201
        return 'COLLATE ' . $this->quoteSingleIdentifier($collation);
1202 13239
    }
1203
1204 13239
    /**
1205
     * {@inheritdoc}
1206
     */
1207
    public function getJsonTypeDeclarationSQL(array $field) : string
1208
    {
1209
        if (! empty($field['jsonb'])) {
1210 14031
            return 'JSONB';
1211
        }
1212 14031
1213 13706
        return 'JSON';
1214
    }
1215
1216 13007
    /**
1217
     * @param mixed[] $field
1218
     */
1219
    private function isSerialField(array $field) : bool
1220
    {
1221
        return isset($field['type'], $field['autoincrement'])
1222 14031
            && $field['autoincrement'] === true
1223
            && $this->isNumericType($field['type']);
1224 14031
    }
1225 14031
1226 14031
    /**
1227
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
1228
     */
1229
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
1230
    {
1231
        if ($columnDiff->fromColumn === null) {
1232 12891
            return $columnDiff->hasChanged('type');
1233
        }
1234 12891
1235 11675
        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
1236
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());
1237
1238 12679
        // default should not be changed when switching between numeric types and the default comes from a sequence
1239 12679
        return $columnDiff->hasChanged('type')
1240
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
1241
    }
1242 12679
1243 12679
    private function isNumericType(Type $type) : bool
1244
    {
1245
        return $type instanceof IntegerType || $type instanceof BigIntType;
1246 13746
    }
1247
1248 13746
    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
1249
    {
1250
        if ($columnDiff->fromColumn === null) {
1251 12901
            return null;
1252
        }
1253 12901
1254
        return $this->getColumnComment($columnDiff->fromColumn);
1255
    }
1256 4529
1257
    public function getListTableMetadataSQL(string $table, ?string $schema = null) : string
1258 4529
    {
1259
        if ($schema !== null) {
1260
            $table = $schema . '.' . $table;
1261
        }
1262 4529
1263
        return sprintf(
1264
            <<<'SQL'
1265
SELECT obj_description(%s::regclass) AS table_comment;
1266
SQL
1267 4529
            ,
1268
            $this->quoteStringLiteral($table)
1269
        );
1270
    }
1271
}
1272