Completed
Pull Request — master (#3547)
by Sergei
15:44
created

SQLServerPlatform::getDateDiffExpression()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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