PostgreSqlPlatform.php 36.1 KB
Newer Older
1
<?php
2 3 4 5 6 7 8 9 10 11 12 13 14 15
/*
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
Benjamin Eberlei's avatar
Benjamin Eberlei committed
16
 * and is licensed under the MIT license. For more information, see
17 18
 * <http://www.doctrine-project.org>.
 */
19

20
namespace Doctrine\DBAL\Platforms;
21

Steve Müller's avatar
Steve Müller committed
22 23
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ColumnDiff;
24
use Doctrine\DBAL\Schema\Identifier;
25
use Doctrine\DBAL\Schema\Index;
jeroendedauw's avatar
jeroendedauw committed
26
use Doctrine\DBAL\Schema\Sequence;
Benjamin Morel's avatar
Benjamin Morel committed
27
use Doctrine\DBAL\Schema\TableDiff;
Steve Müller's avatar
Steve Müller committed
28
use Doctrine\DBAL\Types\BinaryType;
29
use Doctrine\DBAL\Types\BigIntType;
Steve Müller's avatar
Steve Müller committed
30
use Doctrine\DBAL\Types\BlobType;
31
use Doctrine\DBAL\Types\IntegerType;
32
use Doctrine\DBAL\Types\Type;
33

34 35 36
/**
 * PostgreSqlPlatform.
 *
Benjamin Morel's avatar
Benjamin Morel committed
37
 * @since  2.0
38 39
 * @author Roman Borschel <roman@code-factory.org>
 * @author Lukas Smith <smith@pooteeweet.org> (PEAR MDB2 library)
40
 * @author Benjamin Eberlei <kontakt@beberlei.de>
Benjamin Morel's avatar
Benjamin Morel committed
41
 * @todo   Rename: PostgreSQLPlatform
42
 */
43
class PostgreSqlPlatform extends AbstractPlatform
44
{
45 46 47 48 49
    /**
     * @var bool
     */
    private $useBooleanTrueFalseStrings = true;

50 51 52
    /**
     * @var array PostgreSQL booleans literals
     */
53 54
    private $booleanLiterals = [
        'true' => [
55 56 57 58 59 60
            't',
            'true',
            'y',
            'yes',
            'on',
            '1'
61 62
        ],
        'false' => [
63 64 65 66 67 68
            'f',
            'false',
            'n',
            'no',
            'off',
            '0'
69 70
        ]
    ];
71

72 73 74 75 76 77 78 79 80 81
    /**
     * 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
     */
    public function setUseBooleanTrueFalseStrings($flag)
    {
82
        $this->useBooleanTrueFalseStrings = (bool) $flag;
83 84
    }

85
    /**
86
     * {@inheritDoc}
87
     */
88
    public function getSubstringExpression($value, $from, $length = null)
89
    {
90
        if ($length === null) {
91
            return 'SUBSTRING(' . $value . ' FROM ' . $from . ')';
92
        }
93

94
        return 'SUBSTRING(' . $value . ' FROM ' . $from . ' FOR ' . $length . ')';
95 96 97
    }

    /**
98
     * {@inheritDoc}
99 100 101 102 103 104 105
     */
    public function getNowExpression()
    {
        return 'LOCALTIMESTAMP(0)';
    }

    /**
106
     * {@inheritDoc}
107 108 109 110 111
     */
    public function getRegexpExpression()
    {
        return 'SIMILAR TO';
    }
112 113

    /**
114
     * {@inheritDoc}
115 116 117 118 119
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        if ($startPos !== false) {
            $str = $this->getSubstringExpression($str, $startPos);
120

121 122
            return 'CASE WHEN (POSITION('.$substr.' IN '.$str.') = 0) THEN 0 ELSE (POSITION('.$substr.' IN '.$str.') + '.($startPos-1).') END';
        }
123 124

        return 'POSITION('.$substr.' IN '.$str.')';
125
    }
126

127
    /**
128
     * {@inheritdoc}
129
     */
130
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
131
    {
132 133 134 135
        if (self::DATE_INTERVAL_UNIT_QUARTER === $unit) {
            $interval *= 3;
            $unit = self::DATE_INTERVAL_UNIT_MONTH;
        }
136

137
        return "(" . $date ." " . $operator . " (" . $interval . " || ' " . $unit . "')::interval)";
138 139
    }

140 141 142
    /**
     * {@inheritDoc}
     */
143
    public function getDateDiffExpression($date1, $date2)
144
    {
145
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
146
    }
147

148
    /**
149
     * {@inheritDoc}
romanb's avatar
romanb committed
150 151 152 153 154
     */
    public function supportsSequences()
    {
        return true;
    }
155

156
    /**
157
     * {@inheritDoc}
158 159 160 161 162
     */
    public function supportsSchemas()
    {
        return true;
    }
163

164 165 166 167 168 169 170 171
    /**
     * {@inheritdoc}
     */
    public function getDefaultSchemaName()
    {
        return 'public';
    }

romanb's avatar
romanb committed
172
    /**
173
     * {@inheritDoc}
romanb's avatar
romanb committed
174 175 176 177 178
     */
    public function supportsIdentityColumns()
    {
        return true;
    }
179

180
    /**
181
     * {@inheritdoc}
182 183 184 185 186 187
     */
    public function supportsPartialIndexes()
    {
        return true;
    }

188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
    /**
     * {@inheritdoc}
     */
    public function usesSequenceEmulatedIdentityColumns()
    {
        return true;
    }

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

204 205 206
    /**
     * {@inheritDoc}
     */
207 208 209 210
    public function supportsCommentOnStatement()
    {
        return true;
    }
211

romanb's avatar
romanb committed
212
    /**
213
     * {@inheritDoc}
romanb's avatar
romanb committed
214 215 216 217 218
     */
    public function prefersSequences()
    {
        return true;
    }
219

220 221 222 223 224 225 226 227
    /**
     * {@inheritDoc}
     */
    public function hasNativeGuidType()
    {
        return true;
    }

Benjamin Morel's avatar
Benjamin Morel committed
228 229 230
    /**
     * {@inheritDoc}
     */
231
    public function getListDatabasesSQL()
232 233 234
    {
        return 'SELECT datname FROM pg_database';
    }
235

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

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

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

274 275 276
    /**
     * {@inheritDoc}
     */
277
    public function getListViewsSQL($database)
278
    {
279 280 281 282 283
        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';
284
    }
285

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

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

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

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

326
        return "SELECT
327
                    quote_ident(relname) as relname
328 329 330 331 332
                FROM
                    pg_class
                WHERE oid IN (
                    SELECT indexrelid
                    FROM pg_index, pg_class
333
                    WHERE pg_class.relname = $table
334 335 336 337
                        AND pg_class.oid = pg_index.indrelid
                        AND (indisunique = 't' OR indisprimary = 't')
                        )";
    }
338

339
    /**
340 341
     * {@inheritDoc}
     *
342 343 344
     * @license New BSD License
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
     */
345
    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 351
                 FROM pg_class, pg_index
                 WHERE oid IN (
352
                    SELECT indexrelid
353 354
                    FROM pg_index si, pg_class sc, pg_namespace sn
                    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 360 361
    /**
     * @param string $table
     * @param string $classAlias
     * @param string $namespaceAlias
362
     *
363 364
     * @return string
     */
365 366
    private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n')
    {
367
        $whereClause = $namespaceAlias.".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
368 369
        if (strpos($table, ".") !== false) {
            list($schema, $table) = explode(".", $table);
370
            $schema = $this->quoteStringLiteral($schema);
371
        } else {
372
            $schema = "ANY(string_to_array((select replace(replace(setting,'\"\$user\"',user),' ','') from pg_catalog.pg_settings where name = 'search_path'),','))";
373
        }
374 375

        $table = new Identifier($table);
376 377
        $table = $this->quoteStringLiteral($table->getName());
        $whereClause .= "$classAlias.relname = " . $table . " AND $namespaceAlias.nspname = $schema";
378

379 380 381
        return $whereClause;
    }

Benjamin Morel's avatar
Benjamin Morel committed
382 383 384
    /**
     * {@inheritDoc}
     */
385
    public function getListTableColumnsSQL($table, $database = null)
386 387 388
    {
        return "SELECT
                    a.attnum,
389
                    quote_ident(a.attname) AS field,
390 391
                    t.typname AS type,
                    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 394
                    (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,
395 396 397 398 399 400 401
                    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,
402
                    (SELECT pg_get_expr(adbin, adrelid)
403 404 405
                     FROM pg_attrdef
                     WHERE c.oid = pg_attrdef.adrelid
                        AND pg_attrdef.adnum=a.attnum
406 407
                    ) AS default,
                    (SELECT pg_description.description
408
                        FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
409 410
                    ) AS comment
                    FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
411
                    WHERE ".$this->getTableWhereClause($table, 'c', 'n') ."
412 413 414
                        AND a.attnum > 0
                        AND a.attrelid = c.oid
                        AND a.atttypid = t.oid
415
                        AND n.oid = c.relnamespace
416 417
                    ORDER BY a.attnum";
    }
418

419
    /**
420
     * {@inheritDoc}
421
     */
422
    public function getCreateDatabaseSQL($name)
423
    {
424
        return 'CREATE DATABASE ' . $name;
425
    }
426

427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
    /**
     * 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)
    {
        return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = '$database'";
    }

    /**
     * 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)
    {
452 453 454
        $database = $this->quoteStringLiteral($database);

        return "SELECT pg_terminate_backend(procpid) FROM pg_stat_activity WHERE datname = $database";
455 456
    }

457
    /**
458
     * {@inheritDoc}
459
     */
460
    public function getAdvancedForeignKeyOptionsSQL(\Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey)
461 462
    {
        $query = '';
463

464 465
        if ($foreignKey->hasOption('match')) {
            $query .= ' MATCH ' . $foreignKey->getOption('match');
466
        }
467

468
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
469

470
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
471 472 473 474
            $query .= ' DEFERRABLE';
        } else {
            $query .= ' NOT DEFERRABLE';
        }
475

476 477 478
        if (($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false)
            || ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false)
        ) {
479 480 481 482
            $query .= ' INITIALLY DEFERRED';
        } else {
            $query .= ' INITIALLY IMMEDIATE';
        }
483

484 485
        return $query;
    }
486

487
    /**
488
     * {@inheritDoc}
489
     */
490
    public function getAlterTableSQL(TableDiff $diff)
491
    {
492 493 494
        $sql = [];
        $commentsSQL = [];
        $columnSql = [];
495 496

        foreach ($diff->addedColumns as $column) {
497 498
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
499 500
            }

501
            $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
502
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
503 504 505 506

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

            if (null !== $comment && '' !== $comment) {
507 508 509 510 511
                $commentsSQL[] = $this->getCommentOnColumnSQL(
                    $diff->getName($this)->getQuotedName($this),
                    $column->getQuotedName($this),
                    $comment
                );
512
            }
513 514
        }

515
        foreach ($diff->removedColumns as $column) {
516 517
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
518 519
            }

520
            $query = 'DROP ' . $column->getQuotedName($this);
521
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
522 523
        }

524
        foreach ($diff->changedColumns as $columnDiff) {
525
            /** @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
526 527
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
528 529
            }

Steve Müller's avatar
Steve Müller committed
530 531 532 533
            if ($this->isUnchangedBinaryColumn($columnDiff)) {
                continue;
            }

534
            $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
535
            $column = $columnDiff->column;
536

537
            if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('precision') || $columnDiff->hasChanged('scale') || $columnDiff->hasChanged('fixed')) {
538
                $type = $column->getType();
539

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

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

549
            if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
550 551 552 553
                $defaultClause = null === $column->getDefault()
                    ? ' DROP DEFAULT'
                    : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
                $query = 'ALTER ' . $oldColumnName . $defaultClause;
554
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
555
            }
556

557
            if ($columnDiff->hasChanged('notnull')) {
558
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
559
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
560
            }
561

562 563 564
            if ($columnDiff->hasChanged('autoincrement')) {
                if ($column->getAutoincrement()) {
                    // 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 572 573
                } else {
                    // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
                    $query = "ALTER " . $oldColumnName . " " . "DROP DEFAULT";
574
                    $sql[] = "ALTER TABLE " . $diff->getName($this)->getQuotedName($this) . " " . $query;
575 576
                }
            }
577

578 579
            if ($columnDiff->hasChanged('comment')) {
                $commentsSQL[] = $this->getCommentOnColumnSQL(
580 581
                    $diff->getName($this)->getQuotedName($this),
                    $column->getQuotedName($this),
582 583
                    $this->getColumnComment($column)
                );
584
            }
585 586

            if ($columnDiff->hasChanged('length')) {
587
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $column->getType()->getSQLDeclaration($column->toArray(), $this);
588
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
589
            }
590 591
        }

592
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
593 594
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
595 596
            }

597 598
            $oldColumnName = new Identifier($oldColumnName);

599
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
600
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
601 602
        }

603
        $tableSql = [];
604

605
        if ( ! $this->onSchemaAlterTable($diff, $tableSql)) {
606 607
            $sql = array_merge($sql, $commentsSQL);

608
            if ($diff->newName !== false) {
609
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' RENAME TO ' . $diff->getNewName()->getQuotedName($this);
610 611
            }

612 613 614 615 616
            $sql = array_merge(
                $this->getPreAlterTableIndexForeignKeySQL($diff),
                $sql,
                $this->getPostAlterTableIndexForeignKeySQL($diff)
            );
617 618
        }

619
        return array_merge($sql, $tableSql, $columnSql);
620
    }
621

Steve Müller's avatar
Steve Müller committed
622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
    /**
     * 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.
     *
     * @return boolean True if the given column diff is an unchanged binary type column, false otherwise.
     */
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff)
    {
        $columnType = $columnDiff->column->getType();

        if ( ! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
            return false;
        }

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

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

            if ( ! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
                return false;
            }

653
            return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
Steve Müller's avatar
Steve Müller committed
654 655 656 657 658 659
        }

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

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

663 664 665 666 667
    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
668 669 670 671 672
        if (strpos($tableName, '.') !== false) {
            list($schema) = explode('.', $tableName);
            $oldIndexName = $schema . '.' . $oldIndexName;
        }

673
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
674 675
    }

676 677 678 679 680
    /**
     * {@inheritdoc}
     */
    public function getCommentOnColumnSQL($tableName, $columnName, $comment)
    {
681 682
        $tableName = new Identifier($tableName);
        $columnName = new Identifier($columnName);
683
        $comment = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);
684

685 686
        return "COMMENT ON COLUMN " . $tableName->getQuotedName($this) . "." . $columnName->getQuotedName($this) .
            " IS $comment";
687 688
    }

689
    /**
690
     * {@inheritDoc}
691
     */
jeroendedauw's avatar
jeroendedauw committed
692
    public function getCreateSequenceSQL(Sequence $sequence)
693
    {
694
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
695 696 697 698
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
            ' MINVALUE ' . $sequence->getInitialValue() .
            ' START ' . $sequence->getInitialValue() .
            $this->getSequenceCacheSQL($sequence);
699
    }
700

701 702 703
    /**
     * {@inheritDoc}
     */
jeroendedauw's avatar
jeroendedauw committed
704
    public function getAlterSequenceSQL(Sequence $sequence)
705
    {
706
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
707 708
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
            $this->getSequenceCacheSQL($sequence);
709 710 711 712 713
    }

    /**
     * Cache definition for sequences
     *
jeroendedauw's avatar
jeroendedauw committed
714 715
     * @param Sequence $sequence
     *
716 717
     * @return string
     */
jeroendedauw's avatar
jeroendedauw committed
718
    private function getSequenceCacheSQL(Sequence $sequence)
719 720 721 722 723 724
    {
        if ($sequence->getCache() > 1) {
            return ' CACHE ' . $sequence->getCache();
        }

        return '';
725
    }
726

727
    /**
728
     * {@inheritDoc}
729
     */
730
    public function getDropSequenceSQL($sequence)
731
    {
jeroendedauw's avatar
jeroendedauw committed
732
        if ($sequence instanceof Sequence) {
733
            $sequence = $sequence->getQuotedName($this);
734
        }
735

736
        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
737
    }
738

739 740 741 742 743 744 745 746
    /**
     * {@inheritDoc}
     */
    public function getCreateSchemaSQL($schemaName)
    {
        return 'CREATE SCHEMA ' . $schemaName;
    }

747
    /**
748
     * {@inheritDoc}
749
     */
750
    public function getDropForeignKeySQL($foreignKey, $table)
751
    {
752
        return $this->getDropConstraintSQL($foreignKey, $table);
753
    }
754

755
    /**
756
     * {@inheritDoc}
757
     */
758
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
759
    {
760
        $queryFields = $this->getColumnDeclarationListSQL($columns);
761 762

        if (isset($options['primary']) && ! empty($options['primary'])) {
763
            $keyColumns = array_unique(array_values($options['primary']));
764 765 766
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
        }

767
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
768 769 770 771

        $sql[] = $query;

        if (isset($options['indexes']) && ! empty($options['indexes'])) {
772
            foreach ($options['indexes'] as $index) {
773
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
774 775 776 777
            }
        }

        if (isset($options['foreignKeys'])) {
778
            foreach ((array) $options['foreignKeys'] as $definition) {
779
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
780 781 782 783 784
            }
        }

        return $sql;
    }
785

786 787 788 789 790 791 792 793 794 795 796
    /**
     * 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
jeroendedauw's avatar
jeroendedauw committed
797
     * @throws \UnexpectedValueException
798 799 800 801
     */
    private function convertSingleBooleanValue($value, $callback)
    {
        if (null === $value) {
802
            return $callback(null);
803 804 805 806 807 808
        }

        if (is_bool($value) || is_numeric($value)) {
            return $callback($value ? true : false);
        }

809 810 811 812 813 814 815 816 817 818 819 820 821
        if (!is_string($value)) {
            return $callback(true);
        }

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

        if (in_array(trim(strtolower($value)), $this->booleanLiterals['true'], true)) {
            return $callback(true);
822 823
        }

824
        throw new \UnexpectedValueException("Unrecognized boolean literal '${value}'");
825 826 827 828 829 830 831 832 833
    }

    /**
     * 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.
     *
834
     * @param mixed    $item     The value(s) to convert.
835
     * @param callable $callback The callback function to use for converting the real boolean value(s).
836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851
     *
     * @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);
    }

852
    /**
853
     * {@inheritDoc}
854
     *
855
     * Postgres wants boolean values converted to the strings 'true'/'false'.
856
     */
857
    public function convertBooleans($item)
858
    {
859
        if ( ! $this->useBooleanTrueFalseStrings) {
860
            return parent::convertBooleans($item);
861 862
        }

863 864 865
        return $this->doConvertBooleans(
            $item,
            function ($boolean) {
866
                if (null === $boolean) {
867
                    return 'NULL';
868
                }
869

870
                return true === $boolean ? 'true' : 'false';
871
            }
872
        );
873 874 875 876 877
    }

    /**
     * {@inheritDoc}
     */
878
    public function convertBooleansToDatabaseValue($item)
879
    {
880
        if ( ! $this->useBooleanTrueFalseStrings) {
881
            return parent::convertBooleansToDatabaseValue($item);
882 883
        }

884 885 886
        return $this->doConvertBooleans(
            $item,
            function ($boolean) {
887
                return null === $boolean ? null : (int) $boolean;
888 889
            }
        );
890
    }
891

892
    /**
893 894
     * {@inheritDoc}
     */
895 896
    public function convertFromBoolean($item)
    {
897
        if (in_array(strtolower($item), $this->booleanLiterals['false'], true)) {
898
            return false;
899 900
        }

901
        return parent::convertFromBoolean($item);
902
    }
903

Benjamin Morel's avatar
Benjamin Morel committed
904 905 906
    /**
     * {@inheritDoc}
     */
907
    public function getSequenceNextValSQL($sequenceName)
908 909 910
    {
        return "SELECT NEXTVAL('" . $sequenceName . "')";
    }
911

912 913 914
    /**
     * {@inheritDoc}
     */
915
    public function getSetTransactionIsolationSQL($level)
romanb's avatar
romanb committed
916 917
    {
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
918
            . $this->_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
919
    }
920

921
    /**
922
     * {@inheritDoc}
923
     */
924
    public function getBooleanTypeDeclarationSQL(array $field)
925 926 927
    {
        return 'BOOLEAN';
    }
928 929

    /**
930
     * {@inheritDoc}
931
     */
932
    public function getIntegerTypeDeclarationSQL(array $field)
933 934 935 936
    {
        if ( ! empty($field['autoincrement'])) {
            return 'SERIAL';
        }
937

938 939 940 941
        return 'INT';
    }

    /**
942
     * {@inheritDoc}
943
     */
944
    public function getBigIntTypeDeclarationSQL(array $field)
945 946 947 948
    {
        if ( ! empty($field['autoincrement'])) {
            return 'BIGSERIAL';
        }
949

950 951 952 953
        return 'BIGINT';
    }

    /**
954
     * {@inheritDoc}
955
     */
956
    public function getSmallIntTypeDeclarationSQL(array $field)
957 958 959 960
    {
        return 'SMALLINT';
    }

rivaros's avatar
rivaros committed
961
    /**
962
     * {@inheritDoc}
rivaros's avatar
rivaros committed
963
     */
964
    public function getGuidTypeDeclarationSQL(array $field)
rivaros's avatar
rivaros committed
965 966 967 968
    {
        return 'UUID';
    }

969
    /**
970
     * {@inheritDoc}
971
     */
972
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
973 974 975 976 977
    {
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
    }

    /**
978
     * {@inheritDoc}
979
     */
980
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
981
    {
982 983
        return 'TIMESTAMP(0) WITH TIME ZONE';
    }
984

985
    /**
986
     * {@inheritDoc}
987
     */
988
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
989 990
    {
        return 'DATE';
991 992
    }

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

1001 1002 1003 1004 1005 1006 1007 1008
    /**
     * {@inheritDoc}
     */
    public function getGuidExpression()
    {
        return 'UUID_GENERATE_V4()';
    }

1009
    /**
1010
     * {@inheritDoc}
1011
     */
1012
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1013 1014 1015 1016 1017
    {
        return '';
    }

    /**
1018
     * {@inheritDoc}
1019
     */
1020
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1021 1022
    {
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
1023
            : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
1024
    }
1025

Steve Müller's avatar
Steve Müller committed
1026 1027 1028 1029 1030 1031 1032 1033
    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return 'BYTEA';
    }

1034 1035 1036
    /**
     * {@inheritDoc}
     */
1037
    public function getClobTypeDeclarationSQL(array $field)
1038 1039 1040
    {
        return 'TEXT';
    }
1041 1042

    /**
1043
     * {@inheritDoc}
1044 1045 1046 1047 1048
     */
    public function getName()
    {
        return 'postgresql';
    }
1049

1050
    /**
1051
     * {@inheritDoc}
1052
     *
1053 1054
     * PostgreSQL returns all column names in SQL result sets in lowercase.
     */
1055
    public function getSQLResultCasing($column)
1056 1057 1058
    {
        return strtolower($column);
    }
1059

1060 1061 1062
    /**
     * {@inheritDoc}
     */
1063
    public function getDateTimeTzFormatString()
1064
    {
1065 1066
        return 'Y-m-d H:i:sO';
    }
1067 1068

    /**
1069
     * {@inheritDoc}
1070
     */
1071
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
1072 1073 1074
    {
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
    }
1075 1076

    /**
1077
     * {@inheritDoc}
1078
     */
1079
    public function getTruncateTableSQL($tableName, $cascade = false)
1080
    {
1081 1082 1083 1084 1085 1086 1087 1088
        $tableIdentifier = new Identifier($tableName);
        $sql = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);

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

        return $sql;
1089
    }
1090

1091 1092 1093
    /**
     * {@inheritDoc}
     */
1094 1095 1096 1097
    public function getReadLockSQL()
    {
        return 'FOR SHARE';
    }
1098

1099 1100 1101
    /**
     * {@inheritDoc}
     */
1102 1103
    protected function initializeDoctrineTypeMappings()
    {
1104
        $this->doctrineTypeMapping = [
1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118
            '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',
1119
            'tsvector'      => 'text',
1120 1121 1122 1123 1124
            'varchar'       => 'string',
            'interval'      => 'string',
            '_varchar'      => 'string',
            'char'          => 'string',
            'bpchar'        => 'string',
1125
            'inet'          => 'string',
1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141
            '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',
1142
            'uuid'          => 'guid',
1143
            'bytea'         => 'blob',
1144
        ];
1145
    }
1146

1147 1148 1149
    /**
     * {@inheritDoc}
     */
1150 1151 1152 1153
    public function getVarcharMaxLength()
    {
        return 65535;
    }
1154

Steve Müller's avatar
Steve Müller committed
1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170
    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 0;
    }

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

1171 1172 1173
    /**
     * {@inheritDoc}
     */
1174 1175
    protected function getReservedKeywordsClass()
    {
1176
        return Keywords\PostgreSQLKeywords::class;
1177
    }
1178 1179

    /**
1180
     * {@inheritDoc}
1181 1182 1183 1184 1185
     */
    public function getBlobTypeDeclarationSQL(array $field)
    {
        return 'BYTEA';
    }
1186 1187 1188 1189 1190 1191 1192 1193 1194 1195

    /**
     * {@inheritdoc}
     */
    public function quoteStringLiteral($str)
    {
        $str = str_replace('\\', '\\\\', $str); // PostgreSQL requires backslashes to be escaped aswell.

        return parent::quoteStringLiteral($str);
    }
1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211

    /**
     * {@inheritdoc}
     */
    public function getDefaultValueDeclarationSQL($field)
    {
        if ($this->isSerialField($field)) {
            return '';
        }

        return parent::getDefaultValueDeclarationSQL($field);
    }

    private function isSerialField(array $field) : bool
    {
        return $field['autoincrement'] ?? false === true && isset($field['type'])
1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237
            && $this->isNumericType($field['type']);
    }

    /**
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
     *
     * @param ColumnDiff $columnDiff
     * @return bool
     */
    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;
1238
    }
1239
}