SQLServerPlatform.php 51.7 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 Doctrine\DBAL\Types;
14
use InvalidArgumentException;
15 16 17 18 19 20
use function array_merge;
use function array_unique;
use function array_values;
use function count;
use function crc32;
use function dechex;
21
use function explode;
22
use function func_get_args;
23
use function implode;
24 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;
use function preg_replace;
30
use function sprintf;
31 32 33 34
use function str_replace;
use function stripos;
use function stristr;
use function strlen;
35
use function strpos;
36 37 38
use function strtoupper;
use function substr;
use function substr_count;
39 40

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

296
        $sql = [$query];
297

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

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

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

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

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

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

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

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

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

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

390
    /**
391
     * {@inheritDoc}
392 393 394 395 396
     */
    public function getCreateIndexSQL(Index $index, $table)
    {
        $constraint = parent::getCreateIndexSQL($index, $table);

397
        if ($index->isUnique() && ! $index->isPrimary()) {
398 399 400 401 402 403
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
        }

        return $constraint;
    }

404
    /**
405
     * {@inheritDoc}
406 407 408 409 410 411 412 413 414 415
     */
    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
416
        } elseif ($index->hasFlag('nonclustered')) {
417 418 419 420 421 422
            $type .= 'NONCLUSTERED ';
        }

        return $type;
    }

423
    /**
424
     * Extend unique key constraint with required filters
425
     *
426
     * @param string $sql
427
     *
428 429 430 431
     * @return string
     */
    private function _appendUniqueConstraintDefinition($sql, Index $index)
    {
432
        $fields = [];
433

434
        foreach ($index->getQuotedColumns($this) as $field) {
435
            $fields[] = $field . ' IS NOT NULL';
436
        }
437 438 439

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

441
    /**
442
     * {@inheritDoc}
443
     */
444
    public function getAlterTableSQL(TableDiff $diff)
445
    {
446 447 448 449
        $queryParts  = [];
        $sql         = [];
        $columnSql   = [];
        $commentsSql = [];
450

451
        /** @var Column $column */
452
        foreach ($diff->addedColumns as $column) {
453 454
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
455 456
            }

457
            $columnDef    = $column->toArray();
458 459
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);

460
            if (isset($columnDef['default'])) {
461
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
462
            }
463 464 465

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

466 467
            if (empty($comment) && ! is_numeric($comment)) {
                continue;
468
            }
469 470 471 472 473 474

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

477
        foreach ($diff->removedColumns as $column) {
478 479
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
480 481
            }

482
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
483 484
        }

485
        /** @var ColumnDiff $columnDiff */
486
        foreach ($diff->changedColumns as $columnDiff) {
487 488
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
489 490
            }

491 492
            $column     = $columnDiff->column;
            $comment    = $this->getColumnComment($column);
493
            $hasComment = ! empty($comment) || is_numeric($comment);
494 495 496

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

499
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
                    $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;
            }

521
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
522

523 524 525 526 527
            if ($requireDropDefaultConstraint) {
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
                    $diff->name,
                    $columnDiff->oldColumnName
                );
528 529
            }

530 531
            $columnDef = $column->toArray();

532
            $queryParts[] = 'ALTER COLUMN ' .
533 534
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);

535 536
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
                continue;
537
            }
538 539

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

542
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
543 544
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
545 546
            }

547 548
            $oldColumnName = new Identifier($oldColumnName);

549
            $sql[] = "sp_RENAME '" .
550
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
551
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
552

553
            // Recreate default constraint with new column name if necessary (for future reference).
554 555
            if ($column->getDefault() === null) {
                continue;
556
            }
557 558 559 560 561 562

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

565
        $tableSql = [];
566 567

        if ($this->onSchemaAlterTable($diff, $tableSql)) {
568
            return array_merge($tableSql, $columnSql);
569
        }
570

571
        foreach ($queryParts as $query) {
572
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
573
        }
574

575
        $sql = array_merge($sql, $commentsSql);
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
576

577
        if ($diff->newName !== false) {
578
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $diff->getNewName()->getName() . "'";
579 580 581 582 583 584 585 586 587 588 589 590 591

            /**
             * 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'';' " .
592 593
                'FROM sys.default_constraints dc ' .
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
594
                "WHERE tbl.name = '" . $diff->getNewName()->getName() . "';" .
595
                'EXEC sp_executesql @sql';
596 597
        }

598 599 600 601 602 603
        $sql = array_merge(
            $this->getPreAlterTableIndexForeignKeySQL($diff),
            $sql,
            $this->getPostAlterTableIndexForeignKeySQL($diff)
        );

604
        return array_merge($sql, $tableSql, $columnSql);
605
    }
606

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

673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
    /**
     * 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)
    {
692 693 694 695 696 697 698 699 700
        if (strpos($tableName, '.') !== false) {
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
        } else {
            $schemaSQL = "'dbo'";
            $tableSQL  = $this->quoteStringLiteral($tableName);
        }

701 702 703 704
        return $this->getUpdateExtendedPropertySQL(
            'MS_Description',
            $comment,
            'SCHEMA',
705
            $schemaSQL,
706
            'TABLE',
707
            $tableSQL,
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
            '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)
    {
731 732 733 734 735 736 737 738 739
        if (strpos($tableName, '.') !== false) {
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
        } else {
            $schemaSQL = "'dbo'";
            $tableSQL  = $this->quoteStringLiteral($tableName);
        }

740 741 742
        return $this->getDropExtendedPropertySQL(
            'MS_Description',
            'SCHEMA',
743
            $schemaSQL,
744
            'TABLE',
745
            $tableSQL,
746 747 748 749 750
            'COLUMN',
            $columnName
        );
    }

751 752 753 754 755
    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
756 757 758 759 760 761
        return [sprintf(
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
            $tableName,
            $oldIndexName,
            $index->getQuotedName($this)
        ),
762
        ];
763 764
    }

765 766 767
    /**
     * Returns the SQL statement for adding an extended property to a database object.
     *
768 769
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
     *
770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790
     * @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
    ) {
791 792 793 794 795
        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;
796 797 798 799 800
    }

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

    /**
     * Returns the SQL statement for updating an extended property of a database object.
     *
832 833
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
     *
834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854
     * @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
    ) {
855 856 857 858 859
        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;
860 861
    }

862
    /**
863
     * {@inheritDoc}
864
     */
865
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
866
    {
867
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
868 869
    }

870
    /**
871
     * {@inheritDoc}
872
     */
873
    public function getListTablesSQL()
874
    {
875
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
876 877
        // 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";
878 879 880
    }

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

    /**
914
     * {@inheritDoc}
915 916 917
     */
    public function getListTableForeignKeysSQL($table, $database = null)
    {
918
        return 'SELECT f.name AS ForeignKey,
919 920 921 922 923 924 925 926 927 928 929 930
                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
931
                WHERE ' .
932
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
933 934 935
    }

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

    /**
959
     * {@inheritDoc}
960
     */
961
    public function getCreateViewSQL($name, $sql)
962 963 964
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }
965 966

    /**
967
     * {@inheritDoc}
968
     */
969
    public function getListViewsSQL($database)
970 971 972 973
    {
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
    }

974 975 976 977
    /**
     * Returns the where clause to filter schema and table name in a query.
     *
     * @param string $table        The full qualified name of the table.
978 979
     * @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.
980 981 982 983 984
     *
     * @return string
     */
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
    {
985 986 987 988
        if (strpos($table, '.') !== false) {
            [$schema, $table] = explode('.', $table);
            $schema           = $this->quoteStringLiteral($schema);
            $table            = $this->quoteStringLiteral($table);
989
        } else {
990 991
            $schema = 'SCHEMA_NAME()';
            $table  = $this->quoteStringLiteral($table);
992 993
        }

994
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
995 996
    }

997
    /**
998
     * {@inheritDoc}
999 1000 1001
     */
    public function getDropViewSQL($name)
    {
1002
        return 'DROP VIEW ' . $name;
1003
    }
1004 1005

    /**
1006
     * {@inheritDoc}
1007 1008
     *
     * @deprecated Use application-generated UUIDs instead
1009 1010 1011
     */
    public function getGuidExpression()
    {
1012
        return 'NEWID()';
1013
    }
1014 1015

    /**
1016
     * {@inheritDoc}
1017
     */
1018
    public function getLocateExpression($str, $substr, $startPos = false)
1019
    {
1020
        if ($startPos === false) {
1021 1022
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
        }
1023 1024

        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1025
    }
1026

1027
    /**
1028
     * {@inheritDoc}
1029
     */
1030
    public function getModExpression($expression1, $expression2)
1031
    {
1032
        return $expression1 . ' % ' . $expression2;
1033
    }
1034

1035
    /**
1036
     * {@inheritDoc}
1037
     */
1038
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1039
    {
1040
        if (! $char) {
1041
            switch ($pos) {
1042
                case TrimMode::LEADING:
1043 1044 1045
                    $trimFn = 'LTRIM';
                    break;

1046
                case TrimMode::TRAILING:
1047 1048 1049 1050 1051
                    $trimFn = 'RTRIM';
                    break;

                default:
                    return 'LTRIM(RTRIM(' . $str . '))';
1052 1053 1054 1055
            }

            return $trimFn . '(' . $str . ')';
        }
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065

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

1068
        if ($pos === TrimMode::LEADING) {
1069 1070 1071
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
        }

1072
        if ($pos === TrimMode::TRAILING) {
1073 1074 1075 1076
            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))';
1077
    }
1078

1079
    /**
1080
     * {@inheritDoc}
1081 1082
     */
    public function getConcatExpression()
1083
    {
1084
        $args = func_get_args();
1085

1086
        return '(' . implode(' + ', $args) . ')';
1087
    }
1088

Benjamin Morel's avatar
Benjamin Morel committed
1089 1090 1091
    /**
     * {@inheritDoc}
     */
1092
    public function getListDatabasesSQL()
1093
    {
1094
        return 'SELECT * FROM sys.databases';
1095
    }
1096

1097 1098 1099 1100 1101
    /**
     * {@inheritDoc}
     */
    public function getListNamespacesSQL()
    {
1102
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1103 1104
    }

1105
    /**
1106
     * {@inheritDoc}
1107
     */
1108
    public function getSubstringExpression($value, $from, $length = null)
1109
    {
1110
        if ($length !== null) {
1111
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1112
        }
1113

1114
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1115
    }
1116

1117
    /**
1118
     * {@inheritDoc}
1119
     */
1120
    public function getLengthExpression($column)
1121
    {
1122
        return 'LEN(' . $column . ')';
1123 1124
    }

1125
    /**
1126
     * {@inheritDoc}
1127
     */
1128
    public function getSetTransactionIsolationSQL($level)
romanb's avatar
romanb committed
1129
    {
1130
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
1131
    }
1132

1133
    /**
1134
     * {@inheritDoc}
1135
     */
1136
    public function getIntegerTypeDeclarationSQL(array $field)
1137
    {
1138
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1139 1140
    }

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

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

1157
    /**
1158
     * {@inheritDoc}
1159
     */
1160
    public function getGuidTypeDeclarationSQL(array $field)
1161 1162 1163 1164
    {
        return 'UNIQUEIDENTIFIER';
    }

1165 1166 1167
    /**
     * {@inheritDoc}
     */
1168
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1169
    {
1170
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1171
    }
1172

Steve Müller's avatar
Steve Müller committed
1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188
    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
    }

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

1189 1190 1191
    /**
     * {@inheritDoc}
     */
1192
    public function getClobTypeDeclarationSQL(array $field)
1193
    {
1194
        return 'VARCHAR(MAX)';
1195
    }
1196

1197
    /**
1198
     * {@inheritDoc}
1199
     */
1200
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1201
    {
1202
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1203
    }
1204

1205
    /**
1206
     * {@inheritDoc}
1207
     */
1208
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1209
    {
1210
        return 'DATETIME';
1211 1212
    }

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

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

1229
    /**
1230
     * {@inheritDoc}
1231
     */
1232
    public function getBooleanTypeDeclarationSQL(array $field)
1233 1234 1235 1236
    {
        return 'BIT';
    }

1237
    /**
1238
     * {@inheritDoc}
1239
     */
1240
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1241
    {
1242 1243 1244 1245
        $where = [];

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

1248 1249 1250 1251 1252 1253 1254 1255 1256 1257
        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;
        }
1258

1259
        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
1260 1261 1262
        // 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.
1263
        $selectPattern  = '/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/im';
1264
        $replacePattern = sprintf('$1%s $2', $top);
1265
        $query          = preg_replace($selectPattern, $replacePattern, $query);
1266

1267
        if (stristr($query, 'ORDER BY')) {
1268
            // Inner order by is not valid in SQL Server for our purposes
1269
            // unless it's in a TOP N subquery.
1270
            $query = $this->scrubInnerOrderBy($query);
Fabio B. Silva's avatar
Fabio B. Silva committed
1271 1272
        }

1273 1274
        // Build a new limited query around the original, using a CTE
        return sprintf(
1275 1276 1277 1278
            'WITH dctrn_cte AS (%s) '
            . 'SELECT * FROM ('
            . 'SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte'
            . ') AS doctrine_tbl '
1279
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1280
            $query,
1281
            implode(' AND ', $where)
1282 1283
        );
    }
1284

1285 1286
    /**
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
1287
     * Caveat: will leave ORDER BY in TOP N subqueries.
1288
     *
1289
     * @param string $query
1290
     *
1291 1292
     * @return string
     */
Bill Schaller's avatar
Bill Schaller committed
1293 1294
    private function scrubInnerOrderBy($query)
    {
1295
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1296
        $offset = 0;
Bill Schaller's avatar
Bill Schaller committed
1297

1298
        while ($count-- > 0) {
1299
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1300 1301 1302 1303
            if ($orderByPos === false) {
                break;
            }

1304 1305
            $qLen            = strlen($query);
            $parenCount      = 0;
1306
            $currentPosition = $orderByPos;
1307

1308
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1309
                if ($query[$currentPosition] === '(') {
1310
                    $parenCount++;
1311
                } elseif ($query[$currentPosition] === ')') {
1312
                    $parenCount--;
Fabio B. Silva's avatar
Fabio B. Silva committed
1313
                }
1314

1315 1316
                $currentPosition++;
            }
1317 1318 1319 1320 1321 1322 1323 1324

            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;
            }

1325 1326
            if ($currentPosition >= $qLen - 1) {
                continue;
Fabio B. Silva's avatar
Fabio B. Silva committed
1327
            }
1328 1329 1330

            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
            $offset = $orderByPos;
1331
        }
1332
        return $query;
1333
    }
1334

1335 1336 1337
    /**
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
     *
1338 1339
     * @param string $query           The query
     * @param int    $currentPosition Start position of ORDER BY clause
1340
     *
1341 1342 1343 1344 1345 1346
     * @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 = '';
1347
        $parenCount     = 0;
1348

Bill Schaller's avatar
Bill Schaller committed
1349
        // If $parenCount goes negative, we've exited the subquery we're examining.
1350 1351
        // If $currentPosition goes negative, we've reached the beginning of the query.
        while ($parenCount >= 0 && $currentPosition >= 0) {
1352
            if ($query[$currentPosition] === '(') {
1353
                $parenCount--;
Bill Schaller's avatar
Bill Schaller committed
1354
            } elseif ($query[$currentPosition] === ')') {
1355 1356
                $parenCount++;
            }
1357 1358

            // Only yank query text on the same nesting level as the ORDER BY clause.
1359
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1360 1361 1362 1363

            $currentPosition--;
        }

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

1367
    /**
1368
     * {@inheritDoc}
1369 1370 1371 1372 1373 1374
     */
    public function supportsLimitOffset()
    {
        return false;
    }

1375
    /**
1376
     * {@inheritDoc}
1377
     */
1378
    public function convertBooleans($item)
1379
    {
1380 1381
        if (is_array($item)) {
            foreach ($item as $key => $value) {
1382 1383
                if (! is_bool($value) && ! is_numeric($item)) {
                    continue;
1384
                }
1385 1386

                $item[$key] = $value ? 1 : 0;
1387
            }
Steve Müller's avatar
Steve Müller committed
1388
        } elseif (is_bool($item) || is_numeric($item)) {
1389
            $item = $item ? 1 : 0;
1390
        }
1391

1392
        return $item;
1393
    }
1394 1395

    /**
1396
     * {@inheritDoc}
1397
     */
1398
    public function getCreateTemporaryTableSnippetSQL()
1399
    {
1400
        return 'CREATE TABLE';
1401
    }
1402

1403
    /**
1404
     * {@inheritDoc}
1405 1406 1407 1408 1409 1410
     */
    public function getTemporaryTableName($tableName)
    {
        return '#' . $tableName;
    }

1411
    /**
1412
     * {@inheritDoc}
1413 1414 1415
     */
    public function getDateTimeFormatString()
    {
1416 1417
        return 'Y-m-d H:i:s.000';
    }
1418

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

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

1435
    /**
1436
     * {@inheritDoc}
1437 1438 1439 1440 1441
     */
    public function getDateTimeTzFormatString()
    {
        return $this->getDateTimeFormatString();
    }
1442

1443
    /**
1444
     * {@inheritDoc}
1445
     */
1446
    public function getName()
1447
    {
1448
        return 'mssql';
1449
    }
1450

Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1451
    /**
1452
     * {@inheritDoc}
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1453
     */
1454 1455
    protected function initializeDoctrineTypeMappings()
    {
1456
        $this->doctrineTypeMapping = [
1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477
            '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
1478 1479
            'binary' => 'binary',
            'varbinary' => 'binary',
1480
            'image' => 'blob',
1481
            'uniqueidentifier' => 'guid',
1482
        ];
1483
    }
1484 1485

    /**
1486
     * {@inheritDoc}
1487 1488 1489 1490 1491 1492 1493
     */
    public function createSavePoint($savepoint)
    {
        return 'SAVE TRANSACTION ' . $savepoint;
    }

    /**
1494
     * {@inheritDoc}
1495 1496 1497 1498 1499 1500 1501
     */
    public function releaseSavePoint($savepoint)
    {
        return '';
    }

    /**
1502
     * {@inheritDoc}
1503 1504 1505 1506
     */
    public function rollbackSavePoint($savepoint)
    {
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1507
    }
1508

1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521
    /**
     * {@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);
    }

1522
    /**
1523
     * {@inheritDoc}
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1524
     */
1525
    public function appendLockHint($fromClause, $lockMode)
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1526
    {
1527
        switch (true) {
1528
            case $lockMode === LockMode::NONE:
1529 1530
                return $fromClause . ' WITH (NOLOCK)';

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

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

Steve Müller's avatar
Steve Müller committed
1537
            default:
1538
                return $fromClause;
Steve Müller's avatar
Steve Müller committed
1539
        }
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1540 1541 1542
    }

    /**
1543
     * {@inheritDoc}
Juozas Kaziukenas's avatar
Juozas Kaziukenas committed
1544 1545 1546 1547 1548
     */
    public function getForUpdateSQL()
    {
        return ' ';
    }
1549

1550 1551 1552
    /**
     * {@inheritDoc}
     */
1553 1554
    protected function getReservedKeywordsClass()
    {
1555
        return Keywords\SQLServerKeywords::class;
1556
    }
1557 1558

    /**
1559
     * {@inheritDoc}
1560
     */
1561
    public function quoteSingleIdentifier($str)
1562
    {
1563
        return '[' . str_replace(']', '][', $str) . ']';
1564
    }
1565

1566 1567 1568
    /**
     * {@inheritDoc}
     */
1569 1570
    public function getTruncateTableSQL($tableName, $cascade = false)
    {
1571 1572 1573
        $tableIdentifier = new Identifier($tableName);

        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1574
    }
1575 1576

    /**
1577
     * {@inheritDoc}
1578 1579 1580 1581 1582
     */
    public function getBlobTypeDeclarationSQL(array $field)
    {
        return 'VARBINARY(MAX)';
    }
1583 1584 1585 1586

    /**
     * {@inheritDoc}
     */
1587 1588
    public function getDefaultValueDeclarationSQL($field)
    {
1589
        if (! isset($field['default'])) {
1590 1591 1592
            return empty($field['notnull']) ? ' NULL' : '';
        }

1593
        if (! isset($field['type'])) {
1594
            return " DEFAULT '" . $field['default'] . "'";
1595
        }
1596

1597 1598 1599
        $type = $field['type'];

        if ($type instanceof Types\PhpIntegerMappingType) {
1600
            return ' DEFAULT ' . $field['default'];
1601 1602
        }

1603
        if ($type instanceof Types\PhpDateTimeMappingType && $field['default'] === $this->getCurrentTimestampSQL()) {
1604
            return ' DEFAULT ' . $this->getCurrentTimestampSQL();
1605 1606
        }

1607
        if ($type instanceof Types\BooleanType) {
1608 1609 1610 1611
            return " DEFAULT '" . $this->convertBooleans($field['default']) . "'";
        }

        return " DEFAULT '" . $field['default'] . "'";
1612
    }
1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623

    /**
     * {@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 {
1624
            $collation = isset($field['collation']) && $field['collation'] ?
1625
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1626

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

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

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

1635
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1636
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1637 1638 1639 1640
        }

        return $name . ' ' . $columnDef;
    }
1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663

    /**
     * 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)
    {
1664 1665 1666 1667
        // Always generate name for unquoted identifiers to ensure consistency.
        $identifier = new Identifier($identifier);

        return strtoupper(dechex(crc32($identifier->getName())));
1668
    }
1669
}