Passed
Push — 2.9 ( 9eef7c...761ac0 )
by Sergei
02:56 queued 12s
created

SQLServerPlatform::getCreateIndexSQL()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 9
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 2
nop 2
crap 3
1
<?php
2
3
namespace Doctrine\DBAL\Platforms;
4
5
use Doctrine\DBAL\LockMode;
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\ColumnDiff;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Doctrine\DBAL\Schema\Identifier;
10
use Doctrine\DBAL\Schema\Index;
11
use Doctrine\DBAL\Schema\Table;
12
use Doctrine\DBAL\Schema\TableDiff;
13
use InvalidArgumentException;
14
use function array_merge;
15
use function array_unique;
16
use function array_values;
17
use function count;
18
use function crc32;
19
use function dechex;
20
use function explode;
21
use function func_get_args;
22
use function implode;
23
use function is_array;
24
use function is_bool;
25
use function is_numeric;
26
use function is_string;
27
use function preg_match;
28
use function preg_replace;
29
use function sprintf;
30
use function str_replace;
31
use function stripos;
32
use function stristr;
33
use function strlen;
34
use function strpos;
35
use function strtoupper;
36
use function substr;
37
use function substr_count;
38
39
/**
40
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
41
 * Microsoft SQL Server database platform.
42
 */
43
class SQLServerPlatform extends AbstractPlatform
44
{
45
    /**
46
     * {@inheritdoc}
47
     */
48
    public function getCurrentDateSQL()
49 164
    {
50
        return $this->getConvertExpression('date', 'GETDATE()');
51 164
    }
52
53
    /**
54
     * {@inheritdoc}
55
     */
56
    public function getCurrentTimeSQL()
57 83
    {
58
        return $this->getConvertExpression('time', 'GETDATE()');
59 83
    }
60
61
    /**
62
     * Returns an expression that converts an expression of one data type to another.
63
     *
64
     * @param string $dataType   The target native data type. Alias data types cannot be used.
65
     * @param string $expression The SQL expression to convert.
66
     *
67
     * @return string
68
     */
69
    private function getConvertExpression($dataType, $expression)
70 164
    {
71
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
72 164
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
78 2
    {
79
        $factorClause = '';
80 2
81
        if ($operator === '-') {
82 2
            $factorClause = '-1 * ';
83 2
        }
84
85
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
86 2
    }
87
88
    /**
89
     * {@inheritDoc}
90
     */
91
    public function getDateDiffExpression($date1, $date2)
92 6
    {
93
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
94 6
    }
95
96
    /**
97
     * {@inheritDoc}
98
     *
99
     * Microsoft SQL Server prefers "autoincrement" identity columns
100
     * since sequences can only be emulated with a table.
101
     */
102
    public function prefersIdentityColumns()
103 83
    {
104
        return true;
105 83
    }
106
107
    /**
108
     * {@inheritDoc}
109
     *
110
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
111
     */
112
    public function supportsIdentityColumns()
113 87
    {
114
        return true;
115 87
    }
116
117
    /**
118
     * {@inheritDoc}
119
     */
120
    public function supportsReleaseSavepoints()
121 2
    {
122
        return false;
123 2
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128
    public function supportsSchemas()
129 89
    {
130
        return true;
131 89
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function getDefaultSchemaName()
137 2
    {
138
        return 'dbo';
139 2
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144
    public function supportsColumnCollation()
145 245
    {
146
        return true;
147 245
    }
148
149
    /**
150
     * {@inheritDoc}
151
     */
152
    public function hasNativeGuidType()
153 4741
    {
154
        return true;
155 4741
    }
156
157
    /**
158
     * {@inheritDoc}
159
     */
160
    public function getCreateDatabaseSQL($name)
161 85
    {
162
        return 'CREATE DATABASE ' . $name;
163 85
    }
164
165
    /**
166
     * {@inheritDoc}
167
     */
168
    public function getDropDatabaseSQL($name)
169 85
    {
170
        return 'DROP DATABASE ' . $name;
171 85
    }
172
173
    /**
174
     * {@inheritDoc}
175
     */
176
    public function supportsCreateDropDatabase()
177 85
    {
178
        return true;
179 85
    }
180
181
    /**
182
     * {@inheritDoc}
183
     */
184
    public function getCreateSchemaSQL($schemaName)
185 85
    {
186
        return 'CREATE SCHEMA ' . $schemaName;
187 85
    }
188
189
    /**
190
     * {@inheritDoc}
191
     */
192
    public function getDropForeignKeySQL($foreignKey, $table)
193 164
    {
194
        if (! $foreignKey instanceof ForeignKeyConstraint) {
195 164
            $foreignKey = new Identifier($foreignKey);
196 81
        }
197
198
        if (! $table instanceof Table) {
199 164
            $table = new Identifier($table);
200 164
        }
201
202
        $foreignKey = $foreignKey->getQuotedName($this);
203 164
        $table      = $table->getQuotedName($this);
204 164
205
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
206 164
    }
207
208
    /**
209
     * {@inheritDoc}
210
     */
211
    public function getDropIndexSQL($index, $table = null)
212 6
    {
213
        if ($index instanceof Index) {
214 6
            $index = $index->getQuotedName($this);
215 4
        } elseif (! is_string($index)) {
216 2
            throw new InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
217
        }
218
219
        if (! isset($table)) {
220 6
            return 'DROP INDEX ' . $index;
221
        }
222
223
        if ($table instanceof Table) {
224 6
            $table = $table->getQuotedName($this);
225 2
        }
226
227
        return sprintf(
228 6
            <<<SQL
229
IF EXISTS (SELECT * FROM sysobjects WHERE name = '%s')
230 6
    ALTER TABLE %s DROP CONSTRAINT %s
231
ELSE
232
    DROP INDEX %s ON %s
233
SQL
234
            ,
235
            $index,
236 6
            $table,
237 6
            $index,
238 6
            $index,
239 6
            $table
240 6
        );
241
    }
242
243
    /**
244
     * {@inheritDoc}
245
     */
246
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
247 1673
    {
248
        $defaultConstraintsSql = [];
249 1673
        $commentsSql           = [];
250 1673
251
        // @todo does other code breaks because of this?
252
        // force primary keys to be not null
253
        foreach ($columns as &$column) {
254 1673
            if (isset($column['primary']) && $column['primary']) {
255 1673
                $column['notnull'] = true;
256 668
            }
257
258
            // Build default constraints SQL statements.
259
            if (isset($column['default'])) {
260 1673
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
261 346
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
262 346
            }
263
264
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
265 1673
                continue;
266 1499
            }
267
268
            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
269 388
        }
270
271
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
272 1673
273
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
274 1673
            foreach ($options['uniqueConstraints'] as $name => $definition) {
275
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
276
            }
277
        }
278
279
        if (isset($options['primary']) && ! empty($options['primary'])) {
280 1673
            $flags = '';
281 749
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
282 749
                $flags = ' NONCLUSTERED';
283 81
            }
284
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
285 749
        }
286
287
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
288 1673
289
        $check = $this->getCheckDeclarationSQL($columns);
290 1673
        if (! empty($check)) {
291 1673
            $query .= ', ' . $check;
292
        }
293
        $query .= ')';
294 1673
295
        $sql = [$query];
296 1673
297
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
298 1673
            foreach ($options['indexes'] as $index) {
299 259
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
300 259
            }
301
        }
302
303
        if (isset($options['foreignKeys'])) {
304 1673
            foreach ((array) $options['foreignKeys'] as $definition) {
305 346
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
306 91
            }
307
        }
308
309
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
310 1673
    }
311
312
    /**
313
     * {@inheritDoc}
314
     */
315
    public function getCreatePrimaryKeySQL(Index $index, $table)
316 162
    {
317
        $flags = '';
318 162
        if ($index->hasFlag('nonclustered')) {
319 162
            $flags = ' NONCLUSTERED';
320 81
        }
321
322
        return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY' . $flags . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
323 162
    }
324
325
    /**
326
     * Returns the SQL statement for creating a column comment.
327
     *
328
     * SQL Server does not support native column comments,
329
     * therefore the extended properties functionality is used
330
     * as a workaround to store them.
331
     * The property name used to store column comments is "MS_Description"
332
     * which provides compatibility with SQL Server Management Studio,
333
     * as column comments are stored in the same property there when
334
     * specifying a column's "Description" attribute.
335
     *
336
     * @param string $tableName  The quoted table name to which the column belongs.
337
     * @param string $columnName The quoted column name to create the comment for.
338
     * @param string $comment    The column's comment.
339
     *
340
     * @return string
341
     */
342
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
343 631
    {
344
        if (strpos($tableName, '.') !== false) {
345 631
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
346 162
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
347 162
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
348 162
        } else {
349
            $schemaSQL = "'dbo'";
350 469
            $tableSQL  = $this->quoteStringLiteral($tableName);
351 469
        }
352
353
        return $this->getAddExtendedPropertySQL(
354 631
            'MS_Description',
355 631
            $comment,
356 631
            'SCHEMA',
357 631
            $schemaSQL,
358 631
            'TABLE',
359 631
            $tableSQL,
360 631
            'COLUMN',
361 631
            $columnName
362 631
        );
363
    }
364
365
    /**
366
     * Returns the SQL snippet for declaring a default constraint.
367
     *
368
     * @param string  $table  Name of the table to return the default constraint declaration for.
369
     * @param mixed[] $column Column definition.
370
     *
371
     * @return string
372
     *
373
     * @throws InvalidArgumentException
374
     */
375
    public function getDefaultConstraintDeclarationSQL($table, array $column)
376 1237
    {
377
        if (! isset($column['default'])) {
378 1237
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
379
        }
380
381
        $columnName = new Identifier($column['name']);
382 1237
383
        return ' CONSTRAINT ' .
384
            $this->generateDefaultConstraintName($table, $column['name']) .
385 1237
            $this->getDefaultValueDeclarationSQL($column) .
386 1237
            ' FOR ' . $columnName->getQuotedName($this);
387 1237
    }
388
389
    /**
390
     * {@inheritDoc}
391
     */
392
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
393 162
    {
394
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
395 162
396
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
397 162
398
        return $constraint;
399 162
    }
400
401
    /**
402
     * {@inheritDoc}
403
     */
404
    public function getCreateIndexSQL(Index $index, $table)
405 666
    {
406
        $constraint = parent::getCreateIndexSQL($index, $table);
407 666
408
        if ($index->isUnique() && ! $index->isPrimary()) {
409 666
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
410 168
        }
411
412
        return $constraint;
413 666
    }
414
415
    /**
416
     * {@inheritDoc}
417
     */
418
    protected function getCreateIndexSQLFlags(Index $index)
419 666
    {
420
        $type = '';
421 666
        if ($index->isUnique()) {
422 666
            $type .= 'UNIQUE ';
423 168
        }
424
425
        if ($index->hasFlag('clustered')) {
426 666
            $type .= 'CLUSTERED ';
427 81
        } elseif ($index->hasFlag('nonclustered')) {
428 585
            $type .= 'NONCLUSTERED ';
429
        }
430
431
        return $type;
432 666
    }
433
434
    /**
435
     * Extend unique key constraint with required filters
436
     *
437
     * @param string $sql
438
     *
439
     * @return string
440
     */
441
    private function _appendUniqueConstraintDefinition($sql, Index $index)
442 330
    {
443
        $fields = [];
444 330
445
        foreach ($index->getQuotedColumns($this) as $field) {
446 330
            $fields[] = $field . ' IS NOT NULL';
447 330
        }
448
449
        return $sql . ' WHERE ' . implode(' AND ', $fields);
450 330
    }
451
452
    /**
453
     * {@inheritDoc}
454
     */
455
    public function getAlterTableSQL(TableDiff $diff)
456 1818
    {
457
        $queryParts  = [];
458 1818
        $sql         = [];
459 1818
        $columnSql   = [];
460 1818
        $commentsSql = [];
461 1818
462
        foreach ($diff->addedColumns as $column) {
463 1818
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
464 816
                continue;
465
            }
466
467
            $columnDef    = $column->toArray();
468 816
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
469 816
470
            if (isset($columnDef['default'])) {
471 816
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
472 326
            }
473
474
            $comment = $this->getColumnComment($column);
475 816
476
            if (empty($comment) && ! is_numeric($comment)) {
477 816
                continue;
478 654
            }
479
480
            $commentsSql[] = $this->getCreateColumnCommentSQL(
481 245
                $diff->name,
482 245
                $column->getQuotedName($this),
483 245
                $comment
484 245
            );
485
        }
486
487
        foreach ($diff->removedColumns as $column) {
488 1818
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
489 656
                continue;
490
            }
491
492
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
493 656
        }
494
495
        foreach ($diff->changedColumns as $columnDiff) {
496 1818
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
497 1162
                continue;
498
            }
499
500
            $column     = $columnDiff->column;
501 1162
            $comment    = $this->getColumnComment($column);
502 1162
            $hasComment = ! empty($comment) || is_numeric($comment);
503 1162
504
            if ($columnDiff->fromColumn instanceof Column) {
505 1162
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
506 757
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
507 757
508
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
509 757
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
510 170
                        $diff->name,
511 170
                        $column->getQuotedName($this),
512 170
                        $comment
513 170
                    );
514
                } elseif ($hasFromComment && ! $hasComment) {
515 676
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
516 178
                } elseif ($hasComment) {
517 593
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
518 101
                        $diff->name,
519 101
                        $column->getQuotedName($this),
520 101
                        $comment
521 101
                    );
522
                }
523
            }
524
525
            // Do not add query part if only comment has changed.
526
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
527 1162
                continue;
528 346
            }
529
530
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
531 899
532
            if ($requireDropDefaultConstraint) {
533 899
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
534 411
                    $diff->name,
535 411
                    $columnDiff->oldColumnName
536 411
                );
537
            }
538
539
            $columnDef = $column->toArray();
540 899
541
            $queryParts[] = 'ALTER COLUMN ' .
542 899
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
543 899
544
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
545 899
                continue;
546 411
            }
547
548
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
549 492
        }
550
551
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
552 1818
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
553 411
                continue;
554
            }
555
556
            $oldColumnName = new Identifier($oldColumnName);
557 411
558
            $sql[] = "sp_RENAME '" .
559 411
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
560 411
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
561 411
562
            // Recreate default constraint with new column name if necessary (for future reference).
563
            if ($column->getDefault() === null) {
564 411
                continue;
565 328
            }
566
567
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
568 83
                $diff->name,
569 83
                $oldColumnName->getQuotedName($this)
570 83
            );
571
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
572 83
        }
573
574
        $tableSql = [];
575 1818
576
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
577 1818
            return array_merge($tableSql, $columnSql);
578
        }
579
580
        foreach ($queryParts as $query) {
581 1818
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
582 1146
        }
583
584
        $sql = array_merge($sql, $commentsSql);
585 1818
586
        if ($diff->newName !== false) {
587 1818
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $diff->getNewName()->getName() . "'";
588 164
589
            /**
590
             * Rename table's default constraints names
591
             * to match the new table name.
592
             * This is necessary to ensure that the default
593
             * constraints can be referenced in future table
594
             * alterations as the table name is encoded in
595
             * default constraints' names.
596
             */
597
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
598 164
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
599
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
600 164
                "'" . $this->generateIdentifierName($diff->newName) . "') + ''', ''OBJECT'';' " .
0 ignored issues
show
Bug introduced by
It seems like $diff->newName can also be of type true; however, parameter $identifier of Doctrine\DBAL\Platforms\...enerateIdentifierName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

600
                "'" . $this->generateIdentifierName(/** @scrutinizer ignore-type */ $diff->newName) . "') + ''', ''OBJECT'';' " .
Loading history...
601 164
                'FROM sys.default_constraints dc ' .
602 164
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
603 164
                "WHERE tbl.name = '" . $diff->getNewName()->getName() . "';" .
604 164
                'EXEC sp_executesql @sql';
605 164
        }
606
607
        $sql = array_merge(
608 1818
            $this->getPreAlterTableIndexForeignKeySQL($diff),
609 1818
            $sql,
610 1818
            $this->getPostAlterTableIndexForeignKeySQL($diff)
611 1818
        );
612
613
        return array_merge($sql, $tableSql, $columnSql);
614 1818
    }
615
616
    /**
617
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
618
     *
619
     * @param string $tableName The name of the table to generate the clause for.
620
     * @param Column $column    The column to generate the clause for.
621
     *
622
     * @return string
623
     */
624
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
625 573
    {
626
        $columnDef         = $column->toArray();
627 573
        $columnDef['name'] = $column->getQuotedName($this);
628 573
629
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
630 573
    }
631
632
    /**
633
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
634
     *
635
     * @param string $tableName  The name of the table to generate the clause for.
636
     * @param string $columnName The name of the column to generate the clause for.
637
     *
638
     * @return string
639
     */
640
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
641 492
    {
642
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
643 492
    }
644
645
    /**
646
     * Checks whether a column alteration requires dropping its default constraint first.
647
     *
648
     * Different to other database vendors SQL Server implements column default values
649
     * as constraints and therefore changes in a column's default value as well as changes
650
     * in a column's type require dropping the default constraint first before being to
651
     * alter the particular column to the new definition.
652
     *
653
     * @param ColumnDiff $columnDiff The column diff to evaluate.
654
     *
655
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
656
     */
657
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
658 899
    {
659
        // We can only decide whether to drop an existing default constraint
660
        // if we know the original default value.
661
        if (! $columnDiff->fromColumn instanceof Column) {
0 ignored issues
show
introduced by
$columnDiff->fromColumn is always a sub-type of Doctrine\DBAL\Schema\Column.
Loading history...
662 899
            return false;
663 324
        }
664
665
        // We only need to drop an existing default constraint if we know the
666
        // column was defined with a default value before.
667
        if ($columnDiff->fromColumn->getDefault() === null) {
668 575
            return false;
669 166
        }
670
671
        // We need to drop an existing default constraint if the column was
672
        // defined with a default value before and it has changed.
673
        if ($columnDiff->hasChanged('default')) {
674 411
            return true;
675 328
        }
676
677
        // We need to drop an existing default constraint if the column was
678
        // defined with a default value before and the native column type has changed.
679
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
680 85
    }
681
682
    /**
683
     * Returns the SQL statement for altering a column comment.
684
     *
685
     * SQL Server does not support native column comments,
686
     * therefore the extended properties functionality is used
687
     * as a workaround to store them.
688
     * The property name used to store column comments is "MS_Description"
689
     * which provides compatibility with SQL Server Management Studio,
690
     * as column comments are stored in the same property there when
691
     * specifying a column's "Description" attribute.
692
     *
693
     * @param string $tableName  The quoted table name to which the column belongs.
694
     * @param string $columnName The quoted column name to alter the comment for.
695
     * @param string $comment    The column's comment.
696
     *
697
     * @return string
698
     */
699
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
700 170
    {
701
        if (strpos($tableName, '.') !== false) {
702 170
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
703 81
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
704 81
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
705 81
        } else {
706
            $schemaSQL = "'dbo'";
707 89
            $tableSQL  = $this->quoteStringLiteral($tableName);
708 89
        }
709
710
        return $this->getUpdateExtendedPropertySQL(
711 170
            'MS_Description',
712 170
            $comment,
713 170
            'SCHEMA',
714 170
            $schemaSQL,
715 170
            'TABLE',
716 170
            $tableSQL,
717 170
            'COLUMN',
718 170
            $columnName
719 170
        );
720
    }
721
722
    /**
723
     * Returns the SQL statement for dropping a column comment.
724
     *
725
     * SQL Server does not support native column comments,
726
     * therefore the extended properties functionality is used
727
     * as a workaround to store them.
728
     * The property name used to store column comments is "MS_Description"
729
     * which provides compatibility with SQL Server Management Studio,
730
     * as column comments are stored in the same property there when
731
     * specifying a column's "Description" attribute.
732
     *
733
     * @param string $tableName  The quoted table name to which the column belongs.
734
     * @param string $columnName The quoted column name to drop the comment for.
735
     *
736
     * @return string
737
     */
738
    protected function getDropColumnCommentSQL($tableName, $columnName)
739 178
    {
740
        if (strpos($tableName, '.') !== false) {
741 178
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
742 81
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
743 81
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
744 81
        } else {
745
            $schemaSQL = "'dbo'";
746 97
            $tableSQL  = $this->quoteStringLiteral($tableName);
747 97
        }
748
749
        return $this->getDropExtendedPropertySQL(
750 178
            'MS_Description',
751 178
            'SCHEMA',
752 178
            $schemaSQL,
753 178
            'TABLE',
754 178
            $tableSQL,
755 178
            'COLUMN',
756 178
            $columnName
757 178
        );
758
    }
759
760
    /**
761
     * {@inheritdoc}
762
     */
763
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
764 409
    {
765
        return [sprintf(
766 409
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
767 409
            $tableName,
768 409
            $oldIndexName,
769 409
            $index->getQuotedName($this)
770 409
        ),
771
        ];
772
    }
773
774
    /**
775
     * Returns the SQL statement for adding an extended property to a database object.
776
     *
777
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
778
     *
779
     * @param string      $name       The name of the property to add.
780
     * @param string|null $value      The value of the property to add.
781
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
782
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
783
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
784
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
785
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
786
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
787
     *
788
     * @return string
789
     */
790
    public function getAddExtendedPropertySQL(
791 631
        $name,
792
        $value = null,
793
        $level0Type = null,
794
        $level0Name = null,
795
        $level1Type = null,
796
        $level1Name = null,
797
        $level2Type = null,
798
        $level2Name = null
799
    ) {
800
        return 'EXEC sp_addextendedproperty ' .
801
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral($value) . ', ' .
802 631
            'N' . $this->quoteStringLiteral($level0Type) . ', ' . $level0Name . ', ' .
803 631
            'N' . $this->quoteStringLiteral($level1Type) . ', ' . $level1Name . ', ' .
804 631
            'N' . $this->quoteStringLiteral($level2Type) . ', ' . $level2Name;
805 631
    }
806
807
    /**
808
     * Returns the SQL statement for dropping an extended property from a database object.
809
     *
810
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
811
     *
812
     * @param string      $name       The name of the property to drop.
813
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
814
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
815
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
816
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
817
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
818
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
819
     *
820
     * @return string
821
     */
822
    public function getDropExtendedPropertySQL(
823 178
        $name,
824
        $level0Type = null,
825
        $level0Name = null,
826
        $level1Type = null,
827
        $level1Name = null,
828
        $level2Type = null,
829
        $level2Name = null
830
    ) {
831
        return 'EXEC sp_dropextendedproperty ' .
832
            'N' . $this->quoteStringLiteral($name) . ', ' .
833 178
            'N' . $this->quoteStringLiteral($level0Type) . ', ' . $level0Name . ', ' .
834 178
            'N' . $this->quoteStringLiteral($level1Type) . ', ' . $level1Name . ', ' .
835 178
            'N' . $this->quoteStringLiteral($level2Type) . ', ' . $level2Name;
836 178
    }
837
838
    /**
839
     * Returns the SQL statement for updating an extended property of a database object.
840
     *
841
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
842
     *
843
     * @param string      $name       The name of the property to update.
844
     * @param string|null $value      The value of the property to update.
845
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
846
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
847
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
848
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
849
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
850
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
851
     *
852
     * @return string
853
     */
854
    public function getUpdateExtendedPropertySQL(
855 170
        $name,
856
        $value = null,
857
        $level0Type = null,
858
        $level0Name = null,
859
        $level1Type = null,
860
        $level1Name = null,
861
        $level2Type = null,
862
        $level2Name = null
863
    ) {
864
        return 'EXEC sp_updateextendedproperty ' .
865
        'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral($value) . ', ' .
866 170
        'N' . $this->quoteStringLiteral($level0Type) . ', ' . $level0Name . ', ' .
867 170
        'N' . $this->quoteStringLiteral($level1Type) . ', ' . $level1Name . ', ' .
868 170
        'N' . $this->quoteStringLiteral($level2Type) . ', ' . $level2Name;
869 170
    }
870
871
    /**
872
     * {@inheritDoc}
873
     */
874
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
875 2
    {
876
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
877 2
    }
878
879
    /**
880
     * {@inheritDoc}
881
     */
882
    public function getListTablesSQL()
883
    {
884
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
885
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
886
        return "SELECT name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
887
    }
888
889
    /**
890
     * {@inheritDoc}
891
     */
892
    public function getListTableColumnsSQL($table, $database = null)
893 238
    {
894
        return "SELECT    col.name,
895
                          type.name AS type,
896
                          col.max_length AS length,
897
                          ~col.is_nullable AS notnull,
898
                          def.definition AS [default],
899
                          col.scale,
900
                          col.precision,
901
                          col.is_identity AS autoincrement,
902
                          col.collation_name AS collation,
903
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
904
                FROM      sys.columns AS col
905
                JOIN      sys.types AS type
906
                ON        col.user_type_id = type.user_type_id
907
                JOIN      sys.objects AS obj
908
                ON        col.object_id = obj.object_id
909
                JOIN      sys.schemas AS scm
910
                ON        obj.schema_id = scm.schema_id
911
                LEFT JOIN sys.default_constraints def
912
                ON        col.default_object_id = def.object_id
913
                AND       col.object_id = def.parent_object_id
914
                LEFT JOIN sys.extended_properties AS prop
915
                ON        obj.object_id = prop.major_id
916
                AND       col.column_id = prop.minor_id
917
                AND       prop.name = 'MS_Description'
918
                WHERE     obj.type = 'U'
919
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
920 238
    }
921
922
    /**
923
     * {@inheritDoc}
924
     */
925
    public function getListTableForeignKeysSQL($table, $database = null)
0 ignored issues
show
Unused Code introduced by
The parameter $database is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

925
    public function getListTableForeignKeysSQL($table, /** @scrutinizer ignore-unused */ $database = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
926 216
    {
927
        return 'SELECT f.name AS ForeignKey,
928
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
929
                OBJECT_NAME (f.parent_object_id) AS TableName,
930
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
931
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
932
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
933
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
934
                f.delete_referential_action_desc,
935
                f.update_referential_action_desc
936
                FROM sys.foreign_keys AS f
937
                INNER JOIN sys.foreign_key_columns AS fc
938
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
939
                ON f.OBJECT_ID = fc.constraint_object_id
940
                WHERE ' .
941
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
942 216
    }
943
944
    /**
945
     * {@inheritDoc}
946
     */
947
    public function getListTableIndexesSQL($table, $currentDatabase = null)
948 222
    {
949
        return "SELECT idx.name AS key_name,
950
                       col.name AS column_name,
951
                       ~idx.is_unique AS non_unique,
952
                       idx.is_primary_key AS [primary],
953
                       CASE idx.type
954
                           WHEN '1' THEN 'clustered'
955
                           WHEN '2' THEN 'nonclustered'
956
                           ELSE NULL
957
                       END AS flags
958
                FROM sys.tables AS tbl
959
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
960
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
961
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
962
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
963
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
964 222
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
965
    }
966
967
    /**
968
     * {@inheritDoc}
969
     */
970
    public function getCreateViewSQL($name, $sql)
971 2
    {
972
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
973 2
    }
974
975
    /**
976
     * {@inheritDoc}
977
     */
978
    public function getListViewsSQL($database)
979 2
    {
980
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
981 2
    }
982
983
    /**
984
     * Returns the where clause to filter schema and table name in a query.
985
     *
986
     * @param string $table        The full qualified name of the table.
987
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
988
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
989
     *
990
     * @return string
991
     */
992
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
993 576
    {
994
        if (strpos($table, '.') !== false) {
995 576
            [$schema, $table] = explode('.', $table);
996 245
            $schema           = $this->quoteStringLiteral($schema);
997 245
            $table            = $this->quoteStringLiteral($table);
998 245
        } else {
999
            $schema = 'SCHEMA_NAME()';
1000 333
            $table  = $this->quoteStringLiteral($table);
1001 333
        }
1002
1003
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1004 576
    }
1005
1006
    /**
1007
     * {@inheritDoc}
1008
     */
1009
    public function getDropViewSQL($name)
1010 2
    {
1011
        return 'DROP VIEW ' . $name;
1012 2
    }
1013
1014
    /**
1015
     * {@inheritDoc}
1016
     *
1017
     * @deprecated Use application-generated UUIDs instead
1018
     */
1019
    public function getGuidExpression()
1020
    {
1021
        return 'NEWID()';
1022
    }
1023
1024
    /**
1025
     * {@inheritDoc}
1026
     */
1027
    public function getLocateExpression($str, $substr, $startPos = false)
1028 2
    {
1029
        if ($startPos === false) {
1030 2
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1031 2
        }
1032
1033
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1034 2
    }
1035
1036
    /**
1037
     * {@inheritDoc}
1038
     */
1039
    public function getModExpression($expression1, $expression2)
1040
    {
1041
        return $expression1 . ' % ' . $expression2;
1042
    }
1043
1044
    /**
1045
     * {@inheritDoc}
1046
     */
1047
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1048 72
    {
1049
        if (! $char) {
1050 72
            switch ($pos) {
1051 16
                case TrimMode::LEADING:
1052
                    $trimFn = 'LTRIM';
1053 4
                    break;
1054 4
1055
                case TrimMode::TRAILING:
1056
                    $trimFn = 'RTRIM';
1057 4
                    break;
1058 4
1059
                default:
1060
                    return 'LTRIM(RTRIM(' . $str . '))';
1061 8
            }
1062
1063
            return $trimFn . '(' . $str . ')';
1064 8
        }
1065
1066
        /** Original query used to get those expressions
1067
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1068
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1069
          select @c as string
1070
          , @trim_char as trim_char
1071
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1072
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1073
          , 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;
1074
         */
1075
        $pattern = "'%[^' + " . $char . " + ']%'";
1076 56
1077
        if ($pos === TrimMode::LEADING) {
1078 56
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1079 14
        }
1080
1081
        if ($pos === TrimMode::TRAILING) {
1082 42
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1083 14
        }
1084
1085
        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))';
1086 28
    }
1087
1088
    /**
1089
     * {@inheritDoc}
1090
     */
1091
    public function getConcatExpression()
1092 81
    {
1093
        $args = func_get_args();
1094 81
1095
        return '(' . implode(' + ', $args) . ')';
1096 81
    }
1097
1098
    /**
1099
     * {@inheritDoc}
1100
     */
1101
    public function getListDatabasesSQL()
1102 85
    {
1103
        return 'SELECT * FROM sys.databases';
1104 85
    }
1105
1106
    /**
1107
     * {@inheritDoc}
1108
     */
1109
    public function getListNamespacesSQL()
1110 4
    {
1111
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1112 4
    }
1113
1114
    /**
1115
     * {@inheritDoc}
1116
     */
1117
    public function getSubstringExpression($value, $from, $length = null)
1118
    {
1119
        if ($length !== null) {
1120
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1121
        }
1122
1123
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1124
    }
1125
1126
    /**
1127
     * {@inheritDoc}
1128
     */
1129
    public function getLengthExpression($column)
1130
    {
1131
        return 'LEN(' . $column . ')';
1132
    }
1133
1134
    /**
1135
     * {@inheritDoc}
1136
     */
1137
    public function getSetTransactionIsolationSQL($level)
1138 81
    {
1139
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1140 81
    }
1141
1142
    /**
1143
     * {@inheritDoc}
1144
     */
1145
    public function getIntegerTypeDeclarationSQL(array $field)
1146 1412
    {
1147
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1148 1412
    }
1149
1150
    /**
1151
     * {@inheritDoc}
1152
     */
1153
    public function getBigIntTypeDeclarationSQL(array $field)
1154 30
    {
1155
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1156 30
    }
1157
1158
    /**
1159
     * {@inheritDoc}
1160
     */
1161
    public function getSmallIntTypeDeclarationSQL(array $field)
1162 2
    {
1163
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1164 2
    }
1165
1166
    /**
1167
     * {@inheritDoc}
1168
     */
1169
    public function getGuidTypeDeclarationSQL(array $field)
1170 81
    {
1171
        return 'UNIQUEIDENTIFIER';
1172 81
    }
1173
1174
    /**
1175
     * {@inheritDoc}
1176
     */
1177
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1178 2099
    {
1179
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1180 2099
    }
1181
1182
    /**
1183
     * {@inheritdoc}
1184
     */
1185
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1186 85
    {
1187
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1188 85
    }
1189
1190
    /**
1191
     * {@inheritdoc}
1192
     */
1193
    public function getBinaryMaxLength()
1194 247
    {
1195
        return 8000;
1196 247
    }
1197
1198
    /**
1199
     * {@inheritDoc}
1200
     */
1201
    public function getClobTypeDeclarationSQL(array $field)
1202 135
    {
1203
        return 'VARCHAR(MAX)';
1204 135
    }
1205
1206
    /**
1207
     * {@inheritDoc}
1208
     */
1209
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1210 1412
    {
1211
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1212 1412
    }
1213
1214
    /**
1215
     * {@inheritDoc}
1216
     */
1217
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1218
    {
1219
        return 'DATETIME';
1220
    }
1221
1222
    /**
1223
     * {@inheritDoc}
1224
     */
1225
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1226
    {
1227
        return 'DATETIME';
1228
    }
1229
1230
    /**
1231
     * {@inheritDoc}
1232
     */
1233
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1234
    {
1235
        return 'DATETIME';
1236
    }
1237
1238
    /**
1239
     * {@inheritDoc}
1240
     */
1241
    public function getBooleanTypeDeclarationSQL(array $field)
1242 115
    {
1243
        return 'BIT';
1244 115
    }
1245
1246
    /**
1247
     * {@inheritDoc}
1248
     */
1249
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1250 1134
    {
1251
        $where = [];
1252 1134
1253
        if ($offset > 0) {
1254 1134
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1255 108
        }
1256
1257
        if ($limit !== null) {
1258 1134
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1259 1080
            $top     = sprintf('TOP %d', $offset + $limit);
1260 1080
        } else {
1261
            $top = 'TOP 9223372036854775807';
1262 54
        }
1263
1264
        if (empty($where)) {
1265 1134
            return $query;
1266 54
        }
1267
1268
        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
1269
        // Even if the TOP n is very large, the use of a CTE will
1270
        // allow the SQL Server query planner to optimize it so it doesn't
1271
        // actually scan the entire range covered by the TOP clause.
1272
        $selectPattern  = '/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/im';
1273 1080
        $replacePattern = sprintf('$1%s $2', $top);
1274 1080
        $query          = preg_replace($selectPattern, $replacePattern, $query);
1275 1080
1276
        if (stristr($query, 'ORDER BY')) {
1277 1080
            // Inner order by is not valid in SQL Server for our purposes
1278
            // unless it's in a TOP N subquery.
1279
            $query = $this->scrubInnerOrderBy($query);
1280 702
        }
1281
1282
        // Build a new limited query around the original, using a CTE
1283
        return sprintf(
1284 1080
            'WITH dctrn_cte AS (%s) '
1285
            . 'SELECT * FROM ('
1286
            . 'SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte'
1287
            . ') AS doctrine_tbl '
1288
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1289 1080
            $query,
1290 1080
            implode(' AND ', $where)
1291 1080
        );
1292
    }
1293
1294
    /**
1295
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
1296
     * Caveat: will leave ORDER BY in TOP N subqueries.
1297
     *
1298
     * @param string $query
1299
     *
1300
     * @return string
1301
     */
1302
    private function scrubInnerOrderBy($query)
1303 702
    {
1304
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1305 702
        $offset = 0;
1306 702
1307
        while ($count-- > 0) {
1308 702
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1309 702
            if ($orderByPos === false) {
1310 702
                break;
1311 54
            }
1312
1313
            $qLen            = strlen($query);
1314 702
            $parenCount      = 0;
1315 702
            $currentPosition = $orderByPos;
1316 702
1317
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1318 702
                if ($query[$currentPosition] === '(') {
1319 702
                    $parenCount++;
1320 54
                } elseif ($query[$currentPosition] === ')') {
1321 702
                    $parenCount--;
1322 378
                }
1323
1324
                $currentPosition++;
1325 702
            }
1326
1327
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1328 702
                // If the order by clause is in a TOP N subquery, do not remove
1329
                // it and continue iteration from the current position.
1330
                $offset = $currentPosition;
1331 621
                continue;
1332 621
            }
1333
1334
            if ($currentPosition >= $qLen - 1) {
1335 270
                continue;
1336
            }
1337
1338
            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1339 270
            $offset = $orderByPos;
1340 270
        }
1341
        return $query;
1342 702
    }
1343
1344
    /**
1345
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
1346
     *
1347
     * @param string $query           The query
1348
     * @param int    $currentPosition Start position of ORDER BY clause
1349
     *
1350
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
1351
     */
1352
    private function isOrderByInTopNSubquery($query, $currentPosition)
1353 702
    {
1354
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1355
        $subQueryBuffer = '';
1356 702
        $parenCount     = 0;
1357 702
1358
        // If $parenCount goes negative, we've exited the subquery we're examining.
1359
        // If $currentPosition goes negative, we've reached the beginning of the query.
1360
        while ($parenCount >= 0 && $currentPosition >= 0) {
1361 702
            if ($query[$currentPosition] === '(') {
1362 702
                $parenCount--;
1363 459
            } elseif ($query[$currentPosition] === ')') {
1364 702
                $parenCount++;
1365 378
            }
1366
1367
            // Only yank query text on the same nesting level as the ORDER BY clause.
1368
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1369 702
1370
            $currentPosition--;
1371 702
        }
1372
1373
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1374 702
    }
1375
1376
    /**
1377
     * {@inheritDoc}
1378
     */
1379
    public function supportsLimitOffset()
1380
    {
1381
        return false;
1382
    }
1383
1384
    /**
1385
     * {@inheritDoc}
1386
     */
1387
    public function convertBooleans($item)
1388 89
    {
1389
        if (is_array($item)) {
1390 89
            foreach ($item as $key => $value) {
1391
                if (! is_bool($value) && ! is_numeric($item)) {
1392
                    continue;
1393
                }
1394
1395
                $item[$key] = $value ? 1 : 0;
1396
            }
1397
        } elseif (is_bool($item) || is_numeric($item)) {
1398 89
            $item = $item ? 1 : 0;
1399 89
        }
1400
1401
        return $item;
1402 89
    }
1403
1404
    /**
1405
     * {@inheritDoc}
1406
     */
1407
    public function getCreateTemporaryTableSnippetSQL()
1408 4
    {
1409
        return 'CREATE TABLE';
1410 4
    }
1411
1412
    /**
1413
     * {@inheritDoc}
1414
     */
1415
    public function getTemporaryTableName($tableName)
1416 4
    {
1417
        return '#' . $tableName;
1418 4
    }
1419
1420
    /**
1421
     * {@inheritDoc}
1422
     */
1423
    public function getDateTimeFormatString()
1424
    {
1425
        return 'Y-m-d H:i:s.000';
1426
    }
1427
1428
    /**
1429
     * {@inheritDoc}
1430
     */
1431
    public function getDateFormatString()
1432
    {
1433
        return 'Y-m-d H:i:s.000';
1434
    }
1435
1436
    /**
1437
     * {@inheritDoc}
1438
     */
1439
    public function getTimeFormatString()
1440
    {
1441
        return 'Y-m-d H:i:s.000';
1442
    }
1443
1444
    /**
1445
     * {@inheritDoc}
1446
     */
1447
    public function getDateTimeTzFormatString()
1448
    {
1449
        return $this->getDateTimeFormatString();
1450
    }
1451
1452
    /**
1453
     * {@inheritDoc}
1454
     */
1455
    public function getName()
1456 167
    {
1457
        return 'mssql';
1458 167
    }
1459
1460
    /**
1461
     * {@inheritDoc}
1462
     */
1463
    protected function initializeDoctrineTypeMappings()
1464 407
    {
1465
        $this->doctrineTypeMapping = [
1466 407
            'bigint' => 'bigint',
1467
            'numeric' => 'decimal',
1468
            'bit' => 'boolean',
1469
            'smallint' => 'smallint',
1470
            'decimal' => 'decimal',
1471
            'smallmoney' => 'integer',
1472
            'int' => 'integer',
1473
            'tinyint' => 'smallint',
1474
            'money' => 'integer',
1475
            'float' => 'float',
1476
            'real' => 'float',
1477
            'double' => 'float',
1478
            'double precision' => 'float',
1479
            'smalldatetime' => 'datetime',
1480
            'datetime' => 'datetime',
1481
            'char' => 'string',
1482
            'varchar' => 'string',
1483
            'text' => 'text',
1484
            'nchar' => 'string',
1485
            'nvarchar' => 'string',
1486
            'ntext' => 'text',
1487
            'binary' => 'binary',
1488
            'varbinary' => 'binary',
1489
            'image' => 'blob',
1490
            'uniqueidentifier' => 'guid',
1491
        ];
1492
    }
1493 407
1494
    /**
1495
     * {@inheritDoc}
1496
     */
1497
    public function createSavePoint($savepoint)
1498 2
    {
1499
        return 'SAVE TRANSACTION ' . $savepoint;
1500 2
    }
1501
1502
    /**
1503
     * {@inheritDoc}
1504
     */
1505
    public function releaseSavePoint($savepoint)
1506
    {
1507
        return '';
1508
    }
1509
1510
    /**
1511
     * {@inheritDoc}
1512
     */
1513
    public function rollbackSavePoint($savepoint)
1514 2
    {
1515
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1516 2
    }
1517
1518
    /**
1519
     * {@inheritdoc}
1520
     */
1521
    public function getForeignKeyReferentialActionSQL($action)
1522 569
    {
1523
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1524
        if (strtoupper($action) === 'RESTRICT') {
1525 569
            return 'NO ACTION';
1526 81
        }
1527
1528
        return parent::getForeignKeyReferentialActionSQL($action);
1529 488
    }
1530
1531
    /**
1532
     * {@inheritDoc}
1533
     */
1534
    public function appendLockHint($fromClause, $lockMode)
1535 193
    {
1536
        switch (true) {
1537 175
            case $lockMode === LockMode::NONE:
1538 18
                return $fromClause . ' WITH (NOLOCK)';
1539 27
1540
            case $lockMode === LockMode::PESSIMISTIC_READ:
1541 166
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1542 27
1543
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1544 139
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1545 31
1546
            default:
1547
                return $fromClause;
1548 108
        }
1549
    }
1550
1551
    /**
1552
     * {@inheritDoc}
1553
     */
1554
    public function getForUpdateSQL()
1555 4
    {
1556
        return ' ';
1557 4
    }
1558
1559
    /**
1560
     * {@inheritDoc}
1561
     */
1562
    protected function getReservedKeywordsClass()
1563 1674
    {
1564
        return Keywords\SQLServerKeywords::class;
1565 1674
    }
1566
1567
    /**
1568
     * {@inheritDoc}
1569
     */
1570
    public function quoteSingleIdentifier($str)
1571 2465
    {
1572
        return '[' . str_replace(']', '][', $str) . ']';
1573 2465
    }
1574
1575
    /**
1576
     * {@inheritDoc}
1577
     */
1578
    public function getTruncateTableSQL($tableName, $cascade = false)
1579 97
    {
1580
        $tableIdentifier = new Identifier($tableName);
1581 97
1582
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1583 97
    }
1584
1585
    /**
1586
     * {@inheritDoc}
1587
     */
1588
    public function getBlobTypeDeclarationSQL(array $field)
1589 97
    {
1590
        return 'VARBINARY(MAX)';
1591 97
    }
1592
1593
    /**
1594
     * {@inheritdoc}
1595
     *
1596
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1597 1561
     */
1598
    public function getColumnDeclarationSQL($name, array $field)
1599 1561
    {
1600
        if (isset($field['columnDefinition'])) {
1601
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1602
        } else {
1603 1561
            $collation = isset($field['collation']) && $field['collation'] ?
1604 324
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1605
1606
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1607 1237
1608
            $unique = isset($field['unique']) && $field['unique'] ?
1609 1237
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1610 255
1611
            $check = isset($field['check']) && $field['check'] ?
1612
                ' ' . $field['check'] : '';
1613 1069
1614 83
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1615
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1616
        }
1617 988
1618 85
        return $name . ' ' . $columnDef;
1619
    }
1620
1621 986
    /**
1622
     * Returns a unique default constraint name for a table and column.
1623
     *
1624
     * @param string $table  Name of the table to generate the unique default constraint name for.
1625
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1626
     *
1627
     * @return string
1628
     */
1629 2807
    private function generateDefaultConstraintName($table, $column)
1630
    {
1631 2807
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1632 81
    }
1633
1634 2726
    /**
1635 2726
     * Returns a hash value for a given identifier.
1636
     *
1637 2726
     * @param string $identifier Identifier to generate a hash value for.
1638
     *
1639 2726
     * @return string
1640 2726
     */
1641
    private function generateIdentifierName($identifier)
1642 2726
    {
1643 2726
        // Always generate name for unquoted identifiers to ensure consistency.
1644
        $identifier = new Identifier($identifier);
1645 2726
1646 2726
        return strtoupper(dechex(crc32($identifier->getName())));
1647
    }
1648
}
1649