Completed
Push — develop ( dcb0ff...425513 )
by Sergei
23s queued 13s
created

PostgreSqlPlatform::getCurrentDatabaseExpression()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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