Completed
Pull Request — master (#3628)
by Dallas
62:33
created

PostgreSqlPlatform::getAlterTableDropColumnSQL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 1
c 1
b 0
f 1
dl 0
loc 3
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 2
crap 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
 * PostgreSqlPlatform.
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 11845
    public function setUseBooleanTrueFalseStrings($flag)
74
    {
75 11845
        $this->useBooleanTrueFalseStrings = (bool) $flag;
76 11845
    }
77
78
    /**
79
     * {@inheritDoc}
80
     */
81 13724
    public function getSubstringExpression($value, $from, $length = null)
82
    {
83 13724
        if ($length === null) {
84 13724
            return 'SUBSTRING(' . $value . ' FROM ' . $from . ')';
85
        }
86
87 12335
        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 12335
    public function getRegexpExpression()
102
    {
103 12335
        return 'SIMILAR TO';
104
    }
105
106
    /**
107
     * {@inheritDoc}
108
     */
109 4840
    public function getLocateExpression($str, $substr, $startPos = false)
110
    {
111 4840
        if ($startPos !== false) {
112 4840
            $str = $this->getSubstringExpression($str, $startPos);
113
114 4840
            return 'CASE WHEN (POSITION(' . $substr . ' IN ' . $str . ') = 0) THEN 0 ELSE (POSITION(' . $substr . ' IN ' . $str . ') + ' . ($startPos-1) . ') END';
115
        }
116
117 4840
        return 'POSITION(' . $substr . ' IN ' . $str . ')';
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123 4847
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
124
    {
125 4847
        if ($unit === DateIntervalUnit::QUARTER) {
126 4847
            $interval *= 3;
127 4847
            $unit      = DateIntervalUnit::MONTH;
128
        }
129
130 4847
        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
131
    }
132
133
    /**
134
     * {@inheritDoc}
135
     */
136 4637
    public function getDateDiffExpression($date1, $date2)
137
    {
138 4637
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
139
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144 4282
    public function supportsSequences()
145
    {
146 4282
        return true;
147
    }
148
149
    /**
150
     * {@inheritDoc}
151
     */
152 4251
    public function supportsSchemas()
153
    {
154 4251
        return true;
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160 4076
    public function getDefaultSchemaName()
161
    {
162 4076
        return 'public';
163
    }
164
165
    /**
166
     * {@inheritDoc}
167
     */
168 4163
    public function supportsIdentityColumns()
169
    {
170 4163
        return true;
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176 12751
    public function supportsPartialIndexes()
177
    {
178 12751
        return true;
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184 11819
    public function usesSequenceEmulatedIdentityColumns()
185
    {
186 11819
        return true;
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 12730
    public function getIdentitySequenceName($tableName, $columnName)
193
    {
194 12730
        return $tableName . '_' . $columnName . '_seq';
195
    }
196
197
    /**
198
     * {@inheritDoc}
199
     */
200 13882
    public function supportsCommentOnStatement()
201
    {
202 13882
        return true;
203
    }
204
205
    /**
206
     * {@inheritDoc}
207
     */
208 10
    public function prefersSequences()
209
    {
210 10
        return true;
211
    }
212
213
    /**
214
     * {@inheritDoc}
215
     */
216 14232
    public function hasNativeGuidType()
217
    {
218 14232
        return true;
219
    }
220
221
    /**
222
     * {@inheritDoc}
223
     */
224 4279
    public function getListDatabasesSQL()
225
    {
226 4279
        return 'SELECT datname FROM pg_database';
227
    }
228
229
    /**
230
     * {@inheritDoc}
231
     */
232 4251
    public function getListNamespacesSQL()
233
    {
234 4251
        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 3050
    public function getListSequencesSQL($database)
244
    {
245 3050
        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 4693
    public function getListTablesSQL()
256
    {
257 4693
        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 4160
    public function getListViewsSQL($database)
271
    {
272 4160
        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 12628
    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 12628
                      WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
291
                  )
292
                  AND r.contype = 'f'";
293
    }
294
295
    /**
296
     * {@inheritDoc}
297
     */
298 4352
    public function getCreateViewSQL($name, $sql)
299
    {
300 4352
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
301
    }
302
303
    /**
304
     * {@inheritDoc}
305
     */
306 4352
    public function getDropViewSQL($name)
307
    {
308 4352
        return 'DROP VIEW ' . $name;
309
    }
310
311
    /**
312
     * {@inheritDoc}
313
     */
314 11235
    public function getListTableConstraintsSQL($table)
315
    {
316 11235
        $table = new Identifier($table);
317 11235
        $table = $this->quoteStringLiteral($table->getName());
318
319 11235
        return sprintf(
320
            <<<'SQL'
321 10
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 11235
            $table
335
        );
336
    }
337
338
    /**
339
     * {@inheritDoc}
340
     *
341
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
342
     */
343 12574
    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 12574
                    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 12668
    private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n')
364
    {
365 12668
        $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
366 12668
        if (strpos($table, '.') !== false) {
367 12538
            [$schema, $table] = explode('.', $table);
368 12538
            $schema           = $this->quoteStringLiteral($schema);
369
        } else {
370 12638
            $schema = "ANY(string_to_array((select replace(replace(setting,'\"\$user\"',user),' ','') from pg_catalog.pg_settings where name = 'search_path'),','))";
371
        }
372
373 12668
        $table = new Identifier($table);
374 12668
        $table = $this->quoteStringLiteral($table->getName());
375
376 12668
        return $whereClause . sprintf(
377 60
            '%s.relname = %s AND %s.nspname = %s',
378 12668
            $classAlias,
379 12668
            $table,
380 12668
            $namespaceAlias,
381 12668
            $schema
382
        );
383
    }
384
385
    /**
386
     * {@inheritDoc}
387
     */
388 12538
    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 t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
396
                    (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
397
                      pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
398
                    a.attnotnull AS isnotnull,
399
                    (SELECT 't'
400
                     FROM pg_index
401
                     WHERE c.oid = pg_index.indrelid
402
                        AND pg_index.indkey[0] = a.attnum
403
                        AND pg_index.indisprimary = 't'
404
                    ) AS pri,
405
                    (SELECT pg_get_expr(adbin, adrelid)
406
                     FROM pg_attrdef
407
                     WHERE c.oid = pg_attrdef.adrelid
408
                        AND pg_attrdef.adnum=a.attnum
409
                    ) AS default,
410
                    (SELECT pg_description.description
411
                        FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
412
                    ) AS comment
413
                    FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
414 12538
                    WHERE " . $this->getTableWhereClause($table, 'c', 'n') . '
415
                        AND a.attnum > 0
416
                        AND a.attrelid = c.oid
417
                        AND a.atttypid = t.oid
418
                        AND n.oid = c.relnamespace
419
                    ORDER BY a.attnum';
420
    }
421
422
    /**
423
     * {@inheritDoc}
424
     */
425 13758
    public function getCreateDatabaseSQL($name)
426
    {
427 13758
        return 'CREATE DATABASE ' . $name;
428
    }
429
430
    /**
431
     * Returns the SQL statement for disallowing new connections on the given database.
432
     *
433
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
434
     *
435
     * @param string $database The name of the database to disallow new connections for.
436
     *
437
     * @return string
438
     */
439 12425
    public function getDisallowDatabaseConnectionsSQL($database)
440
    {
441 12425
        return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = " . $this->quoteStringLiteral($database);
442
    }
443
444
    /**
445
     * Returns the SQL statement for closing currently active connections on the given database.
446
     *
447
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
448
     *
449
     * @param string $database The name of the database to close currently active connections for.
450
     *
451
     * @return string
452
     */
453 8658
    public function getCloseActiveDatabaseConnectionsSQL($database)
454
    {
455
        return 'SELECT pg_terminate_backend(procpid) FROM pg_stat_activity WHERE datname = '
456 8658
            . $this->quoteStringLiteral($database);
457
    }
458
459
    /**
460
     * {@inheritDoc}
461
     */
462 13761
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
463
    {
464 13761
        $query = '';
465
466 13761
        if ($foreignKey->hasOption('match')) {
467 12360
            $query .= ' MATCH ' . $foreignKey->getOption('match');
468
        }
469
470 13761
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
471
472 13761
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
473 12360
            $query .= ' DEFERRABLE';
474
        } else {
475 13761
            $query .= ' NOT DEFERRABLE';
476
        }
477
478 13761
        if (($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false)
479 13761
            || ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false)
480
        ) {
481 12360
            $query .= ' INITIALLY DEFERRED';
482
        } else {
483 13761
            $query .= ' INITIALLY IMMEDIATE';
484
        }
485
486 13761
        return $query;
487
    }
488
489
    public function getAlterTableAddColumnSQL(string $table, string $column) : string
490
    {
491
        return sprintf('ALTER TABLE %s ADD %s', $table, $column);
492 12964
    }
493
494 12964
    public function getAlterTableDropColumnSQL(string $table, string $column) : string
495 12964
    {
496 12964
        return sprintf('ALTER TABLE %s DROP %s', $table, $column);
497
    }
498 12964
499 11972
    /**
500
     * @param string[] $columnDefinition
501
     */
502
    public function getAlterTableAlterTypeColumnSQL(string $table, string $column, string $type, array $columnDefinition) : string
503 11972
    {
504 11972
        $using = '';
505
506 11972
        if ($columnDefinition['using'] ?? false) {
507
            $using = 'USING ' . $columnDefinition['using'];
508 11972
        }
509 11962
510
        return trim(sprintf('ALTER TABLE %s ALTER %s TYPE %s %s', $table, $column, $type, $using));
511
    }
512 10685
513 10685
    public function getAlterTableAlterDefaultSQL(string $table, string $columnName, Column $column) : string
514 10685
    {
515 10
        if ($column->getDefault() === null) {
516
            return sprintf('ALTER TABLE %s ALTER %s DROP DEFAULT', $table, $columnName);
517
        }
518
519 12964
        return sprintf(
520 12548
            'ALTER TABLE %s ALTER %s SET %s',
521
            $table,
522
            $columnName,
523
            trim($this->getDefaultValueDeclarationSQL($column->toArray()))
524 12548
        );
525 12548
    }
526
527
    public function getAlterTableRenameColumnSQL(string $table, string $oldColumn, string $newColumn) : string
528 12964
    {
529
        return sprintf('ALTER TABLE %s RENAME COLUMN %s TO %s', $table, $oldColumn, $newColumn);
530 12884
    }
531
532
    public function getAlterTableAlterNotNullSQL(string $table, string $columnName, Column $column) : string
533
    {
534 12884
        return sprintf(
535 11460
            'ALTER TABLE %s ALTER %s %s NOT NULL',
536
            $table,
537
            $columnName,
538 12874
            $column->getNotnull() ? 'SET' : 'DROP'
539 12874
        );
540
    }
541 12874
542 12695
    public function getAlterTableRenameToSQL(string $table, string $name) : string
543
    {
544
        return sprintf('ALTER TABLE %s RENAME TO %s', $table, $name);
545 12695
    }
546 12695
547
    public function getSetSequenceValueSQL(string $table, string $column, string $sequence) : string
548
    {
549 12695
        return 'SELECT setval(' . "'" . $sequence . "'" . ', (SELECT MAX(' . $column . ') FROM ' . $table . '))';
550 12695
    }
551
552
    public function getCreateSimpleSequenceSQL(string $sequence) : string
553 12874
    {
554 12306
        return sprintf('CREATE SEQUENCE %s', $sequence);
555 12275
    }
556 12306
557 12306
    /**
558 12306
     * {@inheritDoc}
559
     */
560
    public function getAlterTableSQL(TableDiff $diff)
561 12874
    {
562 10795
        $sql         = [];
563 10795
        $commentsSQL = [];
564
        $columnSql   = [];
565
566 12874
        foreach ($diff->addedColumns as $column) {
567 4422
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
568
                continue;
569 4422
            }
570
571 4422
            $sql[] = $this->getAlterTableAddColumnSQL(
572 4422
                $diff->getName($this)->getQuotedName($this),
573 4422
                $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray())
574 4422
            );
575
576
            $comment = $this->getColumnComment($column);
577 4415
578 4415
            if ($comment === null || $comment === '') {
579
                continue;
580
            }
581
582 12874
            $commentsSQL[] = $this->getCommentOnColumnSQL(
583 12874
                $diff->getName($this)->getQuotedName($this),
584
                $column->getQuotedName($this),
585 12874
                $comment
586 12345
            );
587 12345
        }
588 12345
589 30
        foreach ($diff->removedColumns as $column) {
590
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
591
                continue;
592
            }
593 12874
594 12864
            $sql[] = $this->getAlterTableDropColumnSQL(
595
                $diff->getName($this)->getQuotedName($this),
596
                $column->getQuotedName($this)
597 10210
            );
598 10210
        }
599
600
        foreach ($diff->changedColumns as $columnDiff) {
601 12964
            /** @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
602 11901
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
603
                continue;
604
            }
605
606 11901
            if ($this->isUnchangedBinaryColumn($columnDiff)) {
607
                continue;
608 11901
            }
609 11901
610
            $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
611
            $column        = $columnDiff->column;
612 12964
613
            if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('precision') || $columnDiff->hasChanged('scale') || $columnDiff->hasChanged('fixed')) {
614 12964
                $type = $column->getType();
615 12964
616
                // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
617 12964
                $columnDefinition                  = $column->toArray();
618
                $columnDefinition['autoincrement'] = false;
619 12964
620 10795
                // here was a server version check before, but DBAL API does not support this anymore.
621 20
                $sql[] = $this->getAlterTableAlterTypeColumnSQL(
622 10795
                    $diff->getName($this)->getQuotedName($this),
623 10795
                    $oldColumnName,
624
                    $type->getSQLDeclaration($columnDefinition, $this),
625
                    $columnDefinition
626
                );
627 12964
            }
628 12964
629 12964
            if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
630 12964
                $sql[] = $this->getAlterTableAlterDefaultSQL(
631
                    $diff->getName($this)->getQuotedName($this),
632
                    $oldColumnName,
633
                    $column
634 12964
                );
635
            }
636
637
            if ($columnDiff->hasChanged('notnull')) {
638
                $sql[] = $this->getAlterTableAlterNotNullSQL(
639
                    $diff->getName($this)->getQuotedName($this),
640
                    $oldColumnName,
641
                    $column
642
                );
643
            }
644
645
            if ($columnDiff->hasChanged('autoincrement')) {
646
                if ($column->getAutoincrement()) {
647
                    // add autoincrement
648
                    $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);
649
650
                    $sql[] = $this->getCreateSimpleSequenceSQL($seqName);
651 12884
                    $sql[] = $this->getSetSequenceValueSQL(
652
                        $diff->getName($this)->getQuotedName($this),
653 12884
                        $oldColumnName,
654
                        $seqName
655 12884
                    );
656 12874
                    $sql[] = $this->getAlterTableAlterDefaultSQL(
657
                        $diff->getName($this)->getQuotedName($this),
658
                        $oldColumnName,
659 11460
                        $column->setDefault('nextval(' . "'" . $seqName . "'" . ')')
660
                    );
661 11460
                } else {
662 11460
                    // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
663
                    $sql[] = $this->getAlterTableAlterDefaultSQL(
664 11460
                        $diff->getName($this)->getQuotedName($this),
665
                        $oldColumnName,
666
                        $column->setDefault(null)
667
                    );
668 11460
                }
669
            }
670
671
            $newComment = $this->getColumnComment($column);
672
            $oldComment = $this->getOldColumnComment($columnDiff);
673
674
            if ($columnDiff->hasChanged('comment') || ($columnDiff->fromColumn !== null && $oldComment !== $newComment)) {
675
                $commentsSQL[] = $this->getCommentOnColumnSQL(
676
                    $diff->getName($this)->getQuotedName($this),
677
                    $column->getQuotedName($this),
678
                    $newComment
679
                );
680
            }
681 11622
682
            if (! $columnDiff->hasChanged('length')) {
683 11622
                continue;
684 10195
            }
685 10195
686
            $sql[] = $this->getAlterTableAlterTypeColumnSQL(
687
                $diff->getName($this)->getQuotedName($this),
688 11622
                $oldColumnName,
689
                $column->getType()->getSQLDeclaration($column->toArray(), $this),
690
                $column->toArray()
691
            );
692
        }
693
694 12591
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
695
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
696 12591
                continue;
697 12591
            }
698 12591
699
            $oldColumnName = new Identifier($oldColumnName);
700 12591
701 90
            $sql[] = $this->getAlterTableRenameColumnSQL(
702 12591
                $diff->getName($this)->getQuotedName($this),
703 12591
                $oldColumnName->getQuotedName($this),
704 12591
                $column->getQuotedName($this)
705
            );
706
        }
707
708
        $tableSql = [];
709
710
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
711 12950
            $sql = array_merge($sql, $commentsSQL);
712
713 12950
            $newName = $diff->getNewName();
714 12950
715 12950
            if ($newName !== false) {
716 12950
                $sql[] = $this->getAlterTableRenameToSQL(
717 12950
                    $diff->getName($this)->getQuotedName($this),
718
                    $newName->getQuotedName($this)
719
                );
720
            }
721
722
            $sql = array_merge(
723
                $this->getPreAlterTableIndexForeignKeySQL($diff),
724
                $sql,
725
                $this->getPostAlterTableIndexForeignKeySQL($diff)
726
            );
727
        }
728
729
        return array_merge($sql, $tableSql, $columnSql);
730
    }
731
732
    /**
733
     * Checks whether a given column diff is a logically unchanged binary type column.
734
     *
735 12950
     * Used to determine whether a column alteration for a binary type column can be skipped.
736
     * Doctrine's {@link \Doctrine\DBAL\Types\BinaryType} and {@link \Doctrine\DBAL\Types\BlobType}
737 12950
     * are mapped to the same database column type on this platform as this platform
738 11510
     * does not have a native VARBINARY/BINARY column type. Therefore the {@link \Doctrine\DBAL\Schema\Comparator}
739
     * might detect differences for binary type columns which do not have to be propagated
740
     * to database as there actually is no difference at database level.
741 12940
     *
742
     * @param ColumnDiff $columnDiff The column diff to check against.
743
     *
744
     * @return bool True if the given column diff is an unchanged binary type column, false otherwise.
745
     */
746
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff)
747 12940
    {
748
        $columnType = $columnDiff->column->getType();
749 12940
750
        if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
751
            return false;
752
        }
753 12940
754
        $fromColumn = $columnDiff->fromColumn instanceof Column ? $columnDiff->fromColumn : null;
755
756
        if ($fromColumn) {
757
            $fromColumnType = $fromColumn->getType();
758
759 12631
            if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
760
                return false;
761 12631
            }
762
763
            return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
764
        }
765
766
        if ($columnDiff->hasChanged('type')) {
767 12503
            return false;
768
        }
769 12503
770
        return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0;
771
    }
772
773
    /**
774
     * {@inheritdoc}
775 13872
     */
776
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
777 13872
    {
778
        if (strpos($tableName, '.') !== false) {
779 13872
            [$schema]     = explode('.', $tableName);
780 12816
            $oldIndexName = $schema . '.' . $oldIndexName;
781 12816
        }
782
783
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
784 13872
    }
785
786 13872
    /**
787
     * {@inheritdoc}
788 13872
     */
789 12697
    public function getCommentOnColumnSQL($tableName, $columnName, $comment)
790 12697
    {
791
        $tableName  = new Identifier($tableName);
792
        $columnName = new Identifier($columnName);
793
        $comment    = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);
794 13872
795 12444
        return sprintf(
796 12353
            'COMMENT ON COLUMN %s.%s IS %s',
797
            $tableName->getQuotedName($this),
798
            $columnName->getQuotedName($this),
799
            $comment
800 13872
        );
801
    }
802
803
    /**
804
     * {@inheritDoc}
805
     */
806
    public function getCreateSequenceSQL(Sequence $sequence)
807
    {
808
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
809
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
810
            ' MINVALUE ' . $sequence->getInitialValue() .
811
            ' START ' . $sequence->getInitialValue() .
812
            $this->getSequenceCacheSQL($sequence);
813
    }
814
815
    /**
816
     * {@inheritDoc}
817 13443
     */
818
    public function getAlterSequenceSQL(Sequence $sequence)
819 13443
    {
820 12276
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
821
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
822
            $this->getSequenceCacheSQL($sequence);
823 13423
    }
824 13313
825
    /**
826
     * Cache definition for sequences
827 12470
     *
828
     * @return string
829
     */
830
    private function getSequenceCacheSQL(Sequence $sequence)
831
    {
832
        if ($sequence->getCache() > 1) {
833
            return ' CACHE ' . $sequence->getCache();
834 12470
        }
835 12356
836
        return '';
837
    }
838 12035
839 12025
    /**
840
     * {@inheritDoc}
841
     */
842 11660
    public function getDropSequenceSQL($sequence)
843
    {
844
        if ($sequence instanceof Sequence) {
845
            $sequence = $sequence->getQuotedName($this);
846
        }
847
848
        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
849
    }
850
851
    /**
852
     * {@inheritDoc}
853
     */
854
    public function getCreateSchemaSQL($schemaName)
855
    {
856
        return 'CREATE SCHEMA ' . $schemaName;
857 13443
    }
858
859 13443
    /**
860
     * {@inheritDoc}
861
     */
862
    public function getDropForeignKeySQL($foreignKey, $table)
863
    {
864
        return $this->getDropConstraintSQL($foreignKey, $table);
865
    }
866
867 13443
    /**
868
     * {@inheritDoc}
869
     */
870
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
871
    {
872
        $queryFields = $this->getColumnDeclarationListSQL($columns);
873
874
        if (isset($options['primary']) && ! empty($options['primary'])) {
875 13303
            $keyColumns   = array_unique(array_values($options['primary']));
876
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
877 13303
        }
878 11845
879
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
880
881 13283
        $sql = [$query];
882 13283
883
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
884 13283
            foreach ($options['indexes'] as $index) {
885 11860
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
886
            }
887
        }
888 13273
889 13283
        if (isset($options['foreignKeys'])) {
890
            foreach ((array) $options['foreignKeys'] as $definition) {
891
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
892
            }
893
        }
894
895
        return $sql;
896 12404
    }
897
898 12404
    /**
899 11760
     * Converts a single boolean value.
900
     *
901
     * First converts the value to its native PHP boolean type
902 12394
     * and passes it to the given callback function to be reconverted
903 12394
     * into any custom representation.
904
     *
905 12384
     * @param mixed    $value    The value to convert.
906 12394
     * @param callable $callback The callback function to use for converting the real boolean value.
907
     *
908
     * @return mixed
909
     *
910
     * @throws UnexpectedValueException
911
     */
912
    private function convertSingleBooleanValue($value, $callback)
913 12295
    {
914
        if ($value === null) {
915 12295
            return $callback(null);
916 11760
        }
917
918
        if (is_bool($value) || is_numeric($value)) {
919 12235
            return $callback((bool) $value);
920
        }
921
922
        if (! is_string($value)) {
923
            return $callback(true);
924
        }
925 12189
926
        /**
927 12189
         * Better safe than sorry: http://php.net/in_array#106319
928
         */
929
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
930
            return $callback(false);
931
        }
932
933 12310
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
934
            return $callback(true);
935
        }
936 12310
937
        throw new UnexpectedValueException("Unrecognized boolean literal '${value}'");
938
    }
939
940
    /**
941
     * Converts one or multiple boolean values.
942 12251
     *
943
     * First converts the value(s) to their native PHP boolean type
944 12251
     * and passes them to the given callback function to be reconverted
945
     * into any custom representation.
946
     *
947
     * @param mixed    $item     The value(s) to convert.
948
     * @param callable $callback The callback function to use for converting the real boolean value(s).
949
     *
950 13840
     * @return mixed
951
     */
952 13840
    private function doConvertBooleans($item, $callback)
953 13612
    {
954
        if (is_array($item)) {
955
            foreach ($item as $key => $value) {
956 13674
                $item[$key] = $this->convertSingleBooleanValue($value, $callback);
957
            }
958
959
            return $item;
960
        }
961
962 13132
        return $this->convertSingleBooleanValue($item, $callback);
963
    }
964 13132
965 13118
    /**
966
     * {@inheritDoc}
967
     *
968 4328
     * Postgres wants boolean values converted to the strings 'true'/'false'.
969
     */
970
    public function convertBooleans($item)
971
    {
972
        if (! $this->useBooleanTrueFalseStrings) {
973
            return parent::convertBooleans($item);
974 13244
        }
975
976 13244
        return $this->doConvertBooleans(
977
            $item,
978
            static function ($boolean) {
979
                if ($boolean === null) {
980
                    return 'NULL';
981
                }
982 11410
983
                return $boolean === true ? 'true' : 'false';
984 11410
            }
985
        );
986
    }
987
988
    /**
989
     * {@inheritDoc}
990 13036
     */
991
    public function convertBooleansToDatabaseValue($item)
992 13036
    {
993
        if (! $this->useBooleanTrueFalseStrings) {
994
            return parent::convertBooleansToDatabaseValue($item);
995
        }
996
997
        return $this->doConvertBooleans(
998 3703
            $item,
999
            static function ($boolean) {
1000 3703
                return $boolean === null ? null : (int) $boolean;
1001
            }
1002
        );
1003
    }
1004
1005
    /**
1006 4623
     * {@inheritDoc}
1007
     */
1008 4623
    public function convertFromBoolean($item)
1009
    {
1010
        if (in_array(strtolower($item), $this->booleanLiterals['false'], true)) {
1011
            return false;
1012
        }
1013
1014 4616
        return parent::convertFromBoolean($item);
1015
    }
1016 4616
1017
    /**
1018
     * {@inheritDoc}
1019
     */
1020
    public function getSequenceNextValSQL($sequenceName)
1021
    {
1022
        return "SELECT NEXTVAL('" . $sequenceName . "')";
1023
    }
1024
1025
    /**
1026
     * {@inheritDoc}
1027
     */
1028
    public function getSetTransactionIsolationSQL($level)
1029
    {
1030
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
1031
            . $this->_getTransactionIsolationLevelSQL($level);
1032
    }
1033
1034
    /**
1035
     * {@inheritDoc}
1036
     */
1037
    public function getBooleanTypeDeclarationSQL(array $field)
1038
    {
1039
        return 'BOOLEAN';
1040 13652
    }
1041
1042 13652
    /**
1043 13652
     * {@inheritDoc}
1044
     */
1045
    public function getIntegerTypeDeclarationSQL(array $field)
1046
    {
1047
        if (! empty($field['autoincrement'])) {
1048
            return 'SERIAL';
1049 12638
        }
1050
1051 12638
        return 'INT';
1052
    }
1053
1054
    /**
1055
     * {@inheritDoc}
1056
     */
1057 12592
    public function getBigIntTypeDeclarationSQL(array $field)
1058
    {
1059 12592
        if (! empty($field['autoincrement'])) {
1060
            return 'BIGSERIAL';
1061
        }
1062
1063
        return 'BIGINT';
1064
    }
1065 12880
1066
    /**
1067 12880
     * {@inheritDoc}
1068
     */
1069
    public function getSmallIntTypeDeclarationSQL(array $field)
1070
    {
1071
        return 'SMALLINT';
1072
    }
1073
1074
    /**
1075
     * {@inheritDoc}
1076
     */
1077
    public function getGuidTypeDeclarationSQL(array $field)
1078
    {
1079
        return 'UUID';
1080
    }
1081
1082
    /**
1083 3619
     * {@inheritDoc}
1084
     */
1085 3619
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1086
    {
1087
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
1088
    }
1089
1090
    /**
1091 3493
     * {@inheritDoc}
1092
     */
1093 3493
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
1094
    {
1095
        return 'TIMESTAMP(0) WITH TIME ZONE';
1096
    }
1097
1098
    /**
1099 12296
     * {@inheritDoc}
1100
     */
1101 12296
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1102 12296
    {
1103
        return 'DATE';
1104 12296
    }
1105
1106
    /**
1107
     * {@inheritDoc}
1108 12296
     */
1109
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1110
    {
1111
        return 'TIME(0) WITHOUT TIME ZONE';
1112
    }
1113
1114
    /**
1115
     * {@inheritDoc}
1116
     *
1117
     * @deprecated Use application-generated UUIDs instead
1118
     */
1119
    public function getGuidExpression()
1120
    {
1121
        return 'UUID_GENERATE_V4()';
1122 13474
    }
1123
1124 13474
    /**
1125
     * {@inheritDoc}
1126
     */
1127
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1128
    {
1129
        return '';
1130
    }
1131
1132
    /**
1133
     * {@inheritDoc}
1134
     */
1135
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1136
    {
1137
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
1138
            : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
1139
    }
1140
1141
    /**
1142
     * {@inheritdoc}
1143
     */
1144
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1145
    {
1146
        return 'BYTEA';
1147
    }
1148
1149
    /**
1150
     * {@inheritDoc}
1151
     */
1152
    public function getClobTypeDeclarationSQL(array $field)
1153
    {
1154
        return 'TEXT';
1155
    }
1156
1157
    /**
1158
     * {@inheritDoc}
1159
     */
1160
    public function getName()
1161
    {
1162
        return 'postgresql';
1163
    }
1164
1165 13474
    /**
1166
     * {@inheritDoc}
1167
     *
1168
     * PostgreSQL returns all column names in SQL result sets in lowercase.
1169
     */
1170 13652
    public function getSQLResultCasing($column)
1171
    {
1172 13652
        return strtolower($column);
1173
    }
1174
1175
    /**
1176
     * {@inheritDoc}
1177
     */
1178 12648
    public function getDateTimeTzFormatString()
1179
    {
1180 12648
        return 'Y-m-d H:i:sO';
1181
    }
1182
1183
    /**
1184
     * {@inheritDoc}
1185
     */
1186 12648
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
1187
    {
1188 12648
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
1189
    }
1190
1191
    /**
1192
     * {@inheritDoc}
1193
     */
1194 2629
    public function getTruncateTableSQL($tableName, $cascade = false)
1195
    {
1196 2629
        $tableIdentifier = new Identifier($tableName);
1197
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
1198
1199
        if ($cascade) {
1200
            $sql .= ' CASCADE';
1201
        }
1202 13182
1203
        return $sql;
1204 13182
    }
1205
1206
    /**
1207
     * {@inheritDoc}
1208
     */
1209
    public function getReadLockSQL()
1210 13972
    {
1211
        return 'FOR SHARE';
1212 13972
    }
1213 13642
1214
    /**
1215
     * {@inheritDoc}
1216 12948
     */
1217
    protected function initializeDoctrineTypeMappings()
1218
    {
1219
        $this->doctrineTypeMapping = [
1220
            'smallint'      => 'smallint',
1221
            'int2'          => 'smallint',
1222 13972
            'serial'        => 'integer',
1223
            'serial4'       => 'integer',
1224 13972
            'int'           => 'integer',
1225 13972
            'int4'          => 'integer',
1226 13972
            'integer'       => 'integer',
1227
            'bigserial'     => 'bigint',
1228
            'serial8'       => 'bigint',
1229
            'bigint'        => 'bigint',
1230
            'int8'          => 'bigint',
1231
            'bool'          => 'boolean',
1232 12864
            'boolean'       => 'boolean',
1233
            'text'          => 'text',
1234 12864
            'tsvector'      => 'text',
1235 11650
            'varchar'       => 'string',
1236
            'interval'      => 'string',
1237
            '_varchar'      => 'string',
1238 12652
            'char'          => 'string',
1239 12652
            'bpchar'        => 'string',
1240
            'inet'          => 'string',
1241
            'date'          => 'date',
1242 12652
            'datetime'      => 'datetime',
1243 12652
            'timestamp'     => 'datetime',
1244
            'timestamptz'   => 'datetimetz',
1245
            'time'          => 'time',
1246 13682
            'timetz'        => 'time',
1247
            'float'         => 'float',
1248 13682
            'float4'        => 'float',
1249
            'float8'        => 'float',
1250
            'double'        => 'float',
1251 12874
            'double precision' => 'float',
1252
            'real'          => 'float',
1253 12874
            'decimal'       => 'decimal',
1254
            'money'         => 'decimal',
1255
            'numeric'       => 'decimal',
1256
            'year'          => 'date',
1257
            'uuid'          => 'guid',
1258
            'bytea'         => 'blob',
1259
        ];
1260
    }
1261
1262
    /**
1263
     * {@inheritDoc}
1264
     */
1265
    public function getVarcharMaxLength()
1266
    {
1267
        return 65535;
1268
    }
1269
1270
    /**
1271
     * {@inheritdoc}
1272
     */
1273
    public function getBinaryMaxLength()
1274
    {
1275
        return 0;
1276
    }
1277
1278
    /**
1279
     * {@inheritdoc}
1280
     */
1281
    public function getBinaryDefaultLength()
1282
    {
1283
        return 0;
1284
    }
1285
1286
    /**
1287
     * {@inheritDoc}
1288
     */
1289
    protected function getReservedKeywordsClass()
1290
    {
1291
        return Keywords\PostgreSQLKeywords::class;
1292
    }
1293
1294
    /**
1295
     * {@inheritDoc}
1296
     */
1297
    public function getBlobTypeDeclarationSQL(array $field)
1298
    {
1299
        return 'BYTEA';
1300
    }
1301
1302
    /**
1303
     * {@inheritdoc}
1304
     */
1305
    public function getDefaultValueDeclarationSQL($field)
1306
    {
1307
        if ($this->isSerialField($field)) {
1308
            return '';
1309
        }
1310
1311
        return parent::getDefaultValueDeclarationSQL($field);
1312
    }
1313
1314
    /**
1315
     * @param mixed[] $field
1316
     */
1317
    private function isSerialField(array $field) : bool
1318
    {
1319
        return isset($field['type'], $field['autoincrement'])
1320
            && $field['autoincrement'] === true
1321
            && $this->isNumericType($field['type']);
1322
    }
1323
1324
    /**
1325
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
1326
     */
1327
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
1328
    {
1329
        if (! $columnDiff->fromColumn) {
1330
            return $columnDiff->hasChanged('type');
1331
        }
1332
1333
        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
1334
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());
1335
1336
        // default should not be changed when switching between numeric types and the default comes from a sequence
1337
        return $columnDiff->hasChanged('type')
1338
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
1339
    }
1340
1341
    private function isNumericType(Type $type) : bool
1342
    {
1343
        return $type instanceof IntegerType || $type instanceof BigIntType;
1344
    }
1345
1346
    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
1347
    {
1348
        return $columnDiff->fromColumn ? $this->getColumnComment($columnDiff->fromColumn) : null;
1349
    }
1350
}
1351