Failed Conditions
Pull Request — develop (#3348)
by Sergei
10:40
created

getAlterTableAddDefaultConstraintClause()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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