Failed Conditions
Pull Request — master (#3544)
by Luís
19:33 queued 08:11
created

SQLServerPlatform::doModifyLimitQuery()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 44
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6.0052

Importance

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

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