Completed
Push — 3.0.x ( 0d9a8c...f82f5c )
by Sergei
25s queued 17s
created

getColumnCollationDeclarationSQL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

1152
            '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...
1153
            'jsonb'            => Type::JSON,
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::JSON has been deprecated: Use {@see Types::JSON} instead. ( Ignorable by Annotation )

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

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

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

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

Loading history...
1154
            'money'            => 'decimal',
1155
            'numeric'          => 'decimal',
1156
            'serial'           => 'integer',
1157
            'serial4'          => 'integer',
1158
            'serial8'          => 'bigint',
1159
            'real'             => 'float',
1160
            'smallint'         => 'smallint',
1161
            'text'             => 'text',
1162
            'time'             => 'time',
1163
            'timestamp'        => 'datetime',
1164
            'timestamptz'      => 'datetimetz',
1165
            'timetz'           => 'time',
1166
            'tsvector'         => 'text',
1167
            'uuid'             => 'guid',
1168
            'varchar'          => 'string',
1169
            'year'             => 'date',
1170
            '_varchar'         => 'string',
1171
        ];
1172
    }
1173
1174
    /**
1175
     * {@inheritDoc}
1176
     */
1177
    public function getVarcharMaxLength()
1178
    {
1179
        return 65535;
1180
    }
1181
1182
    /**
1183
     * {@inheritdoc}
1184
     */
1185
    public function getBinaryMaxLength()
1186
    {
1187
        return 0;
1188
    }
1189
1190
    /**
1191
     * {@inheritdoc}
1192
     */
1193
    public function getBinaryDefaultLength()
1194
    {
1195
        return 0;
1196
    }
1197
1198
    /**
1199
     * {@inheritdoc}
1200
     */
1201
    public function hasNativeJsonType()
1202
    {
1203
        return true;
1204
    }
1205
1206
    /**
1207
     * {@inheritDoc}
1208
     */
1209
    protected function getReservedKeywordsClass()
1210
    {
1211
        return Keywords\PostgreSQLKeywords::class;
1212
    }
1213
1214
    /**
1215
     * {@inheritDoc}
1216
     */
1217
    public function getBlobTypeDeclarationSQL(array $field)
1218
    {
1219
        return 'BYTEA';
1220
    }
1221
1222
    /**
1223
     * {@inheritdoc}
1224
     */
1225
    public function getDefaultValueDeclarationSQL($field)
1226
    {
1227
        if ($this->isSerialField($field)) {
1228
            return '';
1229
        }
1230
1231
        return parent::getDefaultValueDeclarationSQL($field);
1232
    }
1233
1234
    /**
1235
     * {@inheritdoc}
1236
     */
1237
    public function supportsColumnCollation()
1238
    {
1239
        return true;
1240
    }
1241
1242
    /**
1243
     * {@inheritdoc}
1244
     */
1245
    public function getColumnCollationDeclarationSQL($collation)
1246
    {
1247
        return 'COLLATE ' . $this->quoteSingleIdentifier($collation);
1248
    }
1249
1250
    /**
1251
     * {@inheritdoc}
1252
     */
1253
    public function getJsonTypeDeclarationSQL(array $field)
1254
    {
1255
        if (! empty($field['jsonb'])) {
1256
            return 'JSONB';
1257
        }
1258
1259
        return 'JSON';
1260
    }
1261
1262
    /**
1263
     * @param mixed[] $field
1264
     */
1265
    private function isSerialField(array $field) : bool
1266
    {
1267
        return isset($field['type'], $field['autoincrement'])
1268
            && $field['autoincrement'] === true
1269
            && $this->isNumericType($field['type']);
1270
    }
1271
1272
    /**
1273
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
1274
     */
1275
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
1276
    {
1277
        if (! $columnDiff->fromColumn) {
1278
            return $columnDiff->hasChanged('type');
1279
        }
1280
1281
        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
1282
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());
1283
1284
        // default should not be changed when switching between numeric types and the default comes from a sequence
1285
        return $columnDiff->hasChanged('type')
1286
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
1287
    }
1288
1289
    private function isNumericType(Type $type) : bool
1290
    {
1291
        return $type instanceof IntegerType || $type instanceof BigIntType;
1292
    }
1293
1294
    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
1295
    {
1296
        return $columnDiff->fromColumn ? $this->getColumnComment($columnDiff->fromColumn) : null;
1297
    }
1298
1299
    public function getListTableMetadataSQL(string $table, ?string $schema = null) : string
1300
    {
1301
        if ($schema !== null) {
1302
            $table = $schema . '.' . $table;
1303
        }
1304
1305
        return sprintf(
1306
            <<<'SQL'
1307
SELECT obj_description(%s::regclass) AS table_comment;
1308
SQL
1309
            ,
1310
            $this->quoteStringLiteral($table)
1311
        );
1312
    }
1313
}
1314