SQLServer2012Platform.php 52.2 KB
Newer Older
1 2
<?php

3
namespace Doctrine\DBAL\Platforms;
4

5
use Doctrine\DBAL\LockMode;
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\ColumnDiff;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Doctrine\DBAL\Schema\Identifier;
10
use Doctrine\DBAL\Schema\Index;
11
use Doctrine\DBAL\Schema\Sequence;
12
use Doctrine\DBAL\Schema\Table;
13
use Doctrine\DBAL\Schema\TableDiff;
14
use InvalidArgumentException;
15
use const PREG_OFFSET_CAPTURE;
16 17 18 19 20 21
use function array_merge;
use function array_unique;
use function array_values;
use function count;
use function crc32;
use function dechex;
22
use function explode;
23
use function func_get_args;
24
use function implode;
25 26 27 28 29
use function is_array;
use function is_bool;
use function is_numeric;
use function is_string;
use function preg_match;
30
use function preg_match_all;
31
use function sprintf;
32
use function str_replace;
33
use function strpos;
34 35
use function strtoupper;
use function substr_count;
36 37

/**
38
 * Provides the behavior, features and SQL dialect of the Microsoft SQL Server 2012 database platform.
39
 */
40
class SQLServer2012Platform extends AbstractPlatform
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
    /**
     * {@inheritdoc}
     */
    public function getCurrentDateSQL()
    {
        return $this->getConvertExpression('date', 'GETDATE()');
    }

    /**
     * {@inheritdoc}
     */
    public function getCurrentTimeSQL()
    {
        return $this->getConvertExpression('time', 'GETDATE()');
    }

    /**
     * Returns an expression that converts an expression of one data type to another.
     *
     * @param string $dataType   The target native data type. Alias data types cannot be used.
     * @param string $expression The SQL expression to convert.
     *
     * @return string
     */
    private function getConvertExpression($dataType, $expression)
    {
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
    }

71
    /**
72
     * {@inheritdoc}
73
     */
74
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
75
    {
76
        $factorClause = '';
77

78
        if ($operator === '-') {
79 80
            $factorClause = '-1 * ';
        }
81

82
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
83
    }
84

85 86 87
    /**
     * {@inheritDoc}
     */
88
    public function getDateDiffExpression($date1, $date2)
89
    {
90
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
91
    }
92

93
    /**
94 95
     * {@inheritDoc}
     *
96 97
     * Microsoft SQL Server prefers "autoincrement" identity columns
     * since sequences can only be emulated with a table.
98
     */
99
    public function prefersIdentityColumns()
100
    {
101 102
        return true;
    }
103

104
    /**
105
     * {@inheritDoc}
106
     *
107
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
108 109 110 111 112 113 114
     */
    public function supportsIdentityColumns()
    {
        return true;
    }

    /**
115
     * {@inheritDoc}
116
     */
117
    public function supportsReleaseSavepoints()
118 119 120
    {
        return false;
    }
121

122 123 124 125 126 127 128 129
    /**
     * {@inheritdoc}
     */
    public function supportsSchemas()
    {
        return true;
    }

130 131 132 133 134 135 136 137
    /**
     * {@inheritdoc}
     */
    public function getDefaultSchemaName()
    {
        return 'dbo';
    }

138 139 140 141 142 143 144 145
    /**
     * {@inheritDoc}
     */
    public function supportsColumnCollation()
    {
        return true;
    }

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
    /**
     * {@inheritdoc}
     */
    public function supportsSequences() : bool
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function getAlterSequenceSQL(Sequence $sequence) : string
    {
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
            ' INCREMENT BY ' . $sequence->getAllocationSize();
    }

    /**
     * {@inheritdoc}
     */
    public function getCreateSequenceSQL(Sequence $sequence) : string
    {
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
            ' START WITH ' . $sequence->getInitialValue() .
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
            ' MINVALUE ' . $sequence->getInitialValue();
    }

    /**
     * {@inheritdoc}
     */
    public function getDropSequenceSQL($sequence) : string
    {
        if ($sequence instanceof Sequence) {
            $sequence = $sequence->getQuotedName($this);
        }

        return 'DROP SEQUENCE ' . $sequence;
    }

    /**
     * {@inheritdoc}
     */
    public function getListSequencesSQL($database)
    {
        return 'SELECT seq.name,
                       CAST(
                           seq.increment AS VARCHAR(MAX)
                       ) AS increment, -- CAST avoids driver error for sql_variant type
                       CAST(
                           seq.start_value AS VARCHAR(MAX)
                       ) AS start_value -- CAST avoids driver error for sql_variant type
                FROM   sys.sequences AS seq';
    }

    /**
     * {@inheritdoc}
     */
    public function getSequenceNextValSQL($sequenceName)
    {
        return 'SELECT NEXT VALUE FOR ' . $sequenceName;
    }

209 210 211 212 213 214 215 216
    /**
     * {@inheritDoc}
     */
    public function hasNativeGuidType()
    {
        return true;
    }

217
    /**
218
     * {@inheritDoc}
219 220 221 222 223
     */
    public function getCreateDatabaseSQL($name)
    {
        return 'CREATE DATABASE ' . $name;
    }
224

225
    /**
226
     * {@inheritDoc}
227 228 229
     */
    public function getDropDatabaseSQL($name)
    {
230 231 232 233
        return 'DROP DATABASE ' . $name;
    }

    /**
234
     * {@inheritDoc}
235
     */
236
    public function supportsCreateDropDatabase()
237
    {
238
        return true;
239 240
    }

241 242 243 244 245 246 247 248
    /**
     * {@inheritDoc}
     */
    public function getCreateSchemaSQL($schemaName)
    {
        return 'CREATE SCHEMA ' . $schemaName;
    }

249
    /**
250
     * {@inheritDoc}
251 252 253
     */
    public function getDropForeignKeySQL($foreignKey, $table)
    {
254 255
        if (! $foreignKey instanceof ForeignKeyConstraint) {
            $foreignKey = new Identifier($foreignKey);
256 257
        }

258 259
        if (! $table instanceof Table) {
            $table = new Identifier($table);
260 261
        }

262
        $foreignKey = $foreignKey->getQuotedName($this);
263
        $table      = $table->getQuotedName($this);
264

265
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
266
    }
267 268

    /**
269
     * {@inheritDoc}
270
     */
271
    public function getDropIndexSQL($index, $table = null)
272
    {
273
        if ($index instanceof Index) {
274
            $index = $index->getQuotedName($this);
275 276
        } elseif (! is_string($index)) {
            throw new InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
277 278
        }

279
        if (! isset($table)) {
280
            return 'DROP INDEX ' . $index;
281
        }
282

283 284
        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
285
        }
286

287 288 289 290 291 292 293 294 295 296 297 298 299 300
        return sprintf(
            <<<SQL
IF EXISTS (SELECT * FROM sysobjects WHERE name = '%s')
    ALTER TABLE %s DROP CONSTRAINT %s
ELSE
    DROP INDEX %s ON %s
SQL
            ,
            $index,
            $table,
            $index,
            $index,
            $table
        );
301
    }
302 303

    /**
304
     * {@inheritDoc}
305
     */
306
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
307
    {
308 309
        $defaultConstraintsSql = [];
        $commentsSql           = [];
310
        $tableComment          = $options['comment'] ?? null;
311

312
        if ($tableComment !== null) {
313
            $commentsSql[] = $this->getCommentOnTableSQL($name, $tableComment);
314 315
        }

316
        // @todo does other code breaks because of this?
317
        // force primary keys to be not null
318
        foreach ($columns as &$column) {
319
            if (! empty($column['primary'])) {
320 321
                $column['notnull'] = true;
            }
322

323
            // Build default constraints SQL statements.
324
            if (isset($column['default'])) {
325 326
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $name .
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($name, $column);
327
            }
328

329 330
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
                continue;
331
            }
332

333
            $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']);
334 335
        }

336
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
337

338
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
339 340
            foreach ($options['uniqueConstraints'] as $constraintName => $definition) {
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($constraintName, $definition);
341 342
            }
        }
343

344
        if (isset($options['primary']) && ! empty($options['primary'])) {
345 346 347 348 349
            $flags = '';
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
                $flags = ' NONCLUSTERED';
            }
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
350 351
        }

352
        $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql;
353
        $check = $this->getCheckDeclarationSQL($columns);
354

355
        if (! empty($check)) {
356 357
            $query .= ', ' . $check;
        }
358

359 360
        $query .= ')';

361
        $sql = [$query];
362

363
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
364
            foreach ($options['indexes'] as $index) {
365
                $sql[] = $this->getCreateIndexSQL($index, $name);
366 367 368 369
            }
        }

        if (isset($options['foreignKeys'])) {
370
            foreach ((array) $options['foreignKeys'] as $definition) {
371
                $sql[] = $this->getCreateForeignKeySQL($definition, $name);
372 373 374
            }
        }

375
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
376
    }
377

378
    /**
379
     * {@inheritDoc}
380 381 382
     */
    public function getCreatePrimaryKeySQL(Index $index, $table)
    {
Sergei Morozov's avatar
Sergei Morozov committed
383 384 385 386 387 388 389 390
        if ($table instanceof Table) {
            $identifier = $table->getQuotedName($this);
        } else {
            $identifier = $table;
        }

        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';

391
        if ($index->hasFlag('nonclustered')) {
Sergei Morozov's avatar
Sergei Morozov committed
392
            $sql .= ' NONCLUSTERED';
393
        }
394

Sergei Morozov's avatar
Sergei Morozov committed
395
        return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
396 397
    }

398 399 400 401 402 403 404 405 406 407 408
    /**
     * 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.
     *
Sergei Morozov's avatar
Sergei Morozov committed
409 410 411
     * @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|null $comment    The column's comment.
412 413 414 415 416
     *
     * @return string
     */
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
    {
417 418 419 420 421 422 423 424 425
        if (strpos($tableName, '.') !== false) {
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
        } else {
            $schemaSQL = "'dbo'";
            $tableSQL  = $this->quoteStringLiteral($tableName);
        }

426 427 428 429
        return $this->getAddExtendedPropertySQL(
            'MS_Description',
            $comment,
            'SCHEMA',
430
            $schemaSQL,
431
            'TABLE',
432
            $tableSQL,
433 434 435 436 437
            'COLUMN',
            $columnName
        );
    }

438 439 440
    /**
     * Returns the SQL snippet for declaring a default constraint.
     *
441 442
     * @param string  $table  Name of the table to return the default constraint declaration for.
     * @param mixed[] $column Column definition.
443 444 445
     *
     * @return string
     *
446
     * @throws InvalidArgumentException
447 448 449
     */
    public function getDefaultConstraintDeclarationSQL($table, array $column)
    {
450 451
        if (! isset($column['default'])) {
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
452 453
        }

454 455
        $columnName = new Identifier($column['name']);

456
        return ' CONSTRAINT ' .
457 458
            $this->generateDefaultConstraintName($table, $column['name']) .
            $this->getDefaultValueDeclarationSQL($column) .
459
            ' FOR ' . $columnName->getQuotedName($this);
460 461
    }

462
    /**
463
     * {@inheritDoc}
464 465 466 467 468
     */
    public function getCreateIndexSQL(Index $index, $table)
    {
        $constraint = parent::getCreateIndexSQL($index, $table);

469
        if ($index->isUnique() && ! $index->isPrimary()) {
470 471 472 473 474 475
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
        }

        return $constraint;
    }

476
    /**
477
     * {@inheritDoc}
478 479 480 481 482 483 484 485 486 487
     */
    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
488
        } elseif ($index->hasFlag('nonclustered')) {
489 490 491 492 493 494
            $type .= 'NONCLUSTERED ';
        }

        return $type;
    }

495
    /**
496
     * Extend unique key constraint with required filters
497
     *
498
     * @param string $sql
499
     *
500 501 502 503
     * @return string
     */
    private function _appendUniqueConstraintDefinition($sql, Index $index)
    {
504
        $fields = [];
505

506
        foreach ($index->getQuotedColumns($this) as $field) {
507
            $fields[] = $field . ' IS NOT NULL';
508
        }
509 510 511

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

513
    /**
514
     * {@inheritDoc}
515
     */
516
    public function getAlterTableSQL(TableDiff $diff)
517
    {
518 519 520 521
        $queryParts  = [];
        $sql         = [];
        $columnSql   = [];
        $commentsSql = [];
522

523
        foreach ($diff->addedColumns as $column) {
524 525
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
526 527
            }

528
            $columnDef    = $column->toArray();
529 530
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);

531
            if (isset($columnDef['default'])) {
532
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
533
            }
534 535 536

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

537 538
            if (empty($comment) && ! is_numeric($comment)) {
                continue;
539
            }
540 541 542 543 544 545

            $commentsSql[] = $this->getCreateColumnCommentSQL(
                $diff->name,
                $column->getQuotedName($this),
                $comment
            );
546 547
        }

548
        foreach ($diff->removedColumns as $column) {
549 550
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
551 552
            }

553
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
554 555
        }

556
        foreach ($diff->changedColumns as $columnDiff) {
557 558
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
559 560
            }

561 562
            $column     = $columnDiff->column;
            $comment    = $this->getColumnComment($column);
563
            $hasComment = ! empty($comment) || is_numeric($comment);
564 565 566

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

569
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590
                    $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
                    );
                }
            }

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

591
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
592

593 594 595 596 597
            if ($requireDropDefaultConstraint) {
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
                    $diff->name,
                    $columnDiff->oldColumnName
                );
598 599
            }

600 601
            $columnDef = $column->toArray();

602
            $queryParts[] = 'ALTER COLUMN ' .
603 604
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);

605 606
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
                continue;
607
            }
608 609

            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
610 611
        }

612
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
613 614
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
615 616
            }

617 618
            $oldColumnName = new Identifier($oldColumnName);

619
            $sql[] = "sp_RENAME '" .
620
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
621
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
622

623
            // Recreate default constraint with new column name if necessary (for future reference).
624 625
            if ($column->getDefault() === null) {
                continue;
626
            }
627 628 629 630 631 632

            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
                $diff->name,
                $oldColumnName->getQuotedName($this)
            );
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
633
        }
634

635
        $tableSql = [];
636 637

        if ($this->onSchemaAlterTable($diff, $tableSql)) {
638
            return array_merge($tableSql, $columnSql);
639
        }
640

641
        foreach ($queryParts as $query) {
642
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
643
        }
644

645
        $sql = array_merge($sql, $commentsSql);
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
646

Sergei Morozov's avatar
Sergei Morozov committed
647 648 649 650
        $newName = $diff->getNewName();

        if ($newName !== false) {
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";
651 652 653 654 655 656 657 658 659 660 661 662

            /**
             * 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) . "', " .
Sergei Morozov's avatar
Sergei Morozov committed
663
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
664 665
                'FROM sys.default_constraints dc ' .
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
Sergei Morozov's avatar
Sergei Morozov committed
666
                "WHERE tbl.name = '" . $newName->getName() . "';" .
667
                'EXEC sp_executesql @sql';
668 669
        }

670 671 672 673 674 675
        $sql = array_merge(
            $this->getPreAlterTableIndexForeignKeySQL($diff),
            $sql,
            $this->getPostAlterTableIndexForeignKeySQL($diff)
        );

676
        return array_merge($sql, $tableSql, $columnSql);
677
    }
678

679 680 681
    /**
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
     *
682 683
     * @param string $tableName The name of the table to generate the clause for.
     * @param Column $column    The column to generate the clause for.
684 685 686 687 688
     *
     * @return string
     */
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
    {
689
        $columnDef         = $column->toArray();
690 691 692 693 694 695 696 697
        $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.
     *
698 699
     * @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.
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715
     *
     * @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.
     *
716
     * @param ColumnDiff $columnDiff The column diff to evaluate.
717
     *
718
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
719 720 721 722 723
     */
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
    {
        // We can only decide whether to drop an existing default constraint
        // if we know the original default value.
724
        if (! $columnDiff->fromColumn instanceof Column) {
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741
            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.
Gabriel Caruso's avatar
Gabriel Caruso committed
742
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
743 744
    }

745 746 747 748 749 750 751 752 753 754 755
    /**
     * 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.
     *
Sergei Morozov's avatar
Sergei Morozov committed
756 757 758
     * @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|null $comment    The column's comment.
759 760 761 762 763
     *
     * @return string
     */
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
    {
764 765 766 767 768 769 770 771 772
        if (strpos($tableName, '.') !== false) {
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
        } else {
            $schemaSQL = "'dbo'";
            $tableSQL  = $this->quoteStringLiteral($tableName);
        }

773 774 775 776
        return $this->getUpdateExtendedPropertySQL(
            'MS_Description',
            $comment,
            'SCHEMA',
777
            $schemaSQL,
778
            'TABLE',
779
            $tableSQL,
780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802
            '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)
    {
803 804 805 806 807 808 809 810 811
        if (strpos($tableName, '.') !== false) {
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
        } else {
            $schemaSQL = "'dbo'";
            $tableSQL  = $this->quoteStringLiteral($tableName);
        }

812 813 814
        return $this->getDropExtendedPropertySQL(
            'MS_Description',
            'SCHEMA',
815
            $schemaSQL,
816
            'TABLE',
817
            $tableSQL,
818 819 820 821 822
            'COLUMN',
            $columnName
        );
    }

823 824 825 826 827
    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
828 829 830 831 832 833
        return [sprintf(
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
            $tableName,
            $oldIndexName,
            $index->getQuotedName($this)
        ),
834
        ];
835 836
    }

837 838 839
    /**
     * Returns the SQL statement for adding an extended property to a database object.
     *
840 841
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
     *
842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862
     * @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
     */
    public function getAddExtendedPropertySQL(
        $name,
        $value = null,
        $level0Type = null,
        $level0Name = null,
        $level1Type = null,
        $level1Name = null,
        $level2Type = null,
        $level2Name = null
    ) {
863
        return 'EXEC sp_addextendedproperty ' .
Sergei Morozov's avatar
Sergei Morozov committed
864 865 866 867
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
868 869 870 871 872
    }

    /**
     * Returns the SQL statement for dropping an extended property from a database object.
     *
873 874
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
     *
875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893
     * @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
     */
    public function getDropExtendedPropertySQL(
        $name,
        $level0Type = null,
        $level0Name = null,
        $level1Type = null,
        $level1Name = null,
        $level2Type = null,
        $level2Name = null
    ) {
894 895
        return 'EXEC sp_dropextendedproperty ' .
            'N' . $this->quoteStringLiteral($name) . ', ' .
Sergei Morozov's avatar
Sergei Morozov committed
896 897 898
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
899 900 901 902 903
    }

    /**
     * Returns the SQL statement for updating an extended property of a database object.
     *
904 905
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
     *
906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926
     * @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
     */
    public function getUpdateExtendedPropertySQL(
        $name,
        $value = null,
        $level0Type = null,
        $level0Name = null,
        $level1Type = null,
        $level1Name = null,
        $level2Type = null,
        $level2Name = null
    ) {
927
        return 'EXEC sp_updateextendedproperty ' .
Sergei Morozov's avatar
Sergei Morozov committed
928 929 930 931
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
932 933
    }

934
    /**
935
     * {@inheritDoc}
936
     */
937
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
938
    {
939
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
940 941
    }

942
    /**
943
     * {@inheritDoc}
944
     */
945
    public function getListTablesSQL()
946
    {
947
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
948
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
949
        return "SELECT name, SCHEMA_NAME (uid) AS schema_name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
950 951 952
    }

    /**
953
     * {@inheritDoc}
954
     */
955
    public function getListTableColumnsSQL($table, $database = null)
956
    {
957 958 959 960 961 962 963 964
        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,
965 966
                          col.collation_name AS collation,
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
967 968 969 970 971
                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
972 973
                JOIN      sys.schemas AS scm
                ON        obj.schema_id = scm.schema_id
974 975 976
                LEFT JOIN sys.default_constraints def
                ON        col.default_object_id = def.object_id
                AND       col.object_id = def.parent_object_id
977 978 979 980
                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'
981
                WHERE     obj.type = 'U'
982
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
983 984 985
    }

    /**
986 987 988 989
     * @param string      $table
     * @param string|null $database
     *
     * @return string
990 991 992
     */
    public function getListTableForeignKeysSQL($table, $database = null)
    {
993
        return 'SELECT f.name AS ForeignKey,
994 995 996 997 998 999 1000 1001 1002 1003 1004 1005
                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
1006
                WHERE ' .
1007
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
1008 1009 1010
    }

    /**
1011
     * {@inheritDoc}
1012
     */
1013
    public function getListTableIndexesSQL($table, $currentDatabase = null)
1014
    {
1015 1016
        return "SELECT idx.name AS key_name,
                       col.name AS column_name,
Steve Müller's avatar
Steve Müller committed
1017 1018
                       ~idx.is_unique AS non_unique,
                       idx.is_primary_key AS [primary],
1019 1020 1021 1022 1023 1024
                       CASE idx.type
                           WHEN '1' THEN 'clustered'
                           WHEN '2' THEN 'nonclustered'
                           ELSE NULL
                       END AS flags
                FROM sys.tables AS tbl
1025
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
1026 1027 1028
                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
1029 1030
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
1031
    }
1032 1033

    /**
1034
     * {@inheritDoc}
1035
     */
1036
    public function getCreateViewSQL($name, $sql)
1037 1038 1039
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }
1040 1041

    /**
1042
     * {@inheritDoc}
1043
     */
1044
    public function getListViewsSQL($database)
1045 1046 1047 1048
    {
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
    }

1049 1050 1051 1052
    /**
     * Returns the where clause to filter schema and table name in a query.
     *
     * @param string $table        The full qualified name of the table.
1053 1054
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
1055 1056 1057 1058 1059
     *
     * @return string
     */
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
    {
1060 1061 1062 1063
        if (strpos($table, '.') !== false) {
            [$schema, $table] = explode('.', $table);
            $schema           = $this->quoteStringLiteral($schema);
            $table            = $this->quoteStringLiteral($table);
1064
        } else {
1065 1066
            $schema = 'SCHEMA_NAME()';
            $table  = $this->quoteStringLiteral($table);
1067 1068
        }

1069
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1070 1071
    }

1072
    /**
1073
     * {@inheritDoc}
1074 1075 1076
     */
    public function getDropViewSQL($name)
    {
1077
        return 'DROP VIEW ' . $name;
1078
    }
1079 1080

    /**
1081
     * {@inheritDoc}
1082 1083
     *
     * @deprecated Use application-generated UUIDs instead
1084 1085 1086
     */
    public function getGuidExpression()
    {
1087
        return 'NEWID()';
1088
    }
1089 1090

    /**
1091
     * {@inheritDoc}
1092
     */
1093
    public function getLocateExpression($str, $substr, $startPos = false)
1094
    {
1095
        if ($startPos === false) {
1096 1097
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
        }
1098 1099

        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1100
    }
1101

1102
    /**
1103
     * {@inheritDoc}
1104
     */
1105
    public function getModExpression($expression1, $expression2)
1106
    {
1107
        return $expression1 . ' % ' . $expression2;
1108
    }
1109

1110
    /**
1111
     * {@inheritDoc}
1112
     */
1113
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1114
    {
1115
        if ($char === false) {
1116
            switch ($pos) {
1117
                case TrimMode::LEADING:
1118 1119 1120
                    $trimFn = 'LTRIM';
                    break;

1121
                case TrimMode::TRAILING:
1122 1123 1124 1125 1126
                    $trimFn = 'RTRIM';
                    break;

                default:
                    return 'LTRIM(RTRIM(' . $str . '))';
1127 1128 1129 1130
            }

            return $trimFn . '(' . $str . ')';
        }
1131 1132 1133 1134 1135 1136 1137 1138 1139 1140

        /** 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;
         */
1141
        $pattern = "'%[^' + " . $char . " + ']%'";
1142

1143
        if ($pos === TrimMode::LEADING) {
1144 1145 1146
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
        }

1147
        if ($pos === TrimMode::TRAILING) {
1148 1149 1150 1151
            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))';
1152
    }
1153

1154
    /**
1155
     * {@inheritDoc}
1156 1157
     */
    public function getConcatExpression()
1158
    {
1159
        $args = func_get_args();
1160

1161
        return '(' . implode(' + ', $args) . ')';
1162
    }
1163

Benjamin Morel's avatar
Benjamin Morel committed
1164 1165 1166
    /**
     * {@inheritDoc}
     */
1167
    public function getListDatabasesSQL()
1168
    {
1169
        return 'SELECT * FROM sys.databases';
1170
    }
1171

1172 1173 1174 1175 1176
    /**
     * {@inheritDoc}
     */
    public function getListNamespacesSQL()
    {
1177
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1178 1179
    }

1180
    /**
1181
     * {@inheritDoc}
1182
     */
1183
    public function getSubstringExpression($value, $from, $length = null)
1184
    {
1185
        if ($length !== null) {
1186
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1187
        }
1188

1189
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1190
    }
1191

1192
    /**
1193
     * {@inheritDoc}
1194
     */
1195
    public function getLengthExpression($column)
1196
    {
1197
        return 'LEN(' . $column . ')';
1198 1199
    }

1200
    /**
1201
     * {@inheritDoc}
1202
     */
1203
    public function getSetTransactionIsolationSQL($level)
romanb's avatar
romanb committed
1204
    {
1205
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
1206
    }
1207

1208
    /**
1209
     * {@inheritDoc}
1210
     */
1211
    public function getIntegerTypeDeclarationSQL(array $field)
1212
    {
1213
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1214 1215
    }

1216
    /**
1217
     * {@inheritDoc}
1218
     */
1219
    public function getBigIntTypeDeclarationSQL(array $field)
1220
    {
1221
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1222 1223
    }

1224
    /**
1225
     * {@inheritDoc}
1226
     */
1227
    public function getSmallIntTypeDeclarationSQL(array $field)
1228
    {
1229
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1230 1231
    }

1232
    /**
1233
     * {@inheritDoc}
1234
     */
1235
    public function getGuidTypeDeclarationSQL(array $field)
1236 1237 1238 1239
    {
        return 'UNIQUEIDENTIFIER';
    }

1240 1241 1242 1243 1244 1245 1246 1247
    /**
     * {@inheritDoc}
     */
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
    {
        return 'DATETIMEOFFSET(6)';
    }

1248 1249 1250
    /**
     * {@inheritDoc}
     */
1251
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1252
    {
1253 1254 1255
        return $fixed
            ? ($length > 0 ? 'NCHAR(' . $length . ')' : 'CHAR(255)')
            : ($length > 0 ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1256
    }
1257

Steve Müller's avatar
Steve Müller committed
1258 1259 1260 1261 1262
    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
1263 1264 1265
        return $fixed
            ? 'BINARY(' . ($length > 0 ? $length : 255) . ')'
            : 'VARBINARY(' . ($length > 0 ? $length : 255) . ')';
Steve Müller's avatar
Steve Müller committed
1266 1267 1268 1269 1270 1271 1272 1273 1274 1275
    }

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

1276 1277 1278
    /**
     * {@inheritDoc}
     */
1279
    public function getClobTypeDeclarationSQL(array $field)
1280
    {
1281
        return 'VARCHAR(MAX)';
1282
    }
1283

1284
    /**
1285
     * {@inheritDoc}
1286
     */
1287
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1288
    {
1289
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1290
    }
1291

1292
    /**
1293
     * {@inheritDoc}
1294
     */
1295
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1296
    {
1297 1298 1299
        // 3 - microseconds precision length
        // http://msdn.microsoft.com/en-us/library/ms187819.aspx
        return 'DATETIME2(6)';
1300 1301
    }

1302
    /**
1303
     * {@inheritDoc}
1304
     */
1305
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1306
    {
1307
        return 'DATE';
1308
    }
1309 1310

    /**
1311
     * {@inheritDoc}
1312
     */
1313
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1314
    {
1315
        return 'TIME(0)';
1316
    }
1317

1318
    /**
1319
     * {@inheritDoc}
1320
     */
1321
    public function getBooleanTypeDeclarationSQL(array $field)
1322 1323 1324 1325
    {
        return 'BIT';
    }

1326
    /**
1327
     * {@inheritDoc}
1328
     */
1329
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1330
    {
1331
        if ($limit === null && $offset <= 0) {
1332 1333
            return $query;
        }
1334

1335 1336 1337 1338 1339 1340 1341 1342
        // Queries using OFFSET... FETCH MUST have an ORDER BY clause
        // Find the position of the last instance of ORDER BY and ensure it is not within a parenthetical statement
        // but can be in a newline
        $matches      = [];
        $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE);
        $orderByPos   = false;
        if ($matchesCount > 0) {
            $orderByPos = $matches[0][($matchesCount - 1)][1];
Sergei Morozov's avatar
Sergei Morozov committed
1343 1344
        }

1345
        if ($orderByPos === false
1346
            || substr_count($query, '(', $orderByPos) !== substr_count($query, ')', $orderByPos)
1347
        ) {
1348
            if (preg_match('/^SELECT\s+DISTINCT/im', $query) > 0) {
1349 1350 1351 1352 1353 1354 1355 1356 1357 1358
                // SQL Server won't let us order by a non-selected column in a DISTINCT query,
                // so we have to do this madness. This says, order by the first column in the
                // result. SQL Server's docs say that a nonordered query's result order is non-
                // deterministic anyway, so this won't do anything that a bunch of update and
                // deletes to the table wouldn't do anyway.
                $query .= ' ORDER BY 1';
            } else {
                // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you
                // use constant expressions in the order by list.
                $query .= ' ORDER BY (SELECT 0)';
1359
            }
1360
        }
1361

1362 1363 1364 1365
        // This looks somewhat like MYSQL, but limit/offset are in inverse positions
        // Supposedly SQL:2008 core standard.
        // Per TSQL spec, FETCH NEXT n ROWS ONLY is not valid without OFFSET n ROWS.
        $query .= sprintf(' OFFSET %d ROWS', $offset);
1366

1367 1368
        if ($limit !== null) {
            $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit);
1369 1370
        }

1371
        return $query;
1372 1373
    }

1374
    /**
1375
     * {@inheritDoc}
1376 1377 1378
     */
    public function supportsLimitOffset()
    {
1379
        return true;
1380 1381
    }

1382
    /**
1383
     * {@inheritDoc}
1384
     */
1385
    public function convertBooleans($item)
1386
    {
1387 1388
        if (is_array($item)) {
            foreach ($item as $key => $value) {
1389
                if (! is_bool($value) && ! is_numeric($value)) {
1390
                    continue;
1391
                }
1392

1393
                $item[$key] = (int) (bool) $value;
1394
            }
Steve Müller's avatar
Steve Müller committed
1395
        } elseif (is_bool($item) || is_numeric($item)) {
1396
            $item = (int) (bool) $item;
1397
        }
1398

1399
        return $item;
1400
    }
1401 1402

    /**
1403
     * {@inheritDoc}
1404
     */
1405
    public function getCreateTemporaryTableSnippetSQL()
1406
    {
1407
        return 'CREATE TABLE';
1408
    }
1409

1410
    /**
1411
     * {@inheritDoc}
1412 1413 1414 1415 1416 1417
     */
    public function getTemporaryTableName($tableName)
    {
        return '#' . $tableName;
    }

1418
    /**
1419
     * {@inheritDoc}
1420 1421 1422
     */
    public function getDateTimeFormatString()
    {
1423
        return 'Y-m-d H:i:s.u';
1424
    }
1425

1426
    /**
1427
     * {@inheritDoc}
1428
     */
1429 1430
    public function getDateFormatString()
    {
1431
        return 'Y-m-d';
1432 1433 1434
    }

    /**
1435
     * {@inheritDoc}
1436
     */
1437 1438
    public function getTimeFormatString()
    {
1439
        return 'H:i:s';
1440
    }
1441

1442
    /**
1443
     * {@inheritDoc}
1444 1445 1446
     */
    public function getDateTimeTzFormatString()
    {
1447
        return 'Y-m-d H:i:s.u P';
1448
    }
1449

1450
    /**
1451
     * {@inheritDoc}
1452
     */
1453
    public function getName()
1454
    {
1455
        return 'mssql';
1456
    }
1457

Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1458
    /**
1459
     * {@inheritDoc}
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1460
     */
1461 1462
    protected function initializeDoctrineTypeMappings()
    {
1463
        $this->doctrineTypeMapping = [
1464 1465 1466 1467 1468 1469 1470 1471 1472 1473
            'bigint'           => 'bigint',
            'binary'           => 'binary',
            'bit'              => 'boolean',
            'char'             => 'string',
            'date'             => 'date',
            'datetime'         => 'datetime',
            'datetime2'        => 'datetime',
            'datetimeoffset'   => 'datetimetz',
            'decimal'          => 'decimal',
            'double'           => 'float',
1474
            'double precision' => 'float',
1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489
            'float'            => 'float',
            'image'            => 'blob',
            'int'              => 'integer',
            'money'            => 'integer',
            'nchar'            => 'string',
            'ntext'            => 'text',
            'numeric'          => 'decimal',
            'nvarchar'         => 'string',
            'real'             => 'float',
            'smalldatetime'    => 'datetime',
            'smallint'         => 'smallint',
            'smallmoney'       => 'integer',
            'text'             => 'text',
            'time'             => 'time',
            'tinyint'          => 'smallint',
1490
            'uniqueidentifier' => 'guid',
1491 1492
            'varbinary'        => 'binary',
            'varchar'          => 'string',
1493
        ];
1494
    }
1495 1496

    /**
1497
     * {@inheritDoc}
1498 1499 1500 1501 1502 1503 1504
     */
    public function createSavePoint($savepoint)
    {
        return 'SAVE TRANSACTION ' . $savepoint;
    }

    /**
1505
     * {@inheritDoc}
1506 1507 1508 1509 1510 1511 1512
     */
    public function releaseSavePoint($savepoint)
    {
        return '';
    }

    /**
1513
     * {@inheritDoc}
1514 1515 1516 1517
     */
    public function rollbackSavePoint($savepoint)
    {
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1518
    }
1519

1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532
    /**
     * {@inheritdoc}
     */
    public function getForeignKeyReferentialActionSQL($action)
    {
        // RESTRICT is not supported, therefore falling back to NO ACTION.
        if (strtoupper($action) === 'RESTRICT') {
            return 'NO ACTION';
        }

        return parent::getForeignKeyReferentialActionSQL($action);
    }

1533
    /**
1534
     * {@inheritDoc}
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1535
     */
1536
    public function appendLockHint($fromClause, $lockMode)
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1537
    {
1538
        switch (true) {
1539
            case $lockMode === LockMode::NONE:
1540
                return $fromClause . ' WITH (NOLOCK)';
1541

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

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

Steve Müller's avatar
Steve Müller committed
1548
            default:
1549
                return $fromClause;
Steve Müller's avatar
Steve Müller committed
1550
        }
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1551 1552 1553
    }

    /**
1554
     * {@inheritDoc}
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1555 1556 1557 1558 1559
     */
    public function getForUpdateSQL()
    {
        return ' ';
    }
1560

1561 1562 1563
    /**
     * {@inheritDoc}
     */
1564 1565
    protected function getReservedKeywordsClass()
    {
1566
        return Keywords\SQLServer2012Keywords::class;
1567
    }
1568 1569

    /**
1570
     * {@inheritDoc}
1571
     */
1572
    public function quoteSingleIdentifier($str)
1573
    {
1574
        return '[' . str_replace(']', '][', $str) . ']';
1575
    }
1576

1577 1578 1579
    /**
     * {@inheritDoc}
     */
1580 1581
    public function getTruncateTableSQL($tableName, $cascade = false)
    {
1582 1583 1584
        $tableIdentifier = new Identifier($tableName);

        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1585
    }
1586 1587

    /**
1588
     * {@inheritDoc}
1589 1590 1591 1592 1593
     */
    public function getBlobTypeDeclarationSQL(array $field)
    {
        return 'VARBINARY(MAX)';
    }
1594

1595 1596 1597 1598 1599 1600 1601 1602 1603 1604
    /**
     * {@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 {
1605
            $collation = ! empty($field['collation']) ?
1606
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1607

1608
            $notnull = ! empty($field['notnull']) ? ' NOT NULL' : '';
1609

1610
            $unique = ! empty($field['unique']) ?
1611 1612
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';

1613
            $check = ! empty($field['check']) ?
1614 1615
                ' ' . $field['check'] : '';

1616
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1617
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1618 1619 1620 1621
        }

        return $name . ' ' . $columnDef;
    }
1622

1623 1624 1625 1626 1627 1628 1629 1630
    /**
     * {@inheritdoc}
     */
    protected function getLikeWildcardCharacters() : string
    {
        return parent::getLikeWildcardCharacters() . '[]^';
    }

1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652
    /**
     * 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)
    {
1653 1654 1655 1656
        // Always generate name for unquoted identifiers to ensure consistency.
        $identifier = new Identifier($identifier);

        return strtoupper(dechex(crc32($identifier->getName())));
1657
    }
1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688

    protected function getCommentOnTableSQL(string $tableName, ?string $comment) : string
    {
        return sprintf(
            <<<'SQL'
EXEC sys.sp_addextendedproperty @name=N'MS_Description', 
  @value=N%s, @level0type=N'SCHEMA', @level0name=N'dbo', 
  @level1type=N'TABLE', @level1name=N%s
SQL
            ,
            $this->quoteStringLiteral((string) $comment),
            $this->quoteStringLiteral($tableName)
        );
    }

    public function getListTableMetadataSQL(string $table) : string
    {
        return sprintf(
            <<<'SQL'
SELECT
  p.value AS [table_comment]
FROM
  sys.tables AS tbl
  INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1
WHERE
  (tbl.name=N%s and SCHEMA_NAME(tbl.schema_id)=N'dbo' and p.name=N'MS_Description')
SQL
            ,
            $this->quoteStringLiteral($table)
        );
    }
1689
}