Passed
Push — 2.9 ( 761ac0...2c6848 )
by Sergei
13:07 queued 10s
created

SQLServerPlatform::supportsIdentityColumns()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 1
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 164
    public function getCurrentDateSQL()
49
    {
50 164
        return $this->getConvertExpression('date', 'GETDATE()');
51
    }
52
53
    /**
54
     * {@inheritdoc}
55
     */
56 83
    public function getCurrentTimeSQL()
57
    {
58 83
        return $this->getConvertExpression('time', 'GETDATE()');
59
    }
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 166
    private function getConvertExpression($dataType, $expression)
70
    {
71 166
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77 2
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
78
    {
79 2
        $factorClause = '';
80
81 2
        if ($operator === '-') {
82 2
            $factorClause = '-1 * ';
83
        }
84
85 2
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
86
    }
87
88
    /**
89
     * {@inheritDoc}
90
     */
91 6
    public function getDateDiffExpression($date1, $date2)
92
    {
93 6
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
94
    }
95
96
    /**
97
     * {@inheritDoc}
98
     *
99
     * Microsoft SQL Server prefers "autoincrement" identity columns
100
     * since sequences can only be emulated with a table.
101
     */
102 83
    public function prefersIdentityColumns()
103
    {
104 83
        return true;
105
    }
106
107
    /**
108
     * {@inheritDoc}
109
     *
110
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
111
     */
112 87
    public function supportsIdentityColumns()
113
    {
114 87
        return true;
115
    }
116
117
    /**
118
     * {@inheritDoc}
119
     */
120 2
    public function supportsReleaseSavepoints()
121
    {
122 2
        return false;
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 89
    public function supportsSchemas()
129
    {
130 89
        return true;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 2
    public function getDefaultSchemaName()
137
    {
138 2
        return 'dbo';
139
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144 245
    public function supportsColumnCollation()
145
    {
146 245
        return true;
147
    }
148
149
    /**
150
     * {@inheritDoc}
151
     */
152 4741
    public function hasNativeGuidType()
153
    {
154 4741
        return true;
155
    }
156
157
    /**
158
     * {@inheritDoc}
159
     */
160 85
    public function getCreateDatabaseSQL($name)
161
    {
162 85
        return 'CREATE DATABASE ' . $name;
163
    }
164
165
    /**
166
     * {@inheritDoc}
167
     */
168 85
    public function getDropDatabaseSQL($name)
169
    {
170 85
        return 'DROP DATABASE ' . $name;
171
    }
172
173
    /**
174
     * {@inheritDoc}
175
     */
176 85
    public function supportsCreateDropDatabase()
177
    {
178 85
        return true;
179
    }
180
181
    /**
182
     * {@inheritDoc}
183
     */
184 85
    public function getCreateSchemaSQL($schemaName)
185
    {
186 85
        return 'CREATE SCHEMA ' . $schemaName;
187
    }
188
189
    /**
190
     * {@inheritDoc}
191
     */
192 164
    public function getDropForeignKeySQL($foreignKey, $table)
193
    {
194 164
        if (! $foreignKey instanceof ForeignKeyConstraint) {
195 81
            $foreignKey = new Identifier($foreignKey);
196
        }
197
198 164
        if (! $table instanceof Table) {
199 164
            $table = new Identifier($table);
200
        }
201
202 164
        $foreignKey = $foreignKey->getQuotedName($this);
203 164
        $table      = $table->getQuotedName($this);
204
205 164
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
206
    }
207
208
    /**
209
     * {@inheritDoc}
210
     */
211 6
    public function getDropIndexSQL($index, $table = null)
212
    {
213 6
        if ($index instanceof Index) {
214 4
            $index = $index->getQuotedName($this);
215 2
        } elseif (! is_string($index)) {
216
            throw new InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
217
        }
218
219 6
        if (! isset($table)) {
220
            return 'DROP INDEX ' . $index;
221
        }
222
223 6
        if ($table instanceof Table) {
224 2
            $table = $table->getQuotedName($this);
225
        }
226
227 6
        return sprintf(
228
            <<<SQL
229 6
IF EXISTS (SELECT * FROM sysobjects WHERE name = '%s')
230
    ALTER TABLE %s DROP CONSTRAINT %s
231
ELSE
232
    DROP INDEX %s ON %s
233
SQL
234
            ,
235 6
            $index,
236 6
            $table,
237 6
            $index,
238 6
            $index,
239 6
            $table
240
        );
241
    }
242
243
    /**
244
     * {@inheritDoc}
245
     */
246 1679
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
247
    {
248 1679
        $defaultConstraintsSql = [];
249 1679
        $commentsSql           = [];
250
251
        // @todo does other code breaks because of this?
252
        // force primary keys to be not null
253 1679
        foreach ($columns as &$column) {
254 1679
            if (isset($column['primary']) && $column['primary']) {
255 668
                $column['notnull'] = true;
256
            }
257
258
            // Build default constraints SQL statements.
259 1679
            if (isset($column['default'])) {
260 352
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
261 352
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
262
            }
263
264 1679
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
265 1505
                continue;
266
            }
267
268 388
            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
269
        }
270
271 1679
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
272
273 1679
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
274
            foreach ($options['uniqueConstraints'] as $name => $definition) {
275
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
276
            }
277
        }
278
279 1679
        if (isset($options['primary']) && ! empty($options['primary'])) {
280 749
            $flags = '';
281 749
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
282 81
                $flags = ' NONCLUSTERED';
283
            }
284 749
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
285
        }
286
287 1679
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
288
289 1679
        $check = $this->getCheckDeclarationSQL($columns);
290 1679
        if (! empty($check)) {
291
            $query .= ', ' . $check;
292
        }
293 1679
        $query .= ')';
294
295 1679
        $sql = [$query];
296
297 1679
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
298 259
            foreach ($options['indexes'] as $index) {
299 259
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
300
            }
301
        }
302
303 1679
        if (isset($options['foreignKeys'])) {
304 352
            foreach ((array) $options['foreignKeys'] as $definition) {
305 91
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
306
            }
307
        }
308
309 1679
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
310
    }
311
312
    /**
313
     * {@inheritDoc}
314
     */
315 162
    public function getCreatePrimaryKeySQL(Index $index, $table)
316
    {
317 162
        $flags = '';
318 162
        if ($index->hasFlag('nonclustered')) {
319 81
            $flags = ' NONCLUSTERED';
320
        }
321
322 162
        return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY' . $flags . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
323
    }
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 631
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
343
    {
344 631
        if (strpos($tableName, '.') !== false) {
345 162
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
346 162
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
347 162
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
348
        } else {
349 469
            $schemaSQL = "'dbo'";
350 469
            $tableSQL  = $this->quoteStringLiteral($tableName);
351
        }
352
353 631
        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
        );
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 1243
    public function getDefaultConstraintDeclarationSQL($table, array $column)
376
    {
377 1243
        if (! isset($column['default'])) {
378
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
379
        }
380
381 1243
        $columnName = new Identifier($column['name']);
382
383
        return ' CONSTRAINT ' .
384 1243
            $this->generateDefaultConstraintName($table, $column['name']) .
385 1243
            $this->getDefaultValueDeclarationSQL($column) .
386 1243
            ' FOR ' . $columnName->getQuotedName($this);
387
    }
388
389
    /**
390
     * {@inheritDoc}
391
     */
392 162
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
393
    {
394 162
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
395
396 162
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
397
398 162
        return $constraint;
399
    }
400
401
    /**
402
     * {@inheritDoc}
403
     */
404 666
    public function getCreateIndexSQL(Index $index, $table)
405
    {
406 666
        $constraint = parent::getCreateIndexSQL($index, $table);
407
408 666
        if ($index->isUnique() && ! $index->isPrimary()) {
409 168
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
410
        }
411
412 666
        return $constraint;
413
    }
414
415
    /**
416
     * {@inheritDoc}
417
     */
418 666
    protected function getCreateIndexSQLFlags(Index $index)
419
    {
420 666
        $type = '';
421 666
        if ($index->isUnique()) {
422 168
            $type .= 'UNIQUE ';
423
        }
424
425 666
        if ($index->hasFlag('clustered')) {
426 81
            $type .= 'CLUSTERED ';
427 585
        } elseif ($index->hasFlag('nonclustered')) {
428
            $type .= 'NONCLUSTERED ';
429
        }
430
431 666
        return $type;
432
    }
433
434
    /**
435
     * Extend unique key constraint with required filters
436
     *
437
     * @param string $sql
438
     *
439
     * @return string
440
     */
441 330
    private function _appendUniqueConstraintDefinition($sql, Index $index)
442
    {
443 330
        $fields = [];
444
445 330
        foreach ($index->getQuotedColumns($this) as $field) {
446 330
            $fields[] = $field . ' IS NOT NULL';
447
        }
448
449 330
        return $sql . ' WHERE ' . implode(' AND ', $fields);
450
    }
451
452
    /**
453
     * {@inheritDoc}
454
     */
455 1818
    public function getAlterTableSQL(TableDiff $diff)
456
    {
457 1818
        $queryParts  = [];
458 1818
        $sql         = [];
459 1818
        $columnSql   = [];
460 1818
        $commentsSql = [];
461
462 1818
        foreach ($diff->addedColumns as $column) {
463 814
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
464
                continue;
465
            }
466
467 814
            $columnDef    = $column->toArray();
468 814
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
469
470 814
            if (isset($columnDef['default'])) {
471 324
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
472
            }
473
474 814
            $comment = $this->getColumnComment($column);
475
476 814
            if (empty($comment) && ! is_numeric($comment)) {
477 652
                continue;
478
            }
479
480 245
            $commentsSql[] = $this->getCreateColumnCommentSQL(
481 245
                $diff->name,
482 245
                $column->getQuotedName($this),
483 245
                $comment
484
            );
485
        }
486
487 1818
        foreach ($diff->removedColumns as $column) {
488 656
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
489
                continue;
490
            }
491
492 656
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
493
        }
494
495 1818
        foreach ($diff->changedColumns as $columnDiff) {
496 1162
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
497
                continue;
498
            }
499
500 1162
            $column     = $columnDiff->column;
501 1162
            $comment    = $this->getColumnComment($column);
502 1162
            $hasComment = ! empty($comment) || is_numeric($comment);
503
504 1162
            if ($columnDiff->fromColumn instanceof Column) {
505 757
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
506 757
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
507
508 757
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
509 170
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
510 170
                        $diff->name,
511 170
                        $column->getQuotedName($this),
512 170
                        $comment
513
                    );
514 676
                } elseif ($hasFromComment && ! $hasComment) {
515 178
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
516 593
                } elseif ($hasComment) {
517 101
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
518 101
                        $diff->name,
519 101
                        $column->getQuotedName($this),
520 101
                        $comment
521
                    );
522
                }
523
            }
524
525
            // Do not add query part if only comment has changed.
526 1162
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
527 346
                continue;
528
            }
529
530 899
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
531
532 899
            if ($requireDropDefaultConstraint) {
533 411
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
534 411
                    $diff->name,
535 411
                    $columnDiff->oldColumnName
536
                );
537
            }
538
539 899
            $columnDef = $column->toArray();
540
541 899
            $queryParts[] = 'ALTER COLUMN ' .
542 899
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
543
544 899
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
545 411
                continue;
546
            }
547
548 492
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
549
        }
550
551 1818
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
552 411
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
553
                continue;
554
            }
555
556 411
            $oldColumnName = new Identifier($oldColumnName);
557
558 411
            $sql[] = "sp_RENAME '" .
559 411
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
560 411
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
561
562
            // Recreate default constraint with new column name if necessary (for future reference).
563 411
            if ($column->getDefault() === null) {
564 328
                continue;
565
            }
566
567 83
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
568 83
                $diff->name,
569 83
                $oldColumnName->getQuotedName($this)
570
            );
571 83
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
572
        }
573
574 1818
        $tableSql = [];
575
576 1818
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
577
            return array_merge($tableSql, $columnSql);
578
        }
579
580 1818
        foreach ($queryParts as $query) {
581 1146
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
582
        }
583
584 1818
        $sql = array_merge($sql, $commentsSql);
585
586 1818
        if ($diff->newName !== false) {
587 164
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $diff->getNewName()->getName() . "'";
588
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 164
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
598
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
599 164
                "+ 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
        }
606
607 1818
        $sql = array_merge(
608 1818
            $this->getPreAlterTableIndexForeignKeySQL($diff),
609 1818
            $sql,
610 1818
            $this->getPostAlterTableIndexForeignKeySQL($diff)
611
        );
612
613 1818
        return array_merge($sql, $tableSql, $columnSql);
614
    }
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 573
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
625
    {
626 573
        $columnDef         = $column->toArray();
627 573
        $columnDef['name'] = $column->getQuotedName($this);
628
629 573
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
630
    }
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 492
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
641
    {
642 492
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
643
    }
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 899
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
658
    {
659
        // We can only decide whether to drop an existing default constraint
660
        // if we know the original default value.
661 899
        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 324
            return false;
663
        }
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 575
        if ($columnDiff->fromColumn->getDefault() === null) {
668 166
            return false;
669
        }
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 411
        if ($columnDiff->hasChanged('default')) {
674 328
            return true;
675
        }
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 85
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
680
    }
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 170
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
700
    {
701 170
        if (strpos($tableName, '.') !== false) {
702 81
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
703 81
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
704 81
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
705
        } else {
706 89
            $schemaSQL = "'dbo'";
707 89
            $tableSQL  = $this->quoteStringLiteral($tableName);
708
        }
709
710 170
        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
        );
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 178
    protected function getDropColumnCommentSQL($tableName, $columnName)
739
    {
740 178
        if (strpos($tableName, '.') !== false) {
741 81
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
742 81
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
743 81
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
744
        } else {
745 97
            $schemaSQL = "'dbo'";
746 97
            $tableSQL  = $this->quoteStringLiteral($tableName);
747
        }
748
749 178
        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
        );
758
    }
759
760
    /**
761
     * {@inheritdoc}
762
     */
763 409
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
764
    {
765 409
        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
        ),
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 631
    public function getAddExtendedPropertySQL(
791
        $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 631
            '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
    }
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 178
    public function getDropExtendedPropertySQL(
823
        $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 178
            '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
    }
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 170
    public function getUpdateExtendedPropertySQL(
855
        $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 170
        '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
    }
870
871
    /**
872
     * {@inheritDoc}
873
     */
874 2
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
875
    {
876 2
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
877
    }
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 238
    public function getListTableColumnsSQL($table, $database = null)
893
    {
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 238
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
920
    }
921
922
    /**
923
     * {@inheritDoc}
924
     */
925 216
    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
    {
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 216
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
942
    }
943
944
    /**
945
     * {@inheritDoc}
946
     */
947 222
    public function getListTableIndexesSQL($table, $currentDatabase = null)
948
    {
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 222
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
964
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
965
    }
966
967
    /**
968
     * {@inheritDoc}
969
     */
970 2
    public function getCreateViewSQL($name, $sql)
971
    {
972 2
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
973
    }
974
975
    /**
976
     * {@inheritDoc}
977
     */
978 2
    public function getListViewsSQL($database)
979
    {
980 2
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
981
    }
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 576
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
993
    {
994 576
        if (strpos($table, '.') !== false) {
995 245
            [$schema, $table] = explode('.', $table);
996 245
            $schema           = $this->quoteStringLiteral($schema);
997 245
            $table            = $this->quoteStringLiteral($table);
998
        } else {
999 333
            $schema = 'SCHEMA_NAME()';
1000 333
            $table  = $this->quoteStringLiteral($table);
1001
        }
1002
1003 576
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1004
    }
1005
1006
    /**
1007
     * {@inheritDoc}
1008
     */
1009 2
    public function getDropViewSQL($name)
1010
    {
1011 2
        return 'DROP VIEW ' . $name;
1012
    }
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 2
    public function getLocateExpression($str, $substr, $startPos = false)
1028
    {
1029 2
        if ($startPos === false) {
1030 2
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1031
        }
1032
1033 2
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1034
    }
1035
1036
    /**
1037
     * {@inheritDoc}
1038
     */
1039
    public function getModExpression($expression1, $expression2)
1040
    {
1041
        return $expression1 . ' % ' . $expression2;
1042
    }
1043
1044
    /**
1045
     * {@inheritDoc}
1046
     */
1047 72
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1048
    {
1049 72
        if (! $char) {
1050 16
            switch ($pos) {
1051
                case TrimMode::LEADING:
1052 4
                    $trimFn = 'LTRIM';
1053 4
                    break;
1054
1055
                case TrimMode::TRAILING:
1056 4
                    $trimFn = 'RTRIM';
1057 4
                    break;
1058
1059
                default:
1060 8
                    return 'LTRIM(RTRIM(' . $str . '))';
1061
            }
1062
1063 8
            return $trimFn . '(' . $str . ')';
1064
        }
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 56
        $pattern = "'%[^' + " . $char . " + ']%'";
1076
1077 56
        if ($pos === TrimMode::LEADING) {
1078 14
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1079
        }
1080
1081 42
        if ($pos === TrimMode::TRAILING) {
1082 14
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1083
        }
1084
1085 28
        return 'reverse(stuff(reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)), 1, patindex(' . $pattern . ', reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null))) - 1, null))';
1086
    }
1087
1088
    /**
1089
     * {@inheritDoc}
1090
     */
1091 81
    public function getConcatExpression()
1092
    {
1093 81
        $args = func_get_args();
1094
1095 81
        return '(' . implode(' + ', $args) . ')';
1096
    }
1097
1098
    /**
1099
     * {@inheritDoc}
1100
     */
1101 85
    public function getListDatabasesSQL()
1102
    {
1103 85
        return 'SELECT * FROM sys.databases';
1104
    }
1105
1106
    /**
1107
     * {@inheritDoc}
1108
     */
1109 4
    public function getListNamespacesSQL()
1110
    {
1111 4
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1112
    }
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 81
    public function getSetTransactionIsolationSQL($level)
1138
    {
1139 81
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1140
    }
1141
1142
    /**
1143
     * {@inheritDoc}
1144
     */
1145 1412
    public function getIntegerTypeDeclarationSQL(array $field)
1146
    {
1147 1412
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1148
    }
1149
1150
    /**
1151
     * {@inheritDoc}
1152
     */
1153 30
    public function getBigIntTypeDeclarationSQL(array $field)
1154
    {
1155 30
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1156
    }
1157
1158
    /**
1159
     * {@inheritDoc}
1160
     */
1161 2
    public function getSmallIntTypeDeclarationSQL(array $field)
1162
    {
1163 2
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1164
    }
1165
1166
    /**
1167
     * {@inheritDoc}
1168
     */
1169 81
    public function getGuidTypeDeclarationSQL(array $field)
1170
    {
1171 81
        return 'UNIQUEIDENTIFIER';
1172
    }
1173
1174
    /**
1175
     * {@inheritDoc}
1176
     */
1177 2099
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1178
    {
1179 2099
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1180
    }
1181
1182
    /**
1183
     * {@inheritdoc}
1184
     */
1185 85
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1186
    {
1187 85
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1188
    }
1189
1190
    /**
1191
     * {@inheritdoc}
1192
     */
1193 247
    public function getBinaryMaxLength()
1194
    {
1195 247
        return 8000;
1196
    }
1197
1198
    /**
1199
     * {@inheritDoc}
1200
     */
1201 135
    public function getClobTypeDeclarationSQL(array $field)
1202
    {
1203 135
        return 'VARCHAR(MAX)';
1204
    }
1205
1206
    /**
1207
     * {@inheritDoc}
1208
     */
1209 1412
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1210
    {
1211 1412
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1212
    }
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 115
    public function getBooleanTypeDeclarationSQL(array $field)
1242
    {
1243 115
        return 'BIT';
1244
    }
1245
1246
    /**
1247
     * {@inheritDoc}
1248
     */
1249 1134
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1250
    {
1251 1134
        $where = [];
1252
1253 1134
        if ($offset > 0) {
1254 108
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1255
        }
1256
1257 1134
        if ($limit !== null) {
1258 1080
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1259 1080
            $top     = sprintf('TOP %d', $offset + $limit);
1260
        } else {
1261 54
            $top = 'TOP 9223372036854775807';
1262
        }
1263
1264 1134
        if (empty($where)) {
1265 54
            return $query;
1266
        }
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 1080
        $selectPattern  = '/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/im';
1273 1080
        $replacePattern = sprintf('$1%s $2', $top);
1274 1080
        $query          = preg_replace($selectPattern, $replacePattern, $query);
1275
1276 1080
        if (stristr($query, 'ORDER BY')) {
1277
            // Inner order by is not valid in SQL Server for our purposes
1278
            // unless it's in a TOP N subquery.
1279 702
            $query = $this->scrubInnerOrderBy($query);
1280
        }
1281
1282
        // Build a new limited query around the original, using a CTE
1283 1080
        return sprintf(
1284
            '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 1080
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1289 1080
            $query,
1290 1080
            implode(' AND ', $where)
1291
        );
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 702
    private function scrubInnerOrderBy($query)
1303
    {
1304 702
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1305 702
        $offset = 0;
1306
1307 702
        while ($count-- > 0) {
1308 702
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1309 702
            if ($orderByPos === false) {
1310 54
                break;
1311
            }
1312
1313 702
            $qLen            = strlen($query);
1314 702
            $parenCount      = 0;
1315 702
            $currentPosition = $orderByPos;
1316
1317 702
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1318 702
                if ($query[$currentPosition] === '(') {
1319 54
                    $parenCount++;
1320 702
                } elseif ($query[$currentPosition] === ')') {
1321 378
                    $parenCount--;
1322
                }
1323
1324 702
                $currentPosition++;
1325
            }
1326
1327 702
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1328
                // If the order by clause is in a TOP N subquery, do not remove
1329
                // it and continue iteration from the current position.
1330 621
                $offset = $currentPosition;
1331 621
                continue;
1332
            }
1333
1334 270
            if ($currentPosition >= $qLen - 1) {
1335
                continue;
1336
            }
1337
1338 270
            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1339 270
            $offset = $orderByPos;
1340
        }
1341 702
        return $query;
1342
    }
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 702
    private function isOrderByInTopNSubquery($query, $currentPosition)
1353
    {
1354
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1355 702
        $subQueryBuffer = '';
1356 702
        $parenCount     = 0;
1357
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 702
        while ($parenCount >= 0 && $currentPosition >= 0) {
1361 702
            if ($query[$currentPosition] === '(') {
1362 459
                $parenCount--;
1363 702
            } elseif ($query[$currentPosition] === ')') {
1364 378
                $parenCount++;
1365
            }
1366
1367
            // Only yank query text on the same nesting level as the ORDER BY clause.
1368 702
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1369
1370 702
            $currentPosition--;
1371
        }
1372
1373 702
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1374
    }
1375
1376
    /**
1377
     * {@inheritDoc}
1378
     */
1379
    public function supportsLimitOffset()
1380
    {
1381
        return false;
1382
    }
1383
1384
    /**
1385
     * {@inheritDoc}
1386
     */
1387 89
    public function convertBooleans($item)
1388
    {
1389 89
        if (is_array($item)) {
1390
            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 89
        } elseif (is_bool($item) || is_numeric($item)) {
1398 89
            $item = $item ? 1 : 0;
1399
        }
1400
1401 89
        return $item;
1402
    }
1403
1404
    /**
1405
     * {@inheritDoc}
1406
     */
1407 4
    public function getCreateTemporaryTableSnippetSQL()
1408
    {
1409 4
        return 'CREATE TABLE';
1410
    }
1411
1412
    /**
1413
     * {@inheritDoc}
1414
     */
1415 4
    public function getTemporaryTableName($tableName)
1416
    {
1417 4
        return '#' . $tableName;
1418
    }
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 167
    public function getName()
1456
    {
1457 167
        return 'mssql';
1458
    }
1459
1460
    /**
1461
     * {@inheritDoc}
1462
     */
1463 407
    protected function initializeDoctrineTypeMappings()
1464
    {
1465 407
        $this->doctrineTypeMapping = [
1466
            '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 407
    }
1493
1494
    /**
1495
     * {@inheritDoc}
1496
     */
1497 2
    public function createSavePoint($savepoint)
1498
    {
1499 2
        return 'SAVE TRANSACTION ' . $savepoint;
1500
    }
1501
1502
    /**
1503
     * {@inheritDoc}
1504
     */
1505
    public function releaseSavePoint($savepoint)
1506
    {
1507
        return '';
1508
    }
1509
1510
    /**
1511
     * {@inheritDoc}
1512
     */
1513 2
    public function rollbackSavePoint($savepoint)
1514
    {
1515 2
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1516
    }
1517
1518
    /**
1519
     * {@inheritdoc}
1520
     */
1521 569
    public function getForeignKeyReferentialActionSQL($action)
1522
    {
1523
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1524 569
        if (strtoupper($action) === 'RESTRICT') {
1525 81
            return 'NO ACTION';
1526
        }
1527
1528 488
        return parent::getForeignKeyReferentialActionSQL($action);
1529
    }
1530
1531
    /**
1532
     * {@inheritDoc}
1533
     */
1534 193
    public function appendLockHint($fromClause, $lockMode)
1535
    {
1536 175
        switch (true) {
1537 18
            case $lockMode === LockMode::NONE:
1538 27
                return $fromClause . ' WITH (NOLOCK)';
1539
1540 166
            case $lockMode === LockMode::PESSIMISTIC_READ:
1541 27
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1542
1543 139
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1544 31
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1545
1546
            default:
1547 108
                return $fromClause;
1548
        }
1549
    }
1550
1551
    /**
1552
     * {@inheritDoc}
1553
     */
1554 4
    public function getForUpdateSQL()
1555
    {
1556 4
        return ' ';
1557
    }
1558
1559
    /**
1560
     * {@inheritDoc}
1561
     */
1562 1674
    protected function getReservedKeywordsClass()
1563
    {
1564 1674
        return Keywords\SQLServerKeywords::class;
1565
    }
1566
1567
    /**
1568
     * {@inheritDoc}
1569
     */
1570 2465
    public function quoteSingleIdentifier($str)
1571
    {
1572 2465
        return '[' . str_replace(']', '][', $str) . ']';
1573
    }
1574
1575
    /**
1576
     * {@inheritDoc}
1577
     */
1578 97
    public function getTruncateTableSQL($tableName, $cascade = false)
1579
    {
1580 97
        $tableIdentifier = new Identifier($tableName);
1581
1582 97
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1583
    }
1584
1585
    /**
1586
     * {@inheritDoc}
1587
     */
1588 97
    public function getBlobTypeDeclarationSQL(array $field)
1589
    {
1590 97
        return 'VARBINARY(MAX)';
1591
    }
1592
1593
    /**
1594
     * {@inheritdoc}
1595
     *
1596
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1597
     */
1598 2813
    public function getColumnDeclarationSQL($name, array $field)
1599
    {
1600 2813
        if (isset($field['columnDefinition'])) {
1601 81
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1602
        } else {
1603 2732
            $collation = isset($field['collation']) && $field['collation'] ?
1604 2732
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1605
1606 2732
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1607
1608 2732
            $unique = isset($field['unique']) && $field['unique'] ?
1609 2732
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1610
1611 2732
            $check = isset($field['check']) && $field['check'] ?
1612 2732
                ' ' . $field['check'] : '';
1613
1614 2732
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1615 2732
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1616
        }
1617
1618 2813
        return $name . ' ' . $columnDef;
1619
    }
1620
1621
    /**
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 1243
    private function generateDefaultConstraintName($table, $column)
1630
    {
1631 1243
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1632
    }
1633
1634
    /**
1635
     * Returns a hash value for a given identifier.
1636
     *
1637
     * @param string $identifier Identifier to generate a hash value for.
1638
     *
1639
     * @return string
1640
     */
1641 1324
    private function generateIdentifierName($identifier)
1642
    {
1643
        // Always generate name for unquoted identifiers to ensure consistency.
1644 1324
        $identifier = new Identifier($identifier);
1645
1646 1324
        return strtoupper(dechex(crc32($identifier->getName())));
1647
    }
1648
}
1649