Failed Conditions
Pull Request — master (#3429)
by Gabriel
12:54
created

SQLServerPlatform::getLocateExpression()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
ccs 0
cts 5
cp 0
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 3
crap 6
1
<?php
2
3
namespace Doctrine\DBAL\Platforms;
4
5
use Doctrine\DBAL\LockMode;
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\ColumnDiff;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Doctrine\DBAL\Schema\Identifier;
10
use Doctrine\DBAL\Schema\Index;
11
use Doctrine\DBAL\Schema\Table;
12
use Doctrine\DBAL\Schema\TableDiff;
13
use Doctrine\DBAL\Types;
14
use InvalidArgumentException;
15
use function array_merge;
16
use function array_unique;
17
use function array_values;
18
use function count;
19
use function crc32;
20
use function dechex;
21
use function explode;
22
use function func_get_args;
23
use function implode;
24
use function is_array;
25
use function is_bool;
26
use function is_numeric;
27
use function is_string;
28
use function preg_match;
29
use function sprintf;
30
use function str_replace;
31
use function stripos;
32
use function stristr;
33
use function strlen;
34
use function strpos;
35
use function strtoupper;
36
use function substr;
37
use function substr_count;
38
39
/**
40
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
41
 * Microsoft SQL Server database platform.
42
 */
43
class SQLServerPlatform extends AbstractPlatform
44
{
45
    /**
46
     * {@inheritdoc}
47
     */
48 90
    public function getCurrentDateSQL()
49 78
    {
50 90
        return $this->getConvertExpression('date', 'GETDATE()');
51 78
    }
52
53
    /**
54
     * {@inheritdoc}
55
     */
56 45
    public function getCurrentTimeSQL()
57 39
    {
58 45
        return $this->getConvertExpression('time', 'GETDATE()');
59 39
    }
60
61
    /**
62
     * Returns an expression that converts an expression of one data type to another.
63
     *
64
     * @param string $dataType   The target native data type. Alias data types cannot be used.
65
     * @param string $expression The SQL expression to convert.
66
     *
67
     * @return string
68
     */
69 90
    private function getConvertExpression($dataType, $expression)
70 78
    {
71 90
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
72 78
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
78
    {
79
        $factorClause = '';
80
81
        if ($operator === '-') {
82
            $factorClause = '-1 * ';
83
        }
84
85
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
86
    }
87
88
    /**
89
     * {@inheritDoc}
90
     */
91
    public function getDateDiffExpression($date1, $date2)
92
    {
93
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
94
    }
95
96
    /**
97
     * {@inheritDoc}
98
     *
99
     * Microsoft SQL Server prefers "autoincrement" identity columns
100
     * since sequences can only be emulated with a table.
101
     */
102 45
    public function prefersIdentityColumns()
103 39
    {
104 45
        return true;
105 39
    }
106
107
    /**
108
     * {@inheritDoc}
109
     *
110
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
111
     */
112 45
    public function supportsIdentityColumns()
113 39
    {
114 45
        return true;
115 39
    }
116
117
    /**
118
     * {@inheritDoc}
119
     */
120
    public function supportsReleaseSavepoints()
121
    {
122
        return false;
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 45
    public function supportsSchemas()
129 39
    {
130 45
        return true;
131 39
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function getDefaultSchemaName()
137
    {
138
        return 'dbo';
139
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144 135
    public function supportsColumnCollation()
145 117
    {
146 135
        return true;
147 117
    }
148
149
    /**
150
     * {@inheritDoc}
151
     */
152 2625
    public function hasNativeGuidType()
153 2275
    {
154 2625
        return true;
155 2275
    }
156
157
    /**
158
     * {@inheritDoc}
159
     */
160 45
    public function getCreateDatabaseSQL($name)
161 39
    {
162 45
        return 'CREATE DATABASE ' . $name;
163 39
    }
164
165
    /**
166
     * {@inheritDoc}
167
     */
168 45
    public function getDropDatabaseSQL($name)
169 39
    {
170 45
        return 'DROP DATABASE ' . $name;
171 39
    }
172
173
    /**
174
     * {@inheritDoc}
175
     */
176 45
    public function supportsCreateDropDatabase()
177 39
    {
178 45
        return true;
179 39
    }
180
181
    /**
182
     * {@inheritDoc}
183
     */
184 45
    public function getCreateSchemaSQL($schemaName)
185 39
    {
186 45
        return 'CREATE SCHEMA ' . $schemaName;
187 39
    }
188
189
    /**
190
     * {@inheritDoc}
191
     */
192 90
    public function getDropForeignKeySQL($foreignKey, $table)
193 78
    {
194 90
        if (! $foreignKey instanceof ForeignKeyConstraint) {
195 123
            $foreignKey = new Identifier($foreignKey);
196 39
        }
197
198 90
        if (! $table instanceof Table) {
199 168
            $table = new Identifier($table);
200 78
        }
201
202 90
        $foreignKey = $foreignKey->getQuotedName($this);
203 168
        $table      = $table->getQuotedName($this);
204 78
205 90
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
206 78
    }
207
208
    /**
209
     * {@inheritDoc}
210
     */
211
    public function getDropIndexSQL($index, $table = null)
212
    {
213
        if ($index instanceof Index) {
214
            $index = $index->getQuotedName($this);
215
        } elseif (! is_string($index)) {
216
            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
        if ($table instanceof Table) {
224
            $table = $table->getQuotedName($this);
225
        }
226
227
        return sprintf(
228
            <<<SQL
229
IF EXISTS (SELECT * FROM sysobjects WHERE name = '%s')
230
    ALTER TABLE %s DROP CONSTRAINT %s
231
ELSE
232
    DROP INDEX %s ON %s
233
SQL
234
            ,
235
            $index,
236
            $table,
237
            $index,
238
            $index,
239
            $table
240
        );
241
    }
242
243
    /**
244
     * {@inheritDoc}
245
     */
246 780
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
247 676
    {
248 780
        $defaultConstraintsSql = [];
249 1456
        $commentsSql           = [];
250 676
251
        // @todo does other code breaks because of this?
252
        // force primary keys to be not null
253 780
        foreach ($columns as &$column) {
254 1456
            if (isset($column['primary']) && $column['primary']) {
255 946
                $column['notnull'] = true;
256 234
            }
257
258
            // Build default constraints SQL statements.
259 780
            if (isset($column['default'])) {
260 856
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
261 336
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
262 156
            }
263
264 780
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
265 1366
                continue;
266 598
            }
267
268 180
            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
269 156
        }
270
271 780
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
272 676
273 780
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
274 676
            foreach ($options['uniqueConstraints'] as $name => $definition) {
275
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
276
            }
277
        }
278
279 780
        if (isset($options['primary']) && ! empty($options['primary'])) {
280 991
            $flags = '';
281 588
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
282 318
                $flags = ' NONCLUSTERED';
283 39
            }
284 315
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
285 273
        }
286
287 780
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
288 676
289 780
        $check = $this->getCheckDeclarationSQL($columns);
290 1456
        if (! empty($check)) {
291 676
            $query .= ', ' . $check;
292
        }
293 780
        $query .= ')';
294 676
295 780
        $sql = [$query];
296 676
297 780
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
298 811
            foreach ($options['indexes'] as $index) {
299 252
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
300 117
            }
301
        }
302
303 780
        if (isset($options['foreignKeys'])) {
304 721
            foreach ((array) $options['foreignKeys'] as $definition) {
305 84
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
306 39
            }
307
        }
308
309 780
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
310 676
    }
311
312
    /**
313
     * {@inheritDoc}
314
     */
315 90
    public function getCreatePrimaryKeySQL(Index $index, $table)
316 78
    {
317 90
        if ($table instanceof Table) {
318 78
            $identifier = $table->getQuotedName($this);
319 78
        } else {
320 129
            $identifier = $table;
321
        }
322
323 168
        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';
324
325 90
        if ($index->hasFlag('nonclustered')) {
326 45
            $sql .= ' NONCLUSTERED';
327
        }
328
329 90
        return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
330
    }
331
332
    /**
333
     * Returns the SQL statement for creating a column comment.
334
     *
335
     * SQL Server does not support native column comments,
336
     * therefore the extended properties functionality is used
337
     * as a workaround to store them.
338
     * The property name used to store column comments is "MS_Description"
339
     * which provides compatibility with SQL Server Management Studio,
340
     * as column comments are stored in the same property there when
341
     * specifying a column's "Description" attribute.
342
     *
343 273
     * @param string      $tableName  The quoted table name to which the column belongs.
344
     * @param string      $columnName The quoted column name to create the comment for.
345 273
     * @param string|null $comment    The column's comment.
346 78
     *
347 78
     * @return string
348 78
     */
349 315
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
350 195
    {
351 510
        if (strpos($tableName, '.') !== false) {
352 90
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
353 90
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
354 363
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
355 273
        } else {
356 498
            $schemaSQL = "'dbo'";
357 498
            $tableSQL  = $this->quoteStringLiteral($tableName);
358 273
        }
359 273
360 588
        return $this->getAddExtendedPropertySQL(
361 588
            'MS_Description',
362 588
            $comment,
363 315
            'SCHEMA',
364 315
            $schemaSQL,
365 315
            'TABLE',
366 315
            $tableSQL,
367 315
            'COLUMN',
368 315
            $columnName
369
        );
370
    }
371
372
    /**
373
     * Returns the SQL snippet for declaring a default constraint.
374
     *
375
     * @param string  $table  Name of the table to return the default constraint declaration for.
376 585
     * @param mixed[] $column Column definition.
377
     *
378 585
     * @return string
379
     *
380
     * @throws InvalidArgumentException
381
     */
382 1260
    public function getDefaultConstraintDeclarationSQL($table, array $column)
383
    {
384 675
        if (! isset($column['default'])) {
385 585
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
386 585
        }
387 585
388 675
        $columnName = new Identifier($column['name']);
389
390
        return ' CONSTRAINT ' .
391 675
            $this->generateDefaultConstraintName($table, $column['name']) .
392 675
            $this->getDefaultValueDeclarationSQL($column) .
393 753
            ' FOR ' . $columnName->getQuotedName($this);
394
    }
395 78
396
    /**
397 78
     * {@inheritDoc}
398
     */
399 168
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
400
    {
401 90
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
402
403 90
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
404
405 402
        return $constraint;
406
    }
407 312
408
    /**
409 312
     * {@inheritDoc}
410 78
     */
411 360
    public function getCreateIndexSQL(Index $index, $table)
412
    {
413 672
        $constraint = parent::getCreateIndexSQL($index, $table);
414
415 360
        if ($index->isUnique() && ! $index->isPrimary()) {
416 90
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
417
        }
418
419 672
        return $constraint;
420
    }
421 312
422 312
    /**
423 78
     * {@inheritDoc}
424
     */
425 360
    protected function getCreateIndexSQLFlags(Index $index)
426 312
    {
427 399
        $type = '';
428 633
        if ($index->isUnique()) {
429 90
            $type .= 'UNIQUE ';
430
        }
431
432 672
        if ($index->hasFlag('clustered')) {
433 45
            $type .= 'CLUSTERED ';
434 315
        } elseif ($index->hasFlag('nonclustered')) {
435
            $type .= 'NONCLUSTERED ';
436
        }
437
438 360
        return $type;
439
    }
440
441
    /**
442 156
     * Extend unique key constraint with required filters
443
     *
444 156
     * @param string $sql
445
     *
446 156
     * @return string
447 156
     */
448 180
    private function _appendUniqueConstraintDefinition($sql, Index $index)
449
    {
450 336
        $fields = [];
451
452 180
        foreach ($index->getQuotedColumns($this) as $field) {
453 180
            $fields[] = $field . ' IS NOT NULL';
454
        }
455
456 1038
        return $sql . ' WHERE ' . implode(' AND ', $fields);
457
    }
458 858
459 858
    /**
460 858
     * {@inheritDoc}
461 858
     */
462 990
    public function getAlterTableSQL(TableDiff $diff)
463 858
    {
464 1380
        $queryParts  = [];
465 990
        $sql         = [];
466 990
        $columnSql   = [];
467 990
        $commentsSql = [];
468 390
469 1380
        foreach ($diff->addedColumns as $column) {
470 450
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
471 390
                continue;
472 156
            }
473
474 450
            $columnDef    = $column->toArray();
475 840
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
476
477 840
            if (isset($columnDef['default'])) {
478 492
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
479
            }
480
481 567
            $comment = $this->getColumnComment($column);
482 117
483 567
            if (empty($comment) && ! is_numeric($comment)) {
484 477
                continue;
485
            }
486
487 135
            $commentsSql[] = $this->getCreateColumnCommentSQL(
488 993
                $diff->name,
489 447
                $column->getQuotedName($this),
490 135
                $comment
491
            );
492
        }
493 312
494 990
        foreach ($diff->removedColumns as $column) {
495 360
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
496 858
                continue;
497 546
            }
498
499 360
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
500
        }
501 546
502 1536
        foreach ($diff->changedColumns as $columnDiff) {
503 1176
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
504
                continue;
505 546
            }
506 351
507 981
            $column     = $columnDiff->column;
508 630
            $comment    = $this->getColumnComment($column);
509 981
            $hasComment = ! empty($comment) || is_numeric($comment);
510 78
511 708
            if ($columnDiff->fromColumn instanceof Column) {
512 483
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
513 483
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
514
515 717
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
516 168
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
517 363
                        $diff->name,
518 129
                        $column->getQuotedName($this),
519 129
                        $comment
520 39
                    );
521 399
                } elseif ($hasFromComment && ! $hasComment) {
522 90
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
523 315
                } elseif ($hasComment) {
524 45
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
525 45
                        $diff->name,
526 45
                        $column->getQuotedName($this),
527 591
                        $comment
528 156
                    );
529
                }
530
            }
531 429
532
            // Do not add query part if only comment has changed.
533 1059
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
534 375
                continue;
535 195
            }
536 195
537 495
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
538
539 495
            if ($requireDropDefaultConstraint) {
540 654
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
541 225
                    $diff->name,
542 654
                    $columnDiff->oldColumnName
543 429
                );
544
            }
545 429
546 690
            $columnDef = $column->toArray();
547
548 495
            $queryParts[] = 'ALTER COLUMN ' .
549 729
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
550
551 495
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
552 1083
                continue;
553 195
            }
554
555 270
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
556
        }
557 195
558 990
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
559 420
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
560 195
                continue;
561 195
            }
562
563 225
            $oldColumnName = new Identifier($oldColumnName);
564 195
565 381
            $sql[] = "sp_RENAME '" .
566 225
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
567 225
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
568 39
569 39
            // Recreate default constraint with new column name if necessary (for future reference).
570 264
            if ($column->getDefault() === null) {
571 180
                continue;
572 39
            }
573
574 45
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
575 903
                $diff->name,
576 45
                $oldColumnName->getQuotedName($this)
577 858
            );
578 45
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
579
        }
580
581 1848
        $tableSql = [];
582 546
583 990
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
584
            return array_merge($tableSql, $columnSql);
585 858
        }
586
587 1848
        foreach ($queryParts as $query) {
588 708
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
589
        }
590
591 990
        $sql = array_merge($sql, $commentsSql);
592
593 990
        $newName = $diff->getNewName();
594
595 990
        if ($newName !== false) {
596 90
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";
597
598 78
            /**
599
             * Rename table's default constraints names
600 78
             * to match the new table name.
601 78
             * This is necessary to ensure that the default
602 78
             * constraints can be referenced in future table
603 78
             * alterations as the table name is encoded in
604 78
             * default constraints' names.
605 78
             */
606 90
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
607
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
608 948
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
609 948
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
610 948
                'FROM sys.default_constraints dc ' .
611 948
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
612 90
                "WHERE tbl.name = '" . $newName->getName() . "';" .
613 90
                'EXEC sp_executesql @sql';
614 858
        }
615
616 990
        $sql = array_merge(
617 990
            $this->getPreAlterTableIndexForeignKeySQL($diff),
618 990
            $sql,
619 990
            $this->getPostAlterTableIndexForeignKeySQL($diff)
620
        );
621
622 990
        return array_merge($sql, $tableSql, $columnSql);
623
    }
624
625 273
    /**
626
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
627 273
     *
628 273
     * @param string $tableName The name of the table to generate the clause for.
629
     * @param Column $column    The column to generate the clause for.
630 273
     *
631
     * @return string
632
     */
633 315
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
634
    {
635 315
        $columnDef         = $column->toArray();
636 315
        $columnDef['name'] = $column->getQuotedName($this);
637
638 315
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
639
    }
640
641 234
    /**
642
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
643 234
     *
644
     * @param string $tableName  The name of the table to generate the clause for.
645
     * @param string $columnName The name of the column to generate the clause for.
646
     *
647
     * @return string
648
     */
649 270
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
650
    {
651 270
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
652
    }
653
654
    /**
655
     * Checks whether a column alteration requires dropping its default constraint first.
656
     *
657
     * Different to other database vendors SQL Server implements column default values
658 429
     * as constraints and therefore changes in a column's default value as well as changes
659
     * in a column's type require dropping the default constraint first before being to
660
     * alter the particular column to the new definition.
661
     *
662 429
     * @param ColumnDiff $columnDiff The column diff to evaluate.
663 156
     *
664
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
665
     */
666 495
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
667
    {
668 273
        // We can only decide whether to drop an existing default constraint
669 78
        // if we know the original default value.
670 495
        if (! $columnDiff->fromColumn instanceof Column) {
671 180
            return false;
672
        }
673
674 195
        // We only need to drop an existing default constraint if we know the
675 156
        // column was defined with a default value before.
676 315
        if ($columnDiff->fromColumn->getDefault() === null) {
677 90
            return false;
678
        }
679
680 39
        // We need to drop an existing default constraint if the column was
681
        // defined with a default value before and it has changed.
682 225
        if ($columnDiff->hasChanged('default')) {
683 180
            return true;
684
        }
685
686
        // We need to drop an existing default constraint if the column was
687
        // defined with a default value before and the native column type has changed.
688 45
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
689
    }
690
691
    /**
692
     * Returns the SQL statement for altering a column comment.
693
     *
694
     * SQL Server does not support native column comments,
695
     * therefore the extended properties functionality is used
696
     * as a workaround to store them.
697
     * The property name used to store column comments is "MS_Description"
698
     * which provides compatibility with SQL Server Management Studio,
699
     * as column comments are stored in the same property there when
700 78
     * specifying a column's "Description" attribute.
701
     *
702 78
     * @param string      $tableName  The quoted table name to which the column belongs.
703 39
     * @param string      $columnName The quoted column name to alter the comment for.
704 39
     * @param string|null $comment    The column's comment.
705 39
     *
706
     * @return string
707 39
     */
708 129
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
709
    {
710 90
        if (strpos($tableName, '.') !== false) {
711 123
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
712 123
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
713 123
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
714 78
        } else {
715 123
            $schemaSQL = "'dbo'";
716 123
            $tableSQL  = $this->quoteStringLiteral($tableName);
717 78
        }
718 78
719 168
        return $this->getUpdateExtendedPropertySQL(
720 90
            'MS_Description',
721 90
            $comment,
722 90
            'SCHEMA',
723 90
            $schemaSQL,
724 90
            'TABLE',
725 90
            $tableSQL,
726 90
            'COLUMN',
727 90
            $columnName
728
        );
729
    }
730
731
    /**
732
     * Returns the SQL statement for dropping a column comment.
733
     *
734
     * SQL Server does not support native column comments,
735
     * therefore the extended properties functionality is used
736
     * as a workaround to store them.
737
     * The property name used to store column comments is "MS_Description"
738
     * which provides compatibility with SQL Server Management Studio,
739 78
     * as column comments are stored in the same property there when
740
     * specifying a column's "Description" attribute.
741 78
     *
742 39
     * @param string $tableName  The quoted table name to which the column belongs.
743 39
     * @param string $columnName The quoted column name to drop the comment for.
744 39
     *
745
     * @return string
746 39
     */
747 129
    protected function getDropColumnCommentSQL($tableName, $columnName)
748
    {
749 90
        if (strpos($tableName, '.') !== false) {
750 123
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
751 123
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
752 123
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
753 78
        } else {
754 123
            $schemaSQL = "'dbo'";
755 123
            $tableSQL  = $this->quoteStringLiteral($tableName);
756 78
        }
757 78
758 90
        return $this->getDropExtendedPropertySQL(
759 90
            'MS_Description',
760 90
            'SCHEMA',
761 90
            $schemaSQL,
762 90
            'TABLE',
763 90
            $tableSQL,
764 285
            'COLUMN',
765 90
            $columnName
766 195
        );
767 195
    }
768 195
769 195
    /**
770 195
     * {@inheritdoc}
771
     */
772 225
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
773
    {
774 225
        return [sprintf(
775 225
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
776 225
            $tableName,
777 225
            $oldIndexName,
778 225
            $index->getQuotedName($this)
779
        ),
780
        ];
781
    }
782
783
    /**
784
     * Returns the SQL statement for adding an extended property to a database object.
785
     *
786
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
787
     *
788
     * @param string      $name       The name of the property to add.
789
     * @param string|null $value      The value of the property to add.
790
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
791 273
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
792
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
793
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
794
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
795
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
796
     *
797
     * @return string
798
     */
799 315
    public function getAddExtendedPropertySQL(
800
        $name,
801
        $value = null,
802 273
        $level0Type = null,
803 273
        $level0Name = null,
804 273
        $level1Type = null,
805 273
        $level1Name = null,
806
        $level2Type = null,
807
        $level2Name = null
808
    ) {
809
        return 'EXEC sp_addextendedproperty ' .
810 315
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
811 315
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
812 315
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
813 315
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
814
    }
815
816
    /**
817
     * Returns the SQL statement for dropping an extended property from a database object.
818
     *
819
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
820
     *
821
     * @param string      $name       The name of the property to drop.
822
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
823 78
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
824
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
825
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
826
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
827
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
828
     *
829
     * @return string
830
     */
831 90
    public function getDropExtendedPropertySQL(
832
        $name,
833 78
        $level0Type = null,
834 78
        $level0Name = null,
835 78
        $level1Type = null,
836 78
        $level1Name = null,
837
        $level2Type = null,
838
        $level2Name = null
839
    ) {
840
        return 'EXEC sp_dropextendedproperty ' .
841 90
            'N' . $this->quoteStringLiteral($name) . ', ' .
842 90
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
843 90
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
844 90
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
845
    }
846
847
    /**
848
     * Returns the SQL statement for updating an extended property of a database object.
849
     *
850
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
851
     *
852
     * @param string      $name       The name of the property to update.
853
     * @param string|null $value      The value of the property to update.
854
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
855 78
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
856
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
857
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
858
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
859
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
860
     *
861
     * @return string
862
     */
863 90
    public function getUpdateExtendedPropertySQL(
864
        $name,
865
        $value = null,
866 78
        $level0Type = null,
867 78
        $level0Name = null,
868 78
        $level1Type = null,
869 78
        $level1Name = null,
870
        $level2Type = null,
871
        $level2Name = null
872
    ) {
873
        return 'EXEC sp_updateextendedproperty ' .
874 90
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
875 90
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
876 90
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
877 90
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
878
    }
879
880
    /**
881
     * {@inheritDoc}
882
     */
883
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
884
    {
885
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
886
    }
887
888
    /**
889
     * {@inheritDoc}
890
     */
891
    public function getListTablesSQL()
892
    {
893 78
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
894
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
895
        return "SELECT name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
896
    }
897
898
    /**
899
     * {@inheritDoc}
900
     */
901 90
    public function getListTableColumnsSQL($table, $database = null)
902
    {
903
        return "SELECT    col.name,
904
                          type.name AS type,
905
                          col.max_length AS length,
906
                          ~col.is_nullable AS notnull,
907
                          def.definition AS [default],
908
                          col.scale,
909
                          col.precision,
910
                          col.is_identity AS autoincrement,
911
                          col.collation_name AS collation,
912
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
913
                FROM      sys.columns AS col
914
                JOIN      sys.types AS type
915
                ON        col.user_type_id = type.user_type_id
916
                JOIN      sys.objects AS obj
917
                ON        col.object_id = obj.object_id
918
                JOIN      sys.schemas AS scm
919
                ON        obj.schema_id = scm.schema_id
920 78
                LEFT JOIN sys.default_constraints def
921
                ON        col.default_object_id = def.object_id
922
                AND       col.object_id = def.parent_object_id
923
                LEFT JOIN sys.extended_properties AS prop
924
                ON        obj.object_id = prop.major_id
925
                AND       col.column_id = prop.minor_id
926 78
                AND       prop.name = 'MS_Description'
927
                WHERE     obj.type = 'U'
928 90
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
929
    }
930
931
    /**
932
     * {@inheritDoc}
933
     */
934 90
    public function getListTableForeignKeysSQL($table, $database = null)
0 ignored issues
show
Unused Code introduced by
The parameter $database is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

934
    public function getListTableForeignKeysSQL($table, /** @scrutinizer ignore-unused */ $database = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
935
    {
936
        return 'SELECT f.name AS ForeignKey,
937
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
938
                OBJECT_NAME (f.parent_object_id) AS TableName,
939
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
940
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
941
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
942 78
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
943
                f.delete_referential_action_desc,
944
                f.update_referential_action_desc
945
                FROM sys.foreign_keys AS f
946
                INNER JOIN sys.foreign_key_columns AS fc
947
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
948 78
                ON f.OBJECT_ID = fc.constraint_object_id
949
                WHERE ' .
950 90
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
951
    }
952
953
    /**
954
     * {@inheritDoc}
955
     */
956 90
    public function getListTableIndexesSQL($table, $currentDatabase = null)
957
    {
958
        return "SELECT idx.name AS key_name,
959
                       col.name AS column_name,
960
                       ~idx.is_unique AS non_unique,
961
                       idx.is_primary_key AS [primary],
962
                       CASE idx.type
963
                           WHEN '1' THEN 'clustered'
964 78
                           WHEN '2' THEN 'nonclustered'
965
                           ELSE NULL
966
                       END AS flags
967
                FROM sys.tables AS tbl
968
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
969
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
970
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
971
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
972 90
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
973
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
974
    }
975
976
    /**
977
     * {@inheritDoc}
978
     */
979
    public function getCreateViewSQL($name, $sql)
980
    {
981
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
982
    }
983
984
    /**
985
     * {@inheritDoc}
986
     */
987
    public function getListViewsSQL($database)
988
    {
989
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
990
    }
991
992
    /**
993 234
     * Returns the where clause to filter schema and table name in a query.
994
     *
995 234
     * @param string $table        The full qualified name of the table.
996 117
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
997 117
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
998 117
     *
999
     * @return string
1000 117
     */
1001 387
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
1002
    {
1003 270
        if (strpos($table, '.') !== false) {
1004 369
            [$schema, $table] = explode('.', $table);
1005 135
            $schema           = $this->quoteStringLiteral($schema);
1006 135
            $table            = $this->quoteStringLiteral($table);
1007
        } else {
1008 135
            $schema = 'SCHEMA_NAME()';
1009 135
            $table  = $this->quoteStringLiteral($table);
1010
        }
1011
1012 270
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1013
    }
1014
1015
    /**
1016
     * {@inheritDoc}
1017
     */
1018
    public function getDropViewSQL($name)
1019
    {
1020
        return 'DROP VIEW ' . $name;
1021
    }
1022
1023
    /**
1024
     * {@inheritDoc}
1025
     *
1026
     * @deprecated Use application-generated UUIDs instead
1027
     */
1028
    public function getGuidExpression()
1029
    {
1030
        return 'NEWID()';
1031
    }
1032
1033
    /**
1034
     * {@inheritDoc}
1035
     */
1036
    public function getLocateExpression($str, $substr, $startPos = false)
1037
    {
1038
        if ($startPos === false) {
1039
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1040
        }
1041
1042
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1043
    }
1044
1045
    /**
1046
     * {@inheritDoc}
1047
     */
1048
    public function getModExpression($expression1, $expression2)
1049
    {
1050
        return $expression1 . ' % ' . $expression2;
1051
    }
1052
1053
    /**
1054
     * {@inheritDoc}
1055
     */
1056
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1057
    {
1058
        if (! $char) {
1059
            switch ($pos) {
1060
                case TrimMode::LEADING:
1061
                    $trimFn = 'LTRIM';
1062
                    break;
1063
1064
                case TrimMode::TRAILING:
1065
                    $trimFn = 'RTRIM';
1066
                    break;
1067
1068
                default:
1069
                    return 'LTRIM(RTRIM(' . $str . '))';
1070
            }
1071
1072
            return $trimFn . '(' . $str . ')';
1073
        }
1074
1075
        /** Original query used to get those expressions
1076
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1077
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1078
          select @c as string
1079
          , @trim_char as trim_char
1080
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1081
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1082
          , 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;
1083
         */
1084
        $pattern = "'%[^' + " . $char . " + ']%'";
1085
1086
        if ($pos === TrimMode::LEADING) {
1087
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1088
        }
1089
1090
        if ($pos === TrimMode::TRAILING) {
1091
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1092 39
        }
1093
1094 39
        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))';
1095
    }
1096 39
1097
    /**
1098
     * {@inheritDoc}
1099
     */
1100 45
    public function getConcatExpression()
1101
    {
1102 84
        $args = func_get_args();
1103
1104 84
        return '(' . implode(' + ', $args) . ')';
1105
    }
1106
1107
    /**
1108
     * {@inheritDoc}
1109
     */
1110 45
    public function getListDatabasesSQL()
1111
    {
1112 45
        return 'SELECT * FROM sys.databases';
1113
    }
1114
1115
    /**
1116
     * {@inheritDoc}
1117
     */
1118
    public function getListNamespacesSQL()
1119
    {
1120
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1121
    }
1122
1123
    /**
1124
     * {@inheritDoc}
1125
     */
1126
    public function getSubstringExpression($value, $from, $length = null)
1127
    {
1128
        if ($length !== null) {
1129
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1130
        }
1131
1132
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1133
    }
1134
1135
    /**
1136
     * {@inheritDoc}
1137
     */
1138 39
    public function getLengthExpression($column)
1139
    {
1140 39
        return 'LEN(' . $column . ')';
1141
    }
1142
1143
    /**
1144
     * {@inheritDoc}
1145
     */
1146 604
    public function getSetTransactionIsolationSQL($level)
1147
    {
1148 604
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1149
    }
1150
1151
    /**
1152
     * {@inheritDoc}
1153
     */
1154 645
    public function getIntegerTypeDeclarationSQL(array $field)
1155
    {
1156 645
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1157
    }
1158
1159
    /**
1160
     * {@inheritDoc}
1161
     */
1162
    public function getBigIntTypeDeclarationSQL(array $field)
1163
    {
1164
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1165
    }
1166
1167
    /**
1168
     * {@inheritDoc}
1169
     */
1170 39
    public function getSmallIntTypeDeclarationSQL(array $field)
1171
    {
1172 39
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1173
    }
1174
1175
    /**
1176
     * {@inheritDoc}
1177
     */
1178 981
    public function getGuidTypeDeclarationSQL(array $field)
1179
    {
1180 981
        return 'UNIQUEIDENTIFIER';
1181
    }
1182
1183
    /**
1184
     * {@inheritDoc}
1185
     */
1186 1119
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1187
    {
1188 1119
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1189
    }
1190
1191
    /**
1192
     * {@inheritdoc}
1193
     */
1194 162
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1195
    {
1196 162
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1197
    }
1198
1199
    /**
1200
     * {@inheritdoc}
1201
     */
1202 200
    public function getBinaryMaxLength()
1203
    {
1204 200
        return 8000;
1205
    }
1206
1207
    /**
1208
     * {@inheritDoc}
1209
     */
1210 634
    public function getClobTypeDeclarationSQL(array $field)
1211
    {
1212 634
        return 'VARCHAR(MAX)';
1213
    }
1214
1215
    /**
1216
     * {@inheritDoc}
1217
     */
1218 645
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1219
    {
1220 645
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1221
    }
1222
1223
    /**
1224
     * {@inheritDoc}
1225
     */
1226
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1227
    {
1228
        return 'DATETIME';
1229
    }
1230
1231
    /**
1232
     * {@inheritDoc}
1233
     */
1234
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1235
    {
1236
        return 'DATETIME';
1237
    }
1238
1239
    /**
1240
     * {@inheritDoc}
1241
     */
1242 39
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1243
    {
1244 39
        return 'DATETIME';
1245
    }
1246
1247
    /**
1248
     * {@inheritDoc}
1249
     */
1250 591
    public function getBooleanTypeDeclarationSQL(array $field)
1251
    {
1252 591
        return 'BIT';
1253
    }
1254 546
1255 52
    /**
1256
     * {@inheritDoc}
1257
     */
1258 1176
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1259 520
    {
1260 1150
        $where = [];
1261
1262 656
        if ($offset > 0) {
1263 60
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1264
        }
1265 546
1266 656
        if ($limit !== null) {
1267 600
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1268 600
            $top     = sprintf('TOP %d', $offset + $limit);
1269
        } else {
1270 30
            $top = 'TOP 9223372036854775807';
1271
        }
1272
1273 1150
        if (empty($where)) {
1274 550
            return $query;
1275 520
        }
1276
1277 520
        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
1278
        // Even if the TOP n is very large, the use of a CTE will
1279
        // allow the SQL Server query planner to optimize it so it doesn't
1280 338
        // actually scan the entire range covered by the TOP clause.
1281 600
        if (! preg_match('/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/is', $query, $matches)) {
1282
            return $query;
1283
        }
1284 520
1285 600
        $query = $matches[1] . $top . ' ' . $matches[2];
1286
1287 600
        if (stristr($query, 'ORDER BY')) {
1288
            // Inner order by is not valid in SQL Server for our purposes
1289 520
            // unless it's in a TOP N subquery.
1290 910
            $query = $this->scrubInnerOrderBy($query);
1291 520
        }
1292
1293
        // Build a new limited query around the original, using a CTE
1294 600
        return sprintf(
1295
            'WITH dctrn_cte AS (%s) '
1296
            . 'SELECT * FROM ('
1297
            . 'SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte'
1298
            . ') AS doctrine_tbl '
1299 600
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1300 600
            $query,
1301 600
            implode(' AND ', $where)
1302
        );
1303 338
    }
1304
1305 338
    /**
1306 338
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
1307
     * Caveat: will leave ORDER BY in TOP N subqueries.
1308 338
     *
1309 338
     * @param string $query
1310 338
     *
1311 26
     * @return string
1312
     */
1313 390
    private function scrubInnerOrderBy($query)
1314 338
    {
1315 728
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1316 728
        $offset = 0;
1317
1318 728
        while ($count-- > 0) {
1319 728
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1320 416
            if ($orderByPos === false) {
1321 368
                break;
1322 182
            }
1323
1324 390
            $qLen            = strlen($query);
1325 728
            $parenCount      = 0;
1326 390
            $currentPosition = $orderByPos;
1327
1328 728
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1329 390
                if ($query[$currentPosition] === '(') {
1330 30
                    $parenCount++;
1331 689
                } elseif ($query[$currentPosition] === ')') {
1332 509
                    $parenCount--;
1333
                }
1334
1335 520
                $currentPosition++;
1336
            }
1337
1338 390
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1339 130
                // If the order by clause is in a TOP N subquery, do not remove
1340 130
                // it and continue iteration from the current position.
1341 345
                $offset = $currentPosition;
1342 683
                continue;
1343
            }
1344
1345 150
            if ($currentPosition >= $qLen - 1) {
1346
                continue;
1347
            }
1348
1349 150
            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1350 150
            $offset = $orderByPos;
1351
        }
1352 390
        return $query;
1353 338
    }
1354
1355
    /**
1356 338
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
1357 338
     *
1358
     * @param string $query           The query
1359
     * @param int    $currentPosition Start position of ORDER BY clause
1360
     *
1361 338
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
1362 338
     */
1363 611
    private function isOrderByInTopNSubquery($query, $currentPosition)
1364 338
    {
1365 182
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1366 390
        $subQueryBuffer = '';
1367 390
        $parenCount     = 0;
1368
1369 338
        // If $parenCount goes negative, we've exited the subquery we're examining.
1370
        // If $currentPosition goes negative, we've reached the beginning of the query.
1371 728
        while ($parenCount >= 0 && $currentPosition >= 0) {
1372 390
            if ($query[$currentPosition] === '(') {
1373 255
                $parenCount--;
1374 728
            } elseif ($query[$currentPosition] === ')') {
1375 210
                $parenCount++;
1376
            }
1377
1378
            // Only yank query text on the same nesting level as the ORDER BY clause.
1379 390
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1380
1381 390
            $currentPosition--;
1382
        }
1383
1384 390
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1385
    }
1386
1387
    /**
1388 39
     * {@inheritDoc}
1389
     */
1390 39
    public function supportsLimitOffset()
1391
    {
1392
        return false;
1393
    }
1394
1395
    /**
1396
     * {@inheritDoc}
1397
     */
1398 84
    public function convertBooleans($item)
1399 39
    {
1400 45
        if (is_array($item)) {
1401
            foreach ($item as $key => $value) {
1402 39
                if (! is_bool($value) && ! is_numeric($item)) {
1403
                    continue;
1404
                }
1405
1406
                $item[$key] = $value ? 1 : 0;
1407
            }
1408 45
        } elseif (is_bool($item) || is_numeric($item)) {
1409 45
            $item = $item ? 1 : 0;
1410
        }
1411
1412 45
        return $item;
1413
    }
1414
1415
    /**
1416
     * {@inheritDoc}
1417
     */
1418
    public function getCreateTemporaryTableSnippetSQL()
1419
    {
1420
        return 'CREATE TABLE';
1421
    }
1422
1423
    /**
1424
     * {@inheritDoc}
1425
     */
1426
    public function getTemporaryTableName($tableName)
1427
    {
1428
        return '#' . $tableName;
1429
    }
1430
1431
    /**
1432
     * {@inheritDoc}
1433
     */
1434
    public function getDateTimeFormatString()
1435
    {
1436
        return 'Y-m-d H:i:s.000';
1437
    }
1438
1439
    /**
1440
     * {@inheritDoc}
1441
     */
1442
    public function getDateFormatString()
1443
    {
1444
        return 'Y-m-d H:i:s.000';
1445
    }
1446
1447
    /**
1448
     * {@inheritDoc}
1449
     */
1450
    public function getTimeFormatString()
1451
    {
1452
        return 'Y-m-d H:i:s.000';
1453
    }
1454
1455
    /**
1456 12
     * {@inheritDoc}
1457
     */
1458 12
    public function getDateTimeTzFormatString()
1459
    {
1460
        return $this->getDateTimeFormatString();
1461
    }
1462
1463
    /**
1464 195
     * {@inheritDoc}
1465
     */
1466 195
    public function getName()
1467
    {
1468
        return 'mssql';
1469
    }
1470
1471
    /**
1472
     * {@inheritDoc}
1473
     */
1474 225
    protected function initializeDoctrineTypeMappings()
1475
    {
1476 225
        $this->doctrineTypeMapping = [
1477
            'bigint' => 'bigint',
1478
            'numeric' => 'decimal',
1479
            'bit' => 'boolean',
1480
            'smallint' => 'smallint',
1481
            'decimal' => 'decimal',
1482
            'smallmoney' => 'integer',
1483
            'int' => 'integer',
1484
            'tinyint' => 'smallint',
1485
            'money' => 'integer',
1486
            'float' => 'float',
1487
            'real' => 'float',
1488
            'double' => 'float',
1489
            'double precision' => 'float',
1490
            'smalldatetime' => 'datetime',
1491
            'datetime' => 'datetime',
1492
            'char' => 'string',
1493 195
            'varchar' => 'string',
1494
            'text' => 'text',
1495
            'nchar' => 'string',
1496
            'nvarchar' => 'string',
1497
            'ntext' => 'text',
1498
            'binary' => 'binary',
1499
            'varbinary' => 'binary',
1500
            'image' => 'blob',
1501
            'uniqueidentifier' => 'guid',
1502
        ];
1503 225
    }
1504
1505
    /**
1506
     * {@inheritDoc}
1507
     */
1508
    public function createSavePoint($savepoint)
1509
    {
1510
        return 'SAVE TRANSACTION ' . $savepoint;
1511
    }
1512
1513
    /**
1514
     * {@inheritDoc}
1515
     */
1516
    public function releaseSavePoint($savepoint)
1517
    {
1518
        return '';
1519
    }
1520
1521
    /**
1522 273
     * {@inheritDoc}
1523
     */
1524
    public function rollbackSavePoint($savepoint)
1525 273
    {
1526 39
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1527
    }
1528
1529 234
    /**
1530
     * {@inheritdoc}
1531
     */
1532 315
    public function getForeignKeyReferentialActionSQL($action)
1533
    {
1534
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1535 406
        if (strtoupper($action) === 'RESTRICT') {
1536 45
            return 'NO ACTION';
1537 91
        }
1538
1539 283
        return parent::getForeignKeyReferentialActionSQL($action);
1540
    }
1541 78
1542 13
    /**
1543
     * {@inheritDoc}
1544 65
     */
1545 118
    public function appendLockHint($fromClause, $lockMode)
1546
    {
1547 105
        switch (true) {
1548 52
            case $lockMode === LockMode::NONE:
1549 15
                return $fromClause . ' WITH (NOLOCK)';
1550
1551 90
            case $lockMode === LockMode::PESSIMISTIC_READ:
1552 15
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1553
1554 75
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1555 15
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1556
1557
            default:
1558 60
                return $fromClause;
1559
        }
1560
    }
1561
1562
    /**
1563 806
     * {@inheritDoc}
1564
     */
1565 806
    public function getForUpdateSQL()
1566
    {
1567
        return ' ';
1568
    }
1569
1570
    /**
1571 1183
     * {@inheritDoc}
1572
     */
1573 2113
    protected function getReservedKeywordsClass()
1574
    {
1575 930
        return Keywords\SQLServerKeywords::class;
1576
    }
1577
1578
    /**
1579 39
     * {@inheritDoc}
1580
     */
1581 1404
    public function quoteSingleIdentifier($str)
1582
    {
1583 1404
        return '[' . str_replace(']', '][', $str) . ']';
1584
    }
1585
1586
    /**
1587
     * {@inheritDoc}
1588
     */
1589 84
    public function getTruncateTableSQL($tableName, $cascade = false)
1590
    {
1591 84
        $tableIdentifier = new Identifier($tableName);
1592
1593 45
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1594
    }
1595
1596
    /**
1597 741
     * {@inheritDoc}
1598
     */
1599 786
    public function getBlobTypeDeclarationSQL(array $field)
1600
    {
1601 45
        return 'VARBINARY(MAX)';
1602
    }
1603 741
1604 156
    /**
1605
     * {@inheritDoc}
1606
     */
1607 1440
    public function getDefaultValueDeclarationSQL($field)
1608
    {
1609 1440
        if (! isset($field['default'])) {
1610 117
            return empty($field['notnull']) ? ' NULL' : '';
1611
        }
1612
1613 1362
        if (! isset($field['type'])) {
1614 219
            return " DEFAULT '" . $field['default'] . "'";
1615
        }
1616
1617 1143
        $type = $field['type'];
1618 39
1619 675
        if ($type instanceof Types\PhpIntegerMappingType) {
1620 135
            return ' DEFAULT ' . $field['default'];
1621 468
        }
1622
1623 585
        if ($type instanceof Types\PhpDateTimeMappingType && $field['default'] === $this->getCurrentTimestampSQL()) {
1624 45
            return ' DEFAULT ' . $this->getCurrentTimestampSQL();
1625
        }
1626
1627 540
        if ($type instanceof Types\BooleanType) {
1628 45
            return " DEFAULT '" . $this->convertBooleans($field['default']) . "'";
1629 1222
        }
1630
1631 1762
        return " DEFAULT '" . $field['default'] . "'";
1632 39
    }
1633
1634 1183
    /**
1635 1183
     * {@inheritdoc}
1636
     *
1637 1183
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1638
     */
1639 2593
    public function getColumnDeclarationSQL($name, array $field)
1640 1183
    {
1641 1410
        if (isset($field['columnDefinition'])) {
1642 1228
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1643 1183
        } else {
1644 1365
            $collation = isset($field['collation']) && $field['collation'] ?
1645 2548
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1646 1183
1647 1365
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1648
1649 2587
            $unique = isset($field['unique']) && $field['unique'] ?
1650 1365
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1651
1652 1365
            $check = isset($field['check']) && $field['check'] ?
1653 1365
                ' ' . $field['check'] : '';
1654
1655 1365
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1656 1365
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1657
        }
1658
1659 1410
        return $name . ' ' . $columnDef;
1660 585
    }
1661
1662 585
    /**
1663
     * Returns a unique default constraint name for a table and column.
1664
     *
1665
     * @param string $table  Name of the table to generate the unique default constraint name for.
1666
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1667
     *
1668
     * @return string
1669
     */
1670 675
    private function generateDefaultConstraintName($table, $column)
1671
    {
1672 1299
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1673
    }
1674
1675 624
    /**
1676
     * Returns a hash value for a given identifier.
1677 624
     *
1678
     * @param string $identifier Identifier to generate a hash value for.
1679
     *
1680
     * @return string
1681
     */
1682 720
    private function generateIdentifierName($identifier)
1683
    {
1684
        // Always generate name for unquoted identifiers to ensure consistency.
1685 720
        $identifier = new Identifier($identifier);
1686
1687 720
        return strtoupper(dechex(crc32($identifier->getName())));
1688
    }
1689
}
1690