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

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