Failed Conditions
Pull Request — 2.10.x (#3979)
by Ben
31:35 queued 27:39
created

PostgreSqlPlatform::getAlterTableSQL()   F

Complexity

Conditions 29
Paths > 20000

Size

Total Lines 143
Code Lines 83

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 76
CRAP Score 29.1051

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 83
c 1
b 0
f 0
dl 0
loc 143
rs 0
ccs 76
cts 80
cp 0.95
cc 29
nc 23652
nop 1
crap 29.1051

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
     * @return void
74
     */
75 10428
    public function setUseBooleanTrueFalseStrings($flag)
76
    {
77 10428
        $this->useBooleanTrueFalseStrings = (bool) $flag;
78 10428
    }
79
80
    /**
81
     * {@inheritDoc}
82
     */
83 12265
    public function getSubstringExpression($value, $from, $length = null)
84
    {
85 12265
        if ($length === null) {
86 12265
            return 'SUBSTRING(' . $value . ' FROM ' . $from . ')';
87
        }
88
89 10868
        return 'SUBSTRING(' . $value . ' FROM ' . $from . ' FOR ' . $length . ')';
90
    }
91
92
    /**
93
     * {@inheritDoc}
94
     */
95
    public function getNowExpression()
96
    {
97
        return 'LOCALTIMESTAMP(0)';
98
    }
99
100
    /**
101
     * {@inheritDoc}
102
     */
103 10868
    public function getRegexpExpression()
104
    {
105 10868
        return 'SIMILAR TO';
106
    }
107
108
    /**
109
     * {@inheritDoc}
110
     */
111 4855
    public function getLocateExpression($str, $substr, $startPos = false)
112
    {
113 4855
        if ($startPos !== false) {
114 4855
            $str = $this->getSubstringExpression($str, $startPos);
115
116 4855
            return 'CASE WHEN (POSITION(' . $substr . ' IN ' . $str . ') = 0) THEN 0 ELSE (POSITION(' . $substr . ' IN ' . $str . ') + ' . ($startPos-1) . ') END';
117
        }
118
119 4855
        return 'POSITION(' . $substr . ' IN ' . $str . ')';
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125 4862
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
126
    {
127 4862
        if ($unit === DateIntervalUnit::QUARTER) {
128 4862
            $interval *= 3;
129 4862
            $unit      = DateIntervalUnit::MONTH;
130
        }
131
132 4862
        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
133
    }
134
135
    /**
136
     * {@inheritDoc}
137
     */
138 4666
    public function getDateDiffExpression($date1, $date2)
139
    {
140 4666
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
141
    }
142
143
    /**
144
     * {@inheritDoc}
145
     */
146 4281
    public function supportsSequences()
147
    {
148 4281
        return true;
149
    }
150
151
    /**
152
     * {@inheritDoc}
153
     */
154 4260
    public function supportsSchemas()
155
    {
156 4260
        return true;
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 4085
    public function getDefaultSchemaName()
163
    {
164 4085
        return 'public';
165
    }
166
167
    /**
168
     * {@inheritDoc}
169
     */
170 4162
    public function supportsIdentityColumns()
171
    {
172 4162
        return true;
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178 11389
    public function supportsPartialIndexes()
179
    {
180 11389
        return true;
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186 10445
    public function usesSequenceEmulatedIdentityColumns()
187
    {
188 10445
        return true;
189
    }
190
191
    /**
192
     * {@inheritdoc}
193
     */
194 11368
    public function getIdentitySequenceName($tableName, $columnName)
195
    {
196 11368
        return $tableName . '_' . $columnName . '_seq';
197
    }
198
199
    /**
200
     * {@inheritDoc}
201
     */
202 12297
    public function supportsCommentOnStatement()
203
    {
204 12297
        return true;
205
    }
206
207
    /**
208
     * {@inheritDoc}
209
     */
210
    public function prefersSequences()
211
    {
212
        return true;
213
    }
214
215
    /**
216
     * {@inheritDoc}
217
     */
218 12297
    public function hasNativeGuidType()
219
    {
220 12297
        return true;
221
    }
222
223
    /**
224
     * {@inheritDoc}
225
     */
226 4288
    public function getListDatabasesSQL()
227
    {
228 4288
        return 'SELECT datname FROM pg_database';
229
    }
230
231
    /**
232
     * {@inheritDoc}
233
     */
234 4260
    public function getListNamespacesSQL()
235
    {
236 4260
        return "SELECT schema_name AS nspname
237
                FROM   information_schema.schemata
238
                WHERE  schema_name NOT LIKE 'pg\_%'
239
                AND    schema_name != 'information_schema'";
240
    }
241
242
    /**
243
     * {@inheritDoc}
244
     */
245 3056
    public function getListSequencesSQL($database)
246
    {
247 3056
        return "SELECT sequence_name AS relname,
248
                       sequence_schema AS schemaname
249
                FROM   information_schema.sequences
250
                WHERE  sequence_schema NOT LIKE 'pg\_%'
251
                AND    sequence_schema != 'information_schema'";
252
    }
253
254
    /**
255
     * {@inheritDoc}
256
     */
257 4722
    public function getListTablesSQL()
258
    {
259 4722
        return "SELECT quote_ident(table_name) AS table_name,
260
                       table_schema AS schema_name
261
                FROM   information_schema.tables
262
                WHERE  table_schema NOT LIKE 'pg\_%'
263
                AND    table_schema != 'information_schema'
264
                AND    table_name != 'geometry_columns'
265
                AND    table_name != 'spatial_ref_sys'
266
                AND    table_type != 'VIEW'";
267
    }
268
269
    /**
270
     * {@inheritDoc}
271
     */
272 4169
    public function getListViewsSQL($database)
273
    {
274 4169
        return 'SELECT quote_ident(table_name) AS viewname,
275
                       table_schema AS schemaname,
276
                       view_definition AS definition
277
                FROM   information_schema.views
278
                WHERE  view_definition IS NOT NULL';
279
    }
280
281
    /**
282
     * @param string      $table
283
     * @param string|null $database
284
     *
285
     * @return string
286
     */
287 11292
    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

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