Failed Conditions
Push — master ( 01c22b...e42c1f )
by Marco
79:13 queued 10s
created

SQLServerPlatform::getTrimExpression()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 41
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 10.1359

Importance

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