SQLServerPlatform.php 48 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

22
use Doctrine\DBAL\LockMode;
23
use Doctrine\DBAL\Schema\Column;
24
use Doctrine\DBAL\Schema\ColumnDiff;
25
use Doctrine\DBAL\Schema\Identifier;
26
use Doctrine\DBAL\Schema\TableDiff;
27 28 29
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
30 31

/**
32 33
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
 * Microsoft SQL Server database platform.
34 35 36 37
 *
 * @since 2.0
 * @author Roman Borschel <roman@code-factory.org>
 * @author Jonathan H. Wage <jonwage@gmail.com>
38
 * @author Benjamin Eberlei <kontakt@beberlei.de>
39
 * @author Steve Müller <st.mueller@dzh-online.de>
40
 */
41
class SQLServerPlatform extends AbstractPlatform
42
{
43
    /**
44
     * {@inheritdoc}
45
     */
46
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
47
    {
48
        $factorClause = '';
49

50 51 52
        if ('-' === $operator) {
            $factorClause = '-1 * ';
        }
53

54
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
55
    }
56

57 58 59
    /**
     * {@inheritDoc}
     */
60
    public function getDateDiffExpression($date1, $date2)
61
    {
62
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
63
    }
64

65
    /**
66 67
     * {@inheritDoc}
     *
68 69
     * Microsoft SQL Server prefers "autoincrement" identity columns
     * since sequences can only be emulated with a table.
70
     */
71
    public function prefersIdentityColumns()
72
    {
73 74
        return true;
    }
75

76
    /**
77
     * {@inheritDoc}
78
     *
79
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
80 81 82 83 84 85 86
     */
    public function supportsIdentityColumns()
    {
        return true;
    }

    /**
87
     * {@inheritDoc}
88
     */
89
    public function supportsReleaseSavepoints()
90 91 92
    {
        return false;
    }
93

94 95 96 97 98 99 100 101
    /**
     * {@inheritdoc}
     */
    public function supportsSchemas()
    {
        return true;
    }

102 103 104 105 106 107 108 109
    /**
     * {@inheritdoc}
     */
    public function getDefaultSchemaName()
    {
        return 'dbo';
    }

110 111 112 113 114 115 116 117
    /**
     * {@inheritDoc}
     */
    public function supportsColumnCollation()
    {
        return true;
    }

118 119 120 121 122 123 124 125
    /**
     * {@inheritDoc}
     */
    public function hasNativeGuidType()
    {
        return true;
    }

126
    /**
127
     * {@inheritDoc}
128 129 130 131 132
     */
    public function getCreateDatabaseSQL($name)
    {
        return 'CREATE DATABASE ' . $name;
    }
133

134
    /**
135
     * {@inheritDoc}
136 137 138
     */
    public function getDropDatabaseSQL($name)
    {
139 140 141 142
        return 'DROP DATABASE ' . $name;
    }

    /**
143
     * {@inheritDoc}
144
     */
145
    public function supportsCreateDropDatabase()
146 147
    {
        return false;
148 149
    }

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
    /**
     * {@inheritDoc}
     */
    public function getCreateSchemaSQL($schemaName)
    {
        return 'CREATE SCHEMA ' . $schemaName;
    }

    /**
     * {@inheritDoc}
     */
    public function schemaNeedsCreation($schemaName)
    {
        return $schemaName !== 'dbo';
    }

166
    /**
167
     * {@inheritDoc}
168 169 170
     */
    public function getDropForeignKeySQL($foreignKey, $table)
    {
171
        if ($foreignKey instanceof ForeignKeyConstraint) {
172
            $foreignKey = $foreignKey->getQuotedName($this);
173 174
        }

175
        if ($table instanceof Table) {
176
            $table = $table->getQuotedName($this);
177 178 179
        }

        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
180
    }
181 182

    /**
183
     * {@inheritDoc}
184
     */
185
    public function getDropIndexSQL($index, $table = null)
186
    {
187
        if ($index instanceof Index) {
188
            $index = $index->getQuotedName($this);
Steve Müller's avatar
Steve Müller committed
189
        } elseif (!is_string($index)) {
190 191 192
            throw new \InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
        }

193 194
        if (!isset($table)) {
            return 'DROP INDEX ' . $index;
195
        }
196

197 198
        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
199
        }
200 201 202 203 204

        return "IF EXISTS (SELECT * FROM sysobjects WHERE name = '$index')
                    ALTER TABLE " . $table . " DROP CONSTRAINT " . $index . "
                ELSE
                    DROP INDEX " . $index . " ON " . $table;
205
    }
206 207

    /**
208
     * {@inheritDoc}
209 210
     */
    protected function _getCreateTableSQL($tableName, array $columns, array $options = array())
211
    {
212
        $defaultConstraintsSql = array();
213
        $commentsSql           = array();
214

215
        // @todo does other code breaks because of this?
216
        // force primary keys to be not null
217 218 219 220
        foreach ($columns as &$column) {
            if (isset($column['primary']) && $column['primary']) {
                $column['notnull'] = true;
            }
221

222
            // Build default constraints SQL statements.
223
            if (isset($column['default'])) {
224 225 226
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
            }
227 228 229 230

            if ( ! empty($column['comment']) || is_numeric($column['comment'])) {
                $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
            }
231 232
        }

233
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
234 235

        if (isset($options['uniqueConstraints']) && !empty($options['uniqueConstraints'])) {
236 237 238 239
            foreach ($options['uniqueConstraints'] as $name => $definition) {
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
            }
        }
240 241

        if (isset($options['primary']) && !empty($options['primary'])) {
242 243 244 245 246
            $flags = '';
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
                $flags = ' NONCLUSTERED';
            }
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
247 248 249 250 251
        }

        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;

        $check = $this->getCheckDeclarationSQL($columns);
252
        if (!empty($check)) {
253 254 255 256 257
            $query .= ', ' . $check;
        }
        $query .= ')';

        $sql[] = $query;
258 259

        if (isset($options['indexes']) && !empty($options['indexes'])) {
260
            foreach ($options['indexes'] as $index) {
261 262 263 264 265
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
            }
        }

        if (isset($options['foreignKeys'])) {
266
            foreach ((array) $options['foreignKeys'] as $definition) {
267 268 269 270
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
            }
        }

271
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
272
    }
273

274
    /**
275
     * {@inheritDoc}
276 277 278 279 280 281 282
     */
    public function getCreatePrimaryKeySQL(Index $index, $table)
    {
        $flags = '';
        if ($index->hasFlag('nonclustered')) {
            $flags = ' NONCLUSTERED';
        }
283
        return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY' . $flags . ' (' . $this->getIndexFieldDeclarationListSQL($index->getQuotedColumns($this)) . ')';
284 285
    }

286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
    /**
     * Returns the SQL statement for creating a column comment.
     *
     * SQL Server does not support native column comments,
     * therefore the extended properties functionality is used
     * as a workaround to store them.
     * The property name used to store column comments is "MS_Description"
     * which provides compatibility with SQL Server Management Studio,
     * as column comments are stored in the same property there when
     * specifying a column's "Description" attribute.
     *
     * @param string $tableName  The quoted table name to which the column belongs.
     * @param string $columnName The quoted column name to create the comment for.
     * @param string $comment    The column's comment.
     *
     * @return string
     */
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
    {
        return $this->getAddExtendedPropertySQL(
            'MS_Description',
            $comment,
            'SCHEMA',
            'dbo',
            'TABLE',
            $tableName,
            'COLUMN',
            $columnName
        );
    }

317 318 319 320 321 322 323 324 325 326 327 328
    /**
     * Returns the SQL snippet for declaring a default constraint.
     *
     * @param string $table  Name of the table to return the default constraint declaration for.
     * @param array  $column Column definition.
     *
     * @return string
     *
     * @throws \InvalidArgumentException
     */
    public function getDefaultConstraintDeclarationSQL($table, array $column)
    {
329
        if ( ! isset($column['default'])) {
330 331 332 333 334 335 336 337 338 339
            throw new \InvalidArgumentException("Incomplete column definition. 'default' required.");
        }

        return
            ' CONSTRAINT ' .
            $this->generateDefaultConstraintName($table, $column['name']) .
            $this->getDefaultValueDeclarationSQL($column) .
            ' FOR ' . $column['name'];
    }

340
    /**
341
     * {@inheritDoc}
342
     */
343
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
344 345
    {
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
346 347 348 349 350 351 352

        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);

        return $constraint;
    }

    /**
353
     * {@inheritDoc}
354 355 356 357 358
     */
    public function getCreateIndexSQL(Index $index, $table)
    {
        $constraint = parent::getCreateIndexSQL($index, $table);

Craig Mason's avatar
Craig Mason committed
359
        if ($index->isUnique() && !$index->isPrimary()) {
360 361 362 363 364 365
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
        }

        return $constraint;
    }

366
    /**
367
     * {@inheritDoc}
368 369 370 371 372 373 374 375 376 377
     */
    protected function getCreateIndexSQLFlags(Index $index)
    {
        $type = '';
        if ($index->isUnique()) {
            $type .= 'UNIQUE ';
        }

        if ($index->hasFlag('clustered')) {
            $type .= 'CLUSTERED ';
Steve Müller's avatar
Steve Müller committed
378
        } elseif ($index->hasFlag('nonclustered')) {
379 380 381 382 383 384
            $type .= 'NONCLUSTERED ';
        }

        return $type;
    }

385
    /**
386
     * Extend unique key constraint with required filters
387
     *
Benjamin Morel's avatar
Benjamin Morel committed
388 389
     * @param string                      $sql
     * @param \Doctrine\DBAL\Schema\Index $index
390
     *
391 392 393 394 395 396
     * @return string
     */
    private function _appendUniqueConstraintDefinition($sql, Index $index)
    {
        $fields = array();

397
        foreach ($index->getQuotedColumns($this) as $field) {
398
            $fields[] = $field . ' IS NOT NULL';
399
        }
400 401 402

        return $sql . ' WHERE ' . implode(' AND ', $fields);
    }
403

404
    /**
405
     * {@inheritDoc}
406
     */
407
    public function getAlterTableSQL(TableDiff $diff)
408
    {
409 410 411 412
        $queryParts  = array();
        $sql         = array();
        $columnSql   = array();
        $commentsSql = array();
413

414
        /** @var \Doctrine\DBAL\Schema\Column $column */
415
        foreach ($diff->addedColumns as $column) {
416 417
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
418 419
            }

420 421 422
            $columnDef = $column->toArray();
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);

423
            if (isset($columnDef['default'])) {
424
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
425
            }
426 427 428 429 430 431 432 433 434 435

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

            if ( ! empty($comment) || is_numeric($comment)) {
                $commentsSql[] = $this->getCreateColumnCommentSQL(
                    $diff->name,
                    $column->getQuotedName($this),
                    $comment
                );
            }
436 437
        }

438
        foreach ($diff->removedColumns as $column) {
439 440
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
441 442
            }

443
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
444 445
        }

446
        /* @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
447
        foreach ($diff->changedColumns as $columnDiff) {
448 449
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
450 451
            }

452 453 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 481 482 483
            $column     = $columnDiff->column;
            $comment    = $this->getColumnComment($column);
            $hasComment = ! empty ($comment) || is_numeric($comment);

            if ($columnDiff->fromColumn instanceof Column) {
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
                $hasFromComment = ! empty ($fromComment) || is_numeric($fromComment);

                if ($hasFromComment && $hasComment && $fromComment != $comment) {
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
                        $diff->name,
                        $column->getQuotedName($this),
                        $comment
                    );
                } elseif ($hasFromComment && ! $hasComment) {
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
                } elseif ($hasComment) {
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
                        $diff->name,
                        $column->getQuotedName($this),
                        $comment
                    );
                }
            } else {
                // todo: Original comment cannot be determined. What to do? Add, update, drop or skip?
            }

            // Do not add query part if only comment has changed.
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
                continue;
            }

484
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
485

486 487 488 489 490
            if ($requireDropDefaultConstraint) {
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
                    $diff->name,
                    $columnDiff->oldColumnName
                );
491 492
            }

493 494
            $columnDef = $column->toArray();

495
            $queryParts[] = 'ALTER COLUMN ' .
496 497
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);

498 499
            if (isset($columnDef['default']) && ($requireDropDefaultConstraint || $columnDiff->hasChanged('default'))) {
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
500
            }
501 502
        }

503
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
504 505
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
506 507
            }

508 509 510 511
            $oldColumnName = new Identifier($oldColumnName);

            $sql[] = "sp_RENAME '" . $diff->name . "." . $oldColumnName->getQuotedName($this) .
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
512

513 514 515 516
            // Recreate default constraint with new column name if necessary (for future reference).
            if ($column->getDefault() !== null) {
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause($diff->name, $oldColumnName);
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
517
            }
518
        }
519

520 521 522
        $tableSql = array();

        if ($this->onSchemaAlterTable($diff, $tableSql)) {
523
            return array_merge($tableSql, $columnSql);
524
        }
525

526
        foreach ($queryParts as $query) {
527
            $sql[] = 'ALTER TABLE ' . $diff->getName()->getQuotedName($this) . ' ' . $query;
528
        }
529

530
        $sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff), $commentsSql);
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
531

532
        if ($diff->newName !== false) {
533
            $sql[] = "sp_RENAME '" . $diff->getName()->getQuotedName($this) . "', '" . $diff->getNewName()->getQuotedName($this) . "'";
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548

            /**
             * Rename table's default constraints names
             * to match the new table name.
             * This is necessary to ensure that the default
             * constraints can be referenced in future table
             * alterations as the table name is encoded in
             * default constraints' names.
             */
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
                "'" . $this->generateIdentifierName($diff->newName) . "') + ''', ''OBJECT'';' " .
                "FROM sys.default_constraints dc " .
                "JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id " .
549
                "WHERE tbl.name = '" . $diff->getNewName()->getQuotedName($this) . "';" .
550
                "EXEC sp_executesql @sql";
551 552
        }

553
        return array_merge($sql, $tableSql, $columnSql);
554
    }
555

556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625
    /**
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
     *
     * @param  string $tableName The name of the table to generate the clause for.
     * @param  Column $column    The column to generate the clause for.
     *
     * @return string
     */
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
    {
        $columnDef = $column->toArray();
        $columnDef['name'] = $column->getQuotedName($this);

        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
    }

    /**
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
     *
     * @param  string $tableName  The name of the table to generate the clause for.
     * @param  string $columnName The name of the column to generate the clause for.
     *
     * @return string
     */
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
    {
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
    }

    /**
     * Checks whether a column alteration requires dropping its default constraint first.
     *
     * Different to other database vendors SQL Server implements column default values
     * as constraints and therefore changes in a column's default value as well as changes
     * in a column's type require dropping the default constraint first before being to
     * alter the particular column to the new definition.
     *
     * @param  ColumnDiff $columnDiff The column diff to evaluate.
     *
     * @return boolean True if the column alteration requires dropping its default constraint first, false otherwise.
     */
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
    {
        // We can only decide whether to drop an existing default constraint
        // if we know the original default value.
        if ( ! $columnDiff->fromColumn instanceof Column) {
            return false;
        }

        // We only need to drop an existing default constraint if we know the
        // column was defined with a default value before.
        if ($columnDiff->fromColumn->getDefault() === null) {
            return false;
        }

        // We need to drop an existing default constraint if the column was
        // defined with a default value before and it has changed.
        if ($columnDiff->hasChanged('default')) {
            return true;
        }

        // We need to drop an existing default constraint if the column was
        // defined with a default value before and the native column type has changed.
        if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed')) {
            return true;
        }

        return false;
    }

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 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685
    /**
     * Returns the SQL statement for altering a column comment.
     *
     * SQL Server does not support native column comments,
     * therefore the extended properties functionality is used
     * as a workaround to store them.
     * The property name used to store column comments is "MS_Description"
     * which provides compatibility with SQL Server Management Studio,
     * as column comments are stored in the same property there when
     * specifying a column's "Description" attribute.
     *
     * @param string $tableName  The quoted table name to which the column belongs.
     * @param string $columnName The quoted column name to alter the comment for.
     * @param string $comment    The column's comment.
     *
     * @return string
     */
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
    {
        return $this->getUpdateExtendedPropertySQL(
            'MS_Description',
            $comment,
            'SCHEMA',
            'dbo',
            'TABLE',
            $tableName,
            'COLUMN',
            $columnName
        );
    }

    /**
     * Returns the SQL statement for dropping a column comment.
     *
     * SQL Server does not support native column comments,
     * therefore the extended properties functionality is used
     * as a workaround to store them.
     * The property name used to store column comments is "MS_Description"
     * which provides compatibility with SQL Server Management Studio,
     * as column comments are stored in the same property there when
     * specifying a column's "Description" attribute.
     *
     * @param string $tableName  The quoted table name to which the column belongs.
     * @param string $columnName The quoted column name to drop the comment for.
     *
     * @return string
     */
    protected function getDropColumnCommentSQL($tableName, $columnName)
    {
        return $this->getDropExtendedPropertySQL(
            'MS_Description',
            'SCHEMA',
            'dbo',
            'TABLE',
            $tableName,
            'COLUMN',
            $columnName
        );
    }

686 687 688 689 690 691 692 693 694 695 696 697 698 699 700
    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
        return array(
            sprintf(
                "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
                $tableName,
                $oldIndexName,
                $index->getQuotedName($this)
            )
        );
    }

701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797
    /**
     * Returns the SQL statement for adding an extended property to a database object.
     *
     * @param string      $name       The name of the property to add.
     * @param string|null $value      The value of the property to add.
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
     *
     * @return string
     *
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
     */
    public function getAddExtendedPropertySQL(
        $name,
        $value = null,
        $level0Type = null,
        $level0Name = null,
        $level1Type = null,
        $level1Name = null,
        $level2Type = null,
        $level2Name = null
    ) {
        return "EXEC sp_addextendedproperty " .
            "N'" . $name . "', N'" . $value . "', " .
            "N'" . $level0Type . "', " . $level0Name . ', ' .
            "N'" . $level1Type . "', " . $level1Name . ', ' .
            "N'" . $level2Type . "', " . $level2Name;
    }

    /**
     * Returns the SQL statement for dropping an extended property from a database object.
     *
     * @param string      $name       The name of the property to drop.
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
     *
     * @return string
     *
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
     */
    public function getDropExtendedPropertySQL(
        $name,
        $level0Type = null,
        $level0Name = null,
        $level1Type = null,
        $level1Name = null,
        $level2Type = null,
        $level2Name = null
    ) {
        return "EXEC sp_dropextendedproperty " .
        "N'" . $name . "', " .
        "N'" . $level0Type . "', " . $level0Name . ', ' .
        "N'" . $level1Type . "', " . $level1Name . ', ' .
        "N'" . $level2Type . "', " . $level2Name;
    }

    /**
     * Returns the SQL statement for updating an extended property of a database object.
     *
     * @param string      $name       The name of the property to update.
     * @param string|null $value      The value of the property to update.
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
     *
     * @return string
     *
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
     */
    public function getUpdateExtendedPropertySQL(
        $name,
        $value = null,
        $level0Type = null,
        $level0Name = null,
        $level1Type = null,
        $level1Name = null,
        $level2Type = null,
        $level2Name = null
    ) {
        return "EXEC sp_updateextendedproperty " .
        "N'" . $name . "', N'" . $value . "', " .
        "N'" . $level0Type . "', " . $level0Name . ', ' .
        "N'" . $level1Type . "', " . $level1Name . ', ' .
        "N'" . $level2Type . "', " . $level2Name;
    }

798
    /**
799
     * {@inheritDoc}
800
     */
801
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
802
    {
803
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
804 805
    }

806
    /**
807
     * {@inheritDoc}
808
     */
809
    public function getListTablesSQL()
810
    {
811 812
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
        return "SELECT name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' ORDER BY name";
813 814 815
    }

    /**
816
     * {@inheritDoc}
817
     */
818
    public function getListTableColumnsSQL($table, $database = null)
819
    {
820 821 822 823 824 825 826 827
        return "SELECT    col.name,
                          type.name AS type,
                          col.max_length AS length,
                          ~col.is_nullable AS notnull,
                          def.definition AS [default],
                          col.scale,
                          col.precision,
                          col.is_identity AS autoincrement,
828 829
                          col.collation_name AS collation,
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
830 831 832 833 834
                FROM      sys.columns AS col
                JOIN      sys.types AS type
                ON        col.user_type_id = type.user_type_id
                JOIN      sys.objects AS obj
                ON        col.object_id = obj.object_id
835 836
                JOIN      sys.schemas AS scm
                ON        obj.schema_id = scm.schema_id
837 838 839
                LEFT JOIN sys.default_constraints def
                ON        col.default_object_id = def.object_id
                AND       col.object_id = def.parent_object_id
840 841 842 843
                LEFT JOIN sys.extended_properties AS prop
                ON        obj.object_id = prop.major_id
                AND       col.column_id = prop.minor_id
                AND       prop.name = 'MS_Description'
844
                WHERE     obj.type = 'U'
845
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
846 847 848
    }

    /**
849
     * {@inheritDoc}
850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865
     */
    public function getListTableForeignKeysSQL($table, $database = null)
    {
        return "SELECT f.name AS ForeignKey,
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
                OBJECT_NAME (f.parent_object_id) AS TableName,
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
                f.delete_referential_action_desc,
                f.update_referential_action_desc
                FROM sys.foreign_keys AS f
                INNER JOIN sys.foreign_key_columns AS fc
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
                ON f.OBJECT_ID = fc.constraint_object_id
866 867
                WHERE " .
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
868 869 870
    }

    /**
871
     * {@inheritDoc}
872
     */
873
    public function getListTableIndexesSQL($table, $currentDatabase = null)
874
    {
875 876
        return "SELECT idx.name AS key_name,
                       col.name AS column_name,
Steve Müller's avatar
Steve Müller committed
877 878
                       ~idx.is_unique AS non_unique,
                       idx.is_primary_key AS [primary],
879 880 881 882 883 884
                       CASE idx.type
                           WHEN '1' THEN 'clustered'
                           WHEN '2' THEN 'nonclustered'
                           ELSE NULL
                       END AS flags
                FROM sys.tables AS tbl
885
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
886 887 888
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
889
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . "
890
                ORDER BY idx.index_id ASC, idxcol.index_column_id ASC";
891
    }
892 893

    /**
894
     * {@inheritDoc}
895
     */
896
    public function getCreateViewSQL($name, $sql)
897 898 899
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }
900 901

    /**
902
     * {@inheritDoc}
903
     */
904
    public function getListViewsSQL($database)
905 906 907 908
    {
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
    }

909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929
    /**
     * Returns the where clause to filter schema and table name in a query.
     *
     * @param string $table        The full qualified name of the table.
     * @param string $tableColumn  The name of the column to compare the schema to in the where clause.
     * @param string $schemaColumn The name of the column to compare the table to in the where clause.
     *
     * @return string
     */
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
    {
        if (strpos($table, ".") !== false) {
            list($schema, $table) = explode(".", $table);
            $schema = "'" . $schema . "'";
        } else {
            $schema = "SCHEMA_NAME()";
        }

        return "({$tableColumn} = '{$table}' AND {$schemaColumn} = {$schema})";
    }

930
    /**
931
     * {@inheritDoc}
932 933 934
     */
    public function getDropViewSQL($name)
    {
935
        return 'DROP VIEW ' . $name;
936
    }
937 938

    /**
939
     * {@inheritDoc}
940 941 942
     */
    public function getGuidExpression()
    {
943
        return 'NEWID()';
944
    }
945 946

    /**
947
     * {@inheritDoc}
948
     */
949
    public function getLocateExpression($str, $substr, $startPos = false)
950
    {
951 952 953
        if ($startPos == false) {
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
        }
954 955

        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
956
    }
957

958
    /**
959
     * {@inheritDoc}
960
     */
961
    public function getModExpression($expression1, $expression2)
962
    {
963
        return $expression1 . ' % ' . $expression2;
964
    }
965

966
    /**
967
     * {@inheritDoc}
968
     */
969
    public function getTrimExpression($str, $pos = self::TRIM_UNSPECIFIED, $char = false)
970
    {
971
        if ( ! $char) {
972 973 974 975 976 977 978 979 980 981 982
            switch ($pos) {
                case self::TRIM_LEADING:
                    $trimFn = 'LTRIM';
                    break;

                case self::TRIM_TRAILING:
                    $trimFn = 'RTRIM';
                    break;

                default:
                    return 'LTRIM(RTRIM(' . $str . '))';
983 984 985 986
            }

            return $trimFn . '(' . $str . ')';
        }
987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007

        /** Original query used to get those expressions
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
          select @c as string
          , @trim_char as trim_char
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
          , reverse(stuff(reverse(stuff(@c, 1, patindex(@pat, @c) - 1, null)), 1, patindex(@pat, reverse(stuff(@c, 1, patindex(@pat, @c) - 1, null))) - 1, null)) as trim_both;
         */
        $pattern = "'%[^' + $char + ']%'";

        if ($pos == self::TRIM_LEADING) {
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
        }

        if ($pos == self::TRIM_TRAILING) {
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
        }

        return 'reverse(stuff(reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)), 1, patindex(' . $pattern . ', reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null))) - 1, null))';
1008
    }
1009

1010
    /**
1011
     * {@inheritDoc}
1012 1013
     */
    public function getConcatExpression()
1014
    {
1015
        $args = func_get_args();
1016

1017
        return '(' . implode(' + ', $args) . ')';
1018
    }
1019

Benjamin Morel's avatar
Benjamin Morel committed
1020 1021 1022
    /**
     * {@inheritDoc}
     */
1023
    public function getListDatabasesSQL()
1024 1025 1026
    {
        return 'SELECT * FROM SYS.DATABASES';
    }
1027

1028
    /**
1029
     * {@inheritDoc}
1030
     */
1031
    public function getSubstringExpression($value, $from, $length = null)
1032
    {
1033 1034
        if (!is_null($length)) {
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1035
        }
1036

1037
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1038
    }
1039

1040
    /**
1041
     * {@inheritDoc}
1042
     */
1043
    public function getLengthExpression($column)
1044
    {
1045
        return 'LEN(' . $column . ')';
1046 1047
    }

1048
    /**
1049
     * {@inheritDoc}
1050
     */
1051
    public function getSetTransactionIsolationSQL($level)
romanb's avatar
romanb committed
1052
    {
1053
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
1054
    }
1055

1056
    /**
1057
     * {@inheritDoc}
1058
     */
1059
    public function getIntegerTypeDeclarationSQL(array $field)
1060
    {
1061
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1062 1063
    }

1064
    /**
1065
     * {@inheritDoc}
1066
     */
1067
    public function getBigIntTypeDeclarationSQL(array $field)
1068
    {
1069
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1070 1071
    }

1072
    /**
1073
     * {@inheritDoc}
1074
     */
1075
    public function getSmallIntTypeDeclarationSQL(array $field)
1076
    {
1077
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1078 1079
    }

1080
    /**
1081
     * {@inheritDoc}
1082
     */
1083
    public function getGuidTypeDeclarationSQL(array $field)
1084 1085 1086 1087
    {
        return 'UNIQUEIDENTIFIER';
    }

1088 1089 1090
    /**
     * {@inheritDoc}
     */
1091
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1092
    {
1093
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1094
    }
1095

Steve Müller's avatar
Steve Müller committed
1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111
    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 8000;
    }

1112 1113 1114
    /**
     * {@inheritDoc}
     */
1115
    public function getClobTypeDeclarationSQL(array $field)
1116
    {
1117
        return 'VARCHAR(MAX)';
1118
    }
1119

1120
    /**
1121
     * {@inheritDoc}
1122
     */
1123
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1124
    {
1125
        return (!empty($columnDef['autoincrement'])) ? ' IDENTITY' : '';
1126
    }
1127

1128
    /**
1129
     * {@inheritDoc}
1130
     */
1131
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1132
    {
1133
        return 'DATETIME';
1134 1135
    }

1136
    /**
1137
     * {@inheritDoc}
1138
     */
1139
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1140
    {
1141
        return 'DATETIME';
1142
    }
1143 1144

    /**
1145
     * {@inheritDoc}
1146
     */
1147
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1148
    {
1149
        return 'DATETIME';
1150
    }
1151

1152
    /**
1153
     * {@inheritDoc}
1154
     */
1155
    public function getBooleanTypeDeclarationSQL(array $field)
1156 1157 1158 1159
    {
        return 'BIT';
    }

1160
    /**
1161
     * {@inheritDoc}
1162
     */
1163
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1164
    {
Fabio B. Silva's avatar
Fabio B. Silva committed
1165
        if ($limit === null) {
Fabio B. Silva's avatar
Fabio B. Silva committed
1166 1167
            return $query;
        }
1168

Fabio B. Silva's avatar
Fabio B. Silva committed
1169 1170 1171
        $start   = $offset + 1;
        $end     = $offset + $limit;
        $orderBy = stristr($query, 'ORDER BY');
1172 1173 1174 1175

        //Remove ORDER BY from $query (including nested parentheses in order by list).
        $query = preg_replace('/\s+ORDER\s+BY\s+([^()]+|\((?:(?:(?>[^()]+)|(?R))*)\))+/i', '', $query);

Fabio B. Silva's avatar
Fabio B. Silva committed
1176 1177
        $format  = 'SELECT * FROM (%s) AS doctrine_tbl WHERE doctrine_rownum BETWEEN %d AND %d';

1178 1179 1180
        // Pattern to match "main" SELECT ... FROM clause (including nested parentheses in select list).
        $selectFromPattern = '/^(\s*SELECT\s+(?:\((?>[^()]+)|(?:R)*\)|[^(])+)\sFROM\s/i';

Fabio B. Silva's avatar
Fabio B. Silva committed
1181
        if ( ! $orderBy) {
1182 1183 1184 1185 1186 1187 1188
            //Replace only "main" FROM with OVER to prevent changing FROM also in subqueries.
            $query = preg_replace(
                $selectFromPattern,
                '$1, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM ',
                $query,
                1
            );
Fabio B. Silva's avatar
Fabio B. Silva committed
1189 1190 1191 1192 1193

            return sprintf($format, $query, $start, $end);
        }

        //Clear ORDER BY
1194
        $orderBy        = preg_replace('/ORDER\s+BY\s+(.*)/i', '$1', $orderBy);
Fabio B. Silva's avatar
Fabio B. Silva committed
1195
        $orderByParts   = explode(',', $orderBy);
1196
        $orderByColumns = array();
Fabio B. Silva's avatar
Fabio B. Silva committed
1197 1198 1199

        //Split ORDER BY into parts
        foreach ($orderByParts as &$part) {
Fabio B. Silva's avatar
Fabio B. Silva committed
1200 1201

            if (preg_match('/(([^\s]*)\.)?([^\.\s]*)\s*(ASC|DESC)?/i', trim($part), $matches)) {
1202
                $orderByColumns[] = array(
Fabio B. Silva's avatar
Fabio B. Silva committed
1203 1204 1205 1206 1207
                    'column'    => $matches[3],
                    'hasTable'  => ( ! empty($matches[2])),
                    'sort'      => isset($matches[4]) ? $matches[4] : null,
                    'table'     => empty($matches[2]) ? '[^\.\s]*' : $matches[2]
                );
1208
            }
Fabio B. Silva's avatar
Fabio B. Silva committed
1209 1210
        }

flip111's avatar
flip111 committed
1211
        $isWrapped = (preg_match('/SELECT DISTINCT .* FROM \(.*\) dctrn_result/', $query)) ? true : false;
1212

1213 1214 1215 1216 1217 1218
        $overColumns = array();

        //Find alias for each column used in ORDER BY
        if ( ! empty($orderByColumns)) {
            foreach ($orderByColumns as $column) {
                $pattern = sprintf('/%s\.%s\s+(?:AS\s+)?([^,\s)]+)/i', $column['table'], $column['column']);
1219

1220
                if ($isWrapped) {
1221 1222
                    $overColumn = preg_match($pattern, $query, $matches)
                        ? $matches[1] : '';
1223 1224
                } else {
                    $overColumn = preg_match($pattern, $query, $matches)
1225 1226
                        ? ($column['hasTable'] ? $column['table']  . '.' : '') . $column['column']
                        : $column['column'];
1227
                }
1228

Fabio B. Silva's avatar
Fabio B. Silva committed
1229 1230 1231
                if (isset($column['sort'])) {
                    $overColumn .= ' ' . $column['sort'];
                }
1232

Fabio B. Silva's avatar
Fabio B. Silva committed
1233 1234
                $overColumns[] = $overColumn;
            }
1235 1236
        }

Fabio B. Silva's avatar
Fabio B. Silva committed
1237 1238
        //Replace only first occurrence of FROM with $over to prevent changing FROM also in subqueries.
        $over  = 'ORDER BY ' . implode(', ', $overColumns);
1239
        $query = preg_replace($selectFromPattern, "$1, ROW_NUMBER() OVER ($over) AS doctrine_rownum FROM ", $query, 1);
Fabio B. Silva's avatar
Fabio B. Silva committed
1240 1241

        return sprintf($format, $query, $start, $end);
1242
    }
1243

1244
    /**
1245
     * {@inheritDoc}
1246 1247 1248 1249 1250 1251
     */
    public function supportsLimitOffset()
    {
        return false;
    }

1252
    /**
1253
     * {@inheritDoc}
1254
     */
1255
    public function convertBooleans($item)
1256
    {
1257 1258 1259
        if (is_array($item)) {
            foreach ($item as $key => $value) {
                if (is_bool($value) || is_numeric($item)) {
1260
                    $item[$key] = ($value) ? 1 : 0;
1261 1262
                }
            }
Steve Müller's avatar
Steve Müller committed
1263
        } elseif (is_bool($item) || is_numeric($item)) {
1264
            $item = ($item) ? 1 : 0;
1265
        }
1266

1267
        return $item;
1268
    }
1269 1270

    /**
1271
     * {@inheritDoc}
1272
     */
1273
    public function getCreateTemporaryTableSnippetSQL()
1274
    {
1275
        return "CREATE TABLE";
1276
    }
1277

1278
    /**
1279
     * {@inheritDoc}
1280 1281 1282 1283 1284 1285
     */
    public function getTemporaryTableName($tableName)
    {
        return '#' . $tableName;
    }

1286
    /**
1287
     * {@inheritDoc}
1288 1289 1290
     */
    public function getDateTimeFormatString()
    {
1291 1292
        return 'Y-m-d H:i:s.000';
    }
1293

1294
    /**
1295
     * {@inheritDoc}
1296
     */
1297 1298 1299 1300 1301 1302
    public function getDateFormatString()
    {
        return 'Y-m-d H:i:s.000';
    }

    /**
1303
     * {@inheritDoc}
1304
     */
1305 1306 1307
    public function getTimeFormatString()
    {
        return 'Y-m-d H:i:s.000';
1308
    }
1309

1310
    /**
1311
     * {@inheritDoc}
1312 1313 1314 1315 1316
     */
    public function getDateTimeTzFormatString()
    {
        return $this->getDateTimeFormatString();
    }
1317

1318
    /**
1319
     * {@inheritDoc}
1320
     */
1321
    public function getName()
1322
    {
1323
        return 'mssql';
1324
    }
1325

Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1326
    /**
1327
     * {@inheritDoc}
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1328
     */
1329 1330
    protected function initializeDoctrineTypeMappings()
    {
1331
        $this->doctrineTypeMapping = array(
1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352
            'bigint' => 'bigint',
            'numeric' => 'decimal',
            'bit' => 'boolean',
            'smallint' => 'smallint',
            'decimal' => 'decimal',
            'smallmoney' => 'integer',
            'int' => 'integer',
            'tinyint' => 'smallint',
            'money' => 'integer',
            'float' => 'float',
            'real' => 'float',
            'double' => 'float',
            'double precision' => 'float',
            'smalldatetime' => 'datetime',
            'datetime' => 'datetime',
            'char' => 'string',
            'varchar' => 'string',
            'text' => 'text',
            'nchar' => 'string',
            'nvarchar' => 'string',
            'ntext' => 'text',
Steve Müller's avatar
Steve Müller committed
1353 1354
            'binary' => 'binary',
            'varbinary' => 'binary',
1355
            'image' => 'blob',
1356
            'uniqueidentifier' => 'guid',
1357
        );
1358
    }
1359 1360

    /**
1361
     * {@inheritDoc}
1362 1363 1364 1365 1366 1367 1368
     */
    public function createSavePoint($savepoint)
    {
        return 'SAVE TRANSACTION ' . $savepoint;
    }

    /**
1369
     * {@inheritDoc}
1370 1371 1372 1373 1374 1375 1376
     */
    public function releaseSavePoint($savepoint)
    {
        return '';
    }

    /**
1377
     * {@inheritDoc}
1378 1379 1380 1381
     */
    public function rollbackSavePoint($savepoint)
    {
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1382
    }
1383 1384

    /**
1385
     * {@inheritDoc}
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1386
     */
1387
    public function appendLockHint($fromClause, $lockMode)
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1388
    {
1389 1390 1391 1392 1393 1394 1395 1396 1397 1398
        switch (true) {
            case LockMode::NONE === $lockMode:
                return $fromClause . ' WITH (NOLOCK)';

            case LockMode::PESSIMISTIC_READ === $lockMode:
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';

            case LockMode::PESSIMISTIC_WRITE === $lockMode:
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';

Steve Müller's avatar
Steve Müller committed
1399
            default:
1400
                return $fromClause;
Steve Müller's avatar
Steve Müller committed
1401
        }
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1402 1403 1404
    }

    /**
1405
     * {@inheritDoc}
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1406 1407 1408 1409 1410
     */
    public function getForUpdateSQL()
    {
        return ' ';
    }
1411

1412 1413 1414
    /**
     * {@inheritDoc}
     */
1415 1416
    protected function getReservedKeywordsClass()
    {
1417
        return 'Doctrine\DBAL\Platforms\Keywords\SQLServerKeywords';
1418
    }
1419 1420

    /**
1421
     * {@inheritDoc}
1422
     */
1423
    public function quoteSingleIdentifier($str)
1424
    {
1425
        return "[" . str_replace("]", "][", $str) . "]";
1426
    }
1427

1428 1429 1430
    /**
     * {@inheritDoc}
     */
1431 1432 1433 1434
    public function getTruncateTableSQL($tableName, $cascade = false)
    {
        return 'TRUNCATE TABLE '.$tableName;
    }
1435 1436

    /**
1437
     * {@inheritDoc}
1438 1439 1440 1441 1442
     */
    public function getBlobTypeDeclarationSQL(array $field)
    {
        return 'VARBINARY(MAX)';
    }
1443 1444 1445 1446

    /**
     * {@inheritDoc}
     */
1447 1448
    public function getDefaultValueDeclarationSQL($field)
    {
1449 1450 1451 1452 1453 1454
        if ( ! isset($field['default'])) {
            return empty($field['notnull']) ? ' NULL' : '';
        }

        if ( ! isset($field['type'])) {
            return " DEFAULT '" . $field['default'] . "'";
1455
        }
1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469

        if (in_array((string) $field['type'], array('Integer', 'BigInteger', 'SmallInteger'))) {
            return " DEFAULT " . $field['default'];
        }

        if ((string) $field['type'] == 'DateTime' && $field['default'] == $this->getCurrentTimestampSQL()) {
            return " DEFAULT " . $this->getCurrentTimestampSQL();
        }

        if ((string) $field['type'] == 'Boolean') {
            return " DEFAULT '" . $this->convertBooleans($field['default']) . "'";
        }

        return " DEFAULT '" . $field['default'] . "'";
1470
    }
1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481

    /**
     * {@inheritdoc}
     *
     * Modifies column declaration order as it differs in Microsoft SQL Server.
     */
    public function getColumnDeclarationSQL($name, array $field)
    {
        if (isset($field['columnDefinition'])) {
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
        } else {
1482 1483
            $collation = (isset($field['collation']) && $field['collation']) ?
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1484 1485 1486 1487 1488 1489 1490 1491 1492 1493

            $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : '';

            $unique = (isset($field['unique']) && $field['unique']) ?
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';

            $check = (isset($field['check']) && $field['check']) ?
                ' ' . $field['check'] : '';

            $typeDecl = $field['type']->getSqlDeclaration($field, $this);
1494
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1495 1496 1497 1498
        }

        return $name . ' ' . $columnDef;
    }
1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523

    /**
     * Returns a unique default constraint name for a table and column.
     *
     * @param string $table  Name of the table to generate the unique default constraint name for.
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
     *
     * @return string
     */
    private function generateDefaultConstraintName($table, $column)
    {
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
    }

    /**
     * Returns a hash value for a given identifier.
     *
     * @param string $identifier Identifier to generate a hash value for.
     *
     * @return string
     */
    private function generateIdentifierName($identifier)
    {
        return strtoupper(dechex(crc32($identifier)));
    }
1524
}