Failed Conditions
Pull Request — develop (#3581)
by Jonathan
12:44
created

SQLServerPlatform::doModifyLimitQuery()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 44
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6.0052

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 44
ccs 18
cts 19
cp 0.9474
rs 8.9297
c 0
b 0
f 0
cc 6
nc 16
nop 3
crap 6.0052
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Platforms;
6
7
use Doctrine\DBAL\LockMode;
8
use Doctrine\DBAL\Schema\Column;
9
use Doctrine\DBAL\Schema\ColumnDiff;
10
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
11
use Doctrine\DBAL\Schema\Identifier;
12
use Doctrine\DBAL\Schema\Index;
13
use Doctrine\DBAL\Schema\Table;
14
use Doctrine\DBAL\Schema\TableDiff;
15
use InvalidArgumentException;
16
use function array_merge;
17
use function array_unique;
18
use function array_values;
19
use function count;
20
use function crc32;
21
use function dechex;
22
use function explode;
23
use function implode;
24
use function in_array;
25
use function is_array;
26
use function is_bool;
27
use function is_numeric;
28
use function is_string;
29
use function preg_match;
30
use function sprintf;
31
use function str_replace;
32
use function stripos;
33
use function stristr;
34
use function strlen;
35
use function strpos;
36
use function strtoupper;
37
use function substr;
38
use function substr_count;
39
40
/**
41
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
42
 * Microsoft SQL Server database platform.
43
 */
44
class SQLServerPlatform extends AbstractPlatform
45
{
46
    /**
47
     * {@inheritdoc}
48
     */
49 5580
    public function getCurrentDateSQL() : string
50
    {
51 5580
        return $this->getConvertExpression('date', 'GETDATE()');
52
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57 5576
    public function getCurrentTimeSQL() : string
58
    {
59 5576
        return $this->getConvertExpression('time', 'GETDATE()');
60
    }
61
62
    /**
63
     * Returns an expression that converts an expression of one data type to another.
64
     *
65
     * @param string $dataType   The target native data type. Alias data types cannot be used.
66
     * @param string $expression The SQL expression to convert.
67
     */
68 5580
    private function getConvertExpression(string $dataType, string $expression) : string
69
    {
70 5580
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76 805
    protected function getDateArithmeticIntervalExpression(string $date, string $operator, string $interval, string $unit) : string
77
    {
78 805
        $factorClause = '';
79
80 805
        if ($operator === '-') {
81 801
            $factorClause = '-1 * ';
82
        }
83
84 805
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
85
    }
86
87
    /**
88
     * {@inheritDoc}
89
     */
90 704
    public function getDateDiffExpression(string $date1, string $date2) : string
91
    {
92 704
        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 5213
    public function prefersIdentityColumns() : bool
102
    {
103 5213
        return true;
104
    }
105
106
    /**
107
     * {@inheritDoc}
108
     *
109
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
110
     */
111 606
    public function supportsIdentityColumns() : bool
112
    {
113 606
        return true;
114
    }
115
116
    /**
117
     * {@inheritDoc}
118
     */
119 863
    public function supportsReleaseSavepoints() : bool
120
    {
121 863
        return false;
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 636
    public function supportsSchemas() : bool
128
    {
129 636
        return true;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135 580
    public function getDefaultSchemaName() : string
136
    {
137 580
        return 'dbo';
138
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143 4566
    public function supportsColumnCollation() : bool
144
    {
145 4566
        return true;
146
    }
147
148
    /**
149
     * {@inheritDoc}
150
     */
151 5981
    public function hasNativeGuidType() : bool
152
    {
153 5981
        return true;
154
    }
155
156
    /**
157
     * {@inheritDoc}
158
     */
159 5707
    public function getCreateDatabaseSQL(string $database) : string
160
    {
161 5707
        return 'CREATE DATABASE ' . $database;
162
    }
163
164
    /**
165
     * {@inheritDoc}
166
     */
167 5707
    public function getDropDatabaseSQL(string $database) : string
168
    {
169 5707
        return 'DROP DATABASE ' . $database;
170
    }
171
172
    /**
173
     * {@inheritDoc}
174
     */
175 877
    public function supportsCreateDropDatabase() : bool
176
    {
177 877
        return true;
178
    }
179
180
    /**
181
     * {@inheritDoc}
182
     */
183 5236
    public function getCreateSchemaSQL(string $schemaName) : string
184
    {
185 5236
        return 'CREATE SCHEMA ' . $schemaName;
186
    }
187
188
    /**
189
     * {@inheritDoc}
190
     */
191 3594
    public function getDropForeignKeySQL($foreignKey, $table) : string
192
    {
193 3594
        if (! $foreignKey instanceof ForeignKeyConstraint) {
194 3252
            $foreignKey = new Identifier($foreignKey);
195
        }
196
197 3594
        if (! $table instanceof Table) {
198 3594
            $table = new Identifier($table);
199
        }
200
201 3594
        $foreignKey = $foreignKey->getQuotedName($this);
202 3594
        $table      = $table->getQuotedName($this);
203
204 3594
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
205
    }
206
207
    /**
208
     * {@inheritDoc}
209
     */
210 618
    public function getDropIndexSQL($index, $table = null) : string
211
    {
212 618
        if ($index instanceof Index) {
213 610
            $index = $index->getQuotedName($this);
214 618
        } elseif (! is_string($index)) {
215
            throw new InvalidArgumentException(sprintf(
216
                'AbstractPlatform::getDropIndexSQL() expects $index parameter to be a string or an instanceof %s.',
217
                Index::class
218
            ));
219
        }
220
221 618
        if (! isset($table)) {
222
            return 'DROP INDEX ' . $index;
223
        }
224
225 618
        if ($table instanceof Table) {
226 618
            $table = $table->getQuotedName($this);
227
        }
228
229 618
        return sprintf(
230
            <<<SQL
231
IF EXISTS (SELECT * FROM sysobjects WHERE name = '%s')
232
    ALTER TABLE %s DROP CONSTRAINT %s
233
ELSE
234
    DROP INDEX %s ON %s
235
SQL
236
            ,
237 618
            $index,
238 618
            $table,
239 618
            $index,
240 618
            $index,
241 618
            $table
242
        );
243
    }
244
245
    /**
246
     * {@inheritDoc}
247
     */
248 5901
    protected function _getCreateTableSQL(string $tableName, array $columns, array $options = []) : array
249
    {
250 5901
        $defaultConstraintsSql = [];
251 5901
        $commentsSql           = [];
252
253
        // @todo does other code breaks because of this?
254
        // force primary keys to be not null
255 5901
        foreach ($columns as &$column) {
256 5901
            if (isset($column['primary']) && $column['primary']) {
257 5556
                $column['notnull'] = true;
258
            }
259
260
            // Build default constraints SQL statements.
261 5901
            if (isset($column['default'])) {
262 4986
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
263 4986
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
264
            }
265
266 5901
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
267 5897
                continue;
268
            }
269
270 5231
            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
271
        }
272
273 5901
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
274
275 5901
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
276
            foreach ($options['uniqueConstraints'] as $name => $definition) {
277
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
278
            }
279
        }
280
281 5901
        if (isset($options['primary']) && ! empty($options['primary'])) {
282 5558
            $flags = '';
283 5558
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
284 5077
                $flags = ' NONCLUSTERED';
285
            }
286 5558
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
287
        }
288
289 5901
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
290
291 5901
        $check = $this->getCheckDeclarationSQL($columns);
292 5901
        if (! empty($check)) {
293
            $query .= ', ' . $check;
294
        }
295 5901
        $query .= ')';
296
297 5901
        $sql = [$query];
298
299 5901
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
300 4241
            foreach ($options['indexes'] as $index) {
301 4241
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
302
            }
303
        }
304
305 5901
        if (isset($options['foreignKeys'])) {
306 4120
            foreach ((array) $options['foreignKeys'] as $definition) {
307 3861
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
308
            }
309
        }
310
311 5901
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
312
    }
313
314
    /**
315
     * {@inheritDoc}
316
     */
317 5054
    public function getCreatePrimaryKeySQL(Index $index, $table) : string
318
    {
319 5054
        if ($table instanceof Table) {
320
            $identifier = $table->getQuotedName($this);
321
        } else {
322 5054
            $identifier = $table;
323
        }
324
325 5054
        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';
326
327 5054
        if ($index->hasFlag('nonclustered')) {
328 5052
            $sql .= ' NONCLUSTERED';
329
        }
330
331 5054
        return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
332
    }
333
334
    /**
335
     * Returns the SQL statement for creating a column comment.
336
     *
337
     * SQL Server does not support native column comments,
338
     * therefore the extended properties functionality is used
339
     * as a workaround to store them.
340
     * The property name used to store column comments is "MS_Description"
341
     * which provides compatibility with SQL Server Management Studio,
342
     * as column comments are stored in the same property there when
343
     * specifying a column's "Description" attribute.
344
     *
345
     * @param string      $tableName  The quoted table name to which the column belongs.
346
     * @param string      $columnName The quoted column name to create the comment for.
347
     * @param string|null $comment    The column's comment.
348
     */
349 5237
    protected function getCreateColumnCommentSQL(string $tableName, string $columnName, ?string $comment) : string
350
    {
351 5237
        if (strpos($tableName, '.') !== false) {
352 4979
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
353 4979
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
354 4979
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
355
        } else {
356 5141
            $schemaSQL = "'dbo'";
357 5141
            $tableSQL  = $this->quoteStringLiteral($tableName);
358
        }
359
360 5237
        return $this->getAddExtendedPropertySQL(
361 5237
            'MS_Description',
362 14
            $comment,
363 5237
            'SCHEMA',
364 14
            $schemaSQL,
365 5237
            'TABLE',
366 14
            $tableSQL,
367 5237
            'COLUMN',
368 14
            $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
     * @param mixed[] $column Column definition.
377
     *
378
     * @throws InvalidArgumentException
379
     */
380 5123
    public function getDefaultConstraintDeclarationSQL(string $table, array $column) : string
381
    {
382 5123
        if (! isset($column['default'])) {
383
            throw new InvalidArgumentException('Incomplete column definition. "default" required.');
384
        }
385
386 5123
        $columnName = new Identifier($column['name']);
387
388
        return ' CONSTRAINT ' .
389 5123
            $this->generateDefaultConstraintName($table, $column['name']) .
390 5123
            $this->getDefaultValueDeclarationSQL($column) .
391 5123
            ' FOR ' . $columnName->getQuotedName($this);
392
    }
393
394
    /**
395
     * {@inheritDoc}
396
     */
397 5332
    public function getCreateIndexSQL(Index $index, $table) : string
398
    {
399 5332
        $constraint = parent::getCreateIndexSQL($index, $table);
400
401 5332
        if ($index->isUnique() && ! $index->isPrimary()) {
402 4239
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
403
        }
404
405 5332
        return $constraint;
406
    }
407
408
    /**
409
     * {@inheritDoc}
410
     */
411 5332
    protected function getCreateIndexSQLFlags(Index $index) : string
412
    {
413 5332
        $type = '';
414 5332
        if ($index->isUnique()) {
415 4239
            $type .= 'UNIQUE ';
416
        }
417
418 5332
        if ($index->hasFlag('clustered')) {
419 5102
            $type .= 'CLUSTERED ';
420 4249
        } elseif ($index->hasFlag('nonclustered')) {
421
            $type .= 'NONCLUSTERED ';
422
        }
423
424 5332
        return $type;
425
    }
426
427
    /**
428
     * Extend unique key constraint with required filters
429
     */
430 4239
    private function _appendUniqueConstraintDefinition(string $sql, Index $index) : string
431
    {
432 4239
        $fields = [];
433
434 4239
        foreach ($index->getQuotedColumns($this) as $field) {
435 4239
            $fields[] = $field . ' IS NOT NULL';
436
        }
437
438 4239
        return $sql . ' WHERE ' . implode(' AND ', $fields);
439
    }
440
441
    /**
442
     * {@inheritDoc}
443
     */
444 5250
    public function getAlterTableSQL(TableDiff $diff) : array
445
    {
446 5250
        $queryParts  = [];
447 5250
        $sql         = [];
448 5250
        $columnSql   = [];
449 5250
        $commentsSql = [];
450
451 5250
        foreach ($diff->addedColumns as $column) {
452 5220
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
453
                continue;
454
            }
455
456 5220
            $columnDef    = $column->toArray();
457 5220
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
458
459 5220
            if (isset($columnDef['default'])) {
460 4558
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
461
            }
462
463 5220
            $comment = $this->getColumnComment($column);
464
465 5220
            if (empty($comment) && ! is_numeric($comment)) {
466 5124
                continue;
467
            }
468
469 5206
            $commentsSql[] = $this->getCreateColumnCommentSQL(
470 5206
                $diff->name,
471 5206
                $column->getQuotedName($this),
472 6
                $comment
473
            );
474
        }
475
476 5250
        foreach ($diff->removedColumns as $column) {
477 4852
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
478
                continue;
479
            }
480
481 4852
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
482
        }
483
484 5250
        foreach ($diff->changedColumns as $columnDiff) {
485 5207
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
486
                continue;
487
            }
488
489 5207
            $column     = $columnDiff->column;
490 5207
            $comment    = $this->getColumnComment($column);
491 5207
            $hasComment = ! empty($comment) || is_numeric($comment);
492
493 5207
            if ($columnDiff->fromColumn instanceof Column) {
494 5197
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
495 5197
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
496
497 5197
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
498 5158
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
499 5158
                        $diff->name,
500 5158
                        $column->getQuotedName($this),
501 4
                        $comment
502
                    );
503 5195
                } elseif ($hasFromComment && ! $hasComment) {
504 5181
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
505 5124
                } elseif ($hasComment) {
506 5110
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
507 5110
                        $diff->name,
508 5110
                        $column->getQuotedName($this),
509 2
                        $comment
510
                    );
511
                }
512
            }
513
514
            // Do not add query part if only comment has changed.
515 5207
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
516 5185
                continue;
517
            }
518
519 5132
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
520
521 5132
            if ($requireDropDefaultConstraint) {
522 5051
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
523 5051
                    $diff->name,
524 5051
                    $columnDiff->oldColumnName
525
                );
526
            }
527
528 5132
            $columnDef = $column->toArray();
529
530 5132
            $queryParts[] = 'ALTER COLUMN ' .
531 5132
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
532
533 5132
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
534 5120
                continue;
535
            }
536
537 5053
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
538
        }
539
540 5250
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
541 4060
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
542
                continue;
543
            }
544
545 4060
            $oldColumnName = new Identifier($oldColumnName);
546
547 4060
            $sql[] = "sp_RENAME '" .
548 4060
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
549 4060
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
550
551
            // Recreate default constraint with new column name if necessary (for future reference).
552 4060
            if ($column->getDefault() === null) {
553 4010
                continue;
554
            }
555
556 3571
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
557 3571
                $diff->name,
558 3571
                $oldColumnName->getQuotedName($this)
559
            );
560 3571
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
561
        }
562
563 5250
        $tableSql = [];
564
565 5250
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
566
            return array_merge($tableSql, $columnSql);
567
        }
568
569 5250
        foreach ($queryParts as $query) {
570 5234
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
571
        }
572
573 5250
        $sql = array_merge($sql, $commentsSql);
574
575 5250
        $newName = $diff->getNewName();
576
577 5250
        if ($newName !== null) {
578 4125
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";
579
580
            /**
581
             * Rename table's default constraints names
582
             * to match the new table name.
583
             * This is necessary to ensure that the default
584
             * constraints can be referenced in future table
585
             * alterations as the table name is encoded in
586
             * default constraints' names.
587
             */
588 4125
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
589
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
590 4125
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
591 4125
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
592 4125
                'FROM sys.default_constraints dc ' .
593 4125
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
594 4125
                "WHERE tbl.name = '" . $newName->getName() . "';" .
595 4125
                'EXEC sp_executesql @sql';
596
        }
597
598 5250
        $sql = array_merge(
599 5250
            $this->getPreAlterTableIndexForeignKeySQL($diff),
600 5250
            $sql,
601 5250
            $this->getPostAlterTableIndexForeignKeySQL($diff)
602
        );
603
604 5250
        return array_merge($sql, $tableSql, $columnSql);
605
    }
606
607
    /**
608
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
609
     *
610
     * @param string $tableName The name of the table to generate the clause for.
611
     * @param Column $column    The column to generate the clause for.
612
     */
613 5055
    private function getAlterTableAddDefaultConstraintClause(string $tableName, Column $column) : string
614
    {
615 5055
        $columnDef         = $column->toArray();
616 5055
        $columnDef['name'] = $column->getQuotedName($this);
617
618 5055
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
619
    }
620
621
    /**
622
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
623
     *
624
     * @param string $tableName  The name of the table to generate the clause for.
625
     * @param string $columnName The name of the column to generate the clause for.
626
     */
627 5053
    private function getAlterTableDropDefaultConstraintClause(string $tableName, string $columnName) : string
628
    {
629 5053
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
630
    }
631
632
    /**
633
     * Checks whether a column alteration requires dropping its default constraint first.
634
     *
635
     * Different to other database vendors SQL Server implements column default values
636
     * as constraints and therefore changes in a column's default value as well as changes
637
     * in a column's type require dropping the default constraint first before being to
638
     * alter the particular column to the new definition.
639
     *
640
     * @param ColumnDiff $columnDiff The column diff to evaluate.
641
     *
642
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
643
     */
644 5132
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff) : bool
645
    {
646
        // We can only decide whether to drop an existing default constraint
647
        // if we know the original default value.
648 5132
        if (! $columnDiff->fromColumn instanceof Column) {
649 3783
            return false;
650
        }
651
652
        // We only need to drop an existing default constraint if we know the
653
        // column was defined with a default value before.
654 5124
        if ($columnDiff->fromColumn->getDefault() === null) {
655 5112
            return false;
656
        }
657
658
        // We need to drop an existing default constraint if the column was
659
        // defined with a default value before and it has changed.
660 5051
        if ($columnDiff->hasChanged('default')) {
661 4842
            return true;
662
        }
663
664
        // We need to drop an existing default constraint if the column was
665
        // defined with a default value before and the native column type has changed.
666 5043
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
667
    }
668
669
    /**
670
     * Returns the SQL statement for altering a column comment.
671
     *
672
     * SQL Server does not support native column comments,
673
     * therefore the extended properties functionality is used
674
     * as a workaround to store them.
675
     * The property name used to store column comments is "MS_Description"
676
     * which provides compatibility with SQL Server Management Studio,
677
     * as column comments are stored in the same property there when
678
     * specifying a column's "Description" attribute.
679
     *
680
     * @param string      $tableName  The quoted table name to which the column belongs.
681
     * @param string      $columnName The quoted column name to alter the comment for.
682
     * @param string|null $comment    The column's comment.
683
     */
684 5158
    protected function getAlterColumnCommentSQL(string $tableName, string $columnName, ?string $comment) : string
685
    {
686 5158
        if (strpos($tableName, '.') !== false) {
687 4902
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
688 4902
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
689 4902
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
690
        } else {
691 5110
            $schemaSQL = "'dbo'";
692 5110
            $tableSQL  = $this->quoteStringLiteral($tableName);
693
        }
694
695 5158
        return $this->getUpdateExtendedPropertySQL(
696 5158
            'MS_Description',
697 4
            $comment,
698 5158
            'SCHEMA',
699 4
            $schemaSQL,
700 5158
            'TABLE',
701 4
            $tableSQL,
702 5158
            'COLUMN',
703 4
            $columnName
704
        );
705
    }
706
707
    /**
708
     * Returns the SQL statement for dropping a column comment.
709
     *
710
     * SQL Server does not support native column comments,
711
     * therefore the extended properties functionality is used
712
     * as a workaround to store them.
713
     * The property name used to store column comments is "MS_Description"
714
     * which provides compatibility with SQL Server Management Studio,
715
     * as column comments are stored in the same property there when
716
     * specifying a column's "Description" attribute.
717
     *
718
     * @param string $tableName  The quoted table name to which the column belongs.
719
     * @param string $columnName The quoted column name to drop the comment for.
720
     */
721 5181
    protected function getDropColumnCommentSQL(string $tableName, string $columnName) : string
722
    {
723 5181
        if (strpos($tableName, '.') !== false) {
724 4927
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
725 4927
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
726 4927
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
727
        } else {
728 5110
            $schemaSQL = "'dbo'";
729 5110
            $tableSQL  = $this->quoteStringLiteral($tableName);
730
        }
731
732 5181
        return $this->getDropExtendedPropertySQL(
733 5181
            'MS_Description',
734 5181
            'SCHEMA',
735 4
            $schemaSQL,
736 5181
            'TABLE',
737 4
            $tableSQL,
738 5181
            'COLUMN',
739 4
            $columnName
740
        );
741
    }
742
743
    /**
744
     * {@inheritdoc}
745
     */
746 3725
    protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName) : array
747
    {
748 3725
        return [sprintf(
749 10
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
750 3725
            $tableName,
751 3725
            $oldIndexName,
752 3725
            $index->getQuotedName($this)
753
        ),
754
        ];
755
    }
756
757
    /**
758
     * Returns the SQL statement for adding an extended property to a database object.
759
     *
760
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
761
     *
762
     * @param string      $name       The name of the property to add.
763
     * @param string|null $value      The value of the property to add.
764
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
765
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
766
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
767
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
768
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
769
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
770
     */
771 5237
    public function getAddExtendedPropertySQL(
772
        string $name,
773
        ?string $value = null,
774
        ?string $level0Type = null,
775
        ?string $level0Name = null,
776
        ?string $level1Type = null,
777
        ?string $level1Name = null,
778
        ?string $level2Type = null,
779
        ?string $level2Name = null
780
    ) : string {
781
        return 'EXEC sp_addextendedproperty ' .
782 5237
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
783 5237
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
784 5237
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
785 5237
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
786
    }
787
788
    /**
789
     * Returns the SQL statement for dropping an extended property from a database object.
790
     *
791
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
792
     *
793
     * @param string      $name       The name of the property to drop.
794
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
795
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
796
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
797
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
798
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
799
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
800
     */
801 5181
    public function getDropExtendedPropertySQL(
802
        string $name,
803
        ?string $level0Type = null,
804
        ?string $level0Name = null,
805
        ?string $level1Type = null,
806
        ?string $level1Name = null,
807
        ?string $level2Type = null,
808
        ?string $level2Name = null
809
    ) : string {
810
        return 'EXEC sp_dropextendedproperty ' .
811 5181
            'N' . $this->quoteStringLiteral($name) . ', ' .
812 5181
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
813 5181
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
814 5181
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
815
    }
816
817
    /**
818
     * Returns the SQL statement for updating an extended property of a database object.
819
     *
820
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
821
     *
822
     * @param string      $name       The name of the property to update.
823
     * @param string|null $value      The value of the property to update.
824
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
825
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
826
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
827
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
828
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
829
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
830
     */
831 5158
    public function getUpdateExtendedPropertySQL(
832
        string $name,
833
        ?string $value = null,
834
        ?string $level0Type = null,
835
        ?string $level0Name = null,
836
        ?string $level1Type = null,
837
        ?string $level1Name = null,
838
        ?string $level2Type = null,
839
        ?string $level2Name = null
840
    ) : string {
841
        return 'EXEC sp_updateextendedproperty ' .
842 5158
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
843 5158
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
844 5158
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
845 5158
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
846
    }
847
848
    /**
849
     * {@inheritDoc}
850
     */
851 440
    public function getEmptyIdentityInsertSQL(string $tableName, string $identifierColumnName) : string
852
    {
853 440
        return 'INSERT INTO ' . $tableName . ' DEFAULT VALUES';
854
    }
855
856
    /**
857
     * {@inheritDoc}
858
     */
859 719
    public function getListTablesSQL() : string
860
    {
861
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
862
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
863 719
        return "SELECT name, SCHEMA_NAME (uid) AS schema_name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
864
    }
865
866
    /**
867
     * {@inheritDoc}
868
     */
869 4737
    public function getListTableColumnsSQL(string $table, ?string $database = null) : string
870
    {
871
        return "SELECT    col.name,
872
                          type.name AS type,
873
                          col.max_length AS length,
874
                          ~col.is_nullable AS notnull,
875
                          def.definition AS [default],
876
                          col.scale,
877
                          col.precision,
878
                          col.is_identity AS autoincrement,
879
                          col.collation_name AS collation,
880
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
881
                FROM      sys.columns AS col
882
                JOIN      sys.types AS type
883
                ON        col.user_type_id = type.user_type_id
884
                JOIN      sys.objects AS obj
885
                ON        col.object_id = obj.object_id
886
                JOIN      sys.schemas AS scm
887
                ON        obj.schema_id = scm.schema_id
888
                LEFT JOIN sys.default_constraints def
889
                ON        col.default_object_id = def.object_id
890
                AND       col.object_id = def.parent_object_id
891
                LEFT JOIN sys.extended_properties AS prop
892
                ON        obj.object_id = prop.major_id
893
                AND       col.column_id = prop.minor_id
894
                AND       prop.name = 'MS_Description'
895
                WHERE     obj.type = 'U'
896 4737
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
897
    }
898
899
    /**
900
     * {@inheritDoc}
901
     */
902 4691
    public function getListTableForeignKeysSQL(string $table, ?string $database = null) : string
903
    {
904
        return 'SELECT f.name AS ForeignKey,
905
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
906
                OBJECT_NAME (f.parent_object_id) AS TableName,
907
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
908
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
909
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
910
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
911
                f.delete_referential_action_desc,
912
                f.update_referential_action_desc
913
                FROM sys.foreign_keys AS f
914
                INNER JOIN sys.foreign_key_columns AS fc
915
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
916
                ON f.OBJECT_ID = fc.constraint_object_id
917
                WHERE ' .
918 4691
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
919
    }
920
921
    /**
922
     * {@inheritDoc}
923
     */
924 4645
    public function getListTableIndexesSQL(string $table, ?string $currentDatabase = null) : string
925
    {
926
        return "SELECT idx.name AS key_name,
927
                       col.name AS column_name,
928
                       ~idx.is_unique AS non_unique,
929
                       idx.is_primary_key AS [primary],
930
                       CASE idx.type
931
                           WHEN '1' THEN 'clustered'
932
                           WHEN '2' THEN 'nonclustered'
933
                           ELSE NULL
934
                       END AS flags
935
                FROM sys.tables AS tbl
936
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
937
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
938
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
939
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
940 4645
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
941
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
942
    }
943
944
    /**
945
     * {@inheritDoc}
946
     */
947 606
    public function getCreateViewSQL(string $name, string $sql) : string
948
    {
949 606
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
950
    }
951
952
    /**
953
     * {@inheritDoc}
954
     */
955 606
    public function getListViewsSQL(string $database) : string
956
    {
957 606
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
958
    }
959
960
    /**
961
     * Returns the where clause to filter schema and table name in a query.
962
     *
963
     * @param string $table        The full qualified name of the table.
964
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
965
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
966
     */
967 4745
    private function getTableWhereClause(string $table, string $schemaColumn, string $tableColumn) : string
968
    {
969 4745
        if (strpos($table, '.') !== false) {
970 4634
            [$schema, $table] = explode('.', $table);
971 4634
            $schema           = $this->quoteStringLiteral($schema);
972 4634
            $table            = $this->quoteStringLiteral($table);
973
        } else {
974 4739
            $schema = 'SCHEMA_NAME()';
975 4739
            $table  = $this->quoteStringLiteral($table);
976
        }
977
978 4745
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
979
    }
980
981
    /**
982
     * {@inheritDoc}
983
     */
984 606
    public function getDropViewSQL(string $name) : string
985
    {
986 606
        return 'DROP VIEW ' . $name;
987
    }
988
989
    /**
990
     * {@inheritDoc}
991
     */
992 741
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
993
    {
994 741
        if ($start === null) {
995 741
            return sprintf('CHARINDEX(%s, %s)', $substring, $string);
996
        }
997
998 741
        return sprintf('CHARINDEX(%s, %s, %s)', $substring, $string, $start);
999
    }
1000
1001
    /**
1002
     * {@inheritDoc}
1003
     */
1004
    public function getModExpression(string $dividend, string $divisor) : string
1005
    {
1006
        return $dividend . ' % ' . $divisor;
1007
    }
1008
1009
    /**
1010
     * {@inheritDoc}
1011
     */
1012 839
    public function getTrimExpression(string $str, int $mode = TrimMode::UNSPECIFIED, ?string $char = null) : string
1013
    {
1014 839
        if (! in_array($mode, [TrimMode::UNSPECIFIED, TrimMode::LEADING, TrimMode::TRAILING, TrimMode::BOTH], true)) {
1015 807
            throw new InvalidArgumentException(
1016 807
                sprintf('The value of $mode is expected to be one of the TrimMode constants, %d given', $mode)
1017
            );
1018
        }
1019
1020 839
        if ($char === null) {
1021 839
            switch ($mode) {
1022
                case TrimMode::LEADING:
1023 837
                    return 'LTRIM(' . $str . ')';
1024
1025
                case TrimMode::TRAILING:
1026 835
                    return 'RTRIM(' . $str . ')';
1027
1028
                default:
1029 839
                    return 'LTRIM(RTRIM(' . $str . '))';
1030
            }
1031
        }
1032
1033
        /** Original query used to get those expressions
1034
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1035
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1036
          select @c as string
1037
          , @trim_char as trim_char
1038
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1039
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1040
          , 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;
1041
         */
1042 831
        $pattern = "'%[^' + " . $char . " + ']%'";
1043
1044 831
        if ($mode === TrimMode::LEADING) {
1045 829
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1046
        }
1047
1048 831
        if ($mode === TrimMode::TRAILING) {
1049 827
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1050
        }
1051
1052 831
        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))';
1053
    }
1054
1055
    /**
1056
     * {@inheritDoc}
1057
     */
1058 5302
    public function getConcatExpression(string ...$string) : string
1059
    {
1060 5302
        return '(' . implode(' + ', $string) . ')';
1061
    }
1062
1063
    /**
1064
     * {@inheritDoc}
1065
     */
1066 5474
    public function getListDatabasesSQL() : string
1067
    {
1068 5474
        return 'SELECT * FROM sys.databases';
1069
    }
1070
1071
    /**
1072
     * {@inheritDoc}
1073
     */
1074 634
    public function getListNamespacesSQL() : string
1075
    {
1076 634
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1077
    }
1078
1079
    /**
1080
     * {@inheritDoc}
1081
     */
1082 739
    public function getSubstringExpression(string $string, string $start, ?string $length = null) : string
1083
    {
1084 739
        if ($length === null) {
1085 739
            return sprintf('SUBSTRING(%s, %s, LEN(%s) - %s + 1)', $string, $start, $string, $start);
1086
        }
1087
1088 737
        return sprintf('SUBSTRING(%s, %s, %s)', $string, $start, $length);
1089
    }
1090
1091
    /**
1092
     * {@inheritDoc}
1093
     */
1094
    public function getLengthExpression(string $string) : string
1095
    {
1096
        return 'LEN(' . $string . ')';
1097
    }
1098
1099
    /**
1100
     * {@inheritDoc}
1101
     */
1102 5277
    public function getSetTransactionIsolationSQL(int $level) : string
1103
    {
1104 5277
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1105
    }
1106
1107
    /**
1108
     * {@inheritDoc}
1109
     */
1110 5895
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
1111
    {
1112 5895
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1113
    }
1114
1115
    /**
1116
     * {@inheritDoc}
1117
     */
1118 501
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
1119
    {
1120 501
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1121
    }
1122
1123
    /**
1124
     * {@inheritDoc}
1125
     */
1126 590
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
1127
    {
1128 590
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1129
    }
1130
1131
    /**
1132
     * {@inheritDoc}
1133
     */
1134 4452
    public function getGuidTypeDeclarationSQL(array $field) : string
1135
    {
1136 4452
        return 'UNIQUEIDENTIFIER';
1137
    }
1138
1139
    /**
1140
     * {@inheritDoc}
1141
     */
1142 3147
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) : string
1143
    {
1144 3147
        return 'DATETIMEOFFSET(6)';
1145
    }
1146
1147
    /**
1148
     * {@inheritDoc}
1149
     */
1150 5689
    protected function getVarcharTypeDeclarationSQLSnippet(int $length, bool $fixed) : string
1151
    {
1152 5689
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1153
    }
1154
1155
    /**
1156
     * {@inheritdoc}
1157
     */
1158 5000
    protected function getBinaryTypeDeclarationSQLSnippet(int $length, bool $fixed) : string
1159
    {
1160 5000
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1161
    }
1162
1163
    /**
1164
     * {@inheritdoc}
1165
     */
1166 2
    public function getBinaryMaxLength() : int
1167
    {
1168 2
        return 8000;
1169
    }
1170
1171
    /**
1172
     * {@inheritDoc}
1173
     */
1174 5669
    public function getClobTypeDeclarationSQL(array $field) : string
1175
    {
1176 5669
        return 'VARCHAR(MAX)';
1177
    }
1178
1179
    /**
1180
     * {@inheritDoc}
1181
     */
1182 5895
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : string
1183
    {
1184 5895
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1185
    }
1186
1187
    /**
1188
     * {@inheritDoc}
1189
     */
1190 857
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
1191
    {
1192
        // 3 - microseconds precision length
1193
        // http://msdn.microsoft.com/en-us/library/ms187819.aspx
1194 857
        return 'DATETIME2(6)';
1195
    }
1196
1197
    /**
1198
     * {@inheritDoc}
1199
     */
1200 700
    public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string
1201
    {
1202 700
        return 'DATE';
1203
    }
1204
1205
    /**
1206
     * {@inheritDoc}
1207
     */
1208 698
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string
1209
    {
1210 698
        return 'TIME(0)';
1211
    }
1212
1213
    /**
1214
     * {@inheritDoc}
1215
     */
1216 4135
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
1217
    {
1218 4135
        return 'BIT';
1219
    }
1220
1221
    /**
1222
     * {@inheritDoc}
1223
     */
1224 2949
    protected function doModifyLimitQuery(string $query, ?int $limit, int $offset) : string
1225
    {
1226 2949
        $where = [];
1227
1228 2949
        if ($offset > 0) {
1229 2679
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1230
        }
1231
1232 2949
        if ($limit !== null) {
1233 2948
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1234 2948
            $top     = sprintf('TOP %d', $offset + $limit);
1235
        } else {
1236 51
            $top = 'TOP 9223372036854775807';
1237
        }
1238
1239 2949
        if (empty($where)) {
1240 51
            return $query;
1241
        }
1242
1243
        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
1244
        // Even if the TOP n is very large, the use of a CTE will
1245
        // allow the SQL Server query planner to optimize it so it doesn't
1246
        // actually scan the entire range covered by the TOP clause.
1247 2948
        if (! preg_match('/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/is', $query, $matches)) {
1248
            return $query;
1249
        }
1250
1251 2948
        $query = $matches[1] . $top . ' ' . $matches[2];
1252
1253 2948
        if (stristr($query, 'ORDER BY')) {
1254
            // Inner order by is not valid in SQL Server for our purposes
1255
            // unless it's in a TOP N subquery.
1256 2941
            $query = $this->scrubInnerOrderBy($query);
1257
        }
1258
1259
        // Build a new limited query around the original, using a CTE
1260 2948
        return sprintf(
1261
            'WITH dctrn_cte AS (%s) '
1262
            . 'SELECT * FROM ('
1263
            . 'SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte'
1264
            . ') AS doctrine_tbl '
1265 23
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1266 2948
            $query,
1267 2948
            implode(' AND ', $where)
1268
        );
1269
    }
1270
1271
    /**
1272
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
1273
     * Caveat: will leave ORDER BY in TOP N subqueries.
1274
     */
1275 2941
    private function scrubInnerOrderBy(string $query) : string
1276
    {
1277 2941
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1278 2941
        $offset = 0;
1279
1280 2941
        while ($count-- > 0) {
1281 2941
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1282 2941
            if ($orderByPos === false) {
1283 2927
                break;
1284
            }
1285
1286 2941
            $qLen            = strlen($query);
1287 2941
            $parenCount      = 0;
1288 2941
            $currentPosition = $orderByPos;
1289
1290 2941
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1291 2941
                if ($query[$currentPosition] === '(') {
1292 2376
                    $parenCount++;
1293 2941
                } elseif ($query[$currentPosition] === ')') {
1294 2933
                    $parenCount--;
1295
                }
1296
1297 2941
                $currentPosition++;
1298
            }
1299
1300 2941
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1301
                // If the order by clause is in a TOP N subquery, do not remove
1302
                // it and continue iteration from the current position.
1303 2939
                $offset = $currentPosition;
1304 2939
                continue;
1305
            }
1306
1307 2931
            if ($currentPosition >= $qLen - 1) {
1308
                continue;
1309
            }
1310
1311 2931
            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1312 2931
            $offset = $orderByPos;
1313
        }
1314
1315 2941
        return $query;
1316
    }
1317
1318
    /**
1319
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
1320
     *
1321
     * @param string $query           The query
1322
     * @param int    $currentPosition Start position of ORDER BY clause
1323
     *
1324
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
1325
     */
1326 2941
    private function isOrderByInTopNSubquery(string $query, int $currentPosition) : bool
1327
    {
1328
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1329 2941
        $subQueryBuffer = '';
1330 2941
        $parenCount     = 0;
1331
1332
        // If $parenCount goes negative, we've exited the subquery we're examining.
1333
        // If $currentPosition goes negative, we've reached the beginning of the query.
1334 2941
        while ($parenCount >= 0 && $currentPosition >= 0) {
1335 2941
            if ($query[$currentPosition] === '(') {
1336 2936
                $parenCount--;
1337 2941
            } elseif ($query[$currentPosition] === ')') {
1338 2934
                $parenCount++;
1339
            }
1340
1341
            // Only yank query text on the same nesting level as the ORDER BY clause.
1342 2941
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1343
1344 2941
            $currentPosition--;
1345
        }
1346
1347 2941
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1348
    }
1349
1350
    /**
1351
     * {@inheritDoc}
1352
     */
1353 5709
    public function supportsLimitOffset() : bool
1354
    {
1355 5709
        return true;
1356
    }
1357
1358
    /**
1359
     * {@inheritDoc}
1360
     */
1361 4135
    public function convertBooleans($item)
1362
    {
1363 4135
        if (is_array($item)) {
1364
            foreach ($item as $key => $value) {
1365
                if (! is_bool($value) && ! is_numeric($value)) {
1366
                    continue;
1367
                }
1368
1369
                $item[$key] = $value ? 1 : 0;
1370
            }
1371 4135
        } elseif (is_bool($item) || is_numeric($item)) {
1372 4135
            $item = $item ? 1 : 0;
1373
        }
1374
1375 4135
        return $item;
1376
    }
1377
1378
    /**
1379
     * {@inheritDoc}
1380
     */
1381 505
    public function getCreateTemporaryTableSnippetSQL() : string
1382
    {
1383 505
        return 'CREATE TABLE';
1384
    }
1385
1386
    /**
1387
     * {@inheritDoc}
1388
     */
1389 505
    public function getTemporaryTableName(string $tableName) : string
1390
    {
1391 505
        return '#' . $tableName;
1392
    }
1393
1394
    /**
1395
     * {@inheritDoc}
1396
     */
1397 853
    public function getDateTimeFormatString() : string
1398
    {
1399 853
        return 'Y-m-d H:i:s.u';
1400
    }
1401
1402
    /**
1403
     * {@inheritDoc}
1404
     */
1405 476
    public function getDateFormatString() : string
1406
    {
1407 476
        return 'Y-m-d';
1408
    }
1409
1410
    /**
1411
     * {@inheritDoc}
1412
     */
1413 474
    public function getTimeFormatString() : string
1414
    {
1415 474
        return 'H:i:s';
1416
    }
1417
1418
    /**
1419
     * {@inheritDoc}
1420
     */
1421 478
    public function getDateTimeTzFormatString() : string
1422
    {
1423 478
        return 'Y-m-d H:i:s.u P';
1424
    }
1425
1426
    /**
1427
     * {@inheritDoc}
1428
     */
1429 861
    public function getName() : string
1430
    {
1431 861
        return 'mssql';
1432
    }
1433
1434
    /**
1435
     * {@inheritDoc}
1436
     */
1437 5111
    protected function initializeDoctrineTypeMappings() : void
1438
    {
1439 5111
        $this->doctrineTypeMapping = [
1440
            'bigint'           => 'bigint',
1441
            'binary'           => 'binary',
1442
            'bit'              => 'boolean',
1443
            'char'             => 'string',
1444
            'date'             => 'date',
1445
            'datetime'         => 'datetime',
1446
            'datetime2'        => 'datetime',
1447
            'datetimeoffset'   => 'datetimetz',
1448
            'decimal'          => 'decimal',
1449
            'double'           => 'float',
1450
            'double precision' => 'float',
1451
            'float'            => 'float',
1452
            'image'            => 'blob',
1453
            'int'              => 'integer',
1454
            'money'            => 'integer',
1455
            'nchar'            => 'string',
1456
            'ntext'            => 'text',
1457
            'numeric'          => 'decimal',
1458
            'nvarchar'         => 'string',
1459
            'real'             => 'float',
1460
            'smalldatetime'    => 'datetime',
1461
            'smallint'         => 'smallint',
1462
            'smallmoney'       => 'integer',
1463
            'text'             => 'text',
1464
            'time'             => 'time',
1465
            'tinyint'          => 'smallint',
1466
            'uniqueidentifier' => 'guid',
1467
            'varbinary'        => 'binary',
1468
            'varchar'          => 'string',
1469
        ];
1470 5111
    }
1471
1472
    /**
1473
     * {@inheritDoc}
1474
     */
1475 863
    public function createSavePoint(string $savepoint) : string
1476
    {
1477 863
        return 'SAVE TRANSACTION ' . $savepoint;
1478
    }
1479
1480
    /**
1481
     * {@inheritDoc}
1482
     */
1483
    public function releaseSavePoint(string $savepoint) : string
1484
    {
1485
        return '';
1486
    }
1487
1488
    /**
1489
     * {@inheritDoc}
1490
     */
1491 863
    public function rollbackSavePoint(string $savepoint) : string
1492
    {
1493 863
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1494
    }
1495
1496
    /**
1497
     * {@inheritdoc}
1498
     */
1499 4469
    public function getForeignKeyReferentialActionSQL(string $action) : string
1500
    {
1501
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1502 4469
        if (strtoupper($action) === 'RESTRICT') {
1503 4152
            return 'NO ACTION';
1504
        }
1505
1506 4467
        return parent::getForeignKeyReferentialActionSQL($action);
1507
    }
1508
1509
    /**
1510
     * {@inheritDoc}
1511
     */
1512 3320
    public function appendLockHint(string $fromClause, ?int $lockMode) : string
1513
    {
1514 3320
        switch (true) {
1515
            case $lockMode === LockMode::NONE:
1516 3026
                return $fromClause . ' WITH (NOLOCK)';
1517
1518 3319
            case $lockMode === LockMode::PESSIMISTIC_READ:
1519 2976
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1520
1521 3318
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1522 3224
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1523
1524
            default:
1525 3052
                return $fromClause;
1526
        }
1527
    }
1528
1529
    /**
1530
     * {@inheritDoc}
1531
     */
1532 509
    public function getForUpdateSQL() : string
1533
    {
1534 509
        return ' ';
1535
    }
1536
1537
    /**
1538
     * {@inheritDoc}
1539
     */
1540 5488
    protected function getReservedKeywordsClass() : string
1541
    {
1542 5488
        return Keywords\SQLServerKeywords::class;
1543
    }
1544
1545
    /**
1546
     * {@inheritDoc}
1547
     */
1548 5654
    public function quoteSingleIdentifier(string $str) : string
1549
    {
1550 5654
        return '[' . str_replace(']', '][', $str) . ']';
1551
    }
1552
1553
    /**
1554
     * {@inheritDoc}
1555
     */
1556 3932
    public function getTruncateTableSQL(string $tableName, bool $cascade = false) : string
1557
    {
1558 3932
        $tableIdentifier = new Identifier($tableName);
1559
1560 3932
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1561
    }
1562
1563
    /**
1564
     * {@inheritDoc}
1565
     */
1566 875
    public function getBlobTypeDeclarationSQL(array $field) : string
1567
    {
1568 875
        return 'VARBINARY(MAX)';
1569
    }
1570
1571
    /**
1572
     * {@inheritdoc}
1573
     *
1574
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1575
     */
1576 5929
    public function getColumnDeclarationSQL(string $name, array $field) : string
1577
    {
1578 5929
        if (isset($field['columnDefinition'])) {
1579 3752
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1580
        } else {
1581 5927
            $collation = isset($field['collation']) && $field['collation'] ?
1582 5927
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1583
1584 5927
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1585
1586 5927
            $unique = isset($field['unique']) && $field['unique'] ?
1587 5927
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1588
1589 5927
            $check = isset($field['check']) && $field['check'] ?
1590 5927
                ' ' . $field['check'] : '';
1591
1592 5927
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1593 5927
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1594
        }
1595
1596 5929
        return $name . ' ' . $columnDef;
1597
    }
1598
1599
    /**
1600
     * {@inheritdoc}
1601
     */
1602 3566
    protected function getLikeWildcardCharacters() : string
1603
    {
1604 3566
        return parent::getLikeWildcardCharacters() . '[]^';
1605
    }
1606
1607
    /**
1608
     * Returns a unique default constraint name for a table and column.
1609
     *
1610
     * @param string $table  Name of the table to generate the unique default constraint name for.
1611
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1612
     */
1613 5123
    private function generateDefaultConstraintName(string $table, string $column) : string
1614
    {
1615 5123
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1616
    }
1617
1618
    /**
1619
     * Returns a hash value for a given identifier.
1620
     *
1621
     * @param string $identifier Identifier to generate a hash value for.
1622
     */
1623 5125
    private function generateIdentifierName(string $identifier) : string
1624
    {
1625
        // Always generate name for unquoted identifiers to ensure consistency.
1626 5125
        $identifier = new Identifier($identifier);
1627
1628 5125
        return strtoupper(dechex(crc32($identifier->getName())));
1629
    }
1630
}
1631