Passed
Pull Request — master (#3547)
by Sergei
61:20
created

SQLServerPlatform::getDefaultValueDeclarationSQL()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 8.0291

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 25
ccs 12
cts 13
cp 0.9231
rs 8.4444
c 0
b 0
f 0
cc 8
nc 7
nop 1
crap 8.0291

1 Method

Rating   Name   Duplication   Size   Complexity  
C SQLServerPlatform::getColumnDeclarationSQL() 0 21 10
1
<?php
2
3
namespace Doctrine\DBAL\Platforms;
4
5
use Doctrine\DBAL\LockMode;
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\ColumnDiff;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Doctrine\DBAL\Schema\Identifier;
10
use Doctrine\DBAL\Schema\Index;
11
use Doctrine\DBAL\Schema\Table;
12
use Doctrine\DBAL\Schema\TableDiff;
13
use InvalidArgumentException;
14
use function array_merge;
15
use function array_unique;
16
use function array_values;
17
use function count;
18
use function crc32;
19
use function dechex;
20
use function explode;
21
use function func_get_args;
22
use function implode;
23
use function is_array;
24
use function is_bool;
25
use function is_numeric;
26
use function is_string;
27
use function preg_match;
28
use function sprintf;
29
use function str_replace;
30
use function stripos;
31
use function stristr;
32
use function strlen;
33
use function strpos;
34
use function strtoupper;
35
use function substr;
36
use function substr_count;
37
38
/**
39
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
40
 * Microsoft SQL Server database platform.
41
 */
42
class SQLServerPlatform extends AbstractPlatform
43
{
44
    /**
45
     * {@inheritdoc}
46
     */
47
    public function getCurrentDateSQL()
48 9892
    {
49
        return $this->getConvertExpression('date', 'GETDATE()');
50 9892
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55
    public function getCurrentTimeSQL()
56 9886
    {
57
        return $this->getConvertExpression('time', 'GETDATE()');
58 9886
    }
59
60
    /**
61
     * Returns an expression that converts an expression of one data type to another.
62
     *
63
     * @param string $dataType   The target native data type. Alias data types cannot be used.
64
     * @param string $expression The SQL expression to convert.
65
     *
66
     * @return string
67
     */
68
    private function getConvertExpression($dataType, $expression)
69 9892
    {
70
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
71 9892
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
77 1094
    {
78
        $factorClause = '';
79 1094
80
        if ($operator === '-') {
81 1094
            $factorClause = '-1 * ';
82 1094
        }
83
84
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
85 1094
    }
86
87
    /**
88
     * {@inheritDoc}
89
     */
90
    public function getDateDiffExpression($date1, $date2)
91 1073
    {
92
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
93 1073
    }
94
95
    /**
96
     * {@inheritDoc}
97
     *
98
     * Microsoft SQL Server prefers "autoincrement" identity columns
99
     * since sequences can only be emulated with a table.
100
     */
101
    public function prefersIdentityColumns()
102 9559
    {
103
        return true;
104 9559
    }
105
106
    /**
107
     * {@inheritDoc}
108
     *
109
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
110
     */
111
    public function supportsIdentityColumns()
112 9704
    {
113
        return true;
114 9704
    }
115
116
    /**
117
     * {@inheritDoc}
118
     */
119
    public function supportsReleaseSavepoints()
120 1160
    {
121
        return false;
122 1160
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127
    public function supportsSchemas()
128 9711
    {
129
        return true;
130 9711
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function getDefaultSchemaName()
136 934
    {
137
        return 'dbo';
138 934
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143
    public function supportsColumnCollation()
144 8405
    {
145
        return true;
146 8405
    }
147
148
    /**
149
     * {@inheritDoc}
150
     */
151
    public function hasNativeGuidType()
152 10423
    {
153
        return true;
154 10423
    }
155
156
    /**
157
     * {@inheritDoc}
158
     */
159
    public function getCreateDatabaseSQL($name)
160 10010
    {
161
        return 'CREATE DATABASE ' . $name;
162 10010
    }
163
164
    /**
165
     * {@inheritDoc}
166
     */
167
    public function getDropDatabaseSQL($name)
168 10010
    {
169
        return 'DROP DATABASE ' . $name;
170 10010
    }
171
172
    /**
173
     * {@inheritDoc}
174
     */
175
    public function supportsCreateDropDatabase()
176 9918
    {
177
        return true;
178 9918
    }
179
180
    /**
181
     * {@inheritDoc}
182
     */
183
    public function getCreateSchemaSQL($schemaName)
184 9113
    {
185
        return 'CREATE SCHEMA ' . $schemaName;
186 9113
    }
187
188
    /**
189
     * {@inheritDoc}
190
     */
191
    public function getDropForeignKeySQL($foreignKey, $table)
192 6969
    {
193
        if (! $foreignKey instanceof ForeignKeyConstraint) {
194 6969
            $foreignKey = new Identifier($foreignKey);
195 6531
        }
196
197
        if (! $table instanceof Table) {
198 6969
            $table = new Identifier($table);
199 6969
        }
200
201
        $foreignKey = $foreignKey->getQuotedName($this);
202 6969
        $table      = $table->getQuotedName($this);
203 6969
204
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
205 6969
    }
206
207
    /**
208
     * {@inheritDoc}
209
     */
210
    public function getDropIndexSQL($index, $table = null)
211 972
    {
212
        if ($index instanceof Index) {
213 972
            $index = $index->getQuotedName($this);
214 964
        } elseif (! is_string($index)) {
215 972
            throw new InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
216
        }
217
218
        if (! isset($table)) {
219 972
            return 'DROP INDEX ' . $index;
220
        }
221
222
        if ($table instanceof Table) {
223 972
            $table = $table->getQuotedName($this);
224 972
        }
225
226
        return sprintf(
227 972
            <<<SQL
228
IF EXISTS (SELECT * FROM sysobjects WHERE name = '%s')
229
    ALTER TABLE %s DROP CONSTRAINT %s
230
ELSE
231
    DROP INDEX %s ON %s
232
SQL
233
            ,
234
            $index,
235 972
            $table,
236 972
            $index,
237 972
            $index,
238 972
            $table
239 972
        );
240
    }
241
242
    /**
243
     * {@inheritDoc}
244
     */
245
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
246 10177
    {
247
        $defaultConstraintsSql = [];
248 10177
        $commentsSql           = [];
249 10177
250
        // @todo does other code breaks because of this?
251
        // force primary keys to be not null
252
        foreach ($columns as &$column) {
253 10177
            if (isset($column['primary']) && $column['primary']) {
254 10177
                $column['notnull'] = true;
255 9396
            }
256
257
            // Build default constraints SQL statements.
258
            if (isset($column['default'])) {
259 10177
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
260 8835
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
261 8835
            }
262
263
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
264 10177
                continue;
265 10165
            }
266
267
            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
268 9120
        }
269
270
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
271 10177
272
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
273 10177
            foreach ($options['uniqueConstraints'] as $name => $definition) {
274
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
275
            }
276
        }
277
278
        if (isset($options['primary']) && ! empty($options['primary'])) {
279 10177
            $flags = '';
280 9402
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
281 9402
                $flags = ' NONCLUSTERED';
282 8906
            }
283
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
284 9402
        }
285
286
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
287 10177
288
        $check = $this->getCheckDeclarationSQL($columns);
289 10177
        if (! empty($check)) {
290 10177
            $query .= ', ' . $check;
291
        }
292
        $query .= ')';
293 10177
294
        $sql = [$query];
295 10177
296
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
297 10177
            foreach ($options['indexes'] as $index) {
298 7689
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
299 7689
            }
300
        }
301
302
        if (isset($options['foreignKeys'])) {
303 10177
            foreach ((array) $options['foreignKeys'] as $definition) {
304 7434
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
305 7232
            }
306
        }
307
308
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
309 10177
    }
310
311
    /**
312
     * {@inheritDoc}
313
     */
314
    public function getCreatePrimaryKeySQL(Index $index, $table)
315 8887
    {
316
        if ($table instanceof Table) {
317 8887
            $identifier = $table->getQuotedName($this);
318
        } else {
319
            $identifier = $table;
320 8887
        }
321
322
        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';
323 8887
324
        if ($index->hasFlag('nonclustered')) {
325 8887
            $sql .= ' NONCLUSTERED';
326 8881
        }
327
328
        return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
329 8887
    }
330
331
    /**
332
     * Returns the SQL statement for creating a column comment.
333
     *
334
     * SQL Server does not support native column comments,
335
     * therefore the extended properties functionality is used
336
     * as a workaround to store them.
337
     * The property name used to store column comments is "MS_Description"
338
     * which provides compatibility with SQL Server Management Studio,
339
     * as column comments are stored in the same property there when
340
     * specifying a column's "Description" attribute.
341
     *
342
     * @param string      $tableName  The quoted table name to which the column belongs.
343
     * @param string      $columnName The quoted column name to create the comment for.
344
     * @param string|null $comment    The column's comment.
345
     *
346
     * @return string
347
     */
348
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
349 9138
    {
350
        if (strpos($tableName, '.') !== false) {
351 9138
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
352 8812
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
353 8812
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
354 8812
        } else {
355
            $schemaSQL = "'dbo'";
356 9034
            $tableSQL  = $this->quoteStringLiteral($tableName);
357 9034
        }
358
359
        return $this->getAddExtendedPropertySQL(
360 9138
            'MS_Description',
361 9138
            $comment,
362 42
            'SCHEMA',
363 9138
            $schemaSQL,
364 42
            'TABLE',
365 9138
            $tableSQL,
366 42
            'COLUMN',
367 9138
            $columnName
368 42
        );
369
    }
370
371
    /**
372
     * Returns the SQL snippet for declaring a default constraint.
373
     *
374
     * @param string  $table  Name of the table to return the default constraint declaration for.
375
     * @param mixed[] $column Column definition.
376
     *
377
     * @return string
378
     *
379
     * @throws InvalidArgumentException
380
     */
381
    public function getDefaultConstraintDeclarationSQL($table, array $column)
382 9016
    {
383
        if (! isset($column['default'])) {
384 9016
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
385
        }
386
387
        $columnName = new Identifier($column['name']);
388 9016
389
        return ' CONSTRAINT ' .
390
            $this->generateDefaultConstraintName($table, $column['name']) .
391 9016
            $this->getDefaultValueDeclarationSQL($column) .
392 9016
            ' FOR ' . $columnName->getQuotedName($this);
393 9016
    }
394
395
    /**
396
     * {@inheritDoc}
397
     */
398
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
399 7212
    {
400
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
401 7212
402
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
403 7212
404
        return $constraint;
405 7212
    }
406
407
    /**
408
     * {@inheritDoc}
409
     */
410
    public function getCreateIndexSQL(Index $index, $table)
411 9237
    {
412
        $constraint = parent::getCreateIndexSQL($index, $table);
413 9237
414
        if ($index->isUnique() && ! $index->isPrimary()) {
415 9237
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
416 7683
        }
417
418
        return $constraint;
419 9237
    }
420
421
    /**
422
     * {@inheritDoc}
423
     */
424
    protected function getCreateIndexSQLFlags(Index $index)
425 9237
    {
426
        $type = '';
427 9237
        if ($index->isUnique()) {
428 9237
            $type .= 'UNIQUE ';
429 7683
        }
430
431
        if ($index->hasFlag('clustered')) {
432 9237
            $type .= 'CLUSTERED ';
433 8931
        } elseif ($index->hasFlag('nonclustered')) {
434 7713
            $type .= 'NONCLUSTERED ';
435
        }
436
437
        return $type;
438 9237
    }
439
440
    /**
441
     * Extend unique key constraint with required filters
442
     *
443
     * @param string $sql
444
     *
445
     * @return string
446
     */
447
    private function _appendUniqueConstraintDefinition($sql, Index $index)
448 7695
    {
449
        $fields = [];
450 7695
451
        foreach ($index->getQuotedColumns($this) as $field) {
452 7695
            $fields[] = $field . ' IS NOT NULL';
453 7695
        }
454
455
        return $sql . ' WHERE ' . implode(' AND ', $fields);
456 7695
    }
457
458
    /**
459
     * {@inheritDoc}
460
     */
461
    public function getAlterTableSQL(TableDiff $diff)
462 9211
    {
463
        $queryParts  = [];
464 9211
        $sql         = [];
465 9211
        $columnSql   = [];
466 9211
        $commentsSql = [];
467 9211
468
        foreach ($diff->addedColumns as $column) {
469 9211
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
470 9135
                continue;
471
            }
472
473
            $columnDef    = $column->toArray();
474 9135
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
475 9135
476
            if (isset($columnDef['default'])) {
477 9135
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
478 8708
            }
479
480
            $comment = $this->getColumnComment($column);
481 9135
482
            if (empty($comment) && ! is_numeric($comment)) {
483 9135
                continue;
484 9031
            }
485
486
            $commentsSql[] = $this->getCreateColumnCommentSQL(
487 9091
                $diff->name,
488 9091
                $column->getQuotedName($this),
489 9091
                $comment
490 18
            );
491
        }
492
493
        foreach ($diff->removedColumns as $column) {
494 9211
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
495 9035
                continue;
496
            }
497
498
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
499 9035
        }
500
501
        foreach ($diff->changedColumns as $columnDiff) {
502 9211
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
503 9136
                continue;
504
            }
505
506
            $column     = $columnDiff->column;
507 9136
            $comment    = $this->getColumnComment($column);
508 9136
            $hasComment = ! empty($comment) || is_numeric($comment);
509 9136
510
            if ($columnDiff->fromColumn instanceof Column) {
511 9136
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
512 9106
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
513 9106
514
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
515 9106
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
516 9039
                        $diff->name,
517 9039
                        $column->getQuotedName($this),
518 9039
                        $comment
519 12
                    );
520
                } elseif ($hasFromComment && ! $hasComment) {
521 9100
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
522 9062
                } elseif ($hasComment) {
523 9025
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
524 8987
                        $diff->name,
525 8987
                        $column->getQuotedName($this),
526 8987
                        $comment
527 6
                    );
528
                }
529
            }
530
531
            // Do not add query part if only comment has changed.
532
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
533 9136
                continue;
534 9074
            }
535
536
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
537 9049
538
            if ($requireDropDefaultConstraint) {
539 9049
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
540 8921
                    $diff->name,
541 8921
                    $columnDiff->oldColumnName
542 8921
                );
543
            }
544
545
            $columnDef = $column->toArray();
546 9049
547
            $queryParts[] = 'ALTER COLUMN ' .
548 9049
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
549 9049
550
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
551 9049
                continue;
552 9013
            }
553
554
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
555 8927
        }
556
557
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
558 9211
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
559 9013
                continue;
560
            }
561
562
            $oldColumnName = new Identifier($oldColumnName);
563 9013
564
            $sql[] = "sp_RENAME '" .
565 9013
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
566 9013
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
567 9013
568
            // Recreate default constraint with new column name if necessary (for future reference).
569
            if ($column->getDefault() === null) {
570 9013
                continue;
571 9005
            }
572
573
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
574 6942
                $diff->name,
575 6942
                $oldColumnName->getQuotedName($this)
576 6942
            );
577
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
578 6942
        }
579
580
        $tableSql = [];
581 9211
582
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
583 9211
            return array_merge($tableSql, $columnSql);
584
        }
585
586
        foreach ($queryParts as $query) {
587 9211
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
588 9163
        }
589
590
        $sql = array_merge($sql, $commentsSql);
591 9211
592
        $newName = $diff->getNewName();
593 9211
594
        if ($newName !== false) {
595 9211
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";
596 7569
597
            /**
598
             * Rename table's default constraints names
599
             * to match the new table name.
600
             * This is necessary to ensure that the default
601
             * constraints can be referenced in future table
602
             * alterations as the table name is encoded in
603
             * default constraints' names.
604
             */
605
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
606 7569
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
607
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
608 7569
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
609 7569
                'FROM sys.default_constraints dc ' .
610 7569
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
611 7569
                "WHERE tbl.name = '" . $newName->getName() . "';" .
612 7569
                'EXEC sp_executesql @sql';
613 7569
        }
614
615
        $sql = array_merge(
616 9211
            $this->getPreAlterTableIndexForeignKeySQL($diff),
617 9211
            $sql,
618 9211
            $this->getPostAlterTableIndexForeignKeySQL($diff)
619 9211
        );
620
621
        return array_merge($sql, $tableSql, $columnSql);
622 9211
    }
623
624
    /**
625
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
626
     *
627
     * @param string $tableName The name of the table to generate the clause for.
628
     * @param Column $column    The column to generate the clause for.
629
     *
630
     * @return string
631
     */
632
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
633 8933
    {
634
        $columnDef         = $column->toArray();
635 8933
        $columnDef['name'] = $column->getQuotedName($this);
636 8933
637
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
638 8933
    }
639
640
    /**
641
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
642
     *
643
     * @param string $tableName  The name of the table to generate the clause for.
644
     * @param string $columnName The name of the column to generate the clause for.
645
     *
646
     * @return string
647
     */
648
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
649 8927
    {
650
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
651 8927
    }
652
653
    /**
654
     * Checks whether a column alteration requires dropping its default constraint first.
655
     *
656
     * Different to other database vendors SQL Server implements column default values
657
     * as constraints and therefore changes in a column's default value as well as changes
658
     * in a column's type require dropping the default constraint first before being to
659
     * alter the particular column to the new definition.
660
     *
661
     * @param ColumnDiff $columnDiff The column diff to evaluate.
662
     *
663
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
664
     */
665
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
666 9049
    {
667
        // We can only decide whether to drop an existing default constraint
668
        // if we know the original default value.
669
        if (! $columnDiff->fromColumn instanceof Column) {
670 9049
            return false;
671 7149
        }
672
673
        // We only need to drop an existing default constraint if we know the
674
        // column was defined with a default value before.
675
        if ($columnDiff->fromColumn->getDefault() === null) {
676 9025
            return false;
677 8993
        }
678
679
        // We need to drop an existing default constraint if the column was
680
        // defined with a default value before and it has changed.
681
        if ($columnDiff->hasChanged('default')) {
682 8921
            return true;
683 8708
        }
684
685
        // We need to drop an existing default constraint if the column was
686
        // defined with a default value before and the native column type has changed.
687
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
688 8897
    }
689
690
    /**
691
     * Returns the SQL statement for altering a column comment.
692
     *
693
     * SQL Server does not support native column comments,
694
     * therefore the extended properties functionality is used
695
     * as a workaround to store them.
696
     * The property name used to store column comments is "MS_Description"
697
     * which provides compatibility with SQL Server Management Studio,
698
     * as column comments are stored in the same property there when
699
     * specifying a column's "Description" attribute.
700
     *
701
     * @param string      $tableName  The quoted table name to which the column belongs.
702
     * @param string      $columnName The quoted column name to alter the comment for.
703
     * @param string|null $comment    The column's comment.
704
     *
705
     * @return string
706
     */
707
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
708 9039
    {
709
        if (strpos($tableName, '.') !== false) {
710 9039
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
711 8731
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
712 8731
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
713 8731
        } else {
714
            $schemaSQL = "'dbo'";
715 8987
            $tableSQL  = $this->quoteStringLiteral($tableName);
716 8987
        }
717
718
        return $this->getUpdateExtendedPropertySQL(
719 9039
            'MS_Description',
720 9039
            $comment,
721 12
            'SCHEMA',
722 9039
            $schemaSQL,
723 12
            'TABLE',
724 9039
            $tableSQL,
725 12
            'COLUMN',
726 9039
            $columnName
727 12
        );
728
    }
729
730
    /**
731
     * Returns the SQL statement for dropping a column comment.
732
     *
733
     * SQL Server does not support native column comments,
734
     * therefore the extended properties functionality is used
735
     * as a workaround to store them.
736
     * The property name used to store column comments is "MS_Description"
737
     * which provides compatibility with SQL Server Management Studio,
738
     * as column comments are stored in the same property there when
739
     * specifying a column's "Description" attribute.
740
     *
741
     * @param string $tableName  The quoted table name to which the column belongs.
742
     * @param string $columnName The quoted column name to drop the comment for.
743
     *
744
     * @return string
745
     */
746
    protected function getDropColumnCommentSQL($tableName, $columnName)
747 9062
    {
748
        if (strpos($tableName, '.') !== false) {
749 9062
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
750 8756
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
751 8756
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
752 8756
        } else {
753
            $schemaSQL = "'dbo'";
754 8987
            $tableSQL  = $this->quoteStringLiteral($tableName);
755 8987
        }
756
757
        return $this->getDropExtendedPropertySQL(
758 9062
            'MS_Description',
759 9062
            'SCHEMA',
760 9062
            $schemaSQL,
761 12
            'TABLE',
762 9062
            $tableSQL,
763 12
            'COLUMN',
764 9062
            $columnName
765 12
        );
766
    }
767
768
    /**
769
     * {@inheritdoc}
770
     */
771
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
772 7112
    {
773
        return [sprintf(
774 7112
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
775 30
            $tableName,
776 7112
            $oldIndexName,
777 7112
            $index->getQuotedName($this)
778 7112
        ),
779
        ];
780
    }
781
782
    /**
783
     * Returns the SQL statement for adding an extended property to a database object.
784
     *
785
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
786
     *
787
     * @param string      $name       The name of the property to add.
788
     * @param string|null $value      The value of the property to add.
789
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
790
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
791
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
792
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
793
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
794
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
795
     *
796
     * @return string
797
     */
798
    public function getAddExtendedPropertySQL(
799 9138
        $name,
800
        $value = null,
801
        $level0Type = null,
802
        $level0Name = null,
803
        $level1Type = null,
804
        $level1Name = null,
805
        $level2Type = null,
806
        $level2Name = null
807
    ) {
808
        return 'EXEC sp_addextendedproperty ' .
809
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
810 9138
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
811 9138
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
812 9138
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
813 9138
    }
814
815
    /**
816
     * Returns the SQL statement for dropping an extended property from a database object.
817
     *
818
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
819
     *
820
     * @param string      $name       The name of the property to drop.
821
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
822
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
823
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
824
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
825
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
826
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
827
     *
828
     * @return string
829
     */
830
    public function getDropExtendedPropertySQL(
831 9062
        $name,
832
        $level0Type = null,
833
        $level0Name = null,
834
        $level1Type = null,
835
        $level1Name = null,
836
        $level2Type = null,
837
        $level2Name = null
838
    ) {
839
        return 'EXEC sp_dropextendedproperty ' .
840
            'N' . $this->quoteStringLiteral($name) . ', ' .
841 9062
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
842 9062
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
843 9062
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
844 9062
    }
845
846
    /**
847
     * Returns the SQL statement for updating an extended property of a database object.
848
     *
849
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
850
     *
851
     * @param string      $name       The name of the property to update.
852
     * @param string|null $value      The value of the property to update.
853
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
854
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
855
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
856
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
857
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
858
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
859
     *
860
     * @return string
861
     */
862
    public function getUpdateExtendedPropertySQL(
863 9039
        $name,
864
        $value = null,
865
        $level0Type = null,
866
        $level0Name = null,
867
        $level1Type = null,
868
        $level1Name = null,
869
        $level2Type = null,
870
        $level2Name = null
871
    ) {
872
        return 'EXEC sp_updateextendedproperty ' .
873
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
874 9039
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
875 9039
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
876 9039
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
877 9039
    }
878
879
    /**
880
     * {@inheritDoc}
881
     */
882
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
883 780
    {
884
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
885 780
    }
886
887
    /**
888
     * {@inheritDoc}
889
     */
890
    public function getListTablesSQL()
891
    {
892
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
893
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
894
        return "SELECT name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
895
    }
896
897
    /**
898
     * {@inheritDoc}
899
     */
900
    public function getListTableColumnsSQL($table, $database = null)
901 8593
    {
902
        return "SELECT    col.name,
903
                          type.name AS type,
904
                          col.max_length AS length,
905
                          ~col.is_nullable AS notnull,
906
                          def.definition AS [default],
907
                          col.scale,
908
                          col.precision,
909
                          col.is_identity AS autoincrement,
910
                          col.collation_name AS collation,
911
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
912
                FROM      sys.columns AS col
913
                JOIN      sys.types AS type
914
                ON        col.user_type_id = type.user_type_id
915
                JOIN      sys.objects AS obj
916
                ON        col.object_id = obj.object_id
917
                JOIN      sys.schemas AS scm
918
                ON        obj.schema_id = scm.schema_id
919
                LEFT JOIN sys.default_constraints def
920
                ON        col.default_object_id = def.object_id
921
                AND       col.object_id = def.parent_object_id
922
                LEFT JOIN sys.extended_properties AS prop
923
                ON        obj.object_id = prop.major_id
924
                AND       col.column_id = prop.minor_id
925
                AND       prop.name = 'MS_Description'
926
                WHERE     obj.type = 'U'
927
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
928 8593
    }
929
930
    /**
931
     * {@inheritDoc}
932
     */
933
    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

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