supportsCreateDropDatabase()   A
last analyzed

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