Completed
Pull Request — master (#3365)
by Benjamin
21:11
created

getForeignKeyReferentialActionSQL()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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

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

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