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