PostgreSqlPlatform.php 36.1 KB
Newer Older
1 2
<?php

3
namespace Doctrine\DBAL\Platforms;
4

Steve Müller's avatar
Steve Müller committed
5 6
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ColumnDiff;
7
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
8
use Doctrine\DBAL\Schema\Identifier;
9
use Doctrine\DBAL\Schema\Index;
jeroendedauw's avatar
jeroendedauw committed
10
use Doctrine\DBAL\Schema\Sequence;
Benjamin Morel's avatar
Benjamin Morel committed
11
use Doctrine\DBAL\Schema\TableDiff;
12
use Doctrine\DBAL\Types\BigIntType;
13
use Doctrine\DBAL\Types\BinaryType;
Steve Müller's avatar
Steve Müller committed
14
use Doctrine\DBAL\Types\BlobType;
15
use Doctrine\DBAL\Types\IntegerType;
16
use Doctrine\DBAL\Types\Type;
17
use UnexpectedValueException;
18 19 20 21
use function array_diff;
use function array_merge;
use function array_unique;
use function array_values;
Grégoire Paris's avatar
Grégoire Paris committed
22
use function assert;
23 24 25 26 27 28 29 30
use function count;
use function explode;
use function implode;
use function in_array;
use function is_array;
use function is_bool;
use function is_numeric;
use function is_string;
31
use function sprintf;
32 33 34
use function strpos;
use function strtolower;
use function trim;
35

36 37 38
/**
 * PostgreSqlPlatform.
 *
Benjamin Morel's avatar
Benjamin Morel committed
39
 * @todo   Rename: PostgreSQLPlatform
40
 */
41
class PostgreSqlPlatform extends AbstractPlatform
42
{
43
    /** @var bool */
44 45
    private $useBooleanTrueFalseStrings = true;

46
    /** @var string[][] PostgreSQL booleans literals */
47 48
    private $booleanLiterals = [
        'true' => [
49 50 51 52 53
            't',
            'true',
            'y',
            'yes',
            'on',
54
            '1',
55 56
        ],
        'false' => [
57 58 59 60 61
            'f',
            'false',
            'n',
            'no',
            'off',
62 63
            '0',
        ],
64
    ];
65

66 67 68 69 70 71 72
    /**
     * PostgreSQL has different behavior with some drivers
     * with regard to how booleans have to be handled.
     *
     * Enables use of 'true'/'false' or otherwise 1 and 0 instead.
     *
     * @param bool $flag
73 74
     *
     * @return void
75 76 77
     */
    public function setUseBooleanTrueFalseStrings($flag)
    {
78
        $this->useBooleanTrueFalseStrings = (bool) $flag;
79 80
    }

81
    /**
82
     * {@inheritDoc}
83
     */
84
    public function getSubstringExpression($value, $from, $length = null)
85
    {
86
        if ($length === null) {
87
            return 'SUBSTRING(' . $value . ' FROM ' . $from . ')';
88
        }
89

90
        return 'SUBSTRING(' . $value . ' FROM ' . $from . ' FOR ' . $length . ')';
91 92 93
    }

    /**
94
     * {@inheritDoc}
95 96 97 98 99 100 101
     */
    public function getNowExpression()
    {
        return 'LOCALTIMESTAMP(0)';
    }

    /**
102
     * {@inheritDoc}
103 104 105 106 107
     */
    public function getRegexpExpression()
    {
        return 'SIMILAR TO';
    }
108 109

    /**
110
     * {@inheritDoc}
111 112 113 114 115
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        if ($startPos !== false) {
            $str = $this->getSubstringExpression($str, $startPos);
116

117
            return 'CASE WHEN (POSITION(' . $substr . ' IN ' . $str . ') = 0) THEN 0 ELSE (POSITION(' . $substr . ' IN ' . $str . ') + ' . ($startPos-1) . ') END';
118
        }
119

120
        return 'POSITION(' . $substr . ' IN ' . $str . ')';
121
    }
122

123
    /**
124
     * {@inheritdoc}
125
     */
126
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
127
    {
128
        if ($unit === DateIntervalUnit::QUARTER) {
129
            $interval *= 3;
130
            $unit      = DateIntervalUnit::MONTH;
131
        }
132

133
        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
134 135
    }

136 137 138
    /**
     * {@inheritDoc}
     */
139
    public function getDateDiffExpression($date1, $date2)
140
    {
141
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
142
    }
143

144
    /**
145
     * {@inheritDoc}
romanb's avatar
romanb committed
146 147 148 149 150
     */
    public function supportsSequences()
    {
        return true;
    }
151

152
    /**
153
     * {@inheritDoc}
154 155 156 157 158
     */
    public function supportsSchemas()
    {
        return true;
    }
159

160 161 162 163 164 165 166 167
    /**
     * {@inheritdoc}
     */
    public function getDefaultSchemaName()
    {
        return 'public';
    }

romanb's avatar
romanb committed
168
    /**
169
     * {@inheritDoc}
romanb's avatar
romanb committed
170 171 172 173 174
     */
    public function supportsIdentityColumns()
    {
        return true;
    }
175

176
    /**
177
     * {@inheritdoc}
178 179 180 181 182 183
     */
    public function supportsPartialIndexes()
    {
        return true;
    }

184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
    /**
     * {@inheritdoc}
     */
    public function usesSequenceEmulatedIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function getIdentitySequenceName($tableName, $columnName)
    {
        return $tableName . '_' . $columnName . '_seq';
    }

200 201 202
    /**
     * {@inheritDoc}
     */
203 204 205 206
    public function supportsCommentOnStatement()
    {
        return true;
    }
207

romanb's avatar
romanb committed
208
    /**
209
     * {@inheritDoc}
romanb's avatar
romanb committed
210 211 212 213 214
     */
    public function prefersSequences()
    {
        return true;
    }
215

216 217 218 219 220 221 222 223
    /**
     * {@inheritDoc}
     */
    public function hasNativeGuidType()
    {
        return true;
    }

Benjamin Morel's avatar
Benjamin Morel committed
224 225 226
    /**
     * {@inheritDoc}
     */
227
    public function getListDatabasesSQL()
228 229 230
    {
        return 'SELECT datname FROM pg_database';
    }
231

232 233 234 235 236
    /**
     * {@inheritDoc}
     */
    public function getListNamespacesSQL()
    {
237 238
        return "SELECT schema_name AS nspname
                FROM   information_schema.schemata
239
                WHERE  schema_name NOT LIKE 'pg\_%'
240
                AND    schema_name != 'information_schema'";
241 242
    }

Benjamin Morel's avatar
Benjamin Morel committed
243 244 245
    /**
     * {@inheritDoc}
     */
246
    public function getListSequencesSQL($database)
247
    {
248
        return "SELECT sequence_name AS relname,
x42p's avatar
x42p committed
249
                       sequence_schema AS schemaname
x42p's avatar
x42p committed
250
                FROM   information_schema.sequences
251
                WHERE  sequence_schema NOT LIKE 'pg\_%'
x42p's avatar
x42p committed
252
                AND    sequence_schema != 'information_schema'";
253
    }
254

Benjamin Morel's avatar
Benjamin Morel committed
255 256 257
    /**
     * {@inheritDoc}
     */
258
    public function getListTablesSQL()
259
    {
260
        return "SELECT quote_ident(table_name) AS table_name,
x42p's avatar
x42p committed
261
                       table_schema AS schema_name
x42p's avatar
x42p committed
262
                FROM   information_schema.tables
263
                WHERE  table_schema NOT LIKE 'pg\_%'
264 265
                AND    table_schema != 'information_schema'
                AND    table_name != 'geometry_columns'
266 267
                AND    table_name != 'spatial_ref_sys'
                AND    table_type != 'VIEW'";
268
    }
269

270 271 272
    /**
     * {@inheritDoc}
     */
273
    public function getListViewsSQL($database)
274
    {
275 276 277 278 279
        return 'SELECT quote_ident(table_name) AS viewname,
                       table_schema AS schemaname,
                       view_definition AS definition
                FROM   information_schema.views
                WHERE  view_definition IS NOT NULL';
280
    }
281

Benjamin Morel's avatar
Benjamin Morel committed
282
    /**
283 284 285 286
     * @param string      $table
     * @param string|null $database
     *
     * @return string
Benjamin Morel's avatar
Benjamin Morel committed
287
     */
288
    public function getListTableForeignKeysSQL($table, $database = null)
289
    {
290
        return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef
291 292 293 294
                  FROM pg_catalog.pg_constraint r
                  WHERE r.conrelid =
                  (
                      SELECT c.oid
295
                      FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n
296
                      WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
297 298 299 300
                  )
                  AND r.contype = 'f'";
    }

Benjamin Morel's avatar
Benjamin Morel committed
301 302 303
    /**
     * {@inheritDoc}
     */
304
    public function getCreateViewSQL($name, $sql)
305 306 307 308
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

Benjamin Morel's avatar
Benjamin Morel committed
309 310 311
    /**
     * {@inheritDoc}
     */
312
    public function getDropViewSQL($name)
313
    {
314
        return 'DROP VIEW ' . $name;
315 316
    }

Benjamin Morel's avatar
Benjamin Morel committed
317 318 319
    /**
     * {@inheritDoc}
     */
320
    public function getListTableConstraintsSQL($table)
321
    {
322
        $table = new Identifier($table);
323
        $table = $this->quoteStringLiteral($table->getName());
324

325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
        return sprintf(
            <<<'SQL'
SELECT
    quote_ident(relname) as relname
FROM
    pg_class
WHERE oid IN (
    SELECT indexrelid
    FROM pg_index, pg_class
    WHERE pg_class.relname = %s
        AND pg_class.oid = pg_index.indrelid
        AND (indisunique = 't' OR indisprimary = 't')
    )
SQL
            ,
            $table
        );
342
    }
343

344
    /**
345 346
     * {@inheritDoc}
     *
347 348
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
     */
349
    public function getListTableIndexesSQL($table, $currentDatabase = null)
350
    {
351
        return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary,
352
                       pg_index.indkey, pg_index.indrelid,
353
                       pg_get_expr(indpred, indrelid) AS where
354 355
                 FROM pg_class, pg_index
                 WHERE oid IN (
356
                    SELECT indexrelid
357
                    FROM pg_index si, pg_class sc, pg_namespace sn
358 359
                    WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . ' AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid
                 ) AND pg_index.indexrelid = oid';
360
    }
361

362 363 364 365
    /**
     * @param string $table
     * @param string $classAlias
     * @param string $namespaceAlias
366
     *
367 368
     * @return string
     */
369 370
    private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n')
    {
371 372 373 374
        $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
        if (strpos($table, '.') !== false) {
            [$schema, $table] = explode('.', $table);
            $schema           = $this->quoteStringLiteral($schema);
375
        } else {
376
            $schema = 'ANY(current_schemas(false))';
377
        }
378 379

        $table = new Identifier($table);
380
        $table = $this->quoteStringLiteral($table->getName());
381 382 383 384 385 386 387 388

        return $whereClause . sprintf(
            '%s.relname = %s AND %s.nspname = %s',
            $classAlias,
            $table,
            $namespaceAlias,
            $schema
        );
389 390
    }

Benjamin Morel's avatar
Benjamin Morel committed
391 392 393
    /**
     * {@inheritDoc}
     */
394
    public function getListTableColumnsSQL($table, $database = null)
395 396 397
    {
        return "SELECT
                    a.attnum,
398
                    quote_ident(a.attname) AS field,
399 400
                    t.typname AS type,
                    format_type(a.atttypid, a.atttypmod) AS complete_type,
401
                    (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
402 403
                    (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
                      pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
404 405 406 407 408 409 410
                    a.attnotnull AS isnotnull,
                    (SELECT 't'
                     FROM pg_index
                     WHERE c.oid = pg_index.indrelid
                        AND pg_index.indkey[0] = a.attnum
                        AND pg_index.indisprimary = 't'
                    ) AS pri,
411
                    (SELECT pg_get_expr(adbin, adrelid)
412 413 414
                     FROM pg_attrdef
                     WHERE c.oid = pg_attrdef.adrelid
                        AND pg_attrdef.adnum=a.attnum
415 416
                    ) AS default,
                    (SELECT pg_description.description
417
                        FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
418 419
                    ) AS comment
                    FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
420
                    WHERE " . $this->getTableWhereClause($table, 'c', 'n') . '
421 422 423
                        AND a.attnum > 0
                        AND a.attrelid = c.oid
                        AND a.atttypid = t.oid
424
                        AND n.oid = c.relnamespace
425
                    ORDER BY a.attnum';
426
    }
427

428
    /**
429
     * {@inheritDoc}
430
     */
431
    public function getCreateDatabaseSQL($name)
432
    {
433
        return 'CREATE DATABASE ' . $name;
434
    }
435

436 437 438 439 440 441 442 443 444 445 446
    /**
     * Returns the SQL statement for disallowing new connections on the given database.
     *
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
     *
     * @param string $database The name of the database to disallow new connections for.
     *
     * @return string
     */
    public function getDisallowDatabaseConnectionsSQL($database)
    {
447
        return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = " . $this->quoteStringLiteral($database);
448 449 450 451 452 453 454 455 456 457 458 459 460
    }

    /**
     * Returns the SQL statement for closing currently active connections on the given database.
     *
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
     *
     * @param string $database The name of the database to close currently active connections for.
     *
     * @return string
     */
    public function getCloseActiveDatabaseConnectionsSQL($database)
    {
461 462
        return 'SELECT pg_terminate_backend(procpid) FROM pg_stat_activity WHERE datname = '
            . $this->quoteStringLiteral($database);
463 464
    }

465
    /**
466
     * {@inheritDoc}
467
     */
468
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
469 470
    {
        $query = '';
471

472 473
        if ($foreignKey->hasOption('match')) {
            $query .= ' MATCH ' . $foreignKey->getOption('match');
474
        }
475

476
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
477

478
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
479 480 481 482
            $query .= ' DEFERRABLE';
        } else {
            $query .= ' NOT DEFERRABLE';
        }
483

484 485 486
        if (($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false)
            || ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false)
        ) {
487 488 489 490
            $query .= ' INITIALLY DEFERRED';
        } else {
            $query .= ' INITIALLY IMMEDIATE';
        }
491

492 493
        return $query;
    }
494

495
    /**
496
     * {@inheritDoc}
497
     */
498
    public function getAlterTableSQL(TableDiff $diff)
499
    {
500
        $sql         = [];
501
        $commentsSQL = [];
502
        $columnSql   = [];
503 504

        foreach ($diff->addedColumns as $column) {
505 506
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
507 508
            }

509
            $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
510
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
511 512 513

            $comment = $this->getColumnComment($column);

514 515
            if ($comment === null || $comment === '') {
                continue;
516
            }
517 518 519 520 521 522

            $commentsSQL[] = $this->getCommentOnColumnSQL(
                $diff->getName($this)->getQuotedName($this),
                $column->getQuotedName($this),
                $comment
            );
523 524
        }

525
        foreach ($diff->removedColumns as $column) {
526 527
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
528 529
            }

530
            $query = 'DROP ' . $column->getQuotedName($this);
531
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
532 533
        }

534
        foreach ($diff->changedColumns as $columnDiff) {
Grégoire Paris's avatar
Grégoire Paris committed
535
            assert($columnDiff instanceof ColumnDiff);
536 537
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
538 539
            }

Steve Müller's avatar
Steve Müller committed
540 541 542 543
            if ($this->isUnchangedBinaryColumn($columnDiff)) {
                continue;
            }

544
            $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
545
            $column        = $columnDiff->column;
546

547
            if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('precision') || $columnDiff->hasChanged('scale') || $columnDiff->hasChanged('fixed')) {
548
                $type = $column->getType();
549

550
                // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
551
                $columnDefinition                  = $column->toArray();
552 553
                $columnDefinition['autoincrement'] = false;

554
                // here was a server version check before, but DBAL API does not support this anymore.
555
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this);
556
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
557
            }
558

559
            if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
560
                $defaultClause = $column->getDefault() === null
561 562
                    ? ' DROP DEFAULT'
                    : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
563 564
                $query         = 'ALTER ' . $oldColumnName . $defaultClause;
                $sql[]         = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
565
            }
566

567
            if ($columnDiff->hasChanged('notnull')) {
568
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
569
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
570
            }
571

572 573 574
            if ($columnDiff->hasChanged('autoincrement')) {
                if ($column->getAutoincrement()) {
                    // add autoincrement
575
                    $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);
576

577 578 579 580
                    $sql[] = 'CREATE SEQUENCE ' . $seqName;
                    $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' . $diff->getName($this)->getQuotedName($this) . '))';
                    $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
581 582
                } else {
                    // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
583
                    $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT';
584
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
585 586
                }
            }
587

588
            $newComment = $this->getColumnComment($column);
589
            $oldComment = $this->getOldColumnComment($columnDiff);
590 591

            if ($columnDiff->hasChanged('comment') || ($columnDiff->fromColumn !== null && $oldComment !== $newComment)) {
592
                $commentsSQL[] = $this->getCommentOnColumnSQL(
593 594
                    $diff->getName($this)->getQuotedName($this),
                    $column->getQuotedName($this),
595
                    $newComment
596
                );
597
            }
598

599 600
            if (! $columnDiff->hasChanged('length')) {
                continue;
601
            }
602 603 604

            $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $column->getType()->getSQLDeclaration($column->toArray(), $this);
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
605 606
        }

607
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
608 609
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
610 611
            }

612 613
            $oldColumnName = new Identifier($oldColumnName);

614
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
615
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
616 617
        }

618
        $tableSql = [];
619

620
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
621 622
            $sql = array_merge($sql, $commentsSQL);

Sergei Morozov's avatar
Sergei Morozov committed
623 624 625 626 627 628 629 630
            $newName = $diff->getNewName();

            if ($newName !== false) {
                $sql[] = sprintf(
                    'ALTER TABLE %s RENAME TO %s',
                    $diff->getName($this)->getQuotedName($this),
                    $newName->getQuotedName($this)
                );
631 632
            }

633 634 635 636 637
            $sql = array_merge(
                $this->getPreAlterTableIndexForeignKeySQL($diff),
                $sql,
                $this->getPostAlterTableIndexForeignKeySQL($diff)
            );
638 639
        }

640
        return array_merge($sql, $tableSql, $columnSql);
641
    }
642

Steve Müller's avatar
Steve Müller committed
643 644 645 646 647 648 649 650 651 652 653 654
    /**
     * Checks whether a given column diff is a logically unchanged binary type column.
     *
     * Used to determine whether a column alteration for a binary type column can be skipped.
     * Doctrine's {@link \Doctrine\DBAL\Types\BinaryType} and {@link \Doctrine\DBAL\Types\BlobType}
     * are mapped to the same database column type on this platform as this platform
     * does not have a native VARBINARY/BINARY column type. Therefore the {@link \Doctrine\DBAL\Schema\Comparator}
     * might detect differences for binary type columns which do not have to be propagated
     * to database as there actually is no difference at database level.
     *
     * @param ColumnDiff $columnDiff The column diff to check against.
     *
655
     * @return bool True if the given column diff is an unchanged binary type column, false otherwise.
Steve Müller's avatar
Steve Müller committed
656 657 658 659 660
     */
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff)
    {
        $columnType = $columnDiff->column->getType();

661
        if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
Steve Müller's avatar
Steve Müller committed
662 663 664 665 666 667 668 669
            return false;
        }

        $fromColumn = $columnDiff->fromColumn instanceof Column ? $columnDiff->fromColumn : null;

        if ($fromColumn) {
            $fromColumnType = $fromColumn->getType();

670
            if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
Steve Müller's avatar
Steve Müller committed
671 672 673
                return false;
            }

674
            return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
Steve Müller's avatar
Steve Müller committed
675 676 677 678 679 680
        }

        if ($columnDiff->hasChanged('type')) {
            return false;
        }

681
        return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0;
Steve Müller's avatar
Steve Müller committed
682 683
    }

684 685 686 687 688
    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
689
        if (strpos($tableName, '.') !== false) {
690
            [$schema]     = explode('.', $tableName);
691 692 693
            $oldIndexName = $schema . '.' . $oldIndexName;
        }

694
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
695 696
    }

697 698 699 700 701
    /**
     * {@inheritdoc}
     */
    public function getCommentOnColumnSQL($tableName, $columnName, $comment)
    {
702
        $tableName  = new Identifier($tableName);
703
        $columnName = new Identifier($columnName);
704
        $comment    = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);
705

706 707 708 709 710 711
        return sprintf(
            'COMMENT ON COLUMN %s.%s IS %s',
            $tableName->getQuotedName($this),
            $columnName->getQuotedName($this),
            $comment
        );
712 713
    }

714
    /**
715
     * {@inheritDoc}
716
     */
jeroendedauw's avatar
jeroendedauw committed
717
    public function getCreateSequenceSQL(Sequence $sequence)
718
    {
719
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
720 721 722 723
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
            ' MINVALUE ' . $sequence->getInitialValue() .
            ' START ' . $sequence->getInitialValue() .
            $this->getSequenceCacheSQL($sequence);
724
    }
725

726 727 728
    /**
     * {@inheritDoc}
     */
jeroendedauw's avatar
jeroendedauw committed
729
    public function getAlterSequenceSQL(Sequence $sequence)
730
    {
731
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
732 733
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
            $this->getSequenceCacheSQL($sequence);
734 735 736 737 738 739 740
    }

    /**
     * Cache definition for sequences
     *
     * @return string
     */
jeroendedauw's avatar
jeroendedauw committed
741
    private function getSequenceCacheSQL(Sequence $sequence)
742 743 744 745 746 747
    {
        if ($sequence->getCache() > 1) {
            return ' CACHE ' . $sequence->getCache();
        }

        return '';
748
    }
749

750
    /**
751
     * {@inheritDoc}
752
     */
753
    public function getDropSequenceSQL($sequence)
754
    {
jeroendedauw's avatar
jeroendedauw committed
755
        if ($sequence instanceof Sequence) {
756
            $sequence = $sequence->getQuotedName($this);
757
        }
758

759
        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
760
    }
761

762 763 764 765 766 767 768 769
    /**
     * {@inheritDoc}
     */
    public function getCreateSchemaSQL($schemaName)
    {
        return 'CREATE SCHEMA ' . $schemaName;
    }

770
    /**
771
     * {@inheritDoc}
772
     */
773
    public function getDropForeignKeySQL($foreignKey, $table)
774
    {
775
        return $this->getDropConstraintSQL($foreignKey, $table);
776
    }
777

778
    /**
779
     * {@inheritDoc}
780
     */
781
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
782
    {
783
        $queryFields = $this->getColumnDeclarationListSQL($columns);
784 785

        if (isset($options['primary']) && ! empty($options['primary'])) {
786
            $keyColumns   = array_unique(array_values($options['primary']));
787 788 789
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
        }

790
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
791

792
        $sql = [$query];
793 794

        if (isset($options['indexes']) && ! empty($options['indexes'])) {
795
            foreach ($options['indexes'] as $index) {
796
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
797 798 799 800
            }
        }

        if (isset($options['foreignKeys'])) {
801
            foreach ((array) $options['foreignKeys'] as $definition) {
802
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
803 804 805 806 807
            }
        }

        return $sql;
    }
808

809 810 811 812 813 814 815 816 817 818 819
    /**
     * Converts a single boolean value.
     *
     * First converts the value to its native PHP boolean type
     * and passes it to the given callback function to be reconverted
     * into any custom representation.
     *
     * @param mixed    $value    The value to convert.
     * @param callable $callback The callback function to use for converting the real boolean value.
     *
     * @return mixed
820 821
     *
     * @throws UnexpectedValueException
822 823 824
     */
    private function convertSingleBooleanValue($value, $callback)
    {
825
        if ($value === null) {
826
            return $callback(null);
827 828 829
        }

        if (is_bool($value) || is_numeric($value)) {
830
            return $callback((bool) $value);
831 832
        }

833
        if (! is_string($value)) {
834 835 836 837 838 839
            return $callback(true);
        }

        /**
         * Better safe than sorry: http://php.net/in_array#106319
         */
840
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
841 842 843
            return $callback(false);
        }

844
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
845
            return $callback(true);
846 847
        }

848
        throw new UnexpectedValueException("Unrecognized boolean literal '${value}'");
849 850 851 852 853 854 855 856 857
    }

    /**
     * Converts one or multiple boolean values.
     *
     * First converts the value(s) to their native PHP boolean type
     * and passes them to the given callback function to be reconverted
     * into any custom representation.
     *
858
     * @param mixed    $item     The value(s) to convert.
859
     * @param callable $callback The callback function to use for converting the real boolean value(s).
860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875
     *
     * @return mixed
     */
    private function doConvertBooleans($item, $callback)
    {
        if (is_array($item)) {
            foreach ($item as $key => $value) {
                $item[$key] = $this->convertSingleBooleanValue($value, $callback);
            }

            return $item;
        }

        return $this->convertSingleBooleanValue($item, $callback);
    }

876
    /**
877
     * {@inheritDoc}
878
     *
879
     * Postgres wants boolean values converted to the strings 'true'/'false'.
880
     */
881
    public function convertBooleans($item)
882
    {
883
        if (! $this->useBooleanTrueFalseStrings) {
884
            return parent::convertBooleans($item);
885 886
        }

887 888
        return $this->doConvertBooleans(
            $item,
889 890
            static function ($boolean) {
                if ($boolean === null) {
891
                    return 'NULL';
892
                }
893

894
                return $boolean === true ? 'true' : 'false';
895
            }
896
        );
897 898 899 900 901
    }

    /**
     * {@inheritDoc}
     */
902
    public function convertBooleansToDatabaseValue($item)
903
    {
904
        if (! $this->useBooleanTrueFalseStrings) {
905
            return parent::convertBooleansToDatabaseValue($item);
906 907
        }

908 909
        return $this->doConvertBooleans(
            $item,
910 911
            static function ($boolean) {
                return $boolean === null ? null : (int) $boolean;
912 913
            }
        );
914
    }
915

916
    /**
917 918
     * {@inheritDoc}
     */
919 920
    public function convertFromBoolean($item)
    {
921
        if (in_array(strtolower($item), $this->booleanLiterals['false'], true)) {
922
            return false;
923 924
        }

925
        return parent::convertFromBoolean($item);
926
    }
927

Benjamin Morel's avatar
Benjamin Morel committed
928 929 930
    /**
     * {@inheritDoc}
     */
931
    public function getSequenceNextValSQL($sequenceName)
932 933 934
    {
        return "SELECT NEXTVAL('" . $sequenceName . "')";
    }
935

936 937 938
    /**
     * {@inheritDoc}
     */
939
    public function getSetTransactionIsolationSQL($level)
romanb's avatar
romanb committed
940 941
    {
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
942
            . $this->_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
943
    }
944

945
    /**
946
     * {@inheritDoc}
947
     */
948
    public function getBooleanTypeDeclarationSQL(array $field)
949 950 951
    {
        return 'BOOLEAN';
    }
952 953

    /**
954
     * {@inheritDoc}
955
     */
956
    public function getIntegerTypeDeclarationSQL(array $field)
957
    {
958
        if (! empty($field['autoincrement'])) {
959 960
            return 'SERIAL';
        }
961

962 963 964 965
        return 'INT';
    }

    /**
966
     * {@inheritDoc}
967
     */
968
    public function getBigIntTypeDeclarationSQL(array $field)
969
    {
970
        if (! empty($field['autoincrement'])) {
971 972
            return 'BIGSERIAL';
        }
973

974 975 976 977
        return 'BIGINT';
    }

    /**
978
     * {@inheritDoc}
979
     */
980
    public function getSmallIntTypeDeclarationSQL(array $field)
981 982 983 984
    {
        return 'SMALLINT';
    }

rivaros's avatar
rivaros committed
985
    /**
986
     * {@inheritDoc}
rivaros's avatar
rivaros committed
987
     */
988
    public function getGuidTypeDeclarationSQL(array $field)
rivaros's avatar
rivaros committed
989 990 991 992
    {
        return 'UUID';
    }

993
    /**
994
     * {@inheritDoc}
995
     */
996
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
997 998 999 1000 1001
    {
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
    }

    /**
1002
     * {@inheritDoc}
1003
     */
1004
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
1005
    {
1006 1007
        return 'TIMESTAMP(0) WITH TIME ZONE';
    }
1008

1009
    /**
1010
     * {@inheritDoc}
1011
     */
1012
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1013 1014
    {
        return 'DATE';
1015 1016
    }

1017
    /**
1018
     * {@inheritDoc}
1019
     */
1020
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1021
    {
1022
        return 'TIME(0) WITHOUT TIME ZONE';
1023 1024
    }

1025 1026
    /**
     * {@inheritDoc}
1027 1028
     *
     * @deprecated Use application-generated UUIDs instead
1029 1030 1031 1032 1033 1034
     */
    public function getGuidExpression()
    {
        return 'UUID_GENERATE_V4()';
    }

1035
    /**
1036
     * {@inheritDoc}
1037
     */
1038
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1039 1040 1041 1042 1043
    {
        return '';
    }

    /**
1044
     * {@inheritDoc}
1045
     */
1046
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1047 1048
    {
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
1049
            : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
1050
    }
1051

Steve Müller's avatar
Steve Müller committed
1052 1053 1054 1055 1056 1057 1058 1059
    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return 'BYTEA';
    }

1060 1061 1062
    /**
     * {@inheritDoc}
     */
1063
    public function getClobTypeDeclarationSQL(array $field)
1064 1065 1066
    {
        return 'TEXT';
    }
1067 1068

    /**
1069
     * {@inheritDoc}
1070 1071 1072 1073 1074
     */
    public function getName()
    {
        return 'postgresql';
    }
1075

1076
    /**
1077
     * {@inheritDoc}
1078
     *
1079 1080
     * PostgreSQL returns all column names in SQL result sets in lowercase.
     */
1081
    public function getSQLResultCasing($column)
1082 1083 1084
    {
        return strtolower($column);
    }
1085

1086 1087 1088
    /**
     * {@inheritDoc}
     */
1089
    public function getDateTimeTzFormatString()
1090
    {
1091 1092
        return 'Y-m-d H:i:sO';
    }
1093 1094

    /**
1095
     * {@inheritDoc}
1096
     */
1097
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
1098 1099 1100
    {
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
    }
1101 1102

    /**
1103
     * {@inheritDoc}
1104
     */
1105
    public function getTruncateTableSQL($tableName, $cascade = false)
1106
    {
1107
        $tableIdentifier = new Identifier($tableName);
1108
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
1109 1110 1111 1112 1113 1114

        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $sql;
1115
    }
1116

1117 1118 1119
    /**
     * {@inheritDoc}
     */
1120 1121 1122 1123
    public function getReadLockSQL()
    {
        return 'FOR SHARE';
    }
1124

1125 1126 1127
    /**
     * {@inheritDoc}
     */
1128 1129
    protected function initializeDoctrineTypeMappings()
    {
1130
        $this->doctrineTypeMapping = [
1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144
            'smallint'      => 'smallint',
            'int2'          => 'smallint',
            'serial'        => 'integer',
            'serial4'       => 'integer',
            'int'           => 'integer',
            'int4'          => 'integer',
            'integer'       => 'integer',
            'bigserial'     => 'bigint',
            'serial8'       => 'bigint',
            'bigint'        => 'bigint',
            'int8'          => 'bigint',
            'bool'          => 'boolean',
            'boolean'       => 'boolean',
            'text'          => 'text',
1145
            'tsvector'      => 'text',
1146 1147 1148 1149 1150
            'varchar'       => 'string',
            'interval'      => 'string',
            '_varchar'      => 'string',
            'char'          => 'string',
            'bpchar'        => 'string',
1151
            'inet'          => 'string',
1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167
            'date'          => 'date',
            'datetime'      => 'datetime',
            'timestamp'     => 'datetime',
            'timestamptz'   => 'datetimetz',
            'time'          => 'time',
            'timetz'        => 'time',
            'float'         => 'float',
            'float4'        => 'float',
            'float8'        => 'float',
            'double'        => 'float',
            'double precision' => 'float',
            'real'          => 'float',
            'decimal'       => 'decimal',
            'money'         => 'decimal',
            'numeric'       => 'decimal',
            'year'          => 'date',
1168
            'uuid'          => 'guid',
1169
            'bytea'         => 'blob',
1170
        ];
1171
    }
1172

1173 1174 1175
    /**
     * {@inheritDoc}
     */
1176 1177 1178 1179
    public function getVarcharMaxLength()
    {
        return 65535;
    }
1180

Steve Müller's avatar
Steve Müller committed
1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196
    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 0;
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryDefaultLength()
    {
        return 0;
    }

1197 1198 1199
    /**
     * {@inheritDoc}
     */
1200 1201
    protected function getReservedKeywordsClass()
    {
1202
        return Keywords\PostgreSQLKeywords::class;
1203
    }
1204 1205

    /**
1206
     * {@inheritDoc}
1207 1208 1209 1210 1211
     */
    public function getBlobTypeDeclarationSQL(array $field)
    {
        return 'BYTEA';
    }
1212

1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224
    /**
     * {@inheritdoc}
     */
    public function getDefaultValueDeclarationSQL($field)
    {
        if ($this->isSerialField($field)) {
            return '';
        }

        return parent::getDefaultValueDeclarationSQL($field);
    }

1225 1226 1227
    /**
     * @param mixed[] $field
     */
1228 1229
    private function isSerialField(array $field) : bool
    {
Sergei Morozov's avatar
Sergei Morozov committed
1230 1231
        return isset($field['type'], $field['autoincrement'])
            && $field['autoincrement'] === true
1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254
            && $this->isNumericType($field['type']);
    }

    /**
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
     */
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
    {
        if (! $columnDiff->fromColumn) {
            return $columnDiff->hasChanged('type');
        }

        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());

        // default should not be changed when switching between numeric types and the default comes from a sequence
        return $columnDiff->hasChanged('type')
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
    }

    private function isNumericType(Type $type) : bool
    {
        return $type instanceof IntegerType || $type instanceof BigIntType;
1255
    }
1256 1257 1258 1259 1260

    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
    {
        return $columnDiff->fromColumn ? $this->getColumnComment($columnDiff->fromColumn) : null;
    }
1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275

    public function getListTableMetadataSQL(string $table, ?string $schema = null) : string
    {
        if ($schema !== null) {
            $table = $schema . '.' . $table;
        }

        return sprintf(
            <<<'SQL'
SELECT obj_description(%s::regclass) AS table_comment;
SQL
            ,
            $this->quoteStringLiteral($table)
        );
    }
1276
}