Completed
Push — master ( 04cb1b...585713 )
by Sergei
20s queued 14s
created

SQLServerPlatform::_getCreateTableSQL()   F

Complexity

Conditions 20
Paths 432

Size

Total Lines 64
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 20.2744

Importance

Changes 0
Metric Value
eloc 33
dl 0
loc 64
ccs 31
cts 34
cp 0.9118
rs 0.7887
c 0
b 0
f 0
cc 20
nc 432
nop 3
crap 20.2744

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 9738
    public function getCurrentDateSQL()
48
    {
49 9738
        return $this->getConvertExpression('date', 'GETDATE()');
50
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55 9730
    public function getCurrentTimeSQL()
56
    {
57 9730
        return $this->getConvertExpression('time', 'GETDATE()');
58
    }
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 9738
    private function getConvertExpression($dataType, $expression)
69
    {
70 9738
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76 1080
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
77
    {
78 1080
        $factorClause = '';
79
80 1080
        if ($operator === '-') {
81 1080
            $factorClause = '-1 * ';
82
        }
83
84 1080
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
85
    }
86
87
    /**
88
     * {@inheritDoc}
89
     */
90 1059
    public function getDateDiffExpression($date1, $date2)
91
    {
92 1059
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
93
    }
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 9334
    public function prefersIdentityColumns()
102
    {
103 9334
        return true;
104
    }
105
106
    /**
107
     * {@inheritDoc}
108
     *
109
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
110
     */
111 9478
    public function supportsIdentityColumns()
112
    {
113 9478
        return true;
114
    }
115
116
    /**
117
     * {@inheritDoc}
118
     */
119 1146
    public function supportsReleaseSavepoints()
120
    {
121 1146
        return false;
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 9485
    public function supportsSchemas()
128
    {
129 9485
        return true;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135 915
    public function getDefaultSchemaName()
136
    {
137 915
        return 'dbo';
138
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143 8179
    public function supportsColumnCollation()
144
    {
145 8179
        return true;
146
    }
147
148
    /**
149
     * {@inheritDoc}
150
     */
151 10202
    public function hasNativeGuidType()
152
    {
153 10202
        return true;
154
    }
155
156
    /**
157
     * {@inheritDoc}
158
     */
159 9789
    public function getCreateDatabaseSQL($name)
160
    {
161 9789
        return 'CREATE DATABASE ' . $name;
162
    }
163
164
    /**
165
     * {@inheritDoc}
166
     */
167 9789
    public function getDropDatabaseSQL($name)
168
    {
169 9789
        return 'DROP DATABASE ' . $name;
170
    }
171
172
    /**
173
     * {@inheritDoc}
174
     */
175 9697
    public function supportsCreateDropDatabase()
176
    {
177 9697
        return true;
178
    }
179
180
    /**
181
     * {@inheritDoc}
182
     */
183 8887
    public function getCreateSchemaSQL($schemaName)
184
    {
185 8887
        return 'CREATE SCHEMA ' . $schemaName;
186
    }
187
188
    /**
189
     * {@inheritDoc}
190
     */
191 6812
    public function getDropForeignKeySQL($foreignKey, $table)
192
    {
193 6812
        if (! $foreignKey instanceof ForeignKeyConstraint) {
194 6381
            $foreignKey = new Identifier($foreignKey);
195
        }
196
197 6812
        if (! $table instanceof Table) {
198 6812
            $table = new Identifier($table);
199
        }
200
201 6812
        $foreignKey = $foreignKey->getQuotedName($this);
202 6812
        $table      = $table->getQuotedName($this);
203
204 6812
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
205
    }
206
207
    /**
208
     * {@inheritDoc}
209
     */
210 953
    public function getDropIndexSQL($index, $table = null)
211
    {
212 953
        if ($index instanceof Index) {
213 945
            $index = $index->getQuotedName($this);
214 953
        } elseif (! is_string($index)) {
215
            throw new InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
216
        }
217
218 953
        if (! isset($table)) {
219
            return 'DROP INDEX ' . $index;
220
        }
221
222 953
        if ($table instanceof Table) {
223 953
            $table = $table->getQuotedName($this);
224
        }
225
226 953
        return sprintf(
227
            <<<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 953
            $index,
235 953
            $table,
236 953
            $index,
237 953
            $index,
238 953
            $table
239
        );
240
    }
241
242
    /**
243
     * {@inheritDoc}
244
     */
245 9956
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
246
    {
247 9956
        $defaultConstraintsSql = [];
248 9956
        $commentsSql           = [];
249
250
        // @todo does other code breaks because of this?
251
        // force primary keys to be not null
252 9956
        foreach ($columns as &$column) {
253 9956
            if (isset($column['primary']) && $column['primary']) {
254 9175
                $column['notnull'] = true;
255
            }
256
257
            // Build default constraints SQL statements.
258 9956
            if (isset($column['default'])) {
259 8646
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
260 8646
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
261
            }
262
263 9956
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
264 9944
                continue;
265
            }
266
267 8894
            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
268
        }
269
270 9956
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
271
272 9956
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
273
            foreach ($options['uniqueConstraints'] as $name => $definition) {
274
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
275
            }
276
        }
277
278 9956
        if (isset($options['primary']) && ! empty($options['primary'])) {
279 9181
            $flags = '';
280 9181
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
281 8681
                $flags = ' NONCLUSTERED';
282
            }
283 9181
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
284
        }
285
286 9956
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
287
288 9956
        $check = $this->getCheckDeclarationSQL($columns);
289 9956
        if (! empty($check)) {
290
            $query .= ', ' . $check;
291
        }
292 9956
        $query .= ')';
293
294 9956
        $sql = [$query];
295
296 9956
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
297 7463
            foreach ($options['indexes'] as $index) {
298 7463
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
299
            }
300
        }
301
302 9956
        if (isset($options['foreignKeys'])) {
303 7282
            foreach ((array) $options['foreignKeys'] as $definition) {
304 7075
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
305
            }
306
        }
307
308 9956
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
309
    }
310
311
    /**
312
     * {@inheritDoc}
313
     */
314 8662
    public function getCreatePrimaryKeySQL(Index $index, $table)
315
    {
316 8662
        if ($table instanceof Table) {
317
            $identifier = $table->getQuotedName($this);
318
        } else {
319 8662
            $identifier = $table;
320
        }
321
322 8662
        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';
323
324 8662
        if ($index->hasFlag('nonclustered')) {
325 8656
            $sql .= ' NONCLUSTERED';
326
        }
327
328 8662
        return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
329
    }
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 8912
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
349
    {
350 8912
        if (strpos($tableName, '.') !== false) {
351 8587
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
352 8587
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
353 8587
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
354
        } else {
355 8808
            $schemaSQL = "'dbo'";
356 8808
            $tableSQL  = $this->quoteStringLiteral($tableName);
357
        }
358
359 8912
        return $this->getAddExtendedPropertySQL(
360 8912
            'MS_Description',
361 42
            $comment,
362 8912
            'SCHEMA',
363 42
            $schemaSQL,
364 8912
            'TABLE',
365 42
            $tableSQL,
366 8912
            'COLUMN',
367 42
            $columnName
368
        );
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 8827
    public function getDefaultConstraintDeclarationSQL($table, array $column)
382
    {
383 8827
        if (! isset($column['default'])) {
384
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
385
        }
386
387 8827
        $columnName = new Identifier($column['name']);
388
389
        return ' CONSTRAINT ' .
390 8827
            $this->generateDefaultConstraintName($table, $column['name']) .
391 8827
            $this->getDefaultValueDeclarationSQL($column) .
392 8827
            ' FOR ' . $columnName->getQuotedName($this);
393
    }
394
395
    /**
396
     * {@inheritDoc}
397
     */
398 6987
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
399
    {
400 6987
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
401
402 6987
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
403
404 6987
        return $constraint;
405
    }
406
407
    /**
408
     * {@inheritDoc}
409
     */
410 9011
    public function getCreateIndexSQL(Index $index, $table)
411
    {
412 9011
        $constraint = parent::getCreateIndexSQL($index, $table);
413
414 9011
        if ($index->isUnique() && ! $index->isPrimary()) {
415 7457
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
416
        }
417
418 9011
        return $constraint;
419
    }
420
421
    /**
422
     * {@inheritDoc}
423
     */
424 9011
    protected function getCreateIndexSQLFlags(Index $index)
425
    {
426 9011
        $type = '';
427 9011
        if ($index->isUnique()) {
428 7457
            $type .= 'UNIQUE ';
429
        }
430
431 9011
        if ($index->hasFlag('clustered')) {
432 8706
            $type .= 'CLUSTERED ';
433 7487
        } elseif ($index->hasFlag('nonclustered')) {
434
            $type .= 'NONCLUSTERED ';
435
        }
436
437 9011
        return $type;
438
    }
439
440
    /**
441
     * Extend unique key constraint with required filters
442
     *
443
     * @param string $sql
444
     *
445
     * @return string
446
     */
447 7469
    private function _appendUniqueConstraintDefinition($sql, Index $index)
448
    {
449 7469
        $fields = [];
450
451 7469
        foreach ($index->getQuotedColumns($this) as $field) {
452 7469
            $fields[] = $field . ' IS NOT NULL';
453
        }
454
455 7469
        return $sql . ' WHERE ' . implode(' AND ', $fields);
456
    }
457
458
    /**
459
     * {@inheritDoc}
460
     */
461 8985
    public function getAlterTableSQL(TableDiff $diff)
462
    {
463 8985
        $queryParts  = [];
464 8985
        $sql         = [];
465 8985
        $columnSql   = [];
466 8985
        $commentsSql = [];
467
468 8985
        foreach ($diff->addedColumns as $column) {
469 8907
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
470
                continue;
471
            }
472
473 8907
            $columnDef    = $column->toArray();
474 8907
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
475
476 8907
            if (isset($columnDef['default'])) {
477 8149
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
478
            }
479
480 8907
            $comment = $this->getColumnComment($column);
481
482 8907
            if (empty($comment) && ! is_numeric($comment)) {
483 8803
                continue;
484
            }
485
486 8865
            $commentsSql[] = $this->getCreateColumnCommentSQL(
487 8865
                $diff->name,
488 8865
                $column->getQuotedName($this),
489 18
                $comment
490
            );
491
        }
492
493 8985
        foreach ($diff->removedColumns as $column) {
494 8809
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
495
                continue;
496
            }
497
498 8809
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
499
        }
500
501 8985
        foreach ($diff->changedColumns as $columnDiff) {
502 8910
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
503
                continue;
504
            }
505
506 8910
            $column     = $columnDiff->column;
507 8910
            $comment    = $this->getColumnComment($column);
508 8910
            $hasComment = ! empty($comment) || is_numeric($comment);
509
510 8910
            if ($columnDiff->fromColumn instanceof Column) {
511 8880
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
512 8880
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
513
514 8880
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
515 8813
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
516 8813
                        $diff->name,
517 8813
                        $column->getQuotedName($this),
518 12
                        $comment
519
                    );
520 8874
                } elseif ($hasFromComment && ! $hasComment) {
521 8836
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
522 8799
                } elseif ($hasComment) {
523 8761
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
524 8761
                        $diff->name,
525 8761
                        $column->getQuotedName($this),
526 6
                        $comment
527
                    );
528
                }
529
            }
530
531
            // Do not add query part if only comment has changed.
532 8910
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
533 8848
                continue;
534
            }
535
536 8823
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
537
538 8823
            if ($requireDropDefaultConstraint) {
539 8695
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
540 8695
                    $diff->name,
541 8695
                    $columnDiff->oldColumnName
542
                );
543
            }
544
545 8823
            $columnDef = $column->toArray();
546
547 8823
            $queryParts[] = 'ALTER COLUMN ' .
548 8823
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
549
550 8823
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
551 8787
                continue;
552
            }
553
554 8701
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
555
        }
556
557 8985
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
558 8787
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
559
                continue;
560
            }
561
562 8787
            $oldColumnName = new Identifier($oldColumnName);
563
564 8787
            $sql[] = "sp_RENAME '" .
565 8787
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
566 8787
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
567
568
            // Recreate default constraint with new column name if necessary (for future reference).
569 8787
            if ($column->getDefault() === null) {
570 8779
                continue;
571
            }
572
573 6785
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
574 6785
                $diff->name,
575 6785
                $oldColumnName->getQuotedName($this)
576
            );
577 6785
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
578
        }
579
580 8985
        $tableSql = [];
581
582 8985
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
583
            return array_merge($tableSql, $columnSql);
584
        }
585
586 8985
        foreach ($queryParts as $query) {
587 8937
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
588
        }
589
590 8985
        $sql = array_merge($sql, $commentsSql);
591
592 8985
        $newName = $diff->getNewName();
593
594 8985
        if ($newName !== false) {
595 7343
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";
596
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 7343
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
606
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
607 7343
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
608 7343
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
609 7343
                'FROM sys.default_constraints dc ' .
610 7343
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
611 7343
                "WHERE tbl.name = '" . $newName->getName() . "';" .
612 7343
                'EXEC sp_executesql @sql';
613
        }
614
615 8985
        $sql = array_merge(
616 8985
            $this->getPreAlterTableIndexForeignKeySQL($diff),
617 8985
            $sql,
618 8985
            $this->getPostAlterTableIndexForeignKeySQL($diff)
619
        );
620
621 8985
        return array_merge($sql, $tableSql, $columnSql);
622
    }
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 8707
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
633
    {
634 8707
        $columnDef         = $column->toArray();
635 8707
        $columnDef['name'] = $column->getQuotedName($this);
636
637 8707
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
638
    }
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 8701
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
649
    {
650 8701
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
651
    }
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 8823
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
666
    {
667
        // We can only decide whether to drop an existing default constraint
668
        // if we know the original default value.
669 8823
        if (! $columnDiff->fromColumn instanceof Column) {
670 6924
            return false;
671
        }
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 8799
        if ($columnDiff->fromColumn->getDefault() === null) {
676 8767
            return false;
677
        }
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 8695
        if ($columnDiff->hasChanged('default')) {
682 8482
            return true;
683
        }
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 8671
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
688
    }
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 8813
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
708
    {
709 8813
        if (strpos($tableName, '.') !== false) {
710 8506
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
711 8506
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
712 8506
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
713
        } else {
714 8761
            $schemaSQL = "'dbo'";
715 8761
            $tableSQL  = $this->quoteStringLiteral($tableName);
716
        }
717
718 8813
        return $this->getUpdateExtendedPropertySQL(
719 8813
            'MS_Description',
720 12
            $comment,
721 8813
            'SCHEMA',
722 12
            $schemaSQL,
723 8813
            'TABLE',
724 12
            $tableSQL,
725 8813
            'COLUMN',
726 12
            $columnName
727
        );
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 8836
    protected function getDropColumnCommentSQL($tableName, $columnName)
747
    {
748 8836
        if (strpos($tableName, '.') !== false) {
749 8531
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
750 8531
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
751 8531
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
752
        } else {
753 8761
            $schemaSQL = "'dbo'";
754 8761
            $tableSQL  = $this->quoteStringLiteral($tableName);
755
        }
756
757 8836
        return $this->getDropExtendedPropertySQL(
758 8836
            'MS_Description',
759 8836
            'SCHEMA',
760 12
            $schemaSQL,
761 8836
            'TABLE',
762 12
            $tableSQL,
763 8836
            'COLUMN',
764 12
            $columnName
765
        );
766
    }
767
768
    /**
769
     * {@inheritdoc}
770
     */
771 6955
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
772
    {
773 6955
        return [sprintf(
774 30
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
775 6955
            $tableName,
776 6955
            $oldIndexName,
777 6955
            $index->getQuotedName($this)
778
        ),
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 8912
    public function getAddExtendedPropertySQL(
799
        $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 8912
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
810 8912
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
811 8912
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
812 8912
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
813
    }
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 8836
    public function getDropExtendedPropertySQL(
831
        $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 8836
            'N' . $this->quoteStringLiteral($name) . ', ' .
841 8836
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
842 8836
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
843 8836
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
844
    }
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 8813
    public function getUpdateExtendedPropertySQL(
863
        $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 8813
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
874 8813
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
875 8813
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
876 8813
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
877
    }
878
879
    /**
880
     * {@inheritDoc}
881
     */
882 762
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
883
    {
884 762
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
885
    }
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 8366
    public function getListTableColumnsSQL($table, $database = null)
901
    {
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 8366
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
928
    }
929
930
    /**
931
     * {@inheritDoc}
932
     */
933 8320
    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
    {
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 8320
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
950
    }
951
952
    /**
953
     * {@inheritDoc}
954
     */
955 8274
    public function getListTableIndexesSQL($table, $currentDatabase = null)
956
    {
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 8274
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
972
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
973
    }
974
975
    /**
976
     * {@inheritDoc}
977
     */
978 941
    public function getCreateViewSQL($name, $sql)
979
    {
980 941
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
981
    }
982
983
    /**
984
     * {@inheritDoc}
985
     */
986 941
    public function getListViewsSQL($database)
987
    {
988 941
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
989
    }
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 8390
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
1001
    {
1002 8390
        if (strpos($table, '.') !== false) {
1003 8247
            [$schema, $table] = explode('.', $table);
1004 8247
            $schema           = $this->quoteStringLiteral($schema);
1005 8247
            $table            = $this->quoteStringLiteral($table);
1006
        } else {
1007 8372
            $schema = 'SCHEMA_NAME()';
1008 8372
            $table  = $this->quoteStringLiteral($table);
1009
        }
1010
1011 8390
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1012
    }
1013
1014
    /**
1015
     * {@inheritDoc}
1016
     */
1017 941
    public function getDropViewSQL($name)
1018
    {
1019 941
        return 'DROP VIEW ' . $name;
1020
    }
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 1078
    public function getLocateExpression($str, $substr, $startPos = false)
1036
    {
1037 1078
        if ($startPos === false) {
1038 1078
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1039
        }
1040
1041 1078
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1042
    }
1043
1044
    /**
1045
     * {@inheritDoc}
1046
     */
1047
    public function getModExpression($expression1, $expression2)
1048
    {
1049
        return $expression1 . ' % ' . $expression2;
1050
    }
1051
1052
    /**
1053
     * {@inheritDoc}
1054
     */
1055 1136
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1056
    {
1057 1136
        if (! $char) {
1058 1136
            switch ($pos) {
1059
                case TrimMode::LEADING:
1060 1134
                    $trimFn = 'LTRIM';
1061 1134
                    break;
1062
1063
                case TrimMode::TRAILING:
1064 1132
                    $trimFn = 'RTRIM';
1065 1132
                    break;
1066
1067
                default:
1068 1136
                    return 'LTRIM(RTRIM(' . $str . '))';
1069
            }
1070
1071 1134
            return $trimFn . '(' . $str . ')';
1072
        }
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 1128
        $pattern = "'%[^' + " . $char . " + ']%'";
1084
1085 1128
        if ($pos === TrimMode::LEADING) {
1086 1124
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1087
        }
1088
1089 1128
        if ($pos === TrimMode::TRAILING) {
1090 1120
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1091
        }
1092
1093 1128
        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
    }
1095
1096
    /**
1097
     * {@inheritDoc}
1098
     */
1099 9431
    public function getConcatExpression()
1100
    {
1101 9431
        $args = func_get_args();
1102
1103 9431
        return '(' . implode(' + ', $args) . ')';
1104
    }
1105
1106
    /**
1107
     * {@inheritDoc}
1108
     */
1109 9608
    public function getListDatabasesSQL()
1110
    {
1111 9608
        return 'SELECT * FROM sys.databases';
1112
    }
1113
1114
    /**
1115
     * {@inheritDoc}
1116
     */
1117 969
    public function getListNamespacesSQL()
1118
    {
1119 969
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1120
    }
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 9406
    public function getSetTransactionIsolationSQL($level)
1146
    {
1147 9406
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1148
    }
1149
1150
    /**
1151
     * {@inheritDoc}
1152
     */
1153 9938
    public function getIntegerTypeDeclarationSQL(array $field)
1154
    {
1155 9938
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1156
    }
1157
1158
    /**
1159
     * {@inheritDoc}
1160
     */
1161 823
    public function getBigIntTypeDeclarationSQL(array $field)
1162
    {
1163 823
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1164
    }
1165
1166
    /**
1167
     * {@inheritDoc}
1168
     */
1169 925
    public function getSmallIntTypeDeclarationSQL(array $field)
1170
    {
1171 925
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1172
    }
1173
1174
    /**
1175
     * {@inheritDoc}
1176
     */
1177 8031
    public function getGuidTypeDeclarationSQL(array $field)
1178
    {
1179 8031
        return 'UNIQUEIDENTIFIER';
1180
    }
1181
1182
    /**
1183
     * {@inheritDoc}
1184
     */
1185 9863
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1186
    {
1187 9863
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1188
    }
1189
1190
    /**
1191
     * {@inheritdoc}
1192
     */
1193 8651
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1194
    {
1195 8651
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1196
    }
1197
1198
    /**
1199
     * {@inheritdoc}
1200
     */
1201 8663
    public function getBinaryMaxLength()
1202
    {
1203 8663
        return 8000;
1204
    }
1205
1206
    /**
1207
     * {@inheritDoc}
1208
     */
1209 3160
    public function getClobTypeDeclarationSQL(array $field)
1210
    {
1211 3160
        return 'VARCHAR(MAX)';
1212
    }
1213
1214
    /**
1215
     * {@inheritDoc}
1216
     */
1217 9938
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1218
    {
1219 9938
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1220
    }
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 7369
    public function getBooleanTypeDeclarationSQL(array $field)
1250
    {
1251 7369
        return 'BIT';
1252
    }
1253
1254
    /**
1255
     * {@inheritDoc}
1256
     */
1257 9309
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1258
    {
1259 9309
        $where = [];
1260
1261 9309
        if ($offset > 0) {
1262 9183
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1263
        }
1264
1265 9309
        if ($limit !== null) {
1266 9305
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1267 9305
            $top     = sprintf('TOP %d', $offset + $limit);
1268
        } else {
1269 6204
            $top = 'TOP 9223372036854775807';
1270
        }
1271
1272 9309
        if (empty($where)) {
1273 6204
            return $query;
1274
        }
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 9305
        if (! preg_match('/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/is', $query, $matches)) {
1281
            return $query;
1282
        }
1283
1284 9305
        $query = $matches[1] . $top . ' ' . $matches[2];
1285
1286 9305
        if (stristr($query, 'ORDER BY')) {
1287
            // Inner order by is not valid in SQL Server for our purposes
1288
            // unless it's in a TOP N subquery.
1289 9227
            $query = $this->scrubInnerOrderBy($query);
1290
        }
1291
1292
        // Build a new limited query around the original, using a CTE
1293 9305
        return sprintf(
1294
            '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 80
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1299 9305
            $query,
1300 9305
            implode(' AND ', $where)
1301
        );
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 9227
    private function scrubInnerOrderBy($query)
1313
    {
1314 9227
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1315 9227
        $offset = 0;
1316
1317 9227
        while ($count-- > 0) {
1318 9227
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1319 9227
            if ($orderByPos === false) {
1320 3304
                break;
1321
            }
1322
1323 9227
            $qLen            = strlen($query);
1324 9227
            $parenCount      = 0;
1325 9227
            $currentPosition = $orderByPos;
1326
1327 9227
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1328 9227
                if ($query[$currentPosition] === '(') {
1329 8879
                    $parenCount++;
1330 9227
                } elseif ($query[$currentPosition] === ')') {
1331 9053
                    $parenCount--;
1332
                }
1333
1334 9227
                $currentPosition++;
1335
            }
1336
1337 9227
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1338
                // If the order by clause is in a TOP N subquery, do not remove
1339
                // it and continue iteration from the current position.
1340 9221
                $offset = $currentPosition;
1341 9221
                continue;
1342
            }
1343
1344 9045
            if ($currentPosition >= $qLen - 1) {
1345
                continue;
1346
            }
1347
1348 9045
            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1349 9045
            $offset = $orderByPos;
1350
        }
1351
1352 9227
        return $query;
1353
    }
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 9227
    private function isOrderByInTopNSubquery($query, $currentPosition)
1364
    {
1365
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1366 9227
        $subQueryBuffer = '';
1367 9227
        $parenCount     = 0;
1368
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 9227
        while ($parenCount >= 0 && $currentPosition >= 0) {
1372 9227
            if ($query[$currentPosition] === '(') {
1373 9059
                $parenCount--;
1374 9227
            } elseif ($query[$currentPosition] === ')') {
1375 8978
                $parenCount++;
1376
            }
1377
1378
            // Only yank query text on the same nesting level as the ORDER BY clause.
1379 9227
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1380
1381 9227
            $currentPosition--;
1382
        }
1383
1384 9227
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1385
    }
1386
1387
    /**
1388
     * {@inheritDoc}
1389
     */
1390 3000
    public function supportsLimitOffset()
1391
    {
1392 3000
        return false;
1393
    }
1394
1395
    /**
1396
     * {@inheritDoc}
1397
     */
1398 7369
    public function convertBooleans($item)
1399
    {
1400 7369
        if (is_array($item)) {
1401
            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 7369
        } elseif (is_bool($item) || is_numeric($item)) {
1409 7369
            $item = $item ? 1 : 0;
1410
        }
1411
1412 7369
        return $item;
1413
    }
1414
1415
    /**
1416
     * {@inheritDoc}
1417
     */
1418 829
    public function getCreateTemporaryTableSnippetSQL()
1419
    {
1420 829
        return 'CREATE TABLE';
1421
    }
1422
1423
    /**
1424
     * {@inheritDoc}
1425
     */
1426 829
    public function getTemporaryTableName($tableName)
1427
    {
1428 829
        return '#' . $tableName;
1429
    }
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 3904
    public function getName()
1467
    {
1468 3904
        return 'mssql';
1469
    }
1470
1471
    /**
1472
     * {@inheritDoc}
1473
     */
1474 8798
    protected function initializeDoctrineTypeMappings()
1475
    {
1476 8798
        $this->doctrineTypeMapping = [
1477
            '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 8798
    }
1504
1505
    /**
1506
     * {@inheritDoc}
1507
     */
1508 1146
    public function createSavePoint($savepoint)
1509
    {
1510 1146
        return 'SAVE TRANSACTION ' . $savepoint;
1511
    }
1512
1513
    /**
1514
     * {@inheritDoc}
1515
     */
1516
    public function releaseSavePoint($savepoint)
1517
    {
1518
        return '';
1519
    }
1520
1521
    /**
1522
     * {@inheritDoc}
1523
     */
1524 1146
    public function rollbackSavePoint($savepoint)
1525
    {
1526 1146
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1527
    }
1528
1529
    /**
1530
     * {@inheritdoc}
1531
     */
1532 8098
    public function getForeignKeyReferentialActionSQL($action)
1533
    {
1534
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1535 8098
        if (strtoupper($action) === 'RESTRICT') {
1536 7681
            return 'NO ACTION';
1537
        }
1538
1539 8092
        return parent::getForeignKeyReferentialActionSQL($action);
1540
    }
1541
1542
    /**
1543
     * {@inheritDoc}
1544
     */
1545 4021
    public function appendLockHint($fromClause, $lockMode)
1546
    {
1547 4021
        switch (true) {
1548
            case $lockMode === LockMode::NONE:
1549 3402
                return $fromClause . ' WITH (NOLOCK)';
1550
1551 4019
            case $lockMode === LockMode::PESSIMISTIC_READ:
1552 3352
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1553
1554 4017
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1555 3894
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1556
1557
            default:
1558 3458
                return $fromClause;
1559
        }
1560
    }
1561
1562
    /**
1563
     * {@inheritDoc}
1564
     */
1565 833
    public function getForUpdateSQL()
1566
    {
1567 833
        return ' ';
1568
    }
1569
1570
    /**
1571
     * {@inheritDoc}
1572
     */
1573 2749
    protected function getReservedKeywordsClass()
1574
    {
1575 2749
        return Keywords\SQLServerKeywords::class;
1576
    }
1577
1578
    /**
1579
     * {@inheritDoc}
1580
     */
1581 9370
    public function quoteSingleIdentifier($str)
1582
    {
1583 9370
        return '[' . str_replace(']', '][', $str) . ']';
1584
    }
1585
1586
    /**
1587
     * {@inheritDoc}
1588
     */
1589 7154
    public function getTruncateTableSQL($tableName, $cascade = false)
1590
    {
1591 7154
        $tableIdentifier = new Identifier($tableName);
1592
1593 7154
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1594
    }
1595
1596
    /**
1597
     * {@inheritDoc}
1598
     */
1599 8869
    public function getBlobTypeDeclarationSQL(array $field)
1600
    {
1601 8869
        return 'VARBINARY(MAX)';
1602
    }
1603
1604
    /**
1605
     * {@inheritdoc}
1606
     *
1607
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1608
     */
1609 10040
    public function getColumnDeclarationSQL($name, array $field)
1610
    {
1611 10040
        if (isset($field['columnDefinition'])) {
1612 6881
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1613
        } else {
1614 10034
            $collation = isset($field['collation']) && $field['collation'] ?
1615 10034
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1616
1617 10034
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1618
1619 10034
            $unique = isset($field['unique']) && $field['unique'] ?
1620 10034
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1621
1622 10034
            $check = isset($field['check']) && $field['check'] ?
1623 10034
                ' ' . $field['check'] : '';
1624
1625 10034
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1626 10034
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1627
        }
1628
1629 10040
        return $name . ' ' . $columnDef;
1630
    }
1631
1632
    /**
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 8827
    private function generateDefaultConstraintName($table, $column)
1641
    {
1642 8827
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1643
    }
1644
1645
    /**
1646
     * Returns a hash value for a given identifier.
1647
     *
1648
     * @param string $identifier Identifier to generate a hash value for.
1649
     *
1650
     * @return string
1651
     */
1652 8833
    private function generateIdentifierName($identifier)
1653
    {
1654
        // Always generate name for unquoted identifiers to ensure consistency.
1655 8833
        $identifier = new Identifier($identifier);
1656
1657 8833
        return strtoupper(dechex(crc32($identifier->getName())));
1658
    }
1659
}
1660