Completed
Pull Request — develop (#3520)
by Jonathan
09:41
created

PostgreSqlPlatform::convertFromBoolean()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Platforms;
6
7
use Doctrine\DBAL\Schema\Column;
8
use Doctrine\DBAL\Schema\ColumnDiff;
9
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
10
use Doctrine\DBAL\Schema\Identifier;
11
use Doctrine\DBAL\Schema\Index;
12
use Doctrine\DBAL\Schema\Sequence;
13
use Doctrine\DBAL\Schema\TableDiff;
14
use Doctrine\DBAL\Types\BigIntType;
15
use Doctrine\DBAL\Types\BinaryType;
16
use Doctrine\DBAL\Types\BlobType;
17
use Doctrine\DBAL\Types\IntegerType;
18
use Doctrine\DBAL\Types\Type;
19
use UnexpectedValueException;
20
use function array_diff;
21
use function array_merge;
22
use function array_unique;
23
use function array_values;
24
use function count;
25
use function explode;
26
use function implode;
27
use function in_array;
28
use function is_array;
29
use function is_bool;
30
use function is_numeric;
31
use function is_string;
32
use function sprintf;
33
use function strpos;
34
use function strtolower;
35
use function trim;
36
37
/**
38
 * Provides the behavior, features and SQL dialect of the PostgreSQL 9.4+ database platform.
39
 *
40
 * @todo   Rename: PostgreSQLPlatform
41
 */
42
class PostgreSqlPlatform extends AbstractPlatform
43
{
44
    /** @var bool */
45
    private $useBooleanTrueFalseStrings = true;
46
47
    /** @var string[][] PostgreSQL booleans literals */
48
    private $booleanLiterals = [
49
        'true' => [
50
            't',
51
            'true',
52
            'y',
53
            'yes',
54
            'on',
55
            '1',
56
        ],
57
        'false' => [
58
            'f',
59
            'false',
60
            'n',
61
            'no',
62
            'off',
63
            '0',
64
        ],
65
    ];
66
67
    /**
68
     * PostgreSQL has different behavior with some drivers
69
     * with regard to how booleans have to be handled.
70
     *
71
     * Enables use of 'true'/'false' or otherwise 1 and 0 instead.
72
     *
73
     * @param bool $flag
74
     */
75 9737
    public function setUseBooleanTrueFalseStrings($flag)
76
    {
77 9737
        $this->useBooleanTrueFalseStrings = (bool) $flag;
78 9737
    }
79
80
    /**
81
     * {@inheritDoc}
82
     */
83
    public function getNowExpression() : string
84
    {
85
        return 'LOCALTIMESTAMP(0)';
86
    }
87
88
    /**
89
     * {@inheritDoc}
90
     */
91 10381
    public function getRegexpExpression() : string
92
    {
93 10381
        return 'SIMILAR TO';
94
    }
95
96
    /**
97
     * {@inheritDoc}
98
     */
99 3669
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
100
    {
101 3669
        if ($start !== null) {
102 3669
            $string = $this->getSubstringExpression($string, $start);
103
104 3669
            return 'CASE WHEN (POSITION(' . $substring . ' IN ' . $string . ') = 0) THEN 0 ELSE (POSITION(' . $substring . ' IN ' . $string . ') + ' . $start . ' - 1) END';
105
        }
106
107 3669
        return sprintf('POSITION(%s IN %s)', $substring, $string);
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 3861
    protected function getDateArithmeticIntervalExpression(string $date, string $operator, string $interval, string $unit) : string
114
    {
115 3861
        if ($unit === DateIntervalUnit::QUARTER) {
116 3717
            $interval = $this->multiplyInterval($interval, 3);
117 3717
            $unit     = DateIntervalUnit::MONTH;
118
        }
119
120 3861
        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
121
    }
122
123
    /**
124
     * {@inheritDoc}
125
     */
126 3495
    public function getDateDiffExpression(string $date1, string $date2) : string
127
    {
128 3495
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
129
    }
130
131
    /**
132
     * {@inheritDoc}
133
     */
134 10846
    public function supportsSequences()
135
    {
136 10846
        return true;
137
    }
138
139
    /**
140
     * {@inheritDoc}
141
     */
142 3203
    public function supportsSchemas()
143
    {
144 3203
        return true;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 3053
    public function getDefaultSchemaName()
151
    {
152 3053
        return 'public';
153
    }
154
155
    /**
156
     * {@inheritDoc}
157
     */
158 10763
    public function supportsIdentityColumns()
159
    {
160 10763
        return true;
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166 11698
    public function supportsPartialIndexes()
167
    {
168 11698
        return true;
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174 9453
    public function usesSequenceEmulatedIdentityColumns()
175
    {
176 9453
        return true;
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182 10235
    public function getIdentitySequenceName($tableName, $columnName)
183
    {
184 10235
        return $tableName . '_' . $columnName . '_seq';
185
    }
186
187
    /**
188
     * {@inheritDoc}
189
     */
190 11833
    public function supportsCommentOnStatement()
191
    {
192 11833
        return true;
193
    }
194
195
    /**
196
     * {@inheritDoc}
197
     */
198 10056
    public function prefersSequences()
199
    {
200 10056
        return true;
201
    }
202
203
    /**
204
     * {@inheritDoc}
205
     */
206 12043
    public function hasNativeGuidType()
207
    {
208 12043
        return true;
209
    }
210
211
    /**
212
     * {@inheritDoc}
213
     */
214 3227
    public function getListDatabasesSQL()
215
    {
216 3227
        return 'SELECT datname FROM pg_database';
217
    }
218
219
    /**
220
     * {@inheritDoc}
221
     */
222 3203
    public function getListNamespacesSQL()
223
    {
224 3203
        return "SELECT schema_name AS nspname
225
                FROM   information_schema.schemata
226
                WHERE  schema_name NOT LIKE 'pg\_%'
227
                AND    schema_name != 'information_schema'";
228
    }
229
230
    /**
231
     * {@inheritDoc}
232
     */
233 2148
    public function getListSequencesSQL($database)
234
    {
235 2148
        return "SELECT sequence_name AS relname,
236
                       sequence_schema AS schemaname
237
                FROM   information_schema.sequences
238
                WHERE  sequence_schema NOT LIKE 'pg\_%'
239
                AND    sequence_schema != 'information_schema'";
240
    }
241
242
    /**
243
     * {@inheritDoc}
244
     */
245 3543
    public function getListTablesSQL()
246
    {
247 3543
        return "SELECT quote_ident(table_name) AS table_name,
248
                       table_schema AS schema_name
249
                FROM   information_schema.tables
250
                WHERE  table_schema NOT LIKE 'pg\_%'
251
                AND    table_schema != 'information_schema'
252
                AND    table_name != 'geometry_columns'
253
                AND    table_name != 'spatial_ref_sys'
254
                AND    table_type != 'VIEW'";
255
    }
256
257
    /**
258
     * {@inheritDoc}
259
     */
260 3125
    public function getListViewsSQL($database)
261
    {
262 3125
        return 'SELECT quote_ident(table_name) AS viewname,
263
                       table_schema AS schemaname,
264
                       view_definition AS definition
265
                FROM   information_schema.views
266
                WHERE  view_definition IS NOT NULL';
267
    }
268
269
    /**
270
     * {@inheritDoc}
271
     */
272 10087
    public function getListTableForeignKeysSQL($table, $database = null)
0 ignored issues
show
Unused Code introduced by
The parameter $database is not used and could be removed. ( Ignorable by Annotation )

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

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

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

Loading history...
273
    {
274
        return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef
275
                  FROM pg_catalog.pg_constraint r
276
                  WHERE r.conrelid =
277
                  (
278
                      SELECT c.oid
279
                      FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n
280 10087
                      WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
281
                  )
282
                  AND r.contype = 'f'";
283
    }
284
285
    /**
286
     * {@inheritDoc}
287
     */
288 3291
    public function getCreateViewSQL($name, $sql)
289
    {
290 3291
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
291
    }
292
293
    /**
294
     * {@inheritDoc}
295
     */
296 3291
    public function getDropViewSQL($name)
297
    {
298 3291
        return 'DROP VIEW ' . $name;
299
    }
300
301
    /**
302
     * {@inheritDoc}
303
     */
304 8756
    public function getListTableConstraintsSQL($table)
305
    {
306 8756
        $table = new Identifier($table);
307 8756
        $table = $this->quoteStringLiteral($table->getName());
308
309 8756
        return sprintf(
310
            <<<'SQL'
311 6
SELECT
312
    quote_ident(relname) as relname
313
FROM
314
    pg_class
315
WHERE oid IN (
316
    SELECT indexrelid
317
    FROM pg_index, pg_class
318
    WHERE pg_class.relname = %s
319
        AND pg_class.oid = pg_index.indrelid
320
        AND (indisunique = 't' OR indisprimary = 't')
321
    )
322
SQL
323
            ,
324 8756
            $table
325
        );
326
    }
327
328
    /**
329
     * {@inheritDoc}
330
     *
331
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
332
     */
333 10030
    public function getListTableIndexesSQL($table, $currentDatabase = null)
334
    {
335
        return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary,
336
                       pg_index.indkey, pg_index.indrelid,
337
                       pg_get_expr(indpred, indrelid) AS where
338
                 FROM pg_class, pg_index
339
                 WHERE oid IN (
340
                    SELECT indexrelid
341
                    FROM pg_index si, pg_class sc, pg_namespace sn
342 10030
                    WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . ' AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid
343
                 ) AND pg_index.indexrelid = oid';
344
    }
345
346
    /**
347
     * @param string $table
348
     * @param string $classAlias
349
     * @param string $namespaceAlias
350
     *
351
     * @return string
352
     */
353 10111
    private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n')
354
    {
355 10111
        $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
356 10111
        if (strpos($table, '.') !== false) {
357 10026
            [$schema, $table] = explode('.', $table);
358 10026
            $schema           = $this->quoteStringLiteral($schema);
359
        } else {
360 10093
            $schema = "ANY(string_to_array((select replace(replace(setting,'\"\$user\"',user),' ','') from pg_catalog.pg_settings where name = 'search_path'),','))";
361
        }
362
363 10111
        $table = new Identifier($table);
364 10111
        $table = $this->quoteStringLiteral($table->getName());
365
366 10111
        return $whereClause . sprintf(
367 36
            '%s.relname = %s AND %s.nspname = %s',
368 10111
            $classAlias,
369 10111
            $table,
370 10111
            $namespaceAlias,
371 10111
            $schema
372
        );
373
    }
374
375
    /**
376
     * {@inheritDoc}
377
     */
378 9992
    public function getListTableColumnsSQL($table, $database = null)
379
    {
380
        return "SELECT
381
                    a.attnum,
382
                    quote_ident(a.attname) AS field,
383
                    t.typname AS type,
384
                    format_type(a.atttypid, a.atttypmod) AS complete_type,
385
                    (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation,
386
                    (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
387
                    (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
388
                      pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
389
                    a.attnotnull AS isnotnull,
390
                    (SELECT 't'
391
                     FROM pg_index
392
                     WHERE c.oid = pg_index.indrelid
393
                        AND pg_index.indkey[0] = a.attnum
394
                        AND pg_index.indisprimary = 't'
395
                    ) AS pri,
396
                    (SELECT pg_get_expr(adbin, adrelid)
397
                     FROM pg_attrdef
398
                     WHERE c.oid = pg_attrdef.adrelid
399
                        AND pg_attrdef.adnum=a.attnum
400
                    ) AS default,
401
                    (SELECT pg_description.description
402
                        FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
403
                    ) AS comment
404
                    FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
405 9992
                    WHERE " . $this->getTableWhereClause($table, 'c', 'n') . '
406
                        AND a.attnum > 0
407
                        AND a.attrelid = c.oid
408
                        AND a.atttypid = t.oid
409
                        AND n.oid = c.relnamespace
410
                    ORDER BY a.attnum';
411
    }
412
413
    /**
414
     * {@inheritDoc}
415
     */
416 11768
    public function getCreateDatabaseSQL($name)
417
    {
418 11768
        return 'CREATE DATABASE ' . $name;
419
    }
420
421
    /**
422
     * Returns the SQL statement for disallowing new connections on the given database.
423
     *
424
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
425
     *
426
     * @param string $database The name of the database to disallow new connections for.
427
     *
428
     * @return string
429
     */
430 9940
    public function getDisallowDatabaseConnectionsSQL($database)
431
    {
432 9940
        return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = " . $this->quoteStringLiteral($database);
433
    }
434
435
    /**
436
     * Returns the SQL statement for closing currently active connections on the given database.
437
     *
438
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
439
     *
440
     * @param string $database The name of the database to close currently active connections for.
441
     *
442
     * @return string
443
     */
444 11162
    public function getCloseActiveDatabaseConnectionsSQL($database)
445
    {
446
        return 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '
447 11162
            . $this->quoteStringLiteral($database);
448
    }
449
450
    /**
451
     * {@inheritDoc}
452
     */
453 11585
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
454
    {
455 11585
        $query = '';
456
457 11585
        if ($foreignKey->hasOption('match')) {
458 10406
            $query .= ' MATCH ' . $foreignKey->getOption('match');
459
        }
460
461 11585
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
462
463 11585
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
464 10406
            $query .= ' DEFERRABLE';
465
        } else {
466 11585
            $query .= ' NOT DEFERRABLE';
467
        }
468
469 11585
        if ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) {
470 10406
            $query .= ' INITIALLY DEFERRED';
471
        } else {
472 11585
            $query .= ' INITIALLY IMMEDIATE';
473
        }
474
475 11585
        return $query;
476
    }
477
478
    /**
479
     * {@inheritDoc}
480
     */
481 10400
    public function getAlterTableSQL(TableDiff $diff)
482
    {
483 10400
        $sql         = [];
484 10400
        $commentsSQL = [];
485 10400
        $columnSql   = [];
486
487 10400
        foreach ($diff->addedColumns as $column) {
488 9146
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
489
                continue;
490
            }
491
492 9146
            $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
493 9146
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
494
495 9146
            $comment = $this->getColumnComment($column);
496
497 9146
            if ($comment === null || $comment === '') {
498 9140
                continue;
499
            }
500
501 7781
            $commentsSQL[] = $this->getCommentOnColumnSQL(
502 7781
                $diff->getName($this)->getQuotedName($this),
503 7781
                $column->getQuotedName($this),
504 6
                $comment
505
            );
506
        }
507
508 10400
        foreach ($diff->removedColumns as $column) {
509 10077
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
510
                continue;
511
            }
512
513 10077
            $query = 'DROP ' . $column->getQuotedName($this);
514 10077
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
515
        }
516
517 10400
        foreach ($diff->changedColumns as $columnDiff) {
518
            /** @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
519 10352
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
520
                continue;
521
            }
522
523 10352
            if ($this->isUnchangedBinaryColumn($columnDiff)) {
524 8981
                continue;
525
            }
526
527 10346
            $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
528 10346
            $column        = $columnDiff->column;
529
530 10346
            if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('precision') || $columnDiff->hasChanged('scale') || $columnDiff->hasChanged('fixed')) {
531 10204
                $type = $column->getType();
532
533
                // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
534 10204
                $columnDefinition                  = $column->toArray();
535 10204
                $columnDefinition['autoincrement'] = false;
536
537
                // here was a server version check before, but DBAL API does not support this anymore.
538 10204
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this);
539 10204
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
540
            }
541
542 10346
            if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
543 9840
                $defaultClause = $column->getDefault() === null
544 9816
                    ? ' DROP DEFAULT'
545 9840
                    : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
546 9840
                $query         = 'ALTER ' . $oldColumnName . $defaultClause;
547 9840
                $sql[]         = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
548
            }
549
550 10346
            if ($columnDiff->hasChanged('notnull')) {
551 7887
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
552 7887
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
553
            }
554
555 10346
            if ($columnDiff->hasChanged('autoincrement')) {
556 3351
                if ($column->getAutoincrement()) {
557
                    // add autoincrement
558 3351
                    $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);
559
560 3351
                    $sql[] = 'CREATE SEQUENCE ' . $seqName;
561 3351
                    $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' . $diff->getName($this)->getQuotedName($this) . '))';
562 3351
                    $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
563 3351
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
564
                } else {
565
                    // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
566 3345
                    $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT';
567 3345
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
568
                }
569
            }
570
571 10346
            $newComment = $this->getColumnComment($column);
572 10346
            $oldComment = $this->getOldColumnComment($columnDiff);
573
574 10346
            if ($columnDiff->hasChanged('comment') || ($columnDiff->fromColumn !== null && $oldComment !== $newComment)) {
575 9877
                $commentsSQL[] = $this->getCommentOnColumnSQL(
576 9877
                    $diff->getName($this)->getQuotedName($this),
577 9877
                    $column->getQuotedName($this),
578 18
                    $newComment
579
                );
580
            }
581
582 10346
            if (! $columnDiff->hasChanged('length')) {
583 10340
                continue;
584
            }
585
586 7331
            $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $column->getType()->getSQLDeclaration($column->toArray(), $this);
587 7331
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
588
        }
589
590 10400
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
591 9078
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
592
                continue;
593
            }
594
595 9078
            $oldColumnName = new Identifier($oldColumnName);
596
597 9078
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
598 9078
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
599
        }
600
601 10400
        $tableSql = [];
602
603 10400
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
604 10400
            $sql = array_merge($sql, $commentsSQL);
605
606 10400
            $newName = $diff->getNewName();
607
608 10400
            if ($newName !== false) {
609 7887
                $sql[] = sprintf(
610 12
                    'ALTER TABLE %s RENAME TO %s',
611 7887
                    $diff->getName($this)->getQuotedName($this),
612 7887
                    $newName->getQuotedName($this)
613
                );
614
            }
615
616 10400
            $sql = array_merge(
617 10400
                $this->getPreAlterTableIndexForeignKeySQL($diff),
618 10400
                $sql,
619 10400
                $this->getPostAlterTableIndexForeignKeySQL($diff)
620
            );
621
        }
622
623 10400
        return array_merge($sql, $tableSql, $columnSql);
624
    }
625
626
    /**
627
     * Checks whether a given column diff is a logically unchanged binary type column.
628
     *
629
     * Used to determine whether a column alteration for a binary type column can be skipped.
630
     * Doctrine's {@link \Doctrine\DBAL\Types\BinaryType} and {@link \Doctrine\DBAL\Types\BlobType}
631
     * are mapped to the same database column type on this platform as this platform
632
     * does not have a native VARBINARY/BINARY column type. Therefore the {@link \Doctrine\DBAL\Schema\Comparator}
633
     * might detect differences for binary type columns which do not have to be propagated
634
     * to database as there actually is no difference at database level.
635
     *
636
     * @param ColumnDiff $columnDiff The column diff to check against.
637
     *
638
     * @return bool True if the given column diff is an unchanged binary type column, false otherwise.
639
     */
640 10352
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff)
641
    {
642 10352
        $columnType = $columnDiff->column->getType();
643
644 10352
        if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
645 10346
            return false;
646
        }
647
648 8981
        $fromColumn = $columnDiff->fromColumn instanceof Column ? $columnDiff->fromColumn : null;
649
650 8981
        if ($fromColumn) {
651 8981
            $fromColumnType = $fromColumn->getType();
652
653 8981
            if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
654
                return false;
655
            }
656
657 8981
            return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
658
        }
659
660
        if ($columnDiff->hasChanged('type')) {
661
            return false;
662
        }
663
664
        return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0;
665
    }
666
667
    /**
668
     * {@inheritdoc}
669
     */
670 8791
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
671
    {
672 8791
        if (strpos($tableName, '.') !== false) {
673 7312
            [$schema]     = explode('.', $tableName);
674 7312
            $oldIndexName = $schema . '.' . $oldIndexName;
675
        }
676
677 8791
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
678
    }
679
680
    /**
681
     * {@inheritdoc}
682
     */
683 10101
    public function getCommentOnColumnSQL($tableName, $columnName, $comment)
684
    {
685 10101
        $tableName  = new Identifier($tableName);
686 10101
        $columnName = new Identifier($columnName);
687 10101
        $comment    = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);
688
689 10101
        return sprintf(
690 54
            'COMMENT ON COLUMN %s.%s IS %s',
691 10101
            $tableName->getQuotedName($this),
692 10101
            $columnName->getQuotedName($this),
693 10101
            $comment
694
        );
695
    }
696
697
    /**
698
     * {@inheritDoc}
699
     */
700 10890
    public function getCreateSequenceSQL(Sequence $sequence)
701
    {
702 10890
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
703 10890
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
704 10890
            ' MINVALUE ' . $sequence->getInitialValue() .
705 10890
            ' START ' . $sequence->getInitialValue() .
706 10890
            $this->getSequenceCacheSQL($sequence);
707
    }
708
709
    /**
710
     * {@inheritDoc}
711
     */
712
    public function getAlterSequenceSQL(Sequence $sequence)
713
    {
714
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
715
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
716
            $this->getSequenceCacheSQL($sequence);
717
    }
718
719
    /**
720
     * Cache definition for sequences
721
     *
722
     * @return string
723
     */
724 10890
    private function getSequenceCacheSQL(Sequence $sequence)
725
    {
726 10890
        if ($sequence->getCache() > 1) {
727 9031
            return ' CACHE ' . $sequence->getCache();
728
        }
729
730 10884
        return '';
731
    }
732
733
    /**
734
     * {@inheritDoc}
735
     */
736 10884
    public function getDropSequenceSQL($sequence)
737
    {
738 10884
        if ($sequence instanceof Sequence) {
739
            $sequence = $sequence->getQuotedName($this);
740
        }
741
742 10884
        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
743
    }
744
745
    /**
746
     * {@inheritDoc}
747
     */
748 10163
    public function getCreateSchemaSQL($schemaName)
749
    {
750 10163
        return 'CREATE SCHEMA ' . $schemaName;
751
    }
752
753
    /**
754
     * {@inheritDoc}
755
     */
756 10041
    public function getDropForeignKeySQL($foreignKey, $table)
757
    {
758 10041
        return $this->getDropConstraintSQL($foreignKey, $table);
759
    }
760
761
    /**
762
     * {@inheritDoc}
763
     */
764 11827
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
765
    {
766 11827
        $queryFields = $this->getColumnDeclarationListSQL($columns);
767
768 11827
        if (isset($options['primary']) && ! empty($options['primary'])) {
769 10038
            $keyColumns   = array_unique(array_values($options['primary']));
770 10038
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
771
        }
772
773 11827
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
774
775 11827
        $sql = [$query];
776
777 11827
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
778 9749
            foreach ($options['indexes'] as $index) {
779 9749
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
780
            }
781
        }
782
783 11827
        if (isset($options['foreignKeys'])) {
784 9659
            foreach ((array) $options['foreignKeys'] as $definition) {
785 9395
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
786
            }
787
        }
788
789 11827
        return $sql;
790
    }
791
792
    /**
793
     * Converts a single boolean value.
794
     *
795
     * First converts the value to its native PHP boolean type
796
     * and passes it to the given callback function to be reconverted
797
     * into any custom representation.
798
     *
799
     * @param mixed    $value    The value to convert.
800
     * @param callable $callback The callback function to use for converting the real boolean value.
801
     *
802
     * @return mixed
803
     *
804
     * @throws UnexpectedValueException
805
     */
806 11173
    private function convertSingleBooleanValue($value, $callback)
807
    {
808 11173
        if ($value === null) {
809 10168
            return $callback(null);
810
        }
811
812 11161
        if (is_bool($value) || is_numeric($value)) {
813 11095
            return $callback((bool) $value);
814
        }
815
816 10405
        if (! is_string($value)) {
817
            return $callback(true);
818
        }
819
820
        /**
821
         * Better safe than sorry: http://php.net/in_array#106319
822
         */
823 10405
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
824 10274
            return $callback(false);
825
        }
826
827 10011
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
828 10005
            return $callback(true);
829
        }
830
831 9181
        throw new UnexpectedValueException("Unrecognized boolean literal '${value}'");
832
    }
833
834
    /**
835
     * Converts one or multiple boolean values.
836
     *
837
     * First converts the value(s) to their native PHP boolean type
838
     * and passes them to the given callback function to be reconverted
839
     * into any custom representation.
840
     *
841
     * @param mixed    $item     The value(s) to convert.
842
     * @param callable $callback The callback function to use for converting the real boolean value(s).
843
     *
844
     * @return mixed
845
     */
846 11173
    private function doConvertBooleans($item, $callback)
847
    {
848 11173
        if (is_array($item)) {
849
            foreach ($item as $key => $value) {
850
                $item[$key] = $this->convertSingleBooleanValue($value, $callback);
851
            }
852
853
            return $item;
854
        }
855
856 11173
        return $this->convertSingleBooleanValue($item, $callback);
857
    }
858
859
    /**
860
     * {@inheritDoc}
861
     *
862
     * Postgres wants boolean values converted to the strings 'true'/'false'.
863
     */
864 11089
    public function convertBooleans($item)
865
    {
866 11089
        if (! $this->useBooleanTrueFalseStrings) {
867 9737
            return parent::convertBooleans($item);
868
        }
869
870 11077
        return $this->doConvertBooleans(
871 11077
            $item,
872
            static function ($boolean) {
873 11077
                if ($boolean === null) {
874 9756
                    return 'NULL';
875
                }
876
877 11071
                return $boolean === true ? 'true' : 'false';
878 11077
            }
879
        );
880
    }
881
882
    /**
883
     * {@inheritDoc}
884
     */
885 10232
    public function convertBooleansToDatabaseValue($item)
886
    {
887 10232
        if (! $this->useBooleanTrueFalseStrings) {
888 9456
            return parent::convertBooleansToDatabaseValue($item);
889
        }
890
891 10226
        return $this->doConvertBooleans(
892 10226
            $item,
893
            static function ($boolean) {
894 10220
                return $boolean === null ? null : (int) $boolean;
895 10226
            }
896
        );
897
    }
898
899
    /**
900
     * {@inheritDoc}
901
     */
902 9981
    public function convertFromBoolean($item)
903
    {
904 9981
        if (in_array($item, $this->booleanLiterals['false'], true)) {
905 9336
            return false;
906
        }
907
908 9945
        return parent::convertFromBoolean($item);
909
    }
910
911
    /**
912
     * {@inheritDoc}
913
     */
914 10237
    public function getSequenceNextValSQL($sequenceName)
915
    {
916 10237
        return "SELECT NEXTVAL('" . $sequenceName . "')";
917
    }
918
919
    /**
920
     * {@inheritDoc}
921
     */
922 10356
    public function getSetTransactionIsolationSQL($level)
923
    {
924
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
925 10356
            . $this->_getTransactionIsolationLevelSQL($level);
926
    }
927
928
    /**
929
     * {@inheritDoc}
930
     */
931 9372
    public function getBooleanTypeDeclarationSQL(array $field)
932
    {
933 9372
        return 'BOOLEAN';
934
    }
935
936
    /**
937
     * {@inheritDoc}
938
     */
939 11809
    public function getIntegerTypeDeclarationSQL(array $field)
940
    {
941 11809
        if (! empty($field['autoincrement'])) {
942 11443
            return 'SERIAL';
943
        }
944
945 11652
        return 'INT';
946
    }
947
948
    /**
949
     * {@inheritDoc}
950
     */
951 11071
    public function getBigIntTypeDeclarationSQL(array $field)
952
    {
953 11071
        if (! empty($field['autoincrement'])) {
954 11059
            return 'BIGSERIAL';
955
        }
956
957 3269
        return 'BIGINT';
958
    }
959
960
    /**
961
     * {@inheritDoc}
962
     */
963 11217
    public function getSmallIntTypeDeclarationSQL(array $field)
964
    {
965 11217
        if (! empty($field['autoincrement'])) {
966 10456
            return 'SMALLSERIAL';
967
        }
968
969 11217
        return 'SMALLINT';
970
    }
971
972
    /**
973
     * {@inheritDoc}
974
     */
975 8931
    public function getGuidTypeDeclarationSQL(array $field)
976
    {
977 8931
        return 'UUID';
978
    }
979
980
    /**
981
     * {@inheritDoc}
982
     */
983 10618
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
984
    {
985 10618
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
986
    }
987
988
    /**
989
     * {@inheritDoc}
990
     */
991 2728
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
992
    {
993 2728
        return 'TIMESTAMP(0) WITH TIME ZONE';
994
    }
995
996
    /**
997
     * {@inheritDoc}
998
     */
999 3191
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1000
    {
1001 3191
        return 'DATE';
1002
    }
1003
1004
    /**
1005
     * {@inheritDoc}
1006
     */
1007 3191
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1008
    {
1009 3191
        return 'TIME(0) WITHOUT TIME ZONE';
1010
    }
1011
1012
    /**
1013
     * {@inheritDoc}
1014
     */
1015
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1016
    {
1017
        return '';
1018
    }
1019
1020
    /**
1021
     * {@inheritDoc}
1022
     */
1023 11615
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1024
    {
1025 11615
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
1026 11615
            : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
1027
    }
1028
1029
    /**
1030
     * {@inheritdoc}
1031
     */
1032 10149
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1033
    {
1034 10149
        return 'BYTEA';
1035
    }
1036
1037
    /**
1038
     * {@inheritDoc}
1039
     */
1040 9811
    public function getClobTypeDeclarationSQL(array $field)
1041
    {
1042 9811
        return 'TEXT';
1043
    }
1044
1045
    /**
1046
     * {@inheritDoc}
1047
     */
1048 10427
    public function getName()
1049
    {
1050 10427
        return 'postgresql';
1051
    }
1052
1053
    /**
1054
     * {@inheritDoc}
1055
     *
1056
     * PostgreSQL returns all column names in SQL result sets in lowercase.
1057
     */
1058
    public function getSQLResultCasing($column)
1059
    {
1060
        return strtolower($column);
1061
    }
1062
1063
    /**
1064
     * {@inheritDoc}
1065
     */
1066 2658
    public function getDateTimeTzFormatString()
1067
    {
1068 2658
        return 'Y-m-d H:i:sO';
1069
    }
1070
1071
    /**
1072
     * {@inheritDoc}
1073
     */
1074 2550
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
1075
    {
1076 2550
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
1077
    }
1078
1079
    /**
1080
     * {@inheritDoc}
1081
     */
1082 9358
    public function getTruncateTableSQL($tableName, $cascade = false)
1083
    {
1084 9358
        $tableIdentifier = new Identifier($tableName);
1085 9358
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
1086
1087 9358
        if ($cascade) {
1088
            $sql .= ' CASCADE';
1089
        }
1090
1091 9358
        return $sql;
1092
    }
1093
1094
    /**
1095
     * {@inheritDoc}
1096
     */
1097
    public function getReadLockSQL()
1098
    {
1099
        return 'FOR SHARE';
1100
    }
1101
1102
    /**
1103
     * {@inheritDoc}
1104
     */
1105 11441
    protected function initializeDoctrineTypeMappings()
1106
    {
1107 11441
        $this->doctrineTypeMapping = [
1108
            '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
            'float8'           => 'float',
1123
            'inet'             => 'string',
1124
            '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 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

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