Completed
Pull Request — master (#3512)
by David
61:26
created

SQLServerPlatform::getLengthExpression()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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