Failed Conditions
Push — master ( 379085...b45ed5 )
by Marco
54s queued 28s
created

SQLServerPlatform::scrubInnerOrderBy()   B

Complexity

Conditions 9
Paths 2

Size

Total Lines 40
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 9.0058

Importance

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

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

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