Failed Conditions
Pull Request — master (#2929)
by Alexander
64:54
created

PostgreSqlPlatform::getDropPrimaryKeySQL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
crap 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL\Platforms;
21
22
use Doctrine\DBAL\Schema\Column;
23
use Doctrine\DBAL\Schema\ColumnDiff;
24
use Doctrine\DBAL\Schema\Identifier;
25
use Doctrine\DBAL\Schema\Index;
26
use Doctrine\DBAL\Schema\Sequence;
27
use Doctrine\DBAL\Schema\TableDiff;
28
use Doctrine\DBAL\Types\BinaryType;
29
use Doctrine\DBAL\Types\BigIntType;
30
use Doctrine\DBAL\Types\BlobType;
31
use Doctrine\DBAL\Types\IntegerType;
32
use Doctrine\DBAL\Types\Type;
33
34
/**
35
 * PostgreSqlPlatform.
36
 *
37
 * @since  2.0
38
 * @author Roman Borschel <[email protected]>
39
 * @author Lukas Smith <[email protected]> (PEAR MDB2 library)
40
 * @author Benjamin Eberlei <[email protected]>
41
 * @todo   Rename: PostgreSQLPlatform
42
 */
43
class PostgreSqlPlatform extends AbstractPlatform
44
{
45
    /**
46
     * @var bool
47
     */
48
    private $useBooleanTrueFalseStrings = true;
49
50
    /**
51
     * @var array PostgreSQL booleans literals
52
     */
53
    private $booleanLiterals = [
54
        'true' => [
55
            't',
56
            'true',
57
            'y',
58
            'yes',
59
            'on',
60
            '1'
61
        ],
62
        'false' => [
63
            'f',
64
            'false',
65
            'n',
66
            'no',
67
            'off',
68
            '0'
69
        ]
70
    ];
71
72
    /**
73
     * PostgreSQL has different behavior with some drivers
74
     * with regard to how booleans have to be handled.
75
     *
76
     * Enables use of 'true'/'false' or otherwise 1 and 0 instead.
77
     *
78
     * @param bool $flag
79
     */
80 10
    public function setUseBooleanTrueFalseStrings($flag)
81
    {
82 10
        $this->useBooleanTrueFalseStrings = (bool) $flag;
83 10
    }
84
85
    /**
86
     * {@inheritDoc}
87
     */
88 5
    public function getSubstringExpression($value, $from, $length = null)
89
    {
90 5
        if ($length === null) {
91 5
            return 'SUBSTRING(' . $value . ' FROM ' . $from . ')';
92
        }
93
94 5
        return 'SUBSTRING(' . $value . ' FROM ' . $from . ' FOR ' . $length . ')';
95
    }
96
97
    /**
98
     * {@inheritDoc}
99
     */
100
    public function getNowExpression()
101
    {
102
        return 'LOCALTIMESTAMP(0)';
103
    }
104
105
    /**
106
     * {@inheritDoc}
107
     */
108 5
    public function getRegexpExpression()
109
    {
110 5
        return 'SIMILAR TO';
111
    }
112
113
    /**
114
     * {@inheritDoc}
115
     */
116
    public function getLocateExpression($str, $substr, $startPos = false)
117
    {
118
        if ($startPos !== false) {
119
            $str = $this->getSubstringExpression($str, $startPos);
120
121
            return 'CASE WHEN (POSITION('.$substr.' IN '.$str.') = 0) THEN 0 ELSE (POSITION('.$substr.' IN '.$str.') + '.($startPos-1).') END';
122
        }
123
124
        return 'POSITION('.$substr.' IN '.$str.')';
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
131
    {
132
        if ($unit === DateIntervalUnit::QUARTER) {
133
            $interval *= 3;
134
            $unit      = DateIntervalUnit::MONTH;
135
        }
136
137
        return "(" . $date ." " . $operator . " (" . $interval . " || ' " . $unit . "')::interval)";
138
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143
    public function getDateDiffExpression($date1, $date2)
144
    {
145
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
146
    }
147
148
    /**
149
     * {@inheritDoc}
150
     */
151 5
    public function supportsSequences()
152
    {
153 5
        return true;
154
    }
155
156
    /**
157
     * {@inheritDoc}
158
     */
159
    public function supportsSchemas()
160
    {
161
        return true;
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167
    public function getDefaultSchemaName()
168
    {
169
        return 'public';
170
    }
171
172
    /**
173
     * {@inheritDoc}
174
     */
175 5
    public function supportsIdentityColumns()
176
    {
177 5
        return true;
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 42
    public function supportsPartialIndexes()
184
    {
185 42
        return true;
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191 5
    public function usesSequenceEmulatedIdentityColumns()
192
    {
193 5
        return true;
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199 5
    public function getIdentitySequenceName($tableName, $columnName)
200
    {
201 5
        return $tableName . '_' . $columnName . '_seq';
202
    }
203
204
    /**
205
     * {@inheritDoc}
206
     */
207 75
    public function supportsCommentOnStatement()
208
    {
209 75
        return true;
210
    }
211
212
    /**
213
     * {@inheritDoc}
214
     */
215 5
    public function prefersSequences()
216
    {
217 5
        return true;
218
    }
219
220
    /**
221
     * {@inheritDoc}
222
     */
223 225
    public function hasNativeGuidType()
224
    {
225 225
        return true;
226
    }
227
228
    /**
229
     * {@inheritDoc}
230
     */
231
    public function getListDatabasesSQL()
232
    {
233
        return 'SELECT datname FROM pg_database';
234
    }
235
236
    /**
237
     * {@inheritDoc}
238
     */
239
    public function getListNamespacesSQL()
240
    {
241
        return "SELECT schema_name AS nspname
242
                FROM   information_schema.schemata
243
                WHERE  schema_name NOT LIKE 'pg\_%'
244
                AND    schema_name != 'information_schema'";
245
    }
246
247
    /**
248
     * {@inheritDoc}
249
     */
250
    public function getListSequencesSQL($database)
251
    {
252
        return "SELECT sequence_name AS relname,
253
                       sequence_schema AS schemaname
254
                FROM   information_schema.sequences
255
                WHERE  sequence_schema NOT LIKE 'pg\_%'
256
                AND    sequence_schema != 'information_schema'";
257
    }
258
259
    /**
260
     * {@inheritDoc}
261
     */
262
    public function getListTablesSQL()
263
    {
264
        return "SELECT quote_ident(table_name) AS table_name,
265
                       table_schema AS schema_name
266
                FROM   information_schema.tables
267
                WHERE  table_schema NOT LIKE 'pg\_%'
268
                AND    table_schema != 'information_schema'
269
                AND    table_name != 'geometry_columns'
270
                AND    table_name != 'spatial_ref_sys'
271
                AND    table_type != 'VIEW'";
272
    }
273
274
    /**
275
     * {@inheritDoc}
276
     */
277
    public function getListViewsSQL($database)
278
    {
279
        return 'SELECT quote_ident(table_name) AS viewname,
280
                       table_schema AS schemaname,
281
                       view_definition AS definition
282
                FROM   information_schema.views
283
                WHERE  view_definition IS NOT NULL';
284
    }
285
286
    /**
287
     * {@inheritDoc}
288
     */
289 10
    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

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