SQLServerPlatform.php 51 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 11
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
12
use Doctrine\DBAL\Schema\TableDiff;
13
use InvalidArgumentException;
14 15 16 17 18 19
use function array_merge;
use function array_unique;
use function array_values;
use function count;
use function crc32;
use function dechex;
20
use function explode;
21
use function func_get_args;
22
use function implode;
23 24 25 26 27 28
use function is_array;
use function is_bool;
use function is_numeric;
use function is_string;
use function preg_match;
use function preg_replace;
29
use function sprintf;
30 31 32 33
use function str_replace;
use function stripos;
use function stristr;
use function strlen;
34
use function strpos;
35 36 37
use function strtoupper;
use function substr;
use function substr_count;
38 39

/**
40 41
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
 * Microsoft SQL Server database platform.
42
 */
43
class SQLServerPlatform extends AbstractPlatform
44
{
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
    /**
     * {@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);
    }

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

81
        if ($operator === '-') {
82 83
            $factorClause = '-1 * ';
        }
84

85
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
86
    }
87

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

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

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

    /**
118
     * {@inheritDoc}
119
     */
120
    public function supportsReleaseSavepoints()
121 122 123
    {
        return false;
    }
124

125 126 127 128 129 130 131 132
    /**
     * {@inheritdoc}
     */
    public function supportsSchemas()
    {
        return true;
    }

133 134 135 136 137 138 139 140
    /**
     * {@inheritdoc}
     */
    public function getDefaultSchemaName()
    {
        return 'dbo';
    }

141 142 143 144 145 146 147 148
    /**
     * {@inheritDoc}
     */
    public function supportsColumnCollation()
    {
        return true;
    }

149 150 151 152 153 154 155 156
    /**
     * {@inheritDoc}
     */
    public function hasNativeGuidType()
    {
        return true;
    }

157
    /**
158
     * {@inheritDoc}
159 160 161 162 163
     */
    public function getCreateDatabaseSQL($name)
    {
        return 'CREATE DATABASE ' . $name;
    }
164

165
    /**
166
     * {@inheritDoc}
167 168 169
     */
    public function getDropDatabaseSQL($name)
    {
170 171 172 173
        return 'DROP DATABASE ' . $name;
    }

    /**
174
     * {@inheritDoc}
175
     */
176
    public function supportsCreateDropDatabase()
177
    {
178
        return true;
179 180
    }

181 182 183 184 185 186 187 188
    /**
     * {@inheritDoc}
     */
    public function getCreateSchemaSQL($schemaName)
    {
        return 'CREATE SCHEMA ' . $schemaName;
    }

189
    /**
190
     * {@inheritDoc}
191 192 193
     */
    public function getDropForeignKeySQL($foreignKey, $table)
    {
194 195
        if (! $foreignKey instanceof ForeignKeyConstraint) {
            $foreignKey = new Identifier($foreignKey);
196 197
        }

198 199
        if (! $table instanceof Table) {
            $table = new Identifier($table);
200 201
        }

202
        $foreignKey = $foreignKey->getQuotedName($this);
203
        $table      = $table->getQuotedName($this);
204

205
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
206
    }
207 208

    /**
209
     * {@inheritDoc}
210
     */
211
    public function getDropIndexSQL($index, $table = null)
212
    {
213
        if ($index instanceof Index) {
214
            $index = $index->getQuotedName($this);
215 216
        } elseif (! is_string($index)) {
            throw new InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
217 218
        }

219
        if (! isset($table)) {
220
            return 'DROP INDEX ' . $index;
221
        }
222

223 224
        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
225
        }
226

227 228 229 230 231 232 233 234 235 236 237 238 239 240
        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
        );
241
    }
242 243

    /**
244
     * {@inheritDoc}
245
     */
246
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
247
    {
248 249
        $defaultConstraintsSql = [];
        $commentsSql           = [];
250

251
        // @todo does other code breaks because of this?
252
        // force primary keys to be not null
253 254 255 256
        foreach ($columns as &$column) {
            if (isset($column['primary']) && $column['primary']) {
                $column['notnull'] = true;
            }
257

258
            // Build default constraints SQL statements.
259
            if (isset($column['default'])) {
260 261 262
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
            }
263

264 265
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
                continue;
266
            }
267 268

            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
269 270
        }

271
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
272

273
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
274 275 276 277
            foreach ($options['uniqueConstraints'] as $name => $definition) {
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
            }
        }
278

279
        if (isset($options['primary']) && ! empty($options['primary'])) {
280 281 282 283 284
            $flags = '';
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
                $flags = ' NONCLUSTERED';
            }
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
285 286 287 288 289
        }

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

        $check = $this->getCheckDeclarationSQL($columns);
290
        if (! empty($check)) {
291 292 293 294
            $query .= ', ' . $check;
        }
        $query .= ')';

295
        $sql = [$query];
296

297
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
298
            foreach ($options['indexes'] as $index) {
299 300 301 302 303
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
            }
        }

        if (isset($options['foreignKeys'])) {
304
            foreach ((array) $options['foreignKeys'] as $definition) {
305 306 307 308
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
            }
        }

309
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
310
    }
311

312
    /**
313
     * {@inheritDoc}
314 315 316 317 318 319 320
     */
    public function getCreatePrimaryKeySQL(Index $index, $table)
    {
        $flags = '';
        if ($index->hasFlag('nonclustered')) {
            $flags = ' NONCLUSTERED';
        }
321

322
        return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY' . $flags . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
323 324
    }

325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343
    /**
     * 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)
    {
344 345 346 347 348 349 350 351 352
        if (strpos($tableName, '.') !== false) {
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
        } else {
            $schemaSQL = "'dbo'";
            $tableSQL  = $this->quoteStringLiteral($tableName);
        }

353 354 355 356
        return $this->getAddExtendedPropertySQL(
            'MS_Description',
            $comment,
            'SCHEMA',
357
            $schemaSQL,
358
            'TABLE',
359
            $tableSQL,
360 361 362 363 364
            'COLUMN',
            $columnName
        );
    }

365 366 367
    /**
     * Returns the SQL snippet for declaring a default constraint.
     *
368 369
     * @param string  $table  Name of the table to return the default constraint declaration for.
     * @param mixed[] $column Column definition.
370 371 372
     *
     * @return string
     *
373
     * @throws InvalidArgumentException
374 375 376
     */
    public function getDefaultConstraintDeclarationSQL($table, array $column)
    {
377 378
        if (! isset($column['default'])) {
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
379 380
        }

381 382
        $columnName = new Identifier($column['name']);

383
        return ' CONSTRAINT ' .
384 385
            $this->generateDefaultConstraintName($table, $column['name']) .
            $this->getDefaultValueDeclarationSQL($column) .
386
            ' FOR ' . $columnName->getQuotedName($this);
387 388
    }

389
    /**
390
     * {@inheritDoc}
391
     */
392
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
393 394
    {
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
395 396 397 398 399 400 401

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

        return $constraint;
    }

    /**
402
     * {@inheritDoc}
403 404 405 406 407
     */
    public function getCreateIndexSQL(Index $index, $table)
    {
        $constraint = parent::getCreateIndexSQL($index, $table);

408
        if ($index->isUnique() && ! $index->isPrimary()) {
409 410 411 412 413 414
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
        }

        return $constraint;
    }

415
    /**
416
     * {@inheritDoc}
417 418 419 420 421 422 423 424 425 426
     */
    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
427
        } elseif ($index->hasFlag('nonclustered')) {
428 429 430 431 432 433
            $type .= 'NONCLUSTERED ';
        }

        return $type;
    }

434
    /**
435
     * Extend unique key constraint with required filters
436
     *
437
     * @param string $sql
438
     *
439 440 441 442
     * @return string
     */
    private function _appendUniqueConstraintDefinition($sql, Index $index)
    {
443
        $fields = [];
444

445
        foreach ($index->getQuotedColumns($this) as $field) {
446
            $fields[] = $field . ' IS NOT NULL';
447
        }
448 449 450

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

452
    /**
453
     * {@inheritDoc}
454
     */
455
    public function getAlterTableSQL(TableDiff $diff)
456
    {
457 458 459 460
        $queryParts  = [];
        $sql         = [];
        $columnSql   = [];
        $commentsSql = [];
461

462
        foreach ($diff->addedColumns as $column) {
463 464
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
465 466
            }

467
            $columnDef    = $column->toArray();
468 469
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);

470
            if (isset($columnDef['default'])) {
471
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
472
            }
473 474 475

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

476 477
            if (empty($comment) && ! is_numeric($comment)) {
                continue;
478
            }
479 480 481 482 483 484

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

487
        foreach ($diff->removedColumns as $column) {
488 489
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
490 491
            }

492
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
493 494
        }

495
        foreach ($diff->changedColumns as $columnDiff) {
496 497
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
498 499
            }

500 501
            $column     = $columnDiff->column;
            $comment    = $this->getColumnComment($column);
502
            $hasComment = ! empty($comment) || is_numeric($comment);
503 504 505

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

508
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
                    $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;
            }

530
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
531

532 533 534 535 536
            if ($requireDropDefaultConstraint) {
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
                    $diff->name,
                    $columnDiff->oldColumnName
                );
537 538
            }

539 540
            $columnDef = $column->toArray();

541
            $queryParts[] = 'ALTER COLUMN ' .
542 543
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);

544 545
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
                continue;
546
            }
547 548

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

551
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
552 553
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
554 555
            }

556 557
            $oldColumnName = new Identifier($oldColumnName);

558
            $sql[] = "sp_RENAME '" .
559
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
560
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
561

562
            // Recreate default constraint with new column name if necessary (for future reference).
563 564
            if ($column->getDefault() === null) {
                continue;
565
            }
566 567 568 569 570 571

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

574
        $tableSql = [];
575 576

        if ($this->onSchemaAlterTable($diff, $tableSql)) {
577
            return array_merge($tableSql, $columnSql);
578
        }
579

580
        foreach ($queryParts as $query) {
581
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
582
        }
583

584
        $sql = array_merge($sql, $commentsSql);
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
585

586
        if ($diff->newName !== false) {
587
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $diff->getNewName()->getName() . "'";
588 589 590 591 592 593 594 595 596 597 598 599 600

            /**
             * 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'';' " .
601 602
                'FROM sys.default_constraints dc ' .
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
603
                "WHERE tbl.name = '" . $diff->getNewName()->getName() . "';" .
604
                'EXEC sp_executesql @sql';
605 606
        }

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

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

616 617 618
    /**
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
     *
619 620
     * @param string $tableName The name of the table to generate the clause for.
     * @param Column $column    The column to generate the clause for.
621 622 623 624 625
     *
     * @return string
     */
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
    {
626
        $columnDef         = $column->toArray();
627 628 629 630 631 632 633 634
        $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.
     *
635 636
     * @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.
637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
     *
     * @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.
     *
653
     * @param ColumnDiff $columnDiff The column diff to evaluate.
654
     *
655
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
656 657 658 659 660
     */
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
    {
        // We can only decide whether to drop an existing default constraint
        // if we know the original default value.
661
        if (! $columnDiff->fromColumn instanceof Column) {
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678
            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
679
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
680 681
    }

682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700
    /**
     * 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)
    {
701 702 703 704 705 706 707 708 709
        if (strpos($tableName, '.') !== false) {
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
        } else {
            $schemaSQL = "'dbo'";
            $tableSQL  = $this->quoteStringLiteral($tableName);
        }

710 711 712 713
        return $this->getUpdateExtendedPropertySQL(
            'MS_Description',
            $comment,
            'SCHEMA',
714
            $schemaSQL,
715
            'TABLE',
716
            $tableSQL,
717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739
            '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)
    {
740 741 742 743 744 745 746 747 748
        if (strpos($tableName, '.') !== false) {
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
        } else {
            $schemaSQL = "'dbo'";
            $tableSQL  = $this->quoteStringLiteral($tableName);
        }

749 750 751
        return $this->getDropExtendedPropertySQL(
            'MS_Description',
            'SCHEMA',
752
            $schemaSQL,
753
            'TABLE',
754
            $tableSQL,
755 756 757 758 759
            'COLUMN',
            $columnName
        );
    }

760 761 762 763 764
    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
765 766 767 768 769 770
        return [sprintf(
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
            $tableName,
            $oldIndexName,
            $index->getQuotedName($this)
        ),
771
        ];
772 773
    }

774 775 776
    /**
     * Returns the SQL statement for adding an extended property to a database object.
     *
777 778
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
     *
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799
     * @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
    ) {
800 801 802 803 804
        return 'EXEC sp_addextendedproperty ' .
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral($value) . ', ' .
            'N' . $this->quoteStringLiteral($level0Type) . ', ' . $level0Name . ', ' .
            'N' . $this->quoteStringLiteral($level1Type) . ', ' . $level1Name . ', ' .
            'N' . $this->quoteStringLiteral($level2Type) . ', ' . $level2Name;
805 806 807 808 809
    }

    /**
     * Returns the SQL statement for dropping an extended property from a database object.
     *
810 811
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
     *
812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830
     * @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
    ) {
831 832 833 834 835
        return 'EXEC sp_dropextendedproperty ' .
            'N' . $this->quoteStringLiteral($name) . ', ' .
            'N' . $this->quoteStringLiteral($level0Type) . ', ' . $level0Name . ', ' .
            'N' . $this->quoteStringLiteral($level1Type) . ', ' . $level1Name . ', ' .
            'N' . $this->quoteStringLiteral($level2Type) . ', ' . $level2Name;
836 837 838 839 840
    }

    /**
     * Returns the SQL statement for updating an extended property of a database object.
     *
841 842
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
     *
843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863
     * @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
    ) {
864 865 866 867 868
        return 'EXEC sp_updateextendedproperty ' .
        'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral($value) . ', ' .
        'N' . $this->quoteStringLiteral($level0Type) . ', ' . $level0Name . ', ' .
        'N' . $this->quoteStringLiteral($level1Type) . ', ' . $level1Name . ', ' .
        'N' . $this->quoteStringLiteral($level2Type) . ', ' . $level2Name;
869 870
    }

871
    /**
872
     * {@inheritDoc}
873
     */
874
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
875
    {
876
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
877 878
    }

879
    /**
880
     * {@inheritDoc}
881
     */
882
    public function getListTablesSQL()
883
    {
884
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
885 886
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
        return "SELECT name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
887 888 889
    }

    /**
890
     * {@inheritDoc}
891
     */
892
    public function getListTableColumnsSQL($table, $database = null)
893
    {
894 895 896 897 898 899 900 901
        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,
902 903
                          col.collation_name AS collation,
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
904 905 906 907 908
                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
909 910
                JOIN      sys.schemas AS scm
                ON        obj.schema_id = scm.schema_id
911 912 913
                LEFT JOIN sys.default_constraints def
                ON        col.default_object_id = def.object_id
                AND       col.object_id = def.parent_object_id
914 915 916 917
                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'
918
                WHERE     obj.type = 'U'
919
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
920 921 922
    }

    /**
923
     * {@inheritDoc}
924 925 926
     */
    public function getListTableForeignKeysSQL($table, $database = null)
    {
927
        return 'SELECT f.name AS ForeignKey,
928 929 930 931 932 933 934 935 936 937 938 939
                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
940
                WHERE ' .
941
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
942 943 944
    }

    /**
945
     * {@inheritDoc}
946
     */
947
    public function getListTableIndexesSQL($table, $currentDatabase = null)
948
    {
949 950
        return "SELECT idx.name AS key_name,
                       col.name AS column_name,
Steve Müller's avatar
Steve Müller committed
951 952
                       ~idx.is_unique AS non_unique,
                       idx.is_primary_key AS [primary],
953 954 955 956 957 958
                       CASE idx.type
                           WHEN '1' THEN 'clustered'
                           WHEN '2' THEN 'nonclustered'
                           ELSE NULL
                       END AS flags
                FROM sys.tables AS tbl
959
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
960 961 962
                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
963 964
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
965
    }
966 967

    /**
968
     * {@inheritDoc}
969
     */
970
    public function getCreateViewSQL($name, $sql)
971 972 973
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }
974 975

    /**
976
     * {@inheritDoc}
977
     */
978
    public function getListViewsSQL($database)
979 980 981 982
    {
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
    }

983 984 985 986
    /**
     * Returns the where clause to filter schema and table name in a query.
     *
     * @param string $table        The full qualified name of the table.
987 988
     * @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.
989 990 991 992 993
     *
     * @return string
     */
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
    {
994 995 996 997
        if (strpos($table, '.') !== false) {
            [$schema, $table] = explode('.', $table);
            $schema           = $this->quoteStringLiteral($schema);
            $table            = $this->quoteStringLiteral($table);
998
        } else {
999 1000
            $schema = 'SCHEMA_NAME()';
            $table  = $this->quoteStringLiteral($table);
1001 1002
        }

1003
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1004 1005
    }

1006
    /**
1007
     * {@inheritDoc}
1008 1009 1010
     */
    public function getDropViewSQL($name)
    {
1011
        return 'DROP VIEW ' . $name;
1012
    }
1013 1014

    /**
1015
     * {@inheritDoc}
1016 1017
     *
     * @deprecated Use application-generated UUIDs instead
1018 1019 1020
     */
    public function getGuidExpression()
    {
1021
        return 'NEWID()';
1022
    }
1023 1024

    /**
1025
     * {@inheritDoc}
1026
     */
1027
    public function getLocateExpression($str, $substr, $startPos = false)
1028
    {
1029
        if ($startPos === false) {
1030 1031
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
        }
1032 1033

        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1034
    }
1035

1036
    /**
1037
     * {@inheritDoc}
1038
     */
1039
    public function getModExpression($expression1, $expression2)
1040
    {
1041
        return $expression1 . ' % ' . $expression2;
1042
    }
1043

1044
    /**
1045
     * {@inheritDoc}
1046
     */
1047
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1048
    {
1049
        if (! $char) {
1050
            switch ($pos) {
1051
                case TrimMode::LEADING:
1052 1053 1054
                    $trimFn = 'LTRIM';
                    break;

1055
                case TrimMode::TRAILING:
1056 1057 1058 1059 1060
                    $trimFn = 'RTRIM';
                    break;

                default:
                    return 'LTRIM(RTRIM(' . $str . '))';
1061 1062 1063 1064
            }

            return $trimFn . '(' . $str . ')';
        }
1065 1066 1067 1068 1069 1070 1071 1072 1073 1074

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

1077
        if ($pos === TrimMode::LEADING) {
1078 1079 1080
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
        }

1081
        if ($pos === TrimMode::TRAILING) {
1082 1083 1084 1085
            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))';
1086
    }
1087

1088
    /**
1089
     * {@inheritDoc}
1090 1091
     */
    public function getConcatExpression()
1092
    {
1093
        $args = func_get_args();
1094

1095
        return '(' . implode(' + ', $args) . ')';
1096
    }
1097

Benjamin Morel's avatar
Benjamin Morel committed
1098 1099 1100
    /**
     * {@inheritDoc}
     */
1101
    public function getListDatabasesSQL()
1102
    {
1103
        return 'SELECT * FROM sys.databases';
1104
    }
1105

1106 1107 1108 1109 1110
    /**
     * {@inheritDoc}
     */
    public function getListNamespacesSQL()
    {
1111
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1112 1113
    }

1114
    /**
1115
     * {@inheritDoc}
1116
     */
1117
    public function getSubstringExpression($value, $from, $length = null)
1118
    {
1119
        if ($length !== null) {
1120
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1121
        }
1122

1123
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1124
    }
1125

1126
    /**
1127
     * {@inheritDoc}
1128
     */
1129
    public function getLengthExpression($column)
1130
    {
1131
        return 'LEN(' . $column . ')';
1132 1133
    }

1134
    /**
1135
     * {@inheritDoc}
1136
     */
1137
    public function getSetTransactionIsolationSQL($level)
romanb's avatar
romanb committed
1138
    {
1139
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
1140
    }
1141

1142
    /**
1143
     * {@inheritDoc}
1144
     */
1145
    public function getIntegerTypeDeclarationSQL(array $field)
1146
    {
1147
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1148 1149
    }

1150
    /**
1151
     * {@inheritDoc}
1152
     */
1153
    public function getBigIntTypeDeclarationSQL(array $field)
1154
    {
1155
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1156 1157
    }

1158
    /**
1159
     * {@inheritDoc}
1160
     */
1161
    public function getSmallIntTypeDeclarationSQL(array $field)
1162
    {
1163
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1164 1165
    }

1166
    /**
1167
     * {@inheritDoc}
1168
     */
1169
    public function getGuidTypeDeclarationSQL(array $field)
1170 1171 1172 1173
    {
        return 'UNIQUEIDENTIFIER';
    }

1174 1175 1176
    /**
     * {@inheritDoc}
     */
1177
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1178
    {
1179
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1180
    }
1181

Steve Müller's avatar
Steve Müller committed
1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197
    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
    }

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

1198 1199 1200
    /**
     * {@inheritDoc}
     */
1201
    public function getClobTypeDeclarationSQL(array $field)
1202
    {
1203
        return 'VARCHAR(MAX)';
1204
    }
1205

1206
    /**
1207
     * {@inheritDoc}
1208
     */
1209
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1210
    {
1211
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1212
    }
1213

1214
    /**
1215
     * {@inheritDoc}
1216
     */
1217
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1218
    {
1219
        return 'DATETIME';
1220 1221
    }

1222
    /**
1223
     * {@inheritDoc}
1224
     */
1225
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1226
    {
1227
        return 'DATETIME';
1228
    }
1229 1230

    /**
1231
     * {@inheritDoc}
1232
     */
1233
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1234
    {
1235
        return 'DATETIME';
1236
    }
1237

1238
    /**
1239
     * {@inheritDoc}
1240
     */
1241
    public function getBooleanTypeDeclarationSQL(array $field)
1242 1243 1244 1245
    {
        return 'BIT';
    }

1246
    /**
1247
     * {@inheritDoc}
1248
     */
1249
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1250
    {
1251 1252 1253 1254
        $where = [];

        if ($offset > 0) {
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
Fabio B. Silva's avatar
Fabio B. Silva committed
1255
        }
1256

1257 1258 1259 1260 1261 1262 1263 1264 1265 1266
        if ($limit !== null) {
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
            $top     = sprintf('TOP %d', $offset + $limit);
        } else {
            $top = 'TOP 9223372036854775807';
        }

        if (empty($where)) {
            return $query;
        }
1267

1268
        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
1269 1270 1271
        // Even if the TOP n is very large, the use of a CTE will
        // allow the SQL Server query planner to optimize it so it doesn't
        // actually scan the entire range covered by the TOP clause.
1272
        $selectPattern  = '/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/im';
1273
        $replacePattern = sprintf('$1%s $2', $top);
1274
        $query          = preg_replace($selectPattern, $replacePattern, $query);
1275

1276
        if (stristr($query, 'ORDER BY')) {
1277
            // Inner order by is not valid in SQL Server for our purposes
1278
            // unless it's in a TOP N subquery.
1279
            $query = $this->scrubInnerOrderBy($query);
Fabio B. Silva's avatar
Fabio B. Silva committed
1280 1281
        }

1282 1283
        // Build a new limited query around the original, using a CTE
        return sprintf(
1284 1285 1286 1287
            'WITH dctrn_cte AS (%s) '
            . 'SELECT * FROM ('
            . 'SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte'
            . ') AS doctrine_tbl '
1288
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1289
            $query,
1290
            implode(' AND ', $where)
1291 1292
        );
    }
1293

1294 1295
    /**
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
1296
     * Caveat: will leave ORDER BY in TOP N subqueries.
1297
     *
1298
     * @param string $query
1299
     *
1300 1301
     * @return string
     */
Bill Schaller's avatar
Bill Schaller committed
1302 1303
    private function scrubInnerOrderBy($query)
    {
1304
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1305
        $offset = 0;
Bill Schaller's avatar
Bill Schaller committed
1306

1307
        while ($count-- > 0) {
1308
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1309 1310 1311 1312
            if ($orderByPos === false) {
                break;
            }

1313 1314
            $qLen            = strlen($query);
            $parenCount      = 0;
1315
            $currentPosition = $orderByPos;
1316

1317
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1318
                if ($query[$currentPosition] === '(') {
1319
                    $parenCount++;
1320
                } elseif ($query[$currentPosition] === ')') {
1321
                    $parenCount--;
Fabio B. Silva's avatar
Fabio B. Silva committed
1322
                }
1323

1324 1325
                $currentPosition++;
            }
1326 1327 1328 1329 1330 1331 1332 1333

            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
                // If the order by clause is in a TOP N subquery, do not remove
                // it and continue iteration from the current position.
                $offset = $currentPosition;
                continue;
            }

1334 1335
            if ($currentPosition >= $qLen - 1) {
                continue;
Fabio B. Silva's avatar
Fabio B. Silva committed
1336
            }
1337 1338 1339

            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
            $offset = $orderByPos;
1340
        }
1341
        return $query;
1342
    }
1343

1344 1345 1346
    /**
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
     *
1347 1348
     * @param string $query           The query
     * @param int    $currentPosition Start position of ORDER BY clause
1349
     *
1350 1351 1352 1353 1354 1355
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
     */
    private function isOrderByInTopNSubquery($query, $currentPosition)
    {
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
        $subQueryBuffer = '';
1356
        $parenCount     = 0;
1357

Bill Schaller's avatar
Bill Schaller committed
1358
        // If $parenCount goes negative, we've exited the subquery we're examining.
1359 1360
        // If $currentPosition goes negative, we've reached the beginning of the query.
        while ($parenCount >= 0 && $currentPosition >= 0) {
1361
            if ($query[$currentPosition] === '(') {
1362
                $parenCount--;
Bill Schaller's avatar
Bill Schaller committed
1363
            } elseif ($query[$currentPosition] === ')') {
1364 1365
                $parenCount++;
            }
1366 1367

            // Only yank query text on the same nesting level as the ORDER BY clause.
1368
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1369 1370 1371 1372

            $currentPosition--;
        }

Gabriel Caruso's avatar
Gabriel Caruso committed
1373
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1374 1375
    }

1376
    /**
1377
     * {@inheritDoc}
1378 1379 1380 1381 1382 1383
     */
    public function supportsLimitOffset()
    {
        return false;
    }

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

                $item[$key] = $value ? 1 : 0;
1396
            }
Steve Müller's avatar
Steve Müller committed
1397
        } elseif (is_bool($item) || is_numeric($item)) {
1398
            $item = $item ? 1 : 0;
1399
        }
1400

1401
        return $item;
1402
    }
1403 1404

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

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

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

1428
    /**
1429
     * {@inheritDoc}
1430
     */
1431 1432 1433 1434 1435 1436
    public function getDateFormatString()
    {
        return 'Y-m-d H:i:s.000';
    }

    /**
1437
     * {@inheritDoc}
1438
     */
1439 1440 1441
    public function getTimeFormatString()
    {
        return 'Y-m-d H:i:s.000';
1442
    }
1443

1444
    /**
1445
     * {@inheritDoc}
1446 1447 1448 1449 1450
     */
    public function getDateTimeTzFormatString()
    {
        return $this->getDateTimeFormatString();
    }
1451

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

Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1460
    /**
1461
     * {@inheritDoc}
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1462
     */
1463 1464
    protected function initializeDoctrineTypeMappings()
    {
1465
        $this->doctrineTypeMapping = [
1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486
            '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
1487 1488
            'binary' => 'binary',
            'varbinary' => 'binary',
1489
            'image' => 'blob',
1490
            'uniqueidentifier' => 'guid',
1491
        ];
1492
    }
1493 1494

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

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

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

1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530
    /**
     * {@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);
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return $name . ' ' . $columnDef;
    }
1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642

    /**
     * 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)
    {
1643 1644 1645 1646
        // Always generate name for unquoted identifiers to ensure consistency.
        $identifier = new Identifier($identifier);

        return strtoupper(dechex(crc32($identifier->getName())));
1647
    }
1648
}