Completed
Push — develop ( dcb0ff...425513 )
by Sergei
23s queued 13s
created

SQLServerPlatform::getCurrentDatabaseExpression()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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 getCurrentDatabaseExpression() : string
1104
    {
1105 5427
        return 'DB_NAME()';
1106
    }
1107
1108
    /**
1109
     * {@inheritDoc}
1110
     */
1111 6051
    public function getSetTransactionIsolationSQL(int $level) : string
1112
    {
1113 6051
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1114
    }
1115
1116
    /**
1117
     * {@inheritDoc}
1118
     */
1119 514
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
1120
    {
1121 514
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1122
    }
1123
1124
    /**
1125
     * {@inheritDoc}
1126
     */
1127 603
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
1128
    {
1129 603
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1130
    }
1131
1132
    /**
1133
     * {@inheritDoc}
1134
     */
1135 4602
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
1136
    {
1137 4602
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1138
    }
1139
1140
    /**
1141
     * {@inheritDoc}
1142
     */
1143 3229
    public function getGuidTypeDeclarationSQL(array $column) : string
1144
    {
1145 3229
        return 'UNIQUEIDENTIFIER';
1146
    }
1147
1148
    /**
1149
     * {@inheritDoc}
1150
     */
1151 5251
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) : string
1152
    {
1153 5251
        return 'DATETIMEOFFSET(6)';
1154
    }
1155 5251
1156 5249
    /**
1157
     * {@inheritDoc}
1158
     */
1159 5251
    protected function getCharTypeDeclarationSQLSnippet(?int $length) : string
1160
    {
1161
        $sql = 'NCHAR';
1162
1163
        if ($length !== null) {
1164
            $sql .= sprintf('(%d)', $length);
1165 5544
        }
1166
1167 5544
        return $sql;
1168 4952
    }
1169
1170
    /**
1171 5542
     * {@inheritDoc}
1172
     */
1173
    protected function getVarcharTypeDeclarationSQLSnippet(?int $length) : string
1174
    {
1175
        if ($length === null) {
1176
            throw ColumnLengthRequired::new($this, 'NVARCHAR');
1177 5825
        }
1178
1179 5825
        return sprintf('NVARCHAR(%d)', $length);
1180
    }
1181
1182
    /**
1183
     * {@inheritDoc}
1184
     */
1185 6051
    public function getClobTypeDeclarationSQL(array $field) : string
1186
    {
1187 6051
        return 'VARCHAR(MAX)';
1188
    }
1189
1190
    /**
1191
     * {@inheritDoc}
1192
     */
1193 875
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : string
1194
    {
1195
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1196
    }
1197 875
1198
    /**
1199
     * {@inheritDoc}
1200
     */
1201
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
1202
    {
1203 718
        // 3 - microseconds precision length
1204
        // http://msdn.microsoft.com/en-us/library/ms187819.aspx
1205 718
        return 'DATETIME2(6)';
1206
    }
1207
1208
    /**
1209
     * {@inheritDoc}
1210
     */
1211 716
    public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string
1212
    {
1213 716
        return 'DATE';
1214
    }
1215
1216
    /**
1217
     * {@inheritDoc}
1218
     */
1219 4292
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string
1220
    {
1221 4292
        return 'TIME(0)';
1222
    }
1223
1224
    /**
1225
     * {@inheritDoc}
1226
     */
1227 3024
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
1228
    {
1229 3024
        return 'BIT';
1230
    }
1231 3024
1232 2754
    /**
1233
     * {@inheritDoc}
1234
     */
1235 3024
    protected function doModifyLimitQuery(string $query, ?int $limit, int $offset) : string
1236 3023
    {
1237 3023
        $where = [];
1238
1239 51
        if ($offset > 0) {
1240
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1241
        }
1242 3024
1243 51
        if ($limit !== null) {
1244
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1245
            $top     = sprintf('TOP %d', $offset + $limit);
1246
        } else {
1247
            $top = 'TOP 9223372036854775807';
1248
        }
1249
1250 3023
        if (empty($where)) {
1251
            return $query;
1252
        }
1253
1254 3023
        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
1255
        // Even if the TOP n is very large, the use of a CTE will
1256 3023
        // allow the SQL Server query planner to optimize it so it doesn't
1257
        // actually scan the entire range covered by the TOP clause.
1258
        if (! preg_match('/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/is', $query, $matches)) {
1259 3016
            return $query;
1260
        }
1261
1262
        $query = $matches[1] . $top . ' ' . $matches[2];
1263 3023
1264
        if (stristr($query, 'ORDER BY')) {
1265
            // Inner order by is not valid in SQL Server for our purposes
1266
            // unless it's in a TOP N subquery.
1267
            $query = $this->scrubInnerOrderBy($query);
1268 23
        }
1269 3023
1270 3023
        // Build a new limited query around the original, using a CTE
1271
        return sprintf(
1272
            'WITH dctrn_cte AS (%s) '
1273
            . 'SELECT * FROM ('
1274
            . 'SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte'
1275
            . ') AS doctrine_tbl '
1276
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1277
            $query,
1278 3016
            implode(' AND ', $where)
1279
        );
1280 3016
    }
1281 3016
1282
    /**
1283 3016
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
1284 3016
     * Caveat: will leave ORDER BY in TOP N subqueries.
1285 3016
     */
1286 3002
    private function scrubInnerOrderBy(string $query) : string
1287
    {
1288
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1289 3016
        $offset = 0;
1290 3016
1291 3016
        while ($count-- > 0) {
1292
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1293 3016
            if ($orderByPos === false) {
1294 3016
                break;
1295 2451
            }
1296 3016
1297 3008
            $qLen            = strlen($query);
1298
            $parenCount      = 0;
1299
            $currentPosition = $orderByPos;
1300 3016
1301
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1302
                if ($query[$currentPosition] === '(') {
1303 3016
                    $parenCount++;
1304
                } elseif ($query[$currentPosition] === ')') {
1305
                    $parenCount--;
1306 3014
                }
1307 3014
1308
                $currentPosition++;
1309
            }
1310 3006
1311
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1312
                // If the order by clause is in a TOP N subquery, do not remove
1313
                // it and continue iteration from the current position.
1314 3006
                $offset = $currentPosition;
1315 3006
                continue;
1316
            }
1317
1318 3016
            if ($currentPosition >= $qLen - 1) {
1319
                continue;
1320
            }
1321
1322
            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1323
            $offset = $orderByPos;
1324
        }
1325
1326
        return $query;
1327
    }
1328
1329 3016
    /**
1330
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
1331
     *
1332 3016
     * @param string $query           The query
1333 3016
     * @param int    $currentPosition Start position of ORDER BY clause
1334
     *
1335
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
1336
     */
1337 3016
    private function isOrderByInTopNSubquery(string $query, int $currentPosition) : bool
1338 3016
    {
1339 3011
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1340 3016
        $subQueryBuffer = '';
1341 3009
        $parenCount     = 0;
1342
1343
        // If $parenCount goes negative, we've exited the subquery we're examining.
1344
        // If $currentPosition goes negative, we've reached the beginning of the query.
1345 3016
        while ($parenCount >= 0 && $currentPosition >= 0) {
1346
            if ($query[$currentPosition] === '(') {
1347 3016
                $parenCount--;
1348
            } elseif ($query[$currentPosition] === ')') {
1349
                $parenCount++;
1350 3016
            }
1351
1352
            // Only yank query text on the same nesting level as the ORDER BY clause.
1353
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1354
1355
            $currentPosition--;
1356 5865
        }
1357
1358 5865
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1359
    }
1360
1361
    /**
1362
     * {@inheritDoc}
1363
     */
1364 4292
    public function supportsLimitOffset() : bool
1365
    {
1366 4292
        return true;
1367
    }
1368
1369
    /**
1370
     * {@inheritDoc}
1371
     */
1372
    public function convertBooleans($item)
1373
    {
1374 4292
        if (is_array($item)) {
1375 4292
            foreach ($item as $key => $value) {
1376
                if (! is_bool($value) && ! is_numeric($value)) {
1377
                    continue;
1378 4292
                }
1379
1380
                $item[$key] = $value ? 1 : 0;
1381
            }
1382
        } elseif (is_bool($item) || is_numeric($item)) {
1383
            $item = $item ? 1 : 0;
1384 518
        }
1385
1386 518
        return $item;
1387
    }
1388
1389
    /**
1390
     * {@inheritDoc}
1391
     */
1392 518
    public function getCreateTemporaryTableSnippetSQL() : string
1393
    {
1394 518
        return 'CREATE TABLE';
1395
    }
1396
1397
    /**
1398
     * {@inheritDoc}
1399
     */
1400 871
    public function getTemporaryTableName(string $tableName) : string
1401
    {
1402 871
        return '#' . $tableName;
1403
    }
1404
1405
    /**
1406
     * {@inheritDoc}
1407
     */
1408 488
    public function getDateTimeFormatString() : string
1409
    {
1410 488
        return 'Y-m-d H:i:s.u';
1411
    }
1412
1413
    /**
1414
     * {@inheritDoc}
1415
     */
1416 486
    public function getDateFormatString() : string
1417
    {
1418 486
        return 'Y-m-d';
1419
    }
1420
1421
    /**
1422
     * {@inheritDoc}
1423
     */
1424 490
    public function getTimeFormatString() : string
1425
    {
1426 490
        return 'H:i:s';
1427
    }
1428
1429
    /**
1430
     * {@inheritDoc}
1431
     */
1432 5437
    public function getDateTimeTzFormatString() : string
1433
    {
1434 5437
        return 'Y-m-d H:i:s.u P';
1435
    }
1436
1437
    /**
1438
     * {@inheritDoc}
1439
     */
1440 5268
    public function getName() : string
1441
    {
1442 5268
        return 'mssql';
1443
    }
1444
1445
    /**
1446
     * {@inheritDoc}
1447
     */
1448
    protected function initializeDoctrineTypeMappings() : void
1449
    {
1450
        $this->doctrineTypeMapping = [
1451
            'bigint'           => 'bigint',
1452
            'binary'           => 'binary',
1453
            'bit'              => 'boolean',
1454
            'char'             => 'string',
1455
            'date'             => 'date',
1456
            'datetime'         => 'datetime',
1457
            'datetime2'        => 'datetime',
1458
            'datetimeoffset'   => 'datetimetz',
1459
            'decimal'          => 'decimal',
1460
            'double'           => 'float',
1461
            'double precision' => 'float',
1462
            'float'            => 'float',
1463
            'image'            => 'blob',
1464
            'int'              => 'integer',
1465
            'money'            => 'integer',
1466
            'nchar'            => 'string',
1467
            'ntext'            => 'text',
1468
            'numeric'          => 'decimal',
1469
            'nvarchar'         => 'string',
1470
            'real'             => 'float',
1471
            'smalldatetime'    => 'datetime',
1472
            'smallint'         => 'smallint',
1473 5268
            'smallmoney'       => 'integer',
1474
            'text'             => 'text',
1475
            'time'             => 'time',
1476
            'tinyint'          => 'smallint',
1477
            'uniqueidentifier' => 'guid',
1478 881
            'varbinary'        => 'binary',
1479
            'varchar'          => 'string',
1480 881
        ];
1481
    }
1482
1483
    /**
1484
     * {@inheritDoc}
1485
     */
1486
    public function createSavePoint(string $savepoint) : string
1487
    {
1488
        return 'SAVE TRANSACTION ' . $savepoint;
1489
    }
1490
1491
    /**
1492
     * {@inheritDoc}
1493
     */
1494 881
    public function releaseSavePoint(string $savepoint) : string
1495
    {
1496 881
        return '';
1497
    }
1498
1499
    /**
1500
     * {@inheritDoc}
1501
     */
1502 4620
    public function rollbackSavePoint(string $savepoint) : string
1503
    {
1504
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1505 4620
    }
1506 4302
1507
    /**
1508
     * {@inheritdoc}
1509 4618
     */
1510
    public function getForeignKeyReferentialActionSQL(string $action) : string
1511
    {
1512
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1513
        if (strtoupper($action) === 'RESTRICT') {
1514
            return 'NO ACTION';
1515 3402
        }
1516
1517 3402
        return parent::getForeignKeyReferentialActionSQL($action);
1518
    }
1519 3101
1520
    /**
1521 3401
     * {@inheritDoc}
1522 3051
     */
1523
    public function appendLockHint(string $fromClause, ?int $lockMode) : string
1524 3400
    {
1525 3306
        switch (true) {
1526
            case $lockMode === LockMode::NONE:
1527
                return $fromClause . ' WITH (NOLOCK)';
1528 3127
1529
            case $lockMode === LockMode::PESSIMISTIC_READ:
1530
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1531
1532
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1533
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1534
1535 522
            default:
1536
                return $fromClause;
1537 522
        }
1538
    }
1539
1540
    /**
1541
     * {@inheritDoc}
1542
     */
1543 5638
    public function getForUpdateSQL() : string
1544
    {
1545 5638
        return ' ';
1546
    }
1547
1548
    /**
1549
     * {@inheritDoc}
1550
     */
1551 5810
    protected function getReservedKeywordsClass() : string
1552
    {
1553 5810
        return Keywords\SQLServerKeywords::class;
1554
    }
1555
1556
    /**
1557
     * {@inheritDoc}
1558
     */
1559 4088
    public function quoteSingleIdentifier(string $str) : string
1560
    {
1561 4088
        return '[' . str_replace(']', '][', $str) . ']';
1562
    }
1563 4088
1564
    /**
1565
     * {@inheritDoc}
1566
     */
1567
    public function getTruncateTableSQL(string $tableName, bool $cascade = false) : string
1568
    {
1569 893
        $tableIdentifier = new Identifier($tableName);
1570
1571 893
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1572
    }
1573
1574
    /**
1575
     * {@inheritDoc}
1576
     */
1577
    public function getBlobTypeDeclarationSQL(array $field) : string
1578
    {
1579 6085
        return 'VARBINARY(MAX)';
1580
    }
1581 6085
1582 3902
    /**
1583
     * {@inheritdoc}
1584 6083
     *
1585 6083
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1586
     */
1587 6083
    public function getColumnDeclarationSQL(string $name, array $field) : string
1588
    {
1589 6083
        if (isset($field['columnDefinition'])) {
1590 6083
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1591
        } else {
1592 6083
            $collation = isset($field['collation']) && $field['collation'] ?
1593 6083
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1594
1595 6083
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1596 6083
1597
            $unique = isset($field['unique']) && $field['unique'] ?
1598
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1599 6085
1600
            $check = isset($field['check']) && $field['check'] ?
1601
                ' ' . $field['check'] : '';
1602
1603
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1604
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1605 3653
        }
1606
1607 3653
        return $name . ' ' . $columnDef;
1608
    }
1609
1610
    /**
1611
     * {@inheritdoc}
1612
     */
1613
    protected function getLikeWildcardCharacters() : string
1614
    {
1615
        return parent::getLikeWildcardCharacters() . '[]^';
1616 5279
    }
1617
1618 5279
    /**
1619
     * Returns a unique default constraint name for a table and column.
1620
     *
1621
     * @param string $table  Name of the table to generate the unique default constraint name for.
1622
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1623
     */
1624
    private function generateDefaultConstraintName(string $table, string $column) : string
1625
    {
1626 5281
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1627
    }
1628
1629 5281
    /**
1630
     * Returns a hash value for a given identifier.
1631 5281
     *
1632
     * @param string $identifier Identifier to generate a hash value for.
1633
     */
1634
    private function generateIdentifierName(string $identifier) : string
1635
    {
1636
        // Always generate name for unquoted identifiers to ensure consistency.
1637
        $identifier = new Identifier($identifier);
1638
1639
        return strtoupper(dechex(crc32($identifier->getName())));
1640
    }
1641
}
1642