Failed Conditions
Push — develop ( c067f0...c4478a )
by Sergei
10:16
created

getAlterTableDropDefaultConstraintClause()   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 2
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 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 78
    public function getCurrentDateSQL()
50
    {
51 78
        return $this->getConvertExpression('date', 'GETDATE()');
52
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57 40
    public function getCurrentTimeSQL()
58
    {
59 40
        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 78
    private function getConvertExpression($dataType, $expression)
71
    {
72 78
        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 40
    public function prefersIdentityColumns()
104
    {
105 40
        return true;
106
    }
107
108
    /**
109
     * {@inheritDoc}
110
     *
111
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
112
     */
113 44
    public function supportsIdentityColumns()
114
    {
115 44
        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 46
    public function supportsSchemas()
130
    {
131 46
        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 116
    public function supportsColumnCollation()
146
    {
147 116
        return true;
148
    }
149
150
    /**
151
     * {@inheritDoc}
152
     */
153 2238
    public function hasNativeGuidType()
154
    {
155 2238
        return true;
156
    }
157
158
    /**
159
     * {@inheritDoc}
160
     */
161 42
    public function getCreateDatabaseSQL($name)
162
    {
163 42
        return 'CREATE DATABASE ' . $name;
164
    }
165
166
    /**
167
     * {@inheritDoc}
168
     */
169 42
    public function getDropDatabaseSQL($name)
170
    {
171 42
        return 'DROP DATABASE ' . $name;
172
    }
173
174
    /**
175
     * {@inheritDoc}
176
     */
177 42
    public function supportsCreateDropDatabase()
178
    {
179 42
        return true;
180
    }
181
182
    /**
183
     * {@inheritDoc}
184
     */
185 42
    public function getCreateSchemaSQL($schemaName)
186
    {
187 42
        return 'CREATE SCHEMA ' . $schemaName;
188
    }
189
190
    /**
191
     * {@inheritDoc}
192
     */
193 78
    public function getDropForeignKeySQL($foreignKey, $table)
194
    {
195 78
        if (! $foreignKey instanceof ForeignKeyConstraint) {
196 38
            $foreignKey = new Identifier($foreignKey);
197
        }
198
199 78
        if (! $table instanceof Table) {
200 78
            $table = new Identifier($table);
201
        }
202
203 78
        $foreignKey = $foreignKey->getQuotedName($this);
204 78
        $table      = $table->getQuotedName($this);
205
206 78
        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 922
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
248
    {
249 922
        $defaultConstraintsSql = [];
250 922
        $commentsSql           = [];
251
252
        // @todo does other code breaks because of this?
253
        // force primary keys to be not null
254 922
        foreach ($columns as &$column) {
255 922
            if (isset($column['primary']) && $column['primary']) {
256 402
                $column['notnull'] = true;
257
            }
258
259
            // Build default constraints SQL statements.
260 922
            if (isset($column['default'])) {
261 170
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
262 170
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
263
            }
264
265 922
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
266 834
                continue;
267
            }
268
269 216
            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
270
        }
271
272 922
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
273
274 922
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
275
            foreach ($options['uniqueConstraints'] as $name => $definition) {
276
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
277
            }
278
        }
279
280 922
        if (isset($options['primary']) && ! empty($options['primary'])) {
281 440
            $flags = '';
282 440
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
283 38
                $flags = ' NONCLUSTERED';
284
            }
285 440
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
286
        }
287
288 922
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
289
290 922
        $check = $this->getCheckDeclarationSQL($columns);
291 922
        if (! empty($check)) {
292
            $query .= ', ' . $check;
293
        }
294 922
        $query .= ')';
295
296 922
        $sql = [$query];
297
298 922
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
299 130
            foreach ($options['indexes'] as $index) {
300 130
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
301
            }
302
        }
303
304 922
        if (isset($options['foreignKeys'])) {
305 291
            foreach ((array) $options['foreignKeys'] as $definition) {
306 48
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
307
            }
308
        }
309
310 922
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
311
    }
312
313
    /**
314
     * {@inheritDoc}
315
     */
316 76
    public function getCreatePrimaryKeySQL(Index $index, $table)
317
    {
318 76
        $flags = '';
319 76
        if ($index->hasFlag('nonclustered')) {
320 38
            $flags = ' NONCLUSTERED';
321
        }
322
323 76
        return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY' . $flags . ' (' . $this->getIndexFieldDeclarationListSQL($index->getQuotedColumns($this)) . ')';
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 330
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
344
    {
345 330
        if (strpos($tableName, '.') !== false) {
346 76
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
347 76
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
348 76
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
349
        } else {
350 254
            $schemaSQL = "'dbo'";
351 254
            $tableSQL  = $this->quoteStringLiteral($tableName);
352
        }
353
354 330
        return $this->getAddExtendedPropertySQL(
355 330
            'MS_Description',
356 330
            $comment,
357 330
            'SCHEMA',
358 330
            $schemaSQL,
359 330
            'TABLE',
360 330
            $tableSQL,
361 330
            'COLUMN',
362 330
            $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 588
    public function getDefaultConstraintDeclarationSQL($table, array $column)
377
    {
378 588
        if (! isset($column['default'])) {
379
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
380
        }
381
382 588
        $columnName = new Identifier($column['name']);
383
384
        return ' CONSTRAINT ' .
385 588
            $this->generateDefaultConstraintName($table, $column['name']) .
386 588
            $this->getDefaultValueDeclarationSQL($column) .
387 588
            ' FOR ' . $columnName->getQuotedName($this);
388
    }
389
390
    /**
391
     * {@inheritDoc}
392
     */
393 322
    public function getCreateIndexSQL(Index $index, $table)
394
    {
395 322
        $constraint = parent::getCreateIndexSQL($index, $table);
396
397 322
        if ($index->isUnique() && ! $index->isPrimary()) {
398 82
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
399
        }
400
401 322
        return $constraint;
402
    }
403
404
    /**
405
     * {@inheritDoc}
406
     */
407 322
    protected function getCreateIndexSQLFlags(Index $index)
408
    {
409 322
        $type = '';
410 322
        if ($index->isUnique()) {
411 82
            $type .= 'UNIQUE ';
412
        }
413
414 322
        if ($index->hasFlag('clustered')) {
415 38
            $type .= 'CLUSTERED ';
416 284
        } elseif ($index->hasFlag('nonclustered')) {
417
            $type .= 'NONCLUSTERED ';
418
        }
419
420 322
        return $type;
421
    }
422
423
    /**
424
     * Extend unique key constraint with required filters
425
     *
426
     * @param string $sql
427
     *
428
     * @return string
429
     */
430 82
    private function _appendUniqueConstraintDefinition($sql, Index $index)
431
    {
432 82
        $fields = [];
433
434 82
        foreach ($index->getQuotedColumns($this) as $field) {
435 82
            $fields[] = $field . ' IS NOT NULL';
436
        }
437
438 82
        return $sql . ' WHERE ' . implode(' AND ', $fields);
439
    }
440
441
    /**
442
     * {@inheritDoc}
443
     */
444 872
    public function getAlterTableSQL(TableDiff $diff)
445
    {
446 872
        $queryParts  = [];
447 872
        $sql         = [];
448 872
        $columnSql   = [];
449 872
        $commentsSql = [];
450
451
        /** @var Column $column */
452 872
        foreach ($diff->addedColumns as $column) {
453 386
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
454
                continue;
455
            }
456
457 386
            $columnDef    = $column->toArray();
458 386
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
459
460 386
            if (isset($columnDef['default'])) {
461 154
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
462
            }
463
464 386
            $comment = $this->getColumnComment($column);
465
466 386
            if (empty($comment) && ! is_numeric($comment)) {
467 310
                continue;
468
            }
469
470 116
            $commentsSql[] = $this->getCreateColumnCommentSQL(
471 116
                $diff->name,
472 116
                $column->getQuotedName($this),
473 116
                $comment
474
            );
475
        }
476
477 872
        foreach ($diff->removedColumns as $column) {
478 312
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
479
                continue;
480
            }
481
482 312
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
483
        }
484
485
        /** @var ColumnDiff $columnDiff */
486 872
        foreach ($diff->changedColumns as $columnDiff) {
487 560
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
488
                continue;
489
            }
490
491 560
            $column     = $columnDiff->column;
492 560
            $comment    = $this->getColumnComment($column);
493 560
            $hasComment = ! empty($comment) || is_numeric($comment);
494
495 560
            if ($columnDiff->fromColumn instanceof Column) {
496 370
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
497 370
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
498
499 370
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
500 84
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
501 84
                        $diff->name,
502 84
                        $column->getQuotedName($this),
503 84
                        $comment
504
                    );
505 332
                } elseif ($hasFromComment && ! $hasComment) {
506 92
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
507 292
                } elseif ($hasComment) {
508 58
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
509 58
                        $diff->name,
510 58
                        $column->getQuotedName($this),
511 58
                        $comment
512
                    );
513
                }
514
            }
515
516
            // Do not add query part if only comment has changed.
517 560
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
518 174
                continue;
519
            }
520
521 426
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
522
523 426
            if ($requireDropDefaultConstraint) {
524 196
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
525 196
                    $diff->name,
526 196
                    $columnDiff->oldColumnName
527
                );
528
            }
529
530 426
            $columnDef = $column->toArray();
531
532 426
            $queryParts[] = 'ALTER COLUMN ' .
533 426
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
534
535 426
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
536 196
                continue;
537
            }
538
539 234
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
540
        }
541
542 872
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
543 196
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
544
                continue;
545
            }
546
547 196
            $oldColumnName = new Identifier($oldColumnName);
548
549 196
            $sql[] = "sp_RENAME '" .
550 196
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
551 196
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
552
553
            // Recreate default constraint with new column name if necessary (for future reference).
554 196
            if ($column->getDefault() === null) {
555 156
                continue;
556
            }
557
558 40
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
559 40
                $diff->name,
560 40
                $oldColumnName->getQuotedName($this)
561
            );
562 40
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
563
        }
564
565 872
        $tableSql = [];
566
567 872
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
568
            return array_merge($tableSql, $columnSql);
569
        }
570
571 872
        foreach ($queryParts as $query) {
572 544
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
573
        }
574
575 872
        $sql = array_merge($sql, $commentsSql);
576
577 872
        if ($diff->newName !== false) {
578 78
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $diff->getNewName()->getName() . "'";
579
580
            /**
581
             * Rename table's default constraints names
582
             * to match the new table name.
583
             * This is necessary to ensure that the default
584
             * constraints can be referenced in future table
585
             * alterations as the table name is encoded in
586
             * default constraints' names.
587
             */
588 78
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
589
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
590 78
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
591 78
                "'" . $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

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

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