Failed Conditions
Pull Request — develop (#3581)
by Jonathan
12:44
created

PostgreSqlPlatform::convertSingleBooleanValue()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7.0178

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 28
ccs 13
cts 14
cp 0.9286
rs 8.8333
c 0
b 0
f 0
cc 7
nc 6
nop 2
crap 7.0178
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 6731
    public function setUseBooleanTrueFalseStrings(bool $flag) : void
73
    {
74 6731
        $this->useBooleanTrueFalseStrings = $flag;
75 6731
    }
76
77
    /**
78
     * {@inheritDoc}
79
     */
80
    public function getNowExpression() : string
81
    {
82
        return 'LOCALTIMESTAMP(0)';
83
    }
84
85
    /**
86
     * {@inheritDoc}
87
     */
88 7153
    public function getRegexpExpression() : string
89
    {
90 7153
        return 'SIMILAR TO';
91
    }
92
93
    /**
94
     * {@inheritDoc}
95
     */
96 2891
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
97
    {
98 2891
        if ($start !== null) {
99 2891
            $string = $this->getSubstringExpression($string, $start);
100
101 2891
            return 'CASE WHEN (POSITION(' . $substring . ' IN ' . $string . ') = 0) THEN 0 ELSE (POSITION(' . $substring . ' IN ' . $string . ') + ' . $start . ' - 1) END';
102
        }
103
104 2891
        return sprintf('POSITION(%s IN %s)', $substring, $string);
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110 3083
    protected function getDateArithmeticIntervalExpression(string $date, string $operator, string $interval, string $unit) : string
111
    {
112 3083
        if ($unit === DateIntervalUnit::QUARTER) {
113 2939
            $interval = $this->multiplyInterval($interval, 3);
114 2939
            $unit     = DateIntervalUnit::MONTH;
115
        }
116
117 3083
        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
118
    }
119
120
    /**
121
     * {@inheritDoc}
122
     */
123 2717
    public function getDateDiffExpression(string $date1, string $date2) : string
124
    {
125 2717
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
126
    }
127
128
    /**
129
     * {@inheritDoc}
130
     */
131 2418
    public function supportsSequences() : bool
132
    {
133 2418
        return true;
134
    }
135
136
    /**
137
     * {@inheritDoc}
138
     */
139 2397
    public function supportsSchemas() : bool
140
    {
141 2397
        return true;
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     */
147 2247
    public function getDefaultSchemaName() : string
148
    {
149 2247
        return 'public';
150
    }
151
152
    /**
153
     * {@inheritDoc}
154
     */
155 2316
    public function supportsIdentityColumns() : bool
156
    {
157 2316
        return true;
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     */
163 8445
    public function supportsPartialIndexes() : bool
164
    {
165 8445
        return true;
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171 6719
    public function usesSequenceEmulatedIdentityColumns() : bool
172
    {
173 6719
        return true;
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179 7464
    public function getIdentitySequenceName(string $tableName, string $columnName) : string
180
    {
181 7464
        return $tableName . '_' . $columnName . '_seq';
182
    }
183
184
    /**
185
     * {@inheritDoc}
186
     */
187 8559
    public function supportsCommentOnStatement() : bool
188
    {
189 8559
        return true;
190
    }
191
192
    /**
193
     * {@inheritDoc}
194
     */
195 3
    public function prefersSequences() : bool
196
    {
197 3
        return true;
198
    }
199
200
    /**
201
     * {@inheritDoc}
202
     */
203 8661
    public function hasNativeGuidType() : bool
204
    {
205 8661
        return true;
206
    }
207
208
    /**
209
     * {@inheritDoc}
210
     */
211 2421
    public function getListDatabasesSQL() : string
212
    {
213 2421
        return 'SELECT datname FROM pg_database';
214
    }
215
216
    /**
217
     * {@inheritDoc}
218
     */
219 2397
    public function getListNamespacesSQL() : string
220
    {
221 2397
        return "SELECT schema_name AS nspname
222
                FROM   information_schema.schemata
223
                WHERE  schema_name NOT LIKE 'pg\_%'
224
                AND    schema_name != 'information_schema'";
225
    }
226
227
    /**
228
     * {@inheritDoc}
229
     */
230 1612
    public function getListSequencesSQL(string $database) : string
231
    {
232 1612
        return "SELECT sequence_name AS relname,
233
                       sequence_schema AS schemaname
234
                FROM   information_schema.sequences
235
                WHERE  sequence_schema NOT LIKE 'pg\_%'
236
                AND    sequence_schema != 'information_schema'";
237
    }
238
239
    /**
240
     * {@inheritDoc}
241
     */
242 2765
    public function getListTablesSQL() : string
243
    {
244 2765
        return "SELECT quote_ident(table_name) AS table_name,
245
                       table_schema AS schema_name
246
                FROM   information_schema.tables
247
                WHERE  table_schema NOT LIKE 'pg\_%'
248
                AND    table_schema != 'information_schema'
249
                AND    table_name != 'geometry_columns'
250
                AND    table_name != 'spatial_ref_sys'
251
                AND    table_type != 'VIEW'";
252
    }
253
254
    /**
255
     * {@inheritDoc}
256
     */
257 2319
    public function getListViewsSQL(string $database) : string
258
    {
259 2319
        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 7347
    public function getListTableForeignKeysSQL(string $table, ?string $database = null) : string
270
    {
271
        return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef
272
                  FROM pg_catalog.pg_constraint r
273
                  WHERE r.conrelid =
274
                  (
275
                      SELECT c.oid
276
                      FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n
277 7347
                      WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
278
                  )
279
                  AND r.contype = 'f'";
280
    }
281
282
    /**
283
     * {@inheritDoc}
284
     */
285 2480
    public function getCreateViewSQL(string $name, string $sql) : string
286
    {
287 2480
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
288
    }
289
290
    /**
291
     * {@inheritDoc}
292
     */
293 2480
    public function getDropViewSQL(string $name) : string
294
    {
295 2480
        return 'DROP VIEW ' . $name;
296
    }
297
298
    /**
299
     * {@inheritDoc}
300
     */
301 6203
    public function getListTableConstraintsSQL(string $table) : string
302
    {
303 6203
        $table = new Identifier($table);
304 6203
        $table = $this->quoteStringLiteral($table->getName());
305
306 6203
        return sprintf(
307
            <<<'SQL'
308 3
SELECT
309
    quote_ident(relname) as relname
310
FROM
311
    pg_class
312
WHERE oid IN (
313
    SELECT indexrelid
314
    FROM pg_index, pg_class
315
    WHERE pg_class.relname = %s
316
        AND pg_class.oid = pg_index.indrelid
317
        AND (indisunique = 't' OR indisprimary = 't')
318
    )
319
SQL
320
            ,
321 6203
            $table
322
        );
323
    }
324
325
    /**
326
     * {@inheritDoc}
327
     *
328
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
329
     */
330 7290
    public function getListTableIndexesSQL(string $table, ?string $currentDatabase = null) : string
331
    {
332
        return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary,
333
                       pg_index.indkey, pg_index.indrelid,
334
                       pg_get_expr(indpred, indrelid) AS where
335
                 FROM pg_class, pg_index
336
                 WHERE oid IN (
337
                    SELECT indexrelid
338
                    FROM pg_index si, pg_class sc, pg_namespace sn
339 7290
                    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 7359
    private function getTableWhereClause(string $table, string $classAlias = 'c', string $namespaceAlias = 'n') : string
344
    {
345 7359
        $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
346 7359
        if (strpos($table, '.') !== false) {
347 7268
            [$schema, $table] = explode('.', $table);
348 7268
            $schema           = $this->quoteStringLiteral($schema);
349
        } else {
350 7350
            $schema = "ANY(string_to_array((select replace(replace(setting,'\"\$user\"',user),' ','') from pg_catalog.pg_settings where name = 'search_path'),','))";
351
        }
352
353 7359
        $table = new Identifier($table);
354 7359
        $table = $this->quoteStringLiteral($table->getName());
355
356 7359
        return $whereClause . sprintf(
357 18
            '%s.relname = %s AND %s.nspname = %s',
358 7359
            $classAlias,
359 7359
            $table,
360 7359
            $namespaceAlias,
361 7359
            $schema
362
        );
363
    }
364
365
    /**
366
     * {@inheritDoc}
367
     */
368 7252
    public function getListTableColumnsSQL(string $table, ?string $database = null) : string
369
    {
370
        return "SELECT
371
                    a.attnum,
372
                    quote_ident(a.attname) AS field,
373
                    t.typname AS type,
374
                    format_type(a.atttypid, a.atttypmod) AS complete_type,
375
                    (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation,
376
                    (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
377
                    (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
378
                      pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
379
                    a.attnotnull AS isnotnull,
380
                    (SELECT 't'
381
                     FROM pg_index
382
                     WHERE c.oid = pg_index.indrelid
383
                        AND pg_index.indkey[0] = a.attnum
384
                        AND pg_index.indisprimary = 't'
385
                    ) AS pri,
386
                    (SELECT pg_get_expr(adbin, adrelid)
387
                     FROM pg_attrdef
388
                     WHERE c.oid = pg_attrdef.adrelid
389
                        AND pg_attrdef.adnum=a.attnum
390
                    ) AS default,
391
                    (SELECT pg_description.description
392
                        FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
393
                    ) AS comment
394
                    FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
395 7252
                    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 8536
    public function getCreateDatabaseSQL(string $database) : string
407
    {
408 8536
        return 'CREATE DATABASE ' . $database;
409
    }
410
411
    /**
412
     * Returns the SQL statement for disallowing new connections on the given database.
413
     *
414
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
415
     *
416
     * @param string $database The name of the database to disallow new connections for.
417
     */
418 7193
    public function getDisallowDatabaseConnectionsSQL(string $database) : string
419
    {
420 7193
        return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = " . $this->quoteStringLiteral($database);
421
    }
422
423
    /**
424
     * Returns the SQL statement for closing currently active connections on the given database.
425
     *
426
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
427
     *
428
     * @param string $database The name of the database to close currently active connections for.
429
     */
430 7899
    public function getCloseActiveDatabaseConnectionsSQL(string $database) : string
431
    {
432
        return 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '
433 7899
            . $this->quoteStringLiteral($database);
434
    }
435
436
    /**
437
     * {@inheritDoc}
438
     */
439 8341
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) : string
440
    {
441 8341
        $query = '';
442
443 8341
        if ($foreignKey->hasOption('match')) {
444 7178
            $query .= ' MATCH ' . $foreignKey->getOption('match');
445
        }
446
447 8341
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
448
449 8341
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
450 7178
            $query .= ' DEFERRABLE';
451
        } else {
452 8341
            $query .= ' NOT DEFERRABLE';
453
        }
454
455 8341
        if ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) {
456 7178
            $query .= ' INITIALLY DEFERRED';
457
        } else {
458 8341
            $query .= ' INITIALLY IMMEDIATE';
459
        }
460
461 8341
        return $query;
462
    }
463
464
    /**
465
     * {@inheritDoc}
466
     */
467 7575
    public function getAlterTableSQL(TableDiff $diff) : array
468
    {
469 7575
        $sql         = [];
470 7575
        $commentsSQL = [];
471 7575
        $columnSql   = [];
472
473 7575
        foreach ($diff->addedColumns as $column) {
474 6694
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
475
                continue;
476
            }
477
478 6694
            $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
479 6694
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
480
481 6694
            $comment = $this->getColumnComment($column);
482
483 6694
            if ($comment === null || $comment === '') {
484 6691
                continue;
485
            }
486
487 5628
            $commentsSQL[] = $this->getCommentOnColumnSQL(
488 5628
                $diff->getName($this)->getQuotedName($this),
489 5628
                $column->getQuotedName($this),
490 3
                $comment
491
            );
492
        }
493
494 7575
        foreach ($diff->removedColumns as $column) {
495 7302
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
496
                continue;
497
            }
498
499 7302
            $query = 'DROP ' . $column->getQuotedName($this);
500 7302
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
501
        }
502
503 7575
        foreach ($diff->changedColumns as $columnDiff) {
504
            /** @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
505 7551
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
506
                continue;
507
            }
508
509 7551
            if ($this->isUnchangedBinaryColumn($columnDiff)) {
510 6403
                continue;
511
            }
512
513 7548
            $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
514 7548
            $column        = $columnDiff->column;
515
516 7548
            if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('precision') || $columnDiff->hasChanged('scale') || $columnDiff->hasChanged('fixed')) {
517 7426
                $type = $column->getType();
518
519
                // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
520 7426
                $columnDefinition                  = $column->toArray();
521 7426
                $columnDefinition['autoincrement'] = false;
522
523
                // here was a server version check before, but DBAL API does not support this anymore.
524 7426
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this);
525 7426
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
526
            }
527
528 7548
            if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
529 7087
                $defaultClause = $column->getDefault() === null
530 7066
                    ? ' DROP DEFAULT'
531 7087
                    : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
532 7087
                $query         = 'ALTER ' . $oldColumnName . $defaultClause;
533 7087
                $sql[]         = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
534
            }
535
536 7548
            if ($columnDiff->hasChanged('notnull')) {
537 5731
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
538 5731
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
539
            }
540
541 7548
            if ($columnDiff->hasChanged('autoincrement')) {
542 2540
                if ($column->getAutoincrement()) {
543
                    // add autoincrement
544 2540
                    $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);
545
546 2540
                    $sql[] = 'CREATE SEQUENCE ' . $seqName;
547 2540
                    $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' . $diff->getName($this)->getQuotedName($this) . '))';
548 2540
                    $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
549 2540
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
550
                } else {
551
                    // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
552 2534
                    $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT';
553 2534
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
554
                }
555
            }
556
557 7548
            $newComment = $this->getColumnComment($column);
558 7548
            $oldComment = $this->getOldColumnComment($columnDiff);
559
560 7548
            if ($columnDiff->hasChanged('comment') || ($columnDiff->fromColumn !== null && $oldComment !== $newComment)) {
561 7124
                $commentsSQL[] = $this->getCommentOnColumnSQL(
562 7124
                    $diff->getName($this)->getQuotedName($this),
563 7124
                    $column->getQuotedName($this),
564 9
                    $newComment
565
                );
566
            }
567
568 7548
            if (! $columnDiff->hasChanged('length')) {
569 7545
                continue;
570
            }
571
572 5178
            $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $column->getType()->getSQLDeclaration($column->toArray(), $this);
573 5178
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
574
        }
575
576 7575
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
577 6626
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
578
                continue;
579
            }
580
581 6626
            $oldColumnName = new Identifier($oldColumnName);
582
583 6626
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
584 6626
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
585
        }
586
587 7575
        $tableSql = [];
588
589 7575
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
590 7575
            $sql = array_merge($sql, $commentsSQL);
591
592 7575
            $newName = $diff->getNewName();
593
594 7575
            if ($newName !== null) {
595 5731
                $sql[] = sprintf(
596 6
                    'ALTER TABLE %s RENAME TO %s',
597 5731
                    $diff->getName($this)->getQuotedName($this),
598 5731
                    $newName->getQuotedName($this)
599
                );
600
            }
601
602 7575
            $sql = array_merge(
603 7575
                $this->getPreAlterTableIndexForeignKeySQL($diff),
604 7575
                $sql,
605 7575
                $this->getPostAlterTableIndexForeignKeySQL($diff)
606
            );
607
        }
608
609 7575
        return array_merge($sql, $tableSql, $columnSql);
610
    }
611
612
    /**
613
     * Checks whether a given column diff is a logically unchanged binary type column.
614
     *
615
     * Used to determine whether a column alteration for a binary type column can be skipped.
616
     * Doctrine's {@link \Doctrine\DBAL\Types\BinaryType} and {@link \Doctrine\DBAL\Types\BlobType}
617
     * are mapped to the same database column type on this platform as this platform
618
     * does not have a native VARBINARY/BINARY column type. Therefore the {@link \Doctrine\DBAL\Schema\Comparator}
619
     * might detect differences for binary type columns which do not have to be propagated
620
     * to database as there actually is no difference at database level.
621
     *
622
     * @param ColumnDiff $columnDiff The column diff to check against.
623
     *
624
     * @return bool True if the given column diff is an unchanged binary type column, false otherwise.
625
     */
626 7551
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff) : bool
627
    {
628 7551
        $columnType = $columnDiff->column->getType();
629
630 7551
        if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
631 7548
            return false;
632
        }
633
634 6403
        $fromColumn = $columnDiff->fromColumn;
635
636 6403
        if ($fromColumn !== null) {
637 6403
            $fromColumnType = $fromColumn->getType();
638
639 6403
            if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
640
                return false;
641
            }
642
643 6403
            return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
644
        }
645
646
        if ($columnDiff->hasChanged('type')) {
647
            return false;
648
        }
649
650
        return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0;
651
    }
652
653
    /**
654
     * {@inheritdoc}
655
     */
656 6336
    protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName) : array
657
    {
658 6336
        if (strpos($tableName, '.') !== false) {
659 5156
            [$schema]     = explode('.', $tableName);
660 5156
            $oldIndexName = $schema . '.' . $oldIndexName;
661
        }
662
663 6336
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
664
    }
665
666
    /**
667
     * {@inheritdoc}
668
     */
669 7139
    public function getCommentOnColumnSQL(string $tableName, string $columnName, ?string $comment) : string
670
    {
671 7139
        $tableName  = new Identifier($tableName);
672 7139
        $columnName = new Identifier($columnName);
673 7139
        $comment    = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);
674
675 7139
        return sprintf(
676 24
            'COMMENT ON COLUMN %s.%s IS %s',
677 7139
            $tableName->getQuotedName($this),
678 7139
            $columnName->getQuotedName($this),
679 7139
            $comment
680
        );
681
    }
682
683
    /**
684
     * {@inheritDoc}
685
     */
686 7627
    public function getCreateSequenceSQL(Sequence $sequence) : string
687
    {
688 7627
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
689 7627
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
690 7627
            ' MINVALUE ' . $sequence->getInitialValue() .
691 7627
            ' START ' . $sequence->getInitialValue() .
692 7627
            $this->getSequenceCacheSQL($sequence);
693
    }
694
695
    /**
696
     * {@inheritDoc}
697
     */
698
    public function getAlterSequenceSQL(Sequence $sequence) : string
699
    {
700
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
701
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
702
            $this->getSequenceCacheSQL($sequence);
703
    }
704
705
    /**
706
     * Cache definition for sequences
707
     */
708 7627
    private function getSequenceCacheSQL(Sequence $sequence) : string
709
    {
710 7627
        if ($sequence->getCache() > 1) {
711 6453
            return ' CACHE ' . $sequence->getCache();
712
        }
713
714 7624
        return '';
715
    }
716
717
    /**
718
     * {@inheritDoc}
719
     */
720 7624
    public function getDropSequenceSQL($sequence) : string
721
    {
722 7624
        if ($sequence instanceof Sequence) {
723
            $sequence = $sequence->getQuotedName($this);
724
        }
725
726 7624
        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
727
    }
728
729
    /**
730
     * {@inheritDoc}
731
     */
732 7397
    public function getCreateSchemaSQL(string $schemaName) : string
733
    {
734 7397
        return 'CREATE SCHEMA ' . $schemaName;
735
    }
736
737
    /**
738
     * {@inheritDoc}
739
     */
740 7269
    public function getDropForeignKeySQL($foreignKey, $table) : string
741
    {
742 7269
        return $this->getDropConstraintSQL($foreignKey, $table);
743
    }
744
745
    /**
746
     * {@inheritDoc}
747
     */
748 8556
    protected function _getCreateTableSQL(string $tableName, array $columns, array $options = []) : array
749
    {
750 8556
        $queryFields = $this->getColumnDeclarationListSQL($columns);
751
752 8556
        if (isset($options['primary']) && ! empty($options['primary'])) {
753 7614
            $keyColumns   = array_unique(array_values($options['primary']));
754 7614
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
755
        }
756
757 8556
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
758
759 8556
        $sql = [$query];
760
761 8556
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
762 7328
            foreach ($options['indexes'] as $index) {
763 7328
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
764
            }
765
        }
766
767 8556
        if (isset($options['foreignKeys'])) {
768 7244
            foreach ((array) $options['foreignKeys'] as $definition) {
769 6980
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
770
            }
771
        }
772
773 8556
        return $sql;
774
    }
775
776
    /**
777
     * Converts a single boolean value.
778
     *
779
     * First converts the value to its native PHP boolean type
780
     * and passes it to the given callback function to be reconverted
781
     * into any custom representation.
782
     *
783
     * @param mixed    $value    The value to convert.
784
     * @param callable $callback The callback function to use for converting the real boolean value.
785
     *
786
     * @return mixed
787
     *
788
     * @throws UnexpectedValueException
789
     */
790 7850
    private function convertSingleBooleanValue($value, callable $callback)
791
    {
792 7850
        if ($value === null) {
793 1970
            return $callback(null);
794
        }
795
796 7850
        if (is_bool($value) || is_numeric($value)) {
797 7798
            return $callback((bool) $value);
798
        }
799
800 7202
        if (! is_string($value)) {
801
            return $callback(true);
802
        }
803
804
        /**
805
         * Better safe than sorry: http://php.net/in_array#106319
806
         */
807 7202
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
808 7146
            return $callback(false);
809
        }
810
811 6843
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
812 6840
            return $callback(true);
813
        }
814
815 6603
        throw new UnexpectedValueException(sprintf(
816 3
            'Unrecognized boolean literal, %s given.',
817 6603
            $value
818
        ));
819
    }
820
821
    /**
822
     * Converts one or multiple boolean values.
823
     *
824
     * First converts the value(s) to their native PHP boolean type
825
     * and passes them to the given callback function to be reconverted
826
     * into any custom representation.
827
     *
828
     * @param mixed    $item     The value(s) to convert.
829
     * @param callable $callback The callback function to use for converting the real boolean value(s).
830
     *
831
     * @return mixed
832
     */
833 7850
    private function doConvertBooleans($item, callable $callback)
834
    {
835 7850
        if (is_array($item)) {
836
            foreach ($item as $key => $value) {
837
                $item[$key] = $this->convertSingleBooleanValue($value, $callback);
838
            }
839
840
            return $item;
841
        }
842
843 7850
        return $this->convertSingleBooleanValue($item, $callback);
844
    }
845
846
    /**
847
     * {@inheritDoc}
848
     *
849
     * Postgres wants boolean values converted to the strings 'true'/'false'.
850
     */
851 7817
    public function convertBooleans($item)
852
    {
853 7817
        if (! $this->useBooleanTrueFalseStrings) {
854 6731
            return parent::convertBooleans($item);
855
        }
856
857 7811
        return $this->doConvertBooleans(
858 7811
            $item,
859
            static function ($boolean) : string {
860 7811
                if ($boolean === null) {
861
                    return 'NULL';
862
                }
863
864 7811
                return $boolean === true ? 'true' : 'false';
865 7811
            }
866
        );
867
    }
868
869
    /**
870
     * {@inheritDoc}
871
     */
872 7116
    public function convertBooleansToDatabaseValue($item)
873
    {
874 7116
        if (! $this->useBooleanTrueFalseStrings) {
875 6678
            return parent::convertBooleansToDatabaseValue($item);
876
        }
877
878 7113
        return $this->doConvertBooleans(
879 7113
            $item,
880
            static function ($boolean) : ?int {
881 7110
                return $boolean === null ? null : (int) $boolean;
882 7113
            }
883
        );
884
    }
885
886
    /**
887
     * {@inheritDoc}
888
     */
889 7042
    public function convertFromBoolean($item) : ?bool
890
    {
891 7042
        if (in_array($item, $this->booleanLiterals['false'], true)) {
892 6643
            return false;
893
        }
894
895 7024
        return parent::convertFromBoolean($item);
896
    }
897
898
    /**
899
     * {@inheritDoc}
900
     */
901 7009
    public function getSequenceNextValSQL(string $sequenceName) : string
902
    {
903 7009
        return "SELECT NEXTVAL('" . $sequenceName . "')";
904
    }
905
906
    /**
907
     * {@inheritDoc}
908
     */
909 7128
    public function getSetTransactionIsolationSQL(int $level) : string
910
    {
911
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
912 7128
            . $this->_getTransactionIsolationLevelSQL($level);
913
    }
914
915
    /**
916
     * {@inheritDoc}
917
     */
918 6939
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
919
    {
920 6939
        return 'BOOLEAN';
921
    }
922
923
    /**
924
     * {@inheritDoc}
925
     */
926 8547
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
927
    {
928 8547
        if (! empty($columnDef['autoincrement'])) {
929 8199
            return 'SERIAL';
930
        }
931
932 8402
        return 'INT';
933
    }
934
935
    /**
936
     * {@inheritDoc}
937
     */
938 7808
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
939
    {
940 7808
        if (! empty($columnDef['autoincrement'])) {
941 7796
            return 'BIGSERIAL';
942
        }
943
944 2463
        return 'BIGINT';
945
    }
946
947
    /**
948
     * {@inheritDoc}
949
     */
950 7957
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
951
    {
952 7957
        if (! empty($columnDef['autoincrement'])) {
953 7228
            return 'SMALLSERIAL';
954
        }
955
956 7957
        return 'SMALLINT';
957
    }
958
959
    /**
960
     * {@inheritDoc}
961
     */
962 6378
    public function getGuidTypeDeclarationSQL(array $field) : string
963
    {
964 6378
        return 'UUID';
965
    }
966
967
    /**
968
     * {@inheritDoc}
969
     */
970 7899
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
971
    {
972 7899
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
973
    }
974
975
    /**
976
     * {@inheritDoc}
977
     */
978 1952
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) : string
979
    {
980 1952
        return 'TIMESTAMP(0) WITH TIME ZONE';
981
    }
982
983
    /**
984
     * {@inheritDoc}
985
     */
986 2705
    public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string
987
    {
988 2705
        return 'DATE';
989
    }
990
991
    /**
992
     * {@inheritDoc}
993
     */
994 2699
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string
995
    {
996 2699
        return 'TIME(0) WITHOUT TIME ZONE';
997
    }
998
999
    /**
1000
     * {@inheritDoc}
1001
     */
1002
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : string
1003
    {
1004
        return '';
1005
    }
1006
1007
    /**
1008
     * {@inheritDoc}
1009
     */
1010 8350
    protected function getVarcharTypeDeclarationSQLSnippet(int $length, bool $fixed) : string
1011
    {
1012 8350
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
1013 8350
            : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
1014
    }
1015
1016
    /**
1017
     * {@inheritdoc}
1018
     */
1019 7378
    protected function getBinaryTypeDeclarationSQLSnippet(int $length, bool $fixed) : string
1020
    {
1021 7378
        return 'BYTEA';
1022
    }
1023
1024
    /**
1025
     * {@inheritDoc}
1026
     */
1027 7396
    public function getClobTypeDeclarationSQL(array $field) : string
1028
    {
1029 7396
        return 'TEXT';
1030
    }
1031
1032
    /**
1033
     * {@inheritDoc}
1034
     */
1035 7705
    public function getName() : string
1036
    {
1037 7705
        return 'postgresql';
1038
    }
1039
1040
    /**
1041
     * {@inheritDoc}
1042
     *
1043
     * PostgreSQL returns all column names in SQL result sets in lowercase.
1044
     */
1045
    public function getSQLResultCasing(string $column) : string
1046
    {
1047
        return strtolower($column);
1048
    }
1049
1050
    /**
1051
     * {@inheritDoc}
1052
     */
1053 1880
    public function getDateTimeTzFormatString() : string
1054
    {
1055 1880
        return 'Y-m-d H:i:sO';
1056
    }
1057
1058
    /**
1059
     * {@inheritDoc}
1060
     */
1061 1776
    public function getEmptyIdentityInsertSQL(string $tableName, string $identifierColumnName) : string
1062
    {
1063 1776
        return 'INSERT INTO ' . $tableName . ' (' . $identifierColumnName . ') VALUES (DEFAULT)';
1064
    }
1065
1066
    /**
1067
     * {@inheritDoc}
1068
     */
1069 6943
    public function getTruncateTableSQL(string $tableName, bool $cascade = false) : string
1070
    {
1071 6943
        $tableIdentifier = new Identifier($tableName);
1072 6943
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
1073
1074 6943
        if ($cascade) {
1075
            $sql .= ' CASCADE';
1076
        }
1077
1078 6943
        return $sql;
1079
    }
1080
1081
    /**
1082
     * {@inheritDoc}
1083
     */
1084
    public function getReadLockSQL() : string
1085
    {
1086
        return 'FOR SHARE';
1087
    }
1088
1089
    /**
1090
     * {@inheritDoc}
1091
     */
1092 8176
    protected function initializeDoctrineTypeMappings() : void
1093
    {
1094 8176
        $this->doctrineTypeMapping = [
1095
            'bigint'           => 'bigint',
1096
            'bigserial'        => 'bigint',
1097
            'bool'             => 'boolean',
1098
            'boolean'          => 'boolean',
1099
            'bpchar'           => 'string',
1100
            'bytea'            => 'blob',
1101
            'char'             => 'string',
1102
            'date'             => 'date',
1103
            'datetime'         => 'datetime',
1104
            'decimal'          => 'decimal',
1105
            'double'           => 'float',
1106
            'double precision' => 'float',
1107
            'float'            => 'float',
1108
            'float4'           => 'float',
1109
            'float8'           => 'float',
1110
            'inet'             => 'string',
1111
            'int'              => 'integer',
1112
            'int2'             => 'smallint',
1113
            'int4'             => 'integer',
1114
            'int8'             => 'bigint',
1115
            'integer'          => 'integer',
1116
            'interval'         => 'string',
1117
            'json'             => Type::JSON,
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::JSON has been deprecated: Use {@see DefaultTypes::JSON} instead. ( Ignorable by Annotation )

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

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

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

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

Loading history...
1118
            'money'            => 'decimal',
1119
            'numeric'          => 'decimal',
1120
            'serial'           => 'integer',
1121
            'serial4'          => 'integer',
1122
            'serial8'          => 'bigint',
1123
            'real'             => 'float',
1124
            'smallint'         => 'smallint',
1125
            'text'             => 'text',
1126
            'time'             => 'time',
1127
            'timestamp'        => 'datetime',
1128
            'timestamptz'      => 'datetimetz',
1129
            'timetz'           => 'time',
1130
            'tsvector'         => 'text',
1131
            'uuid'             => 'guid',
1132
            'varchar'          => 'string',
1133
            'year'             => 'date',
1134
            '_varchar'         => 'string',
1135
        ];
1136 8176
    }
1137
1138
    /**
1139
     * {@inheritDoc}
1140
     */
1141 8350
    public function getVarcharMaxLength() : int
1142
    {
1143 8350
        return 65535;
1144
    }
1145
1146
    /**
1147
     * {@inheritdoc}
1148
     */
1149 3
    public function getBinaryMaxLength() : int
1150
    {
1151 3
        return 0;
1152
    }
1153
1154
    /**
1155
     * {@inheritdoc}
1156
     */
1157 7381
    public function getBinaryDefaultLength() : int
1158
    {
1159 7381
        return 0;
1160
    }
1161
1162
    /**
1163
     * {@inheritdoc}
1164
     */
1165 8797
    public function hasNativeJsonType() : bool
1166
    {
1167 8797
        return true;
1168
    }
1169
1170
    /**
1171
     * {@inheritDoc}
1172
     */
1173 2830
    protected function getReservedKeywordsClass() : string
1174
    {
1175 2830
        return Keywords\PostgreSQLKeywords::class;
1176
    }
1177
1178
    /**
1179
     * {@inheritDoc}
1180
     */
1181 3137
    public function getBlobTypeDeclarationSQL(array $field) : string
1182
    {
1183 3137
        return 'BYTEA';
1184
    }
1185
1186
    /**
1187
     * {@inheritdoc}
1188
     */
1189 8586
    public function getDefaultValueDeclarationSQL(array $field) : string
1190
    {
1191 8586
        if ($this->isSerialField($field)) {
1192 8208
            return '';
1193
        }
1194
1195 7653
        return parent::getDefaultValueDeclarationSQL($field);
1196
    }
1197
1198
    /**
1199
     * {@inheritdoc}
1200
     */
1201
    public function supportsColumnCollation() : bool
1202
    {
1203
        return true;
1204
    }
1205
1206
    /**
1207
     * {@inheritdoc}
1208
     */
1209 7278
    public function getColumnCollationDeclarationSQL(string $collation) : string
1210
    {
1211 7278
        return 'COLLATE ' . $this->quoteSingleIdentifier($collation);
1212
    }
1213
1214
    /**
1215
     * {@inheritdoc}
1216
     */
1217 2678
    public function getJsonTypeDeclarationSQL(array $field) : string
1218
    {
1219 2678
        return 'JSON';
1220
    }
1221
1222
    /**
1223
     * @param mixed[] $field
1224
     */
1225 8586
    private function isSerialField(array $field) : bool
1226
    {
1227 8586
        return isset($field['type'], $field['autoincrement'])
1228 8586
            && $field['autoincrement'] === true
1229 8586
            && $this->isNumericType($field['type']);
1230
    }
1231
1232
    /**
1233
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
1234
     */
1235 7545
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
1236
    {
1237 7545
        if ($columnDiff->fromColumn === null) {
1238 6565
            return $columnDiff->hasChanged('type');
1239
        }
1240
1241 7378
        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
1242 7378
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());
1243
1244
        // default should not be changed when switching between numeric types and the default comes from a sequence
1245 7378
        return $columnDiff->hasChanged('type')
1246 7378
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
1247
    }
1248
1249 8220
    private function isNumericType(Type $type) : bool
1250
    {
1251 8220
        return $type instanceof IntegerType || $type instanceof BigIntType;
1252
    }
1253
1254 7548
    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
1255
    {
1256 7548
        if ($columnDiff->fromColumn === null) {
1257 6568
            return null;
1258
        }
1259
1260 7378
        return $this->getColumnComment($columnDiff->fromColumn);
1261
    }
1262
}
1263