PostgreSqlPlatform.php 19.5 KB
Newer Older
1
<?php
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/*
 *  $Id$
 *
 * 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
 * and is licensed under the LGPL. For more information, see
 * <http://www.doctrine-project.org>.
 */
21

22
namespace Doctrine\DBAL\Platforms;
23

24 25
use Doctrine\DBAL\Schema\TableDiff,
    Doctrine\DBAL\Schema\Table;
26

27 28 29 30 31 32
/**
 * PostgreSqlPlatform.
 *
 * @since 2.0
 * @author Roman Borschel <roman@code-factory.org>
 * @author Lukas Smith <smith@pooteeweet.org> (PEAR MDB2 library)
33
 * @author Benjamin Eberlei <kontakt@beberlei.de>
34
 * @todo Rename: PostgreSQLPlatform
35
 */
36
class PostgreSqlPlatform extends AbstractPlatform
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
{
    /**
     * Returns part of a string.
     *
     * Note: Not SQL92, but common functionality.
     *
     * @param string $value the target $value the string or the string column.
     * @param int $from extract from this characeter.
     * @param int $len extract this amount of characters.
     * @return string sql that extracts part of a string.
     * @override
     */
    public function getSubstringExpression($value, $from, $len = null)
    {
        if ($len === null) {
            return 'SUBSTR(' . $value . ', ' . $from . ')';
        } else {
            return 'SUBSTR(' . $value . ', ' . $from . ', ' . $len . ')';
        }
    }

    /**
     * Returns the SQL string to return the current system date and time.
     *
     * @return string
     */
    public function getNowExpression()
    {
        return 'LOCALTIMESTAMP(0)';
    }

    /**
     * regexp
     *
     * @return string           the regular expression operator
     * @override
     */
    public function getRegexpExpression()
    {
        return 'SIMILAR TO';
    }
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95

    /**
     * returns the position of the first occurrence of substring $substr in string $str
     *
     * @param string $substr    literal string to find
     * @param string $str       literal string
     * @param int    $pos       position to start at, beginning of string by default
     * @return integer
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        if ($startPos !== false) {
            $str = $this->getSubstringExpression($str, $startPos);
            return 'CASE WHEN (POSITION('.$substr.' IN '.$str.') = 0) THEN 0 ELSE (POSITION('.$substr.' IN '.$str.') + '.($startPos-1).') END';
        } else {
            return 'POSITION('.$substr.' IN '.$str.')';
        }
    }
96 97 98 99 100 101 102 103
    
    /**
     * parses a literal boolean value and returns
     * proper sql equivalent
     *
     * @param string $value     boolean value to be parsed
     * @return string           parsed boolean value
     */
104
    /*public function parseBoolean($value)
105 106
    {
        return $value;
107
    }*/
romanb's avatar
romanb committed
108 109 110 111 112 113 114 115 116 117 118 119
    
    /**
     * Whether the platform supports sequences.
     * Postgres has native support for sequences.
     *
     * @return boolean
     */
    public function supportsSequences()
    {
        return true;
    }
    
120 121 122 123 124 125 126 127 128 129
    /**
     * Whether the platform supports database schemas.
     * 
     * @return boolean
     */
    public function supportsSchemas()
    {
        return true;
    }
    
romanb's avatar
romanb committed
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
    /**
     * Whether the platform supports identity columns.
     * Postgres supports these through the SERIAL keyword.
     *
     * @return boolean
     */
    public function supportsIdentityColumns()
    {
        return true;
    }
    
    /**
     * Whether the platform prefers sequences for ID generation.
     *
     * @return boolean
     */
    public function prefersSequences()
    {
        return true;
    }
150

151
    public function getListDatabasesSQL()
152 153 154
    {
        return 'SELECT datname FROM pg_database';
    }
155

156
    public function getListSequencesSQL($database)
157 158 159 160 161 162 163 164 165
    {
        return "SELECT
                    relname
                FROM
                   pg_class
                WHERE relkind = 'S' AND relnamespace IN
                    (SELECT oid FROM pg_namespace
                        WHERE nspname NOT LIKE 'pg_%' AND nspname != 'information_schema')";
    }
166

167
    public function getListTablesSQL()
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
    {
        return "SELECT
                    c.relname AS table_name
                FROM pg_class c, pg_user u
                WHERE c.relowner = u.usesysid
                    AND c.relkind = 'r'
                    AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname)
                    AND c.relname !~ '^(pg_|sql_)'
                UNION
                SELECT c.relname AS table_name
                FROM pg_class c
                WHERE c.relkind = 'r'
                    AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname)
                    AND NOT EXISTS (SELECT 1 FROM pg_user WHERE usesysid = c.relowner)
                    AND c.relname !~ '^pg_'";
    }
184

185
    public function getListViewsSQL($database)
186
    {
187
        return 'SELECT viewname, definition FROM pg_views';
188
    }
189

190
    public function getListTableForeignKeysSQL($table, $database = null)
191
    {
192
        return "SELECT r.conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef
193 194 195 196 197 198 199 200 201 202 203
                  FROM pg_catalog.pg_constraint r
                  WHERE r.conrelid =
                  (
                      SELECT c.oid
                      FROM pg_catalog.pg_class c
                      LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
                      WHERE c.relname = '" . $table . "' AND pg_catalog.pg_table_is_visible(c.oid)
                  )
                  AND r.contype = 'f'";
    }

204
    public function getCreateViewSQL($name, $sql)
205 206 207 208
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

209
    public function getDropViewSQL($name)
210 211 212 213
    {
        return 'DROP VIEW '. $name;
    }

214
    public function getListTableConstraintsSQL($table)
215 216 217 218 219 220 221 222
    {
        return "SELECT
                    relname
                FROM
                    pg_class
                WHERE oid IN (
                    SELECT indexrelid
                    FROM pg_index, pg_class
223
                    WHERE pg_class.relname = '$table'
224 225 226 227
                        AND pg_class.oid = pg_index.indrelid
                        AND (indisunique = 't' OR indisprimary = 't')
                        )";
    }
228

229 230 231 232 233 234
    /**
     * @license New BSD License
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
     * @param  string $table
     * @return string
     */
235
    public function getListTableIndexesSQL($table)
236
    {
237 238 239 240
        return "SELECT relname, pg_index.indisunique, pg_index.indisprimary,
                       pg_index.indkey, pg_index.indrelid
                 FROM pg_class, pg_index
                 WHERE oid IN (
241 242
                    SELECT indexrelid
                    FROM pg_index, pg_class
243 244
                    WHERE pg_class.relname='$table' AND pg_class.oid=pg_index.indrelid
                 ) AND pg_index.indexrelid = oid";
245
    }
246

247
    public function getListTableColumnsSQL($table)
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
    {
        return "SELECT
                    a.attnum,
                    a.attname AS field,
                    t.typname AS type,
                    format_type(a.atttypid, a.atttypmod) AS complete_type,
                    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,
                    (SELECT pg_attrdef.adsrc
                     FROM pg_attrdef
                     WHERE c.oid = pg_attrdef.adrelid
                        AND pg_attrdef.adnum=a.attnum
                    ) AS default
                    FROM pg_attribute a, pg_class c, pg_type t
267
                    WHERE c.relname = '$table'
268 269 270 271 272 273 274 275 276 277 278 279 280 281
                        AND a.attnum > 0
                        AND a.attrelid = c.oid
                        AND a.atttypid = t.oid
                    ORDER BY a.attnum";
    }
    
    /**
     * create a new database
     *
     * @param string $name name of the database that should be created
     * @throws PDOException
     * @return void
     * @override
     */
282
    public function getCreateDatabaseSQL($name)
283
    {
284
        return 'CREATE DATABASE ' . $name;
285
    }
286

287 288 289 290 291 292 293
    /**
     * drop an existing database
     *
     * @param string $name name of the database that should be dropped
     * @throws PDOException
     * @access public
     */
294
    public function getDropDatabaseSQL($name)
295
    {
296
        return 'DROP DATABASE ' . $name;
297
    }
298

299 300 301 302
    /**
     * Return the FOREIGN KEY query section dealing with non-standard options
     * as MATCH, INITIALLY DEFERRED, ON UPDATE, ...
     *
303
     * @param \Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey         foreign key definition
304 305 306
     * @return string
     * @override
     */
307
    public function getAdvancedForeignKeyOptionsSQL(\Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey)
308 309
    {
        $query = '';
310 311
        if ($foreignKey->hasOption('match')) {
            $query .= ' MATCH ' . $foreignKey->getOption('match');
312
        }
313
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
314
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
315 316 317 318
            $query .= ' DEFERRABLE';
        } else {
            $query .= ' NOT DEFERRABLE';
        }
319
        if ($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false) {
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
            $query .= ' INITIALLY DEFERRED';
        } else {
            $query .= ' INITIALLY IMMEDIATE';
        }
        return $query;
    }
    
    /**
     * generates the sql for altering an existing table on postgresql
     *
     * @param string $name          name of the table that is intended to be changed.
     * @param array $changes        associative array that contains the details of each type      *
     * @param boolean $check        indicates whether the function should just check if the DBMS driver
     *                              can perform the requested table alterations if the value is true or
     *                              actually perform them otherwise.
     * @see Doctrine_Export::alterTable()
     * @return array
     * @override
     */
339
    public function getAlterTableSQL(TableDiff $diff)
340
    {
341
        $sql = array();
342 343

        foreach ($diff->addedColumns as $column) {
344
            $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getName(), $column->toArray());
345
            $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query;
346 347
        }

348 349 350
        foreach ($diff->removedColumns as $column) {
            $query = 'DROP ' . $column->getName();
            $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query;
351 352
        }

353 354 355 356 357 358
        foreach ($diff->changedColumns AS $columnDiff) {
            $oldColumnName = $columnDiff->oldColumnName;
            $column = $columnDiff->column;
            
            if ($columnDiff->hasChanged('type')) {
                $type = $column->getType();
359

360 361 362
                // here was a server version check before, but DBAL API does not support this anymore.
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSqlDeclaration($column->toArray(), $this);
                $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query;
363
            }
364
            if ($columnDiff->hasChanged('default')) {
365
                $query = 'ALTER ' . $oldColumnName . ' SET ' . $this->getDefaultValueDeclarationSQL($column->toArray());
366
                $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query;
367
            }
368 369 370
            if ($columnDiff->hasChanged('notnull')) {
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotNull() ? 'SET' : 'DROP') . ' NOT NULL';
                $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query;
371 372 373
            }
        }

374 375
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
            $sql[] = 'ALTER TABLE ' . $diff->name . ' RENAME COLUMN ' . $oldColumnName . ' TO ' . $column->getName();
376 377
        }

378 379
        if ($diff->newName !== false) {
            $sql[] = 'ALTER TABLE ' . $diff->name . ' RENAME TO ' . $diff->newName;
380 381
        }

382
        $sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff));
383

384 385 386 387
        return $sql;
    }
    
    /**
388 389 390
     * Gets the SQL to create a sequence on this platform.
     *
     * @param \Doctrine\DBAL\Schema\Sequence $sequence
391
     * @return string
392
     */
393
    public function getCreateSequenceSQL(\Doctrine\DBAL\Schema\Sequence $sequence)
394
    {
395 396
        return 'CREATE SEQUENCE ' . $sequence->getName() .
               ' INCREMENT BY ' . $sequence->getAllocationSize() .
397
               ' MINVALUE ' . $sequence->getInitialValue() .
398
               ' START ' . $sequence->getInitialValue();
399 400 401
    }
    
    /**
402 403 404
     * Drop existing sequence
     * @param  \Doctrine\DBAL\Schema\Sequence $sequence
     * @return string
405
     */
406
    public function getDropSequenceSQL($sequence)
407
    {
408 409 410 411
        if ($sequence instanceof \Doctrine\DBAL\Schema\Sequence) {
            $sequence = $sequence->getName();
        }
        return 'DROP SEQUENCE ' . $sequence;
412
    }
413 414 415 416 417 418

    /**
     * @param  ForeignKeyConstraint|string $foreignKey
     * @param  Table|string $table
     * @return string
     */
419
    public function getDropForeignKeySQL($foreignKey, $table)
420
    {
421
        return $this->getDropConstraintSQL($foreignKey, $table);
422
    }
423 424 425 426
    
    /**
     * Gets the SQL used to create a table.
     *
427 428
     * @param unknown_type $tableName
     * @param array $columns
429 430 431
     * @param array $options
     * @return unknown
     */
432
    protected function _getCreateTableSQL($tableName, array $columns, array $options = array())
433
    {
434
        $queryFields = $this->getColumnDeclarationListSQL($columns);
435 436

        if (isset($options['primary']) && ! empty($options['primary'])) {
437
            $keyColumns = array_unique(array_values($options['primary']));
438 439 440
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
        }

441
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
442 443 444 445

        $sql[] = $query;

        if (isset($options['indexes']) && ! empty($options['indexes'])) {
446
            foreach ($options['indexes'] AS $index) {
447
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
448 449 450 451
            }
        }

        if (isset($options['foreignKeys'])) {
452
            foreach ((array) $options['foreignKeys'] as $definition) {
453
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
            }
        }

        return $sql;
    }
    
    /**
     * Postgres wants boolean values converted to the strings 'true'/'false'.
     *
     * @param array $item
     * @override
     */
    public function convertBooleans($item)
    {
        if (is_array($item)) {
            foreach ($item as $key => $value) {
                if (is_bool($value) || is_numeric($item)) {
                    $item[$key] = ($value) ? 'true' : 'false';
                }
            }
        } else {
           if (is_bool($item) || is_numeric($item)) {
               $item = ($item) ? 'true' : 'false';
           }
        }
        return $item;
    }
481

482
    public function getSequenceNextValSQL($sequenceName)
483 484 485
    {
        return "SELECT NEXTVAL('" . $sequenceName . "')";
    }
486

487
    public function getSetTransactionIsolationSQL($level)
romanb's avatar
romanb committed
488 489
    {
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
490
                . $this->_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
491
    }
492 493 494 495
    
    /**
     * @override
     */
496
    public function getBooleanTypeDeclarationSQL(array $field)
497 498 499
    {
        return 'BOOLEAN';
    }
500 501 502 503

    /**
     * @override
     */
504
    public function getIntegerTypeDeclarationSQL(array $field)
505 506 507 508
    {
        if ( ! empty($field['autoincrement'])) {
            return 'SERIAL';
        }
509
        
510 511 512 513 514 515
        return 'INT';
    }

    /**
     * @override
     */
516
    public function getBigIntTypeDeclarationSQL(array $field)
517 518 519 520 521 522 523 524 525 526
    {
        if ( ! empty($field['autoincrement'])) {
            return 'BIGSERIAL';
        }
        return 'BIGINT';
    }

    /**
     * @override
     */
527
    public function getSmallIntTypeDeclarationSQL(array $field)
528 529 530 531
    {
        return 'SMALLINT';
    }

532 533 534
    /**
     * @override
     */
535
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
536
    {
537 538 539 540 541 542
        return 'TIMESTAMP(0) WITH TIME ZONE';
    }
    
    /**
     * @override
     */
543
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
544 545
    {
        return 'DATE';
546 547
    }

548 549 550
    /**
     * @override
     */
551
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
552 553 554 555
    {
        return 'TIME';
    }

556 557 558
    /**
     * @override
     */
559
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
560 561 562 563 564 565 566 567 568 569
    {
        return '';
    }

    /**
     * Gets the SQL snippet used to declare a VARCHAR column on the MySql platform.
     *
     * @params array $field
     * @override
     */
570
    public function getVarcharTypeDeclarationSQL(array $field)
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
    {
        if ( ! isset($field['length'])) {
            if (array_key_exists('default', $field)) {
                $field['length'] = $this->getVarcharMaxLength();
            } else {
                $field['length'] = false;
            }
        }

        $length = ($field['length'] <= $this->getVarcharMaxLength()) ? $field['length'] : false;
        $fixed = (isset($field['fixed'])) ? $field['fixed'] : false;

        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
                : ($length ? 'VARCHAR(' . $length . ')' : 'TEXT');
    }
586 587
    
    /** @override */
588
    public function getClobTypeDeclarationSQL(array $field)
589 590 591
    {
        return 'TEXT';
    }
592 593 594 595 596 597 598 599 600 601

    /**
     * Get the platform name for this instance
     *
     * @return string
     */
    public function getName()
    {
        return 'postgresql';
    }
602
    
603 604 605 606 607 608 609 610
    /**
     * Gets the character casing of a column in an SQL result set.
     * 
     * PostgreSQL returns all column names in SQL result sets in lowercase.
     * 
     * @param string $column The column name for which to get the correct character casing.
     * @return string The column name in the character casing used in SQL result sets.
     */
611
    public function getSQLResultCasing($column)
612 613 614
    {
        return strtolower($column);
    }
615
    
616
    public function getDateTimeFormatString()
617
    {
618 619
        return 'Y-m-d H:i:sO';
    }
620 621 622 623 624 625 626 627

    /**
     * Get the insert sql for an empty insert statement
     *
     * @param string $tableName 
     * @param string $identifierColumnName 
     * @return string $sql
     */
628
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
629 630 631
    {
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
    }
632 633 634 635

    /**
     * @inheritdoc
     */
636
    public function getTruncateTableSQL($tableName, $cascade = false)
637 638 639
    {
        return 'TRUNCATE '.$tableName.' '.($cascade)?'CASCADE':'';
    }
640 641 642 643 644

    public function getReadLockSQL()
    {
        return 'FOR SHARE';
    }
645
}