PostgreSqlPlatform.php 35 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Benjamin Morel's avatar
Benjamin Morel committed
285 286 287
    /**
     * {@inheritDoc}
     */
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 315 316
    {
        return 'DROP VIEW '. $name;
    }

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

338
    /**
339 340
     * {@inheritDoc}
     *
341 342 343
     * @license New BSD License
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
     */
344
    public function getListTableIndexesSQL($table, $currentDatabase = null)
345
    {
346
        return "SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary,
347
                       pg_index.indkey, pg_index.indrelid,
348
                       pg_get_expr(indpred, indrelid) AS where
349 350
                 FROM pg_class, pg_index
                 WHERE oid IN (
351
                    SELECT indexrelid
352 353
                    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
354
                 ) AND pg_index.indexrelid = oid";
355
    }
356

357 358 359 360
    /**
     * @param string $table
     * @param string $classAlias
     * @param string $namespaceAlias
361
     *
362 363
     * @return string
     */
364 365
    private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n')
    {
366
        $whereClause = $namespaceAlias.".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
367 368
        if (strpos($table, ".") !== false) {
            list($schema, $table) = explode(".", $table);
369
            $schema = $this->quoteStringLiteral($schema);
370
        } else {
371
            $schema = "ANY(string_to_array((select replace(replace(setting,'\"\$user\"',user),' ','') from pg_catalog.pg_settings where name = 'search_path'),','))";
372
        }
373 374

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

378 379 380
        return $whereClause;
    }

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

418
    /**
419
     * {@inheritDoc}
420
     */
421
    public function getCreateDatabaseSQL($name)
422
    {
423
        return 'CREATE DATABASE ' . $name;
424
    }
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
    /**
     * 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)
    {
451 452 453
        $database = $this->quoteStringLiteral($database);

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

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

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

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

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

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

483 484
        return $query;
    }
485

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

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

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

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

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

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

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

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

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

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

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

539
                // here was a server version check before, but DBAL API does not support this anymore.
540
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($column->toArray(), $this);
541
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
542
            }
543

544
            if ($columnDiff->hasChanged('default') || $columnDiff->hasChanged('type')) {
545 546 547 548
                $defaultClause = null === $column->getDefault()
                    ? ' DROP DEFAULT'
                    : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
                $query = 'ALTER ' . $oldColumnName . $defaultClause;
549
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
550
            }
551

552
            if ($columnDiff->hasChanged('notnull')) {
553
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
554
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
555
            }
556

557 558 559
            if ($columnDiff->hasChanged('autoincrement')) {
                if ($column->getAutoincrement()) {
                    // add autoincrement
560
                    $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);
561 562

                    $sql[] = "CREATE SEQUENCE " . $seqName;
563
                    $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ") FROM " . $diff->getName($this)->getQuotedName($this) . "))";
564
                    $query = "ALTER " . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
565
                    $sql[] = "ALTER TABLE " . $diff->getName($this)->getQuotedName($this) . " " . $query;
566 567 568
                } else {
                    // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
                    $query = "ALTER " . $oldColumnName . " " . "DROP DEFAULT";
569
                    $sql[] = "ALTER TABLE " . $diff->getName($this)->getQuotedName($this) . " " . $query;
570 571
                }
            }
572

573 574
            if ($columnDiff->hasChanged('comment')) {
                $commentsSQL[] = $this->getCommentOnColumnSQL(
575 576
                    $diff->getName($this)->getQuotedName($this),
                    $column->getQuotedName($this),
577 578
                    $this->getColumnComment($column)
                );
579
            }
580 581

            if ($columnDiff->hasChanged('length')) {
582
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $column->getType()->getSQLDeclaration($column->toArray(), $this);
583
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
584
            }
585 586
        }

587
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
588 589
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
590 591
            }

592 593
            $oldColumnName = new Identifier($oldColumnName);

594
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
595
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
596 597
        }

598 599
        $tableSql = array();

600
        if ( ! $this->onSchemaAlterTable($diff, $tableSql)) {
601 602
            $sql = array_merge($sql, $commentsSQL);

603
            if ($diff->newName !== false) {
604
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' RENAME TO ' . $diff->getNewName()->getQuotedName($this);
605 606
            }

607 608 609 610 611
            $sql = array_merge(
                $this->getPreAlterTableIndexForeignKeySQL($diff),
                $sql,
                $this->getPostAlterTableIndexForeignKeySQL($diff)
            );
612 613
        }

614
        return array_merge($sql, $tableSql, $columnSql);
615
    }
616

Steve Müller's avatar
Steve Müller committed
617 618 619 620 621 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 653 654 655 656 657
    /**
     * 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;
            }

            return count(array_diff($columnDiff->changedProperties, array('type', 'length', 'fixed'))) === 0;
        }

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

        return count(array_diff($columnDiff->changedProperties, array('length', 'fixed'))) === 0;
    }

658 659 660 661 662
    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
663 664 665 666 667
        if (strpos($tableName, '.') !== false) {
            list($schema) = explode('.', $tableName);
            $oldIndexName = $schema . '.' . $oldIndexName;
        }

668 669 670
        return array('ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this));
    }

671 672 673 674 675
    /**
     * {@inheritdoc}
     */
    public function getCommentOnColumnSQL($tableName, $columnName, $comment)
    {
676 677
        $tableName = new Identifier($tableName);
        $columnName = new Identifier($columnName);
678
        $comment = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);
679

680 681
        return "COMMENT ON COLUMN " . $tableName->getQuotedName($this) . "." . $columnName->getQuotedName($this) .
            " IS $comment";
682 683
    }

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

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

    /**
     * Cache definition for sequences
     *
jeroendedauw's avatar
jeroendedauw committed
709 710
     * @param Sequence $sequence
     *
711 712
     * @return string
     */
jeroendedauw's avatar
jeroendedauw committed
713
    private function getSequenceCacheSQL(Sequence $sequence)
714 715 716 717 718 719
    {
        if ($sequence->getCache() > 1) {
            return ' CACHE ' . $sequence->getCache();
        }

        return '';
720
    }
721

722
    /**
723
     * {@inheritDoc}
724
     */
725
    public function getDropSequenceSQL($sequence)
726
    {
jeroendedauw's avatar
jeroendedauw committed
727
        if ($sequence instanceof Sequence) {
728
            $sequence = $sequence->getQuotedName($this);
729
        }
730

731
        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
732
    }
733

734 735 736 737 738 739 740 741
    /**
     * {@inheritDoc}
     */
    public function getCreateSchemaSQL($schemaName)
    {
        return 'CREATE SCHEMA ' . $schemaName;
    }

742
    /**
743
     * {@inheritDoc}
744
     */
745
    public function getDropForeignKeySQL($foreignKey, $table)
746
    {
747
        return $this->getDropConstraintSQL($foreignKey, $table);
748
    }
749

750
    /**
751
     * {@inheritDoc}
752
     */
753
    protected function _getCreateTableSQL($tableName, array $columns, array $options = array())
754
    {
755
        $queryFields = $this->getColumnDeclarationListSQL($columns);
756 757

        if (isset($options['primary']) && ! empty($options['primary'])) {
758
            $keyColumns = array_unique(array_values($options['primary']));
759 760 761
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
        }

762
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
763 764 765 766

        $sql[] = $query;

        if (isset($options['indexes']) && ! empty($options['indexes'])) {
767
            foreach ($options['indexes'] as $index) {
768
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
769 770 771 772
            }
        }

        if (isset($options['foreignKeys'])) {
773
            foreach ((array) $options['foreignKeys'] as $definition) {
774
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
775 776 777 778 779
            }
        }

        return $sql;
    }
780

781 782 783 784 785 786 787 788 789 790 791
    /**
     * 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
792
     * @throws \UnexpectedValueException
793 794 795 796
     */
    private function convertSingleBooleanValue($value, $callback)
    {
        if (null === $value) {
797
            return $callback(null);
798 799 800 801 802 803
        }

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

804 805 806 807 808 809 810 811 812 813 814 815 816
        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);
817 818
        }

819
        throw new \UnexpectedValueException("Unrecognized boolean literal '${value}'");
820 821 822 823 824 825 826 827 828
    }

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

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

858 859 860
        return $this->doConvertBooleans(
            $item,
            function ($boolean) {
861
                if (null === $boolean) {
862
                    return 'NULL';
863
                }
864

865
                return true === $boolean ? 'true' : 'false';
866
            }
867
        );
868 869 870 871 872
    }

    /**
     * {@inheritDoc}
     */
873
    public function convertBooleansToDatabaseValue($item)
874
    {
875
        if ( ! $this->useBooleanTrueFalseStrings) {
876
            return parent::convertBooleansToDatabaseValue($item);
877 878
        }

879 880 881
        return $this->doConvertBooleans(
            $item,
            function ($boolean) {
882
                return null === $boolean ? null : (int) $boolean;
883 884
            }
        );
885
    }
886

887
    /**
888 889
     * {@inheritDoc}
     */
890 891
    public function convertFromBoolean($item)
    {
892
        if (in_array(strtolower($item), $this->booleanLiterals['false'], true)) {
893
            return false;
894 895
        }

896
        return parent::convertFromBoolean($item);
897
    }
898

Benjamin Morel's avatar
Benjamin Morel committed
899 900 901
    /**
     * {@inheritDoc}
     */
902
    public function getSequenceNextValSQL($sequenceName)
903 904 905
    {
        return "SELECT NEXTVAL('" . $sequenceName . "')";
    }
906

907 908 909
    /**
     * {@inheritDoc}
     */
910
    public function getSetTransactionIsolationSQL($level)
romanb's avatar
romanb committed
911 912
    {
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
913
                . $this->_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
914
    }
915

916
    /**
917
     * {@inheritDoc}
918
     */
919
    public function getBooleanTypeDeclarationSQL(array $field)
920 921 922
    {
        return 'BOOLEAN';
    }
923 924

    /**
925
     * {@inheritDoc}
926
     */
927
    public function getIntegerTypeDeclarationSQL(array $field)
928 929 930 931
    {
        if ( ! empty($field['autoincrement'])) {
            return 'SERIAL';
        }
932

933 934 935 936
        return 'INT';
    }

    /**
937
     * {@inheritDoc}
938
     */
939
    public function getBigIntTypeDeclarationSQL(array $field)
940 941 942 943
    {
        if ( ! empty($field['autoincrement'])) {
            return 'BIGSERIAL';
        }
944

945 946 947 948
        return 'BIGINT';
    }

    /**
949
     * {@inheritDoc}
950
     */
951
    public function getSmallIntTypeDeclarationSQL(array $field)
952 953 954 955
    {
        return 'SMALLINT';
    }

rivaros's avatar
rivaros committed
956
    /**
957
     * {@inheritDoc}
rivaros's avatar
rivaros committed
958
     */
959
    public function getGuidTypeDeclarationSQL(array $field)
rivaros's avatar
rivaros committed
960 961 962 963
    {
        return 'UUID';
    }

964
    /**
965
     * {@inheritDoc}
966
     */
967
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
968 969 970 971 972
    {
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
    }

    /**
973
     * {@inheritDoc}
974
     */
975
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
976
    {
977 978
        return 'TIMESTAMP(0) WITH TIME ZONE';
    }
979

980
    /**
981
     * {@inheritDoc}
982
     */
983
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
984 985
    {
        return 'DATE';
986 987
    }

988
    /**
989
     * {@inheritDoc}
990
     */
991
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
992
    {
993
        return 'TIME(0) WITHOUT TIME ZONE';
994 995
    }

996 997 998 999 1000 1001 1002 1003
    /**
     * {@inheritDoc}
     */
    public function getGuidExpression()
    {
        return 'UUID_GENERATE_V4()';
    }

1004
    /**
1005
     * {@inheritDoc}
1006
     */
1007
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1008 1009 1010 1011 1012
    {
        return '';
    }

    /**
1013
     * {@inheritDoc}
1014
     */
1015
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1016 1017
    {
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
1018
                : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
1019
    }
1020

Steve Müller's avatar
Steve Müller committed
1021 1022 1023 1024 1025 1026 1027 1028
    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return 'BYTEA';
    }

1029 1030 1031
    /**
     * {@inheritDoc}
     */
1032
    public function getClobTypeDeclarationSQL(array $field)
1033 1034 1035
    {
        return 'TEXT';
    }
1036 1037

    /**
1038
     * {@inheritDoc}
1039 1040 1041 1042 1043
     */
    public function getName()
    {
        return 'postgresql';
    }
1044

1045
    /**
1046
     * {@inheritDoc}
1047
     *
1048 1049
     * PostgreSQL returns all column names in SQL result sets in lowercase.
     */
1050
    public function getSQLResultCasing($column)
1051 1052 1053
    {
        return strtolower($column);
    }
1054

1055 1056 1057
    /**
     * {@inheritDoc}
     */
1058
    public function getDateTimeTzFormatString()
1059
    {
1060 1061
        return 'Y-m-d H:i:sO';
    }
1062 1063

    /**
1064
     * {@inheritDoc}
1065
     */
1066
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
1067 1068 1069
    {
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
    }
1070 1071

    /**
1072
     * {@inheritDoc}
1073
     */
1074
    public function getTruncateTableSQL($tableName, $cascade = false)
1075
    {
1076 1077 1078 1079 1080 1081 1082 1083
        $tableIdentifier = new Identifier($tableName);
        $sql = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);

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

        return $sql;
1084
    }
1085

1086 1087 1088
    /**
     * {@inheritDoc}
     */
1089 1090 1091 1092
    public function getReadLockSQL()
    {
        return 'FOR SHARE';
    }
1093

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

1142 1143 1144
    /**
     * {@inheritDoc}
     */
1145 1146 1147 1148
    public function getVarcharMaxLength()
    {
        return 65535;
    }
1149

Steve Müller's avatar
Steve Müller committed
1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165
    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 0;
    }

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

1166 1167 1168
    /**
     * {@inheritDoc}
     */
1169 1170
    protected function getReservedKeywordsClass()
    {
1171
        return Keywords\PostgreSQLKeywords::class;
1172
    }
1173 1174

    /**
1175
     * {@inheritDoc}
1176 1177 1178 1179 1180
     */
    public function getBlobTypeDeclarationSQL(array $field)
    {
        return 'BYTEA';
    }
1181 1182 1183 1184 1185 1186 1187 1188 1189 1190

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

        return parent::quoteStringLiteral($str);
    }
1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208

    /**
     * {@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'])
            && ($field['type'] instanceof IntegerType || $field['type'] instanceof BigIntType);
    }
1209
}