Completed
Pull Request — 2.10.x (#3951)
by Grégoire
24:02 queued 20:55
created

SQLServerPlatform::scrubInnerOrderBy()   B

Complexity

Conditions 9
Paths 2

Size

Total Lines 41
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 9.0058

Importance

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

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