Completed
Pull Request — develop (#3576)
by Jonathan
64:38 queued 61:53
created

SQLServerPlatform::appendLockHint()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 14
ccs 8
cts 8
cp 1
rs 9.9666
c 0
b 0
f 0
cc 4
nc 4
nop 2
crap 4
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 5934
    public function getCurrentDateSQL() : string
50
    {
51 5934
        return $this->getConvertExpression('date', 'GETDATE()');
52
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57 5929
    public function getCurrentTimeSQL() : string
58
    {
59 5929
        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 5934
    private function getConvertExpression(string $dataType, string $expression) : string
69
    {
70 5934
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76 1211
    protected function getDateArithmeticIntervalExpression(string $date, string $operator, string $interval, string $unit) : string
77
    {
78 1211
        $factorClause = '';
79
80 1211
        if ($operator === '-') {
81 1205
            $factorClause = '-1 * ';
82
        }
83
84 1211
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
85
    }
86
87
    /**
88
     * {@inheritDoc}
89
     */
90 1060
    public function getDateDiffExpression(string $date1, string $date2) : string
91
    {
92 1060
        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 5438
    public function prefersIdentityColumns() : bool
102
    {
103 5438
        return true;
104
    }
105
106
    /**
107
     * {@inheritDoc}
108
     *
109
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
110
     */
111 908
    public function supportsIdentityColumns() : bool
112
    {
113 908
        return true;
114
    }
115
116
    /**
117
     * {@inheritDoc}
118
     */
119 1298
    public function supportsReleaseSavepoints() : bool
120
    {
121 1298
        return false;
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 953
    public function supportsSchemas() : bool
128
    {
129 953
        return true;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135 870
    public function getDefaultSchemaName() : string
136
    {
137 870
        return 'dbo';
138
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143 4891
    public function supportsColumnCollation() : bool
144
    {
145 4891
        return true;
146
    }
147
148
    /**
149
     * {@inheritDoc}
150
     */
151 6422
    public function hasNativeGuidType() : bool
152
    {
153 6422
        return true;
154
    }
155
156
    /**
157
     * {@inheritDoc}
158
     */
159 6148
    public function getCreateDatabaseSQL(string $database) : string
160
    {
161 6148
        return 'CREATE DATABASE ' . $database;
162
    }
163
164
    /**
165
     * {@inheritDoc}
166
     */
167 6148
    public function getDropDatabaseSQL(string $database) : string
168
    {
169 6148
        return 'DROP DATABASE ' . $database;
170
    }
171
172
    /**
173
     * {@inheritDoc}
174
     */
175 1318
    public function supportsCreateDropDatabase() : bool
176
    {
177 1318
        return true;
178
    }
179
180
    /**
181
     * {@inheritDoc}
182
     */
183 5553
    public function getCreateSchemaSQL(string $schemaName) : string
184
    {
185 5553
        return 'CREATE SCHEMA ' . $schemaName;
186
    }
187
188
    /**
189
     * {@inheritDoc}
190
     */
191 3894
    public function getDropForeignKeySQL($foreignKey, $table) : string
192
    {
193 3894
        if (! $foreignKey instanceof ForeignKeyConstraint) {
194 3382
            $foreignKey = new Identifier($foreignKey);
195
        }
196
197 3894
        if (! $table instanceof Table) {
198 3894
            $table = new Identifier($table);
199
        }
200
201 3894
        $foreignKey = $foreignKey->getQuotedName($this);
202 3894
        $table      = $table->getQuotedName($this);
203
204 3894
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
205
    }
206
207
    /**
208
     * {@inheritDoc}
209
     */
210 927
    public function getDropIndexSQL($index, $table = null) : string
211
    {
212 927
        if ($index instanceof Index) {
213 915
            $index = $index->getQuotedName($this);
214 927
        } 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 927
        if (! isset($table)) {
222
            return 'DROP INDEX ' . $index;
223
        }
224
225 927
        if ($table instanceof Table) {
226 927
            $table = $table->getQuotedName($this);
227
        }
228
229 927
        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 927
            $index,
238 927
            $table,
239 927
            $index,
240 927
            $index,
241 927
            $table
242
        );
243
    }
244
245
    /**
246
     * {@inheritDoc}
247
     */
248 6342
    protected function _getCreateTableSQL(string $tableName, array $columns, array $options = []) : array
249
    {
250 6342
        $defaultConstraintsSql = [];
251 6342
        $commentsSql           = [];
252
253
        // @todo does other code breaks because of this?
254
        // force primary keys to be not null
255 6342
        foreach ($columns as &$column) {
256 6342
            if (isset($column['primary']) && $column['primary']) {
257 5997
                $column['notnull'] = true;
258
            }
259
260
            // Build default constraints SQL statements.
261 6342
            if (isset($column['default'])) {
262 5340
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
263 5340
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
264
            }
265
266 6342
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
267 6338
                continue;
268
            }
269
270 5554
            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
271
        }
272
273 6342
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
274
275 6342
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
276
            foreach ($options['uniqueConstraints'] as $name => $definition) {
277
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
278
            }
279
        }
280
281 6342
        if (isset($options['primary']) && ! empty($options['primary'])) {
282 5999
            $flags = '';
283 5999
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
284 5280
                $flags = ' NONCLUSTERED';
285
            }
286 5999
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
287
        }
288
289 6342
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
290
291 6342
        $check = $this->getCheckDeclarationSQL($columns);
292 6342
        if (! empty($check)) {
293
            $query .= ', ' . $check;
294
        }
295 6342
        $query .= ')';
296
297 6342
        $sql = [$query];
298
299 6342
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
300 4553
            foreach ($options['indexes'] as $index) {
301 4553
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
302
            }
303
        }
304
305 6342
        if (isset($options['foreignKeys'])) {
306 4561
            foreach ((array) $options['foreignKeys'] as $definition) {
307 4169
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
308
            }
309
        }
310
311 6342
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
312
    }
313
314
    /**
315
     * {@inheritDoc}
316
     */
317 5256
    public function getCreatePrimaryKeySQL(Index $index, $table) : string
318
    {
319 5256
        if ($table instanceof Table) {
320
            $identifier = $table->getQuotedName($this);
321
        } else {
322 5256
            $identifier = $table;
323
        }
324
325 5256
        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';
326
327 5256
        if ($index->hasFlag('nonclustered')) {
328 5254
            $sql .= ' NONCLUSTERED';
329
        }
330
331 5256
        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 5560
    protected function getCreateColumnCommentSQL(string $tableName, string $columnName, ?string $comment) : string
350
    {
351 5560
        if (strpos($tableName, '.') !== false) {
352 5178
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
353 5178
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
354 5178
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
355
        } else {
356 5464
            $schemaSQL = "'dbo'";
357 5464
            $tableSQL  = $this->quoteStringLiteral($tableName);
358
        }
359
360 5560
        return $this->getAddExtendedPropertySQL(
361 5560
            'MS_Description',
362 14
            $comment,
363 5560
            'SCHEMA',
364 14
            $schemaSQL,
365 5560
            'TABLE',
366 14
            $tableSQL,
367 5560
            '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 5477
    public function getDefaultConstraintDeclarationSQL(string $table, array $column) : string
381
    {
382 5477
        if (! isset($column['default'])) {
383
            throw new InvalidArgumentException('Incomplete column definition. "default" required.');
384
        }
385
386 5477
        $columnName = new Identifier($column['name']);
387
388
        return ' CONSTRAINT ' .
389 5477
            $this->generateDefaultConstraintName($table, $column['name']) .
390 5477
            $this->getDefaultValueDeclarationSQL($column) .
391 5477
            ' FOR ' . $columnName->getQuotedName($this);
392
    }
393
394
    /**
395
     * {@inheritDoc}
396
     */
397 5644
    public function getCreateIndexSQL(Index $index, $table) : string
398
    {
399 5644
        $constraint = parent::getCreateIndexSQL($index, $table);
400
401 5644
        if ($index->isUnique() && ! $index->isPrimary()) {
402 4551
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
403
        }
404
405 5644
        return $constraint;
406
    }
407
408
    /**
409
     * {@inheritDoc}
410
     */
411 5644
    protected function getCreateIndexSQLFlags(Index $index) : string
412
    {
413 5644
        $type = '';
414 5644
        if ($index->isUnique()) {
415 4551
            $type .= 'UNIQUE ';
416
        }
417
418 5644
        if ($index->hasFlag('clustered')) {
419 5306
            $type .= 'CLUSTERED ';
420 4561
        } elseif ($index->hasFlag('nonclustered')) {
421
            $type .= 'NONCLUSTERED ';
422
        }
423
424 5644
        return $type;
425
    }
426
427
    /**
428
     * Extend unique key constraint with required filters
429
     */
430 4551
    private function _appendUniqueConstraintDefinition(string $sql, Index $index) : string
431
    {
432 4551
        $fields = [];
433
434 4551
        foreach ($index->getQuotedColumns($this) as $field) {
435 4551
            $fields[] = $field . ' IS NOT NULL';
436
        }
437
438 4551
        return $sql . ' WHERE ' . implode(' AND ', $fields);
439
    }
440
441
    /**
442
     * {@inheritDoc}
443
     */
444 5576
    public function getAlterTableSQL(TableDiff $diff) : array
445
    {
446 5576
        $queryParts  = [];
447 5576
        $sql         = [];
448 5576
        $columnSql   = [];
449 5576
        $commentsSql = [];
450
451 5576
        foreach ($diff->addedColumns as $column) {
452 5543
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
453
                continue;
454
            }
455
456 5543
            $columnDef    = $column->toArray();
457 5543
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
458
459 5543
            if (isset($columnDef['default'])) {
460 4740
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
461
            }
462
463 5543
            $comment = $this->getColumnComment($column);
464
465 5543
            if (empty($comment) && ! is_numeric($comment)) {
466 5447
                continue;
467
            }
468
469 5529
            $commentsSql[] = $this->getCreateColumnCommentSQL(
470 5529
                $diff->name,
471 5529
                $column->getQuotedName($this),
472 6
                $comment
473
            );
474
        }
475
476 5576
        foreach ($diff->removedColumns as $column) {
477 5178
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
478
                continue;
479
            }
480
481 5178
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
482
        }
483
484 5576
        foreach ($diff->changedColumns as $columnDiff) {
485 5531
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
486
                continue;
487
            }
488
489 5531
            $column     = $columnDiff->column;
490 5531
            $comment    = $this->getColumnComment($column);
491 5531
            $hasComment = ! empty($comment) || is_numeric($comment);
492
493 5531
            if ($columnDiff->fromColumn instanceof Column) {
494 5521
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
495 5521
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
496
497 5521
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
498 5481
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
499 5481
                        $diff->name,
500 5481
                        $column->getQuotedName($this),
501 4
                        $comment
502
                    );
503 5519
                } elseif ($hasFromComment && ! $hasComment) {
504 5504
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
505 5448
                } elseif ($hasComment) {
506 5433
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
507 5433
                        $diff->name,
508 5433
                        $column->getQuotedName($this),
509 2
                        $comment
510
                    );
511
                }
512
            }
513
514
            // Do not add query part if only comment has changed.
515 5531
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
516 5508
                continue;
517
            }
518
519 5456
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
520
521 5456
            if ($requireDropDefaultConstraint) {
522 5375
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
523 5375
                    $diff->name,
524 5375
                    $columnDiff->oldColumnName
525
                );
526
            }
527
528 5456
            $columnDef = $column->toArray();
529
530 5456
            $queryParts[] = 'ALTER COLUMN ' .
531 5456
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
532
533 5456
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
534 5444
                continue;
535
            }
536
537 5377
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
538
        }
539
540 5576
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
541 4384
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
542
                continue;
543
            }
544
545 4384
            $oldColumnName = new Identifier($oldColumnName);
546
547 4384
            $sql[] = "sp_RENAME '" .
548 4384
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
549 4384
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
550
551
            // Recreate default constraint with new column name if necessary (for future reference).
552 4384
            if ($column->getDefault() === null) {
553 4310
                continue;
554
            }
555
556 3895
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
557 3895
                $diff->name,
558 3895
                $oldColumnName->getQuotedName($this)
559
            );
560 3895
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
561
        }
562
563 5576
        $tableSql = [];
564
565 5576
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
566
            return array_merge($tableSql, $columnSql);
567
        }
568
569 5576
        foreach ($queryParts as $query) {
570 5560
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
571
        }
572
573 5576
        $sql = array_merge($sql, $commentsSql);
574
575 5576
        $newName = $diff->getNewName();
576
577 5576
        if ($newName !== null) {
578 4449
            $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 4449
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
589
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
590 4449
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
591 4449
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
592 4449
                'FROM sys.default_constraints dc ' .
593 4449
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
594 4449
                "WHERE tbl.name = '" . $newName->getName() . "';" .
595 4449
                'EXEC sp_executesql @sql';
596
        }
597
598 5576
        $sql = array_merge(
599 5576
            $this->getPreAlterTableIndexForeignKeySQL($diff),
600 5576
            $sql,
601 5576
            $this->getPostAlterTableIndexForeignKeySQL($diff)
602
        );
603
604 5576
        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 5379
    private function getAlterTableAddDefaultConstraintClause(string $tableName, Column $column) : string
614
    {
615 5379
        $columnDef         = $column->toArray();
616 5379
        $columnDef['name'] = $column->getQuotedName($this);
617
618 5379
        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 5377
    private function getAlterTableDropDefaultConstraintClause(string $tableName, string $columnName) : string
628
    {
629 5377
        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 5456
    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 5456
        if (! $columnDiff->fromColumn instanceof Column) {
649 3934
            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 5448
        if ($columnDiff->fromColumn->getDefault() === null) {
655 5435
            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 5375
        if ($columnDiff->hasChanged('default')) {
661 5166
            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 5367
        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 5481
    protected function getAlterColumnCommentSQL(string $tableName, string $columnName, ?string $comment) : string
685
    {
686 5481
        if (strpos($tableName, '.') !== false) {
687 5098
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
688 5098
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
689 5098
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
690
        } else {
691 5433
            $schemaSQL = "'dbo'";
692 5433
            $tableSQL  = $this->quoteStringLiteral($tableName);
693
        }
694
695 5481
        return $this->getUpdateExtendedPropertySQL(
696 5481
            'MS_Description',
697 4
            $comment,
698 5481
            'SCHEMA',
699 4
            $schemaSQL,
700 5481
            'TABLE',
701 4
            $tableSQL,
702 5481
            '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 5504
    protected function getDropColumnCommentSQL(string $tableName, string $columnName) : string
722
    {
723 5504
        if (strpos($tableName, '.') !== false) {
724 5124
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
725 5124
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
726 5124
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
727
        } else {
728 5433
            $schemaSQL = "'dbo'";
729 5433
            $tableSQL  = $this->quoteStringLiteral($tableName);
730
        }
731
732 5504
        return $this->getDropExtendedPropertySQL(
733 5504
            'MS_Description',
734 5504
            'SCHEMA',
735 4
            $schemaSQL,
736 5504
            'TABLE',
737 4
            $tableSQL,
738 5504
            'COLUMN',
739 4
            $columnName
740
        );
741
    }
742
743
    /**
744
     * {@inheritdoc}
745
     */
746 4030
    protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName) : array
747
    {
748 4030
        return [sprintf(
749 10
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
750 4030
            $tableName,
751 4030
            $oldIndexName,
752 4030
            $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 5560
    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 5560
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
783 5560
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
784 5560
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
785 5560
            '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 5504
    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 5504
            'N' . $this->quoteStringLiteral($name) . ', ' .
812 5504
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
813 5504
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
814 5504
            '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 5481
    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 5481
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
843 5481
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
844 5481
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
845 5481
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
846
    }
847
848
    /**
849
     * {@inheritDoc}
850
     */
851 660
    public function getEmptyIdentityInsertSQL(string $tableName, string $identifierColumnName) : string
852
    {
853 660
        return 'INSERT INTO ' . $tableName . ' DEFAULT VALUES';
854
    }
855
856
    /**
857
     * {@inheritDoc}
858
     */
859 1082
    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 1082
        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 5072
    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 5072
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
897
    }
898
899
    /**
900
     * {@inheritDoc}
901
     */
902 5026
    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 5026
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
919
    }
920
921
    /**
922
     * {@inheritDoc}
923
     */
924 4980
    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 4980
                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 909
    public function getCreateViewSQL(string $name, string $sql) : string
948
    {
949 909
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
950
    }
951
952
    /**
953
     * {@inheritDoc}
954
     */
955 909
    public function getListViewsSQL(string $database) : string
956
    {
957 909
        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 5080
    private function getTableWhereClause(string $table, string $schemaColumn, string $tableColumn) : string
968
    {
969 5080
        if (strpos($table, '.') !== false) {
970 4924
            [$schema, $table] = explode('.', $table);
971 4924
            $schema           = $this->quoteStringLiteral($schema);
972 4924
            $table            = $this->quoteStringLiteral($table);
973
        } else {
974 5074
            $schema = 'SCHEMA_NAME()';
975 5074
            $table  = $this->quoteStringLiteral($table);
976
        }
977
978 5080
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
979
    }
980
981
    /**
982
     * {@inheritDoc}
983
     */
984 909
    public function getDropViewSQL(string $name) : string
985
    {
986 909
        return 'DROP VIEW ' . $name;
987
    }
988
989
    /**
990
     * {@inheritDoc}
991
     */
992 1115
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
993
    {
994 1115
        if ($start === null) {
995 1115
            return sprintf('CHARINDEX(%s, %s)', $substring, $string);
996
        }
997
998 1115
        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 1262
    public function getTrimExpression(string $str, int $mode = TrimMode::UNSPECIFIED, ?string $char = null) : string
1013
    {
1014 1262
        if (! in_array($mode, [TrimMode::UNSPECIFIED, TrimMode::LEADING, TrimMode::TRAILING, TrimMode::BOTH], true)) {
1015 1214
            throw new InvalidArgumentException(
1016 1214
                sprintf('The value of $mode is expected to be one of the TrimMode constants, %d given', $mode)
1017
            );
1018
        }
1019
1020 1262
        if ($char === null) {
1021 1262
            switch ($mode) {
1022
                case TrimMode::LEADING:
1023 1259
                    return 'LTRIM(' . $str . ')';
1024
1025
                case TrimMode::TRAILING:
1026 1256
                    return 'RTRIM(' . $str . ')';
1027
1028
                default:
1029 1262
                    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 1250
        $pattern = "'%[^' + " . $char . " + ']%'";
1043
1044 1250
        if ($mode === TrimMode::LEADING) {
1045 1247
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1046
        }
1047
1048 1250
        if ($mode === TrimMode::TRAILING) {
1049 1244
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1050
        }
1051
1052 1250
        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 5514
    public function getConcatExpression(string ...$string) : string
1059
    {
1060 5514
        return '(' . implode(' + ', $string) . ')';
1061
    }
1062
1063
    /**
1064
     * {@inheritDoc}
1065
     */
1066 5795
    public function getListDatabasesSQL() : string
1067
    {
1068 5795
        return 'SELECT * FROM sys.databases';
1069
    }
1070
1071
    /**
1072
     * {@inheritDoc}
1073
     */
1074 951
    public function getListNamespacesSQL() : string
1075
    {
1076 951
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1077
    }
1078
1079
    /**
1080
     * {@inheritDoc}
1081
     */
1082 1112
    public function getSubstringExpression(string $string, string $start, ?string $length = null) : string
1083
    {
1084 1112
        if ($length === null) {
1085 1112
            return sprintf('SUBSTRING(%s, %s, LEN(%s) - %s + 1)', $string, $start, $string, $start);
1086
        }
1087
1088 1109
        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 5488
    public function getSetTransactionIsolationSQL(int $level) : string
1103
    {
1104 5488
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1105
    }
1106
1107
    /**
1108
     * {@inheritDoc}
1109
     */
1110 6336
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
1111
    {
1112 6336
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1113
    }
1114
1115
    /**
1116
     * {@inheritDoc}
1117
     */
1118 751
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
1119
    {
1120 751
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1121
    }
1122
1123
    /**
1124
     * {@inheritDoc}
1125
     */
1126 885
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
1127
    {
1128 885
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1129
    }
1130
1131
    /**
1132
     * {@inheritDoc}
1133
     */
1134 4630
    public function getGuidTypeDeclarationSQL(array $field) : string
1135
    {
1136 4630
        return 'UNIQUEIDENTIFIER';
1137
    }
1138
1139
    /**
1140
     * {@inheritDoc}
1141
     */
1142 3397
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) : string
1143
    {
1144 3397
        return 'DATETIMEOFFSET(6)';
1145
    }
1146
1147
    /**
1148
     * {@inheritDoc}
1149
     */
1150 6121
    protected function getVarcharTypeDeclarationSQLSnippet(int $length, bool $fixed) : string
1151
    {
1152 6121
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1153
    }
1154
1155
    /**
1156
     * {@inheritdoc}
1157
     */
1158 5291
    protected function getBinaryTypeDeclarationSQLSnippet(int $length, bool $fixed) : string
1159
    {
1160 5291
        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 6110
    public function getClobTypeDeclarationSQL(array $field) : string
1175
    {
1176 6110
        return 'VARCHAR(MAX)';
1177
    }
1178
1179
    /**
1180
     * {@inheritDoc}
1181
     */
1182 6336
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : string
1183
    {
1184 6336
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1185
    }
1186
1187
    /**
1188
     * {@inheritDoc}
1189
     */
1190 1289
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
1191
    {
1192
        // 3 - microseconds precision length
1193
        // http://msdn.microsoft.com/en-us/library/ms187819.aspx
1194 1289
        return 'DATETIME2(6)';
1195
    }
1196
1197
    /**
1198
     * {@inheritDoc}
1199
     */
1200 1054
    public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string
1201
    {
1202 1054
        return 'DATE';
1203
    }
1204
1205
    /**
1206
     * {@inheritDoc}
1207
     */
1208 1051
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string
1209
    {
1210 1051
        return 'TIME(0)';
1211
    }
1212
1213
    /**
1214
     * {@inheritDoc}
1215
     */
1216 4469
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
1217
    {
1218 4469
        return 'BIT';
1219
    }
1220
1221
    /**
1222
     * {@inheritDoc}
1223
     */
1224 3066
    protected function doModifyLimitQuery(string $query, ?int $limit, int $offset) : string
1225
    {
1226 3066
        $where = [];
1227
1228 3066
        if ($offset > 0) {
1229 2786
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1230
        }
1231
1232 3066
        if ($limit !== null) {
1233 3065
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1234 3065
            $top     = sprintf('TOP %d', $offset + $limit);
1235
        } else {
1236 53
            $top = 'TOP 9223372036854775807';
1237
        }
1238
1239 3066
        if (empty($where)) {
1240 53
            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 3065
        if (! preg_match('/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/is', $query, $matches)) {
1248
            return $query;
1249
        }
1250
1251 3065
        $query = $matches[1] . $top . ' ' . $matches[2];
1252
1253 3065
        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 3058
            $query = $this->scrubInnerOrderBy($query);
1257
        }
1258
1259
        // Build a new limited query around the original, using a CTE
1260 3065
        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 3065
            $query,
1267 3065
            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 3058
    private function scrubInnerOrderBy(string $query) : string
1276
    {
1277 3058
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1278 3058
        $offset = 0;
1279
1280 3058
        while ($count-- > 0) {
1281 3058
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1282 3058
            if ($orderByPos === false) {
1283 3044
                break;
1284
            }
1285
1286 3058
            $qLen            = strlen($query);
1287 3058
            $parenCount      = 0;
1288 3058
            $currentPosition = $orderByPos;
1289
1290 3058
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1291 3058
                if ($query[$currentPosition] === '(') {
1292 2471
                    $parenCount++;
1293 3058
                } elseif ($query[$currentPosition] === ')') {
1294 3050
                    $parenCount--;
1295
                }
1296
1297 3058
                $currentPosition++;
1298
            }
1299
1300 3058
            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 3056
                $offset = $currentPosition;
1304 3056
                continue;
1305
            }
1306
1307 3048
            if ($currentPosition >= $qLen - 1) {
1308
                continue;
1309
            }
1310
1311 3048
            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1312 3048
            $offset = $orderByPos;
1313
        }
1314
1315 3058
        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 3058
    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 3058
        $subQueryBuffer = '';
1330 3058
        $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 3058
        while ($parenCount >= 0 && $currentPosition >= 0) {
1335 3058
            if ($query[$currentPosition] === '(') {
1336 3053
                $parenCount--;
1337 3058
            } elseif ($query[$currentPosition] === ')') {
1338 3051
                $parenCount++;
1339
            }
1340
1341
            // Only yank query text on the same nesting level as the ORDER BY clause.
1342 3058
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1343
1344 3058
            $currentPosition--;
1345
        }
1346
1347 3058
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1348
    }
1349
1350
    /**
1351
     * {@inheritDoc}
1352
     */
1353 6079
    public function supportsLimitOffset() : bool
1354
    {
1355 6079
        return true;
1356
    }
1357
1358
    /**
1359
     * {@inheritDoc}
1360
     */
1361 4469
    public function convertBooleans($item)
1362
    {
1363 4469
        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 4469
        } elseif (is_bool($item) || is_numeric($item)) {
1372 4469
            $item = $item ? 1 : 0;
1373
        }
1374
1375 4469
        return $item;
1376
    }
1377
1378
    /**
1379
     * {@inheritDoc}
1380
     */
1381 757
    public function getCreateTemporaryTableSnippetSQL() : string
1382
    {
1383 757
        return 'CREATE TABLE';
1384
    }
1385
1386
    /**
1387
     * {@inheritDoc}
1388
     */
1389 757
    public function getTemporaryTableName(string $tableName) : string
1390
    {
1391 757
        return '#' . $tableName;
1392
    }
1393
1394
    /**
1395
     * {@inheritDoc}
1396
     */
1397 1283
    public function getDateTimeFormatString() : string
1398
    {
1399 1283
        return 'Y-m-d H:i:s.u';
1400
    }
1401
1402
    /**
1403
     * {@inheritDoc}
1404
     */
1405 714
    public function getDateFormatString() : string
1406
    {
1407 714
        return 'Y-m-d';
1408
    }
1409
1410
    /**
1411
     * {@inheritDoc}
1412
     */
1413 711
    public function getTimeFormatString() : string
1414
    {
1415 711
        return 'H:i:s';
1416
    }
1417
1418
    /**
1419
     * {@inheritDoc}
1420
     */
1421 717
    public function getDateTimeTzFormatString() : string
1422
    {
1423 717
        return 'Y-m-d H:i:s.u P';
1424
    }
1425
1426
    /**
1427
     * {@inheritDoc}
1428
     */
1429 1295
    public function getName() : string
1430
    {
1431 1295
        return 'mssql';
1432
    }
1433
1434
    /**
1435
     * {@inheritDoc}
1436
     */
1437 5446
    protected function initializeDoctrineTypeMappings() : void
1438
    {
1439 5446
        $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 5446
    }
1471
1472
    /**
1473
     * {@inheritDoc}
1474
     */
1475 1298
    public function createSavePoint(string $savepoint) : string
1476
    {
1477 1298
        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 1298
    public function rollbackSavePoint(string $savepoint) : string
1492
    {
1493 1298
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1494
    }
1495
1496
    /**
1497
     * {@inheritdoc}
1498
     */
1499 4776
    public function getForeignKeyReferentialActionSQL(string $action) : string
1500
    {
1501
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1502 4776
        if (strtoupper($action) === 'RESTRICT') {
1503 4318
            return 'NO ACTION';
1504
        }
1505
1506 4774
        return parent::getForeignKeyReferentialActionSQL($action);
1507
    }
1508
1509
    /**
1510
     * {@inheritDoc}
1511
     */
1512 3574
    public function appendLockHint(string $fromClause, ?int $lockMode) : string
1513
    {
1514 3574
        switch (true) {
1515
            case $lockMode === LockMode::NONE:
1516 3147
                return $fromClause . ' WITH (NOLOCK)';
1517
1518 3573
            case $lockMode === LockMode::PESSIMISTIC_READ:
1519 3095
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1520
1521 3572
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1522 3478
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1523
1524
            default:
1525 3174
                return $fromClause;
1526
        }
1527
    }
1528
1529
    /**
1530
     * {@inheritDoc}
1531
     */
1532 763
    public function getForUpdateSQL() : string
1533
    {
1534 763
        return ' ';
1535
    }
1536
1537
    /**
1538
     * {@inheritDoc}
1539
     */
1540 5705
    protected function getReservedKeywordsClass() : string
1541
    {
1542 5705
        return Keywords\SQLServerKeywords::class;
1543
    }
1544
1545
    /**
1546
     * {@inheritDoc}
1547
     */
1548 6085
    public function quoteSingleIdentifier(string $str) : string
1549
    {
1550 6085
        return '[' . str_replace(']', '][', $str) . ']';
1551
    }
1552
1553
    /**
1554
     * {@inheritDoc}
1555
     */
1556 4302
    public function getTruncateTableSQL(string $tableName, bool $cascade = false) : string
1557
    {
1558 4302
        $tableIdentifier = new Identifier($tableName);
1559
1560 4302
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1561
    }
1562
1563
    /**
1564
     * {@inheritDoc}
1565
     */
1566 1316
    public function getBlobTypeDeclarationSQL(array $field) : string
1567
    {
1568 1316
        return 'VARBINARY(MAX)';
1569
    }
1570
1571
    /**
1572
     * {@inheritdoc}
1573
     *
1574
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1575
     */
1576 6370
    public function getColumnDeclarationSQL(string $name, array $field) : string
1577
    {
1578 6370
        if (isset($field['columnDefinition'])) {
1579 3902
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1580
        } else {
1581 6368
            $collation = isset($field['collation']) && $field['collation'] ?
1582 6368
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1583
1584 6368
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1585
1586 6368
            $unique = isset($field['unique']) && $field['unique'] ?
1587 6368
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1588
1589 6368
            $check = isset($field['check']) && $field['check'] ?
1590 6368
                ' ' . $field['check'] : '';
1591
1592 6368
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1593 6368
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1594
        }
1595
1596 6370
        return $name . ' ' . $columnDef;
1597
    }
1598
1599
    /**
1600
     * {@inheritdoc}
1601
     */
1602 3937
    protected function getLikeWildcardCharacters() : string
1603
    {
1604 3937
        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 5477
    private function generateDefaultConstraintName(string $table, string $column) : string
1614
    {
1615 5477
        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 5479
    private function generateIdentifierName(string $identifier) : string
1624
    {
1625
        // Always generate name for unquoted identifiers to ensure consistency.
1626 5479
        $identifier = new Identifier($identifier);
1627
1628 5479
        return strtoupper(dechex(crc32($identifier->getName())));
1629
    }
1630
}
1631