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

SQLServerPlatform::getCreateSchemaSQL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Doctrine\DBAL\Platforms;
4
5
use Doctrine\DBAL\LockMode;
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\ColumnDiff;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Doctrine\DBAL\Schema\Identifier;
10
use Doctrine\DBAL\Schema\Index;
11
use Doctrine\DBAL\Schema\Table;
12
use Doctrine\DBAL\Schema\TableDiff;
13
use Doctrine\DBAL\Types;
14
use InvalidArgumentException;
15
use function array_merge;
16
use function array_unique;
17
use function array_values;
18
use function count;
19
use function crc32;
20
use function dechex;
21
use function explode;
22
use function func_get_args;
23
use function implode;
24
use function is_array;
25
use function is_bool;
26
use function is_numeric;
27
use function is_string;
28
use function preg_match;
29
use function sprintf;
30
use function str_replace;
31
use function stripos;
32
use function stristr;
33
use function strlen;
34
use function strpos;
35
use function strtoupper;
36
use function substr;
37
use function substr_count;
38
39
/**
40
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
41
 * Microsoft SQL Server database platform.
42
 */
43
class SQLServerPlatform extends AbstractPlatform
44
{
45
    /**
46
     * {@inheritdoc}
47
     */
48 9892
    public function getCurrentDateSQL()
49
    {
50 9892
        return $this->getConvertExpression('date', 'GETDATE()');
51
    }
52
53
    /**
54
     * {@inheritdoc}
55
     */
56 9886
    public function getCurrentTimeSQL()
57
    {
58 9886
        return $this->getConvertExpression('time', 'GETDATE()');
59
    }
60
61
    /**
62
     * Returns an expression that converts an expression of one data type to another.
63
     *
64
     * @param string $dataType   The target native data type. Alias data types cannot be used.
65
     * @param string $expression The SQL expression to convert.
66
     *
67
     * @return string
68
     */
69 9892
    private function getConvertExpression($dataType, $expression)
70
    {
71 9892
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77 1067
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
78
    {
79 1067
        $factorClause = '';
80
81 1067
        if ($operator === '-') {
82 1067
            $factorClause = '-1 * ';
83
        }
84
85 1067
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
86
    }
87
88
    /**
89
     * {@inheritDoc}
90
     */
91 1046
    public function getDateDiffExpression($date1, $date2)
92
    {
93 1046
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
94
    }
95
96
    /**
97
     * {@inheritDoc}
98
     *
99
     * Microsoft SQL Server prefers "autoincrement" identity columns
100
     * since sequences can only be emulated with a table.
101
     */
102 9559
    public function prefersIdentityColumns()
103
    {
104 9559
        return true;
105
    }
106
107
    /**
108
     * {@inheritDoc}
109
     *
110
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
111
     */
112 9704
    public function supportsIdentityColumns()
113
    {
114 9704
        return true;
115
    }
116
117
    /**
118
     * {@inheritDoc}
119
     */
120 1133
    public function supportsReleaseSavepoints()
121
    {
122 1133
        return false;
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 9711
    public function supportsSchemas()
129
    {
130 9711
        return true;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 934
    public function getDefaultSchemaName()
137
    {
138 934
        return 'dbo';
139
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144 8405
    public function supportsColumnCollation()
145
    {
146 8405
        return true;
147
    }
148
149
    /**
150
     * {@inheritDoc}
151
     */
152 10396
    public function hasNativeGuidType()
153
    {
154 10396
        return true;
155
    }
156
157
    /**
158
     * {@inheritDoc}
159
     */
160 9983
    public function getCreateDatabaseSQL($name)
161
    {
162 9983
        return 'CREATE DATABASE ' . $name;
163
    }
164
165
    /**
166
     * {@inheritDoc}
167
     */
168 9983
    public function getDropDatabaseSQL($name)
169
    {
170 9983
        return 'DROP DATABASE ' . $name;
171
    }
172
173
    /**
174
     * {@inheritDoc}
175
     */
176 9891
    public function supportsCreateDropDatabase()
177
    {
178 9891
        return true;
179
    }
180
181
    /**
182
     * {@inheritDoc}
183
     */
184 9113
    public function getCreateSchemaSQL($schemaName)
185
    {
186 9113
        return 'CREATE SCHEMA ' . $schemaName;
187
    }
188
189
    /**
190
     * {@inheritDoc}
191
     */
192 6969
    public function getDropForeignKeySQL($foreignKey, $table)
193
    {
194 6969
        if (! $foreignKey instanceof ForeignKeyConstraint) {
195 6531
            $foreignKey = new Identifier($foreignKey);
196
        }
197
198 6969
        if (! $table instanceof Table) {
199 6969
            $table = new Identifier($table);
200
        }
201
202 6969
        $foreignKey = $foreignKey->getQuotedName($this);
203 6969
        $table      = $table->getQuotedName($this);
204
205 6969
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
206
    }
207
208
    /**
209
     * {@inheritDoc}
210
     */
211 972
    public function getDropIndexSQL($index, $table = null)
212
    {
213 972
        if ($index instanceof Index) {
214 964
            $index = $index->getQuotedName($this);
215 972
        } elseif (! is_string($index)) {
216
            throw new InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
217
        }
218
219 972
        if (! isset($table)) {
220
            return 'DROP INDEX ' . $index;
221
        }
222
223 972
        if ($table instanceof Table) {
224 972
            $table = $table->getQuotedName($this);
225
        }
226
227 972
        return sprintf(
228
            <<<SQL
229
IF EXISTS (SELECT * FROM sysobjects WHERE name = '%s')
230
    ALTER TABLE %s DROP CONSTRAINT %s
231
ELSE
232
    DROP INDEX %s ON %s
233
SQL
234
            ,
235 972
            $index,
236 972
            $table,
237 972
            $index,
238 972
            $index,
239 972
            $table
240
        );
241
    }
242
243
    /**
244
     * {@inheritDoc}
245
     */
246 10150
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
247
    {
248 10150
        $defaultConstraintsSql = [];
249 10150
        $commentsSql           = [];
250
251
        $tableComment = $options['comment'] ?? null;
252
        if ($tableComment !== null) {
253 10150
            $commentsSql[] = $this->getCommentOnTableSQL($tableName, $tableComment);
254 10150
        }
255 9369
256
        // @todo does other code breaks because of this?
257
        // force primary keys to be not null
258
        foreach ($columns as &$column) {
259 10150
            if (isset($column['primary']) && $column['primary']) {
260 8808
                $column['notnull'] = true;
261 8808
            }
262
263
            // Build default constraints SQL statements.
264 10150
            if (isset($column['default'])) {
265 10138
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
266
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
267
            }
268 9120
269
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
270
                continue;
271 10150
            }
272
273 10150
            $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 10150
            foreach ($options['uniqueConstraints'] as $name => $definition) {
280 9375
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
281 9375
            }
282 8906
        }
283
284 9375
        if (isset($options['primary']) && ! empty($options['primary'])) {
285
            $flags = '';
286
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
287 10150
                $flags = ' NONCLUSTERED';
288
            }
289 10150
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
290 10150
        }
291
292
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
293 10150
294
        $check = $this->getCheckDeclarationSQL($columns);
295 10150
        if (! empty($check)) {
296
            $query .= ', ' . $check;
297 10150
        }
298 7689
        $query .= ')';
299 7689
300
        $sql = [$query];
301
302
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
303 10150
            foreach ($options['indexes'] as $index) {
304 7407
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
305 7232
            }
306
        }
307
308
        if (isset($options['foreignKeys'])) {
309 10150
            foreach ((array) $options['foreignKeys'] as $definition) {
310
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
311
            }
312
        }
313
314
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
315 8887
    }
316
317 8887
    /**
318
     * {@inheritDoc}
319
     */
320 8887
    public function getCreatePrimaryKeySQL(Index $index, $table)
321
    {
322
        if ($table instanceof Table) {
323 8887
            $identifier = $table->getQuotedName($this);
324
        } else {
325 8887
            $identifier = $table;
326 8881
        }
327
328
        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';
329 8887
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 9138
     * @param string      $columnName The quoted column name to create the comment for.
350
     * @param string|null $comment    The column's comment.
351 9138
     *
352 8812
     * @return string
353 8812
     */
354 8812
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
355
    {
356 9034
        if (strpos($tableName, '.') !== false) {
357 9034
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
358
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
359
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
360 9138
        } else {
361 9138
            $schemaSQL = "'dbo'";
362 42
            $tableSQL  = $this->quoteStringLiteral($tableName);
363 9138
        }
364 42
365 9138
        return $this->getAddExtendedPropertySQL(
366 42
            'MS_Description',
367 9138
            $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 8989
     *
383
     * @return string
384 8989
     *
385
     * @throws InvalidArgumentException
386
     */
387
    public function getDefaultConstraintDeclarationSQL($table, array $column)
388 8989
    {
389
        if (! isset($column['default'])) {
390
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
391 8989
        }
392 8989
393 8989
        $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 7212
    }
400
401 7212
    /**
402
     * {@inheritDoc}
403 7212
     */
404
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
405 7212
    {
406
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
407
408
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
409
410
        return $constraint;
411 9237
    }
412
413 9237
    /**
414
     * {@inheritDoc}
415 9237
     */
416 7683
    public function getCreateIndexSQL(Index $index, $table)
417
    {
418
        $constraint = parent::getCreateIndexSQL($index, $table);
419 9237
420
        if ($index->isUnique() && ! $index->isPrimary()) {
421
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
422
        }
423
424
        return $constraint;
425 9237
    }
426
427 9237
    /**
428 9237
     * {@inheritDoc}
429 7683
     */
430
    protected function getCreateIndexSQLFlags(Index $index)
431
    {
432 9237
        $type = '';
433 8931
        if ($index->isUnique()) {
434 7713
            $type .= 'UNIQUE ';
435
        }
436
437
        if ($index->hasFlag('clustered')) {
438 9237
            $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 7695
     *
449
     * @param string $sql
450 7695
     *
451
     * @return string
452 7695
     */
453 7695
    private function _appendUniqueConstraintDefinition($sql, Index $index)
454
    {
455
        $fields = [];
456 7695
457
        foreach ($index->getQuotedColumns($this) as $field) {
458
            $fields[] = $field . ' IS NOT NULL';
459
        }
460
461
        return $sql . ' WHERE ' . implode(' AND ', $fields);
462 9211
    }
463
464 9211
    /**
465 9211
     * {@inheritDoc}
466 9211
     */
467 9211
    public function getAlterTableSQL(TableDiff $diff)
468
    {
469 9211
        $queryParts  = [];
470 9135
        $sql         = [];
471
        $columnSql   = [];
472
        $commentsSql = [];
473
474 9135
        foreach ($diff->addedColumns as $column) {
475 9135
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
476
                continue;
477 9135
            }
478 8708
479
            $columnDef    = $column->toArray();
480
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
481 9135
482
            if (isset($columnDef['default'])) {
483 9135
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
484 9031
            }
485
486
            $comment = $this->getColumnComment($column);
487 9091
488 9091
            if (empty($comment) && ! is_numeric($comment)) {
489 9091
                continue;
490 18
            }
491
492
            $commentsSql[] = $this->getCreateColumnCommentSQL(
493
                $diff->name,
494 9211
                $column->getQuotedName($this),
495 9035
                $comment
496
            );
497
        }
498
499 9035
        foreach ($diff->removedColumns as $column) {
500
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
501
                continue;
502 9211
            }
503 9136
504
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
505
        }
506
507 9136
        foreach ($diff->changedColumns as $columnDiff) {
508 9136
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
509 9136
                continue;
510
            }
511 9136
512 9106
            $column     = $columnDiff->column;
513 9106
            $comment    = $this->getColumnComment($column);
514
            $hasComment = ! empty($comment) || is_numeric($comment);
515 9106
516 9039
            if ($columnDiff->fromColumn instanceof Column) {
517 9039
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
518 9039
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
519 12
520
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
521 9100
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
522 9062
                        $diff->name,
523 9025
                        $column->getQuotedName($this),
524 8987
                        $comment
525 8987
                    );
526 8987
                } 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 9136
                    );
534 9074
                }
535
            }
536
537 9049
            // Do not add query part if only comment has changed.
538
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
539 9049
                continue;
540 8921
            }
541 8921
542 8921
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
543
544
            if ($requireDropDefaultConstraint) {
545
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
546 9049
                    $diff->name,
547
                    $columnDiff->oldColumnName
548 9049
                );
549 9049
            }
550
551 9049
            $columnDef = $column->toArray();
552 9013
553
            $queryParts[] = 'ALTER COLUMN ' .
554
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
555 8927
556
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
557
                continue;
558 9211
            }
559 9013
560
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
561
        }
562
563 9013
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
564
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
565 9013
                continue;
566 9013
            }
567 9013
568
            $oldColumnName = new Identifier($oldColumnName);
569
570 9013
            $sql[] = "sp_RENAME '" .
571 9005
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
572
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
573
574 6942
            // Recreate default constraint with new column name if necessary (for future reference).
575 6942
            if ($column->getDefault() === null) {
576 6942
                continue;
577
            }
578 6942
579
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
580
                $diff->name,
581 9211
                $oldColumnName->getQuotedName($this)
582
            );
583 9211
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
584
        }
585
586
        $tableSql = [];
587 9211
588 9163
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
589
            return array_merge($tableSql, $columnSql);
590
        }
591 9211
592
        foreach ($queryParts as $query) {
593 9211
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
594
        }
595 9211
596 7569
        $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 7569
             * This is necessary to ensure that the default
607
             * constraints can be referenced in future table
608 7569
             * alterations as the table name is encoded in
609 7569
             * default constraints' names.
610 7569
             */
611 7569
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
612 7569
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
613 7569
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
614
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
615
                'FROM sys.default_constraints dc ' .
616 9211
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
617 9211
                "WHERE tbl.name = '" . $newName->getName() . "';" .
618 9211
                'EXEC sp_executesql @sql';
619 9211
        }
620
621
        $sql = array_merge(
622 9211
            $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 8933
     * @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 8933
     *
636 8933
     * @return string
637
     */
638 8933
    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 8927
     * @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 8927
     *
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 9049
     *
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 9049
     */
671 7149
    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 9025
            return false;
677 8993
        }
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 8921
            return false;
683 8708
        }
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 8897
            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 9039
     * @param string      $columnName The quoted column name to alter the comment for.
709
     * @param string|null $comment    The column's comment.
710 9039
     *
711 8731
     * @return string
712 8731
     */
713 8731
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
714
    {
715 8987
        if (strpos($tableName, '.') !== false) {
716 8987
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
717
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
718
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
719 9039
        } else {
720 9039
            $schemaSQL = "'dbo'";
721 12
            $tableSQL  = $this->quoteStringLiteral($tableName);
722 9039
        }
723 12
724 9039
        return $this->getUpdateExtendedPropertySQL(
725 12
            'MS_Description',
726 9039
            $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 9062
     * @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 9062
     *
750 8756
     * @return string
751 8756
     */
752 8756
    protected function getDropColumnCommentSQL($tableName, $columnName)
753
    {
754 8987
        if (strpos($tableName, '.') !== false) {
755 8987
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
756
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
757
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
758 9062
        } else {
759 9062
            $schemaSQL = "'dbo'";
760 9062
            $tableSQL  = $this->quoteStringLiteral($tableName);
761 12
        }
762 9062
763 12
        return $this->getDropExtendedPropertySQL(
764 9062
            'MS_Description',
765 12
            'SCHEMA',
766
            $schemaSQL,
767
            'TABLE',
768
            $tableSQL,
769
            'COLUMN',
770
            $columnName
771
        );
772 7112
    }
773
774 7112
    /**
775 30
     * {@inheritdoc}
776 7112
     */
777 7112
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
778 7112
    {
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 9138
     * @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 9138
        $level1Name = null,
811 9138
        $level2Type = null,
812 9138
        $level2Name = null
813 9138
    ) {
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 9062
     * @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 9062
        $level1Name = null,
842 9062
        $level2Type = null,
843 9062
        $level2Name = null
844 9062
    ) {
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 9039
     * @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 9039
        $level1Name = null,
875 9039
        $level2Type = null,
876 9039
        $level2Name = null
877 9039
    ) {
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 8566
    }
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 8566
                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 8520
    }
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 8520
                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 8474
    }
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 8474
                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 8590
     * @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 8590
     *
1004 8473
     * @return string
1005 8473
     */
1006 8473
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
1007
    {
1008 8572
        if (strpos($table, '.') !== false) {
1009 8572
            [$schema, $table] = explode('.', $table);
1010
            $schema           = $this->quoteStringLiteral($schema);
1011
            $table            = $this->quoteStringLiteral($table);
1012 8590
        } 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 1065
    }
1037
1038 1065
    /**
1039 1065
     * {@inheritDoc}
1040
     */
1041
    public function getLocateExpression($str, $substr, $startPos = false)
1042 1065
    {
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 1123
    }
1057
1058 1123
    /**
1059 1123
     * {@inheritDoc}
1060
     */
1061 1121
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1062 1121
    {
1063
        if (! $char) {
1064
            switch ($pos) {
1065 1119
                case TrimMode::LEADING:
1066 1119
                    $trimFn = 'LTRIM';
1067
                    break;
1068
1069 1123
                case TrimMode::TRAILING:
1070
                    $trimFn = 'RTRIM';
1071
                    break;
1072 1121
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 1115
          , @trim_char as trim_char
1085
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1086 1115
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1087 1111
          , 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 1115
1091 1107
        if ($pos === TrimMode::LEADING) {
1092
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1093
        }
1094 1115
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 9656
    }
1101
1102 9656
    /**
1103
     * {@inheritDoc}
1104 9656
     */
1105
    public function getConcatExpression()
1106
    {
1107
        $args = func_get_args();
1108
1109
        return '(' . implode(' + ', $args) . ')';
1110 9834
    }
1111
1112 9834
    /**
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 9631
    }
1147
1148 9631
    /**
1149
     * {@inheritDoc}
1150
     */
1151
    public function getSetTransactionIsolationSQL($level)
1152
    {
1153
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1154 10132
    }
1155
1156 10132
    /**
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 8256
    }
1179
1180 8256
    /**
1181
     * {@inheritDoc}
1182
     */
1183
    public function getGuidTypeDeclarationSQL(array $field)
1184
    {
1185
        return 'UNIQUEIDENTIFIER';
1186 10057
    }
1187
1188 10057
    /**
1189
     * {@inheritDoc}
1190
     */
1191
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1192
    {
1193
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1194 8877
    }
1195
1196 8877
    /**
1197
     * {@inheritdoc}
1198
     */
1199
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1200
    {
1201
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1202 8889
    }
1203
1204 8889
    /**
1205
     * {@inheritdoc}
1206
     */
1207
    public function getBinaryMaxLength()
1208
    {
1209
        return 8000;
1210 3235
    }
1211
1212 3235
    /**
1213
     * {@inheritDoc}
1214
     */
1215
    public function getClobTypeDeclarationSQL(array $field)
1216
    {
1217
        return 'VARCHAR(MAX)';
1218 10132
    }
1219
1220 10132
    /**
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 7569
    }
1251
1252 7569
    /**
1253
     * {@inheritDoc}
1254
     */
1255
    public function getBooleanTypeDeclarationSQL(array $field)
1256
    {
1257
        return 'BIT';
1258 9534
    }
1259
1260 9534
    /**
1261
     * {@inheritDoc}
1262 9534
     */
1263 9408
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1264
    {
1265
        $where = [];
1266 9534
1267 9530
        if ($offset > 0) {
1268 9530
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1269
        }
1270 6354
1271
        if ($limit !== null) {
1272
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1273 9534
            $top     = sprintf('TOP %d', $offset + $limit);
1274 6354
        } else {
1275
            $top = 'TOP 9223372036854775807';
1276
        }
1277
1278
        if (empty($where)) {
1279
            return $query;
1280
        }
1281 9530
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 9530
        // actually scan the entire range covered by the TOP clause.
1286
        if (! preg_match('/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/is', $query, $matches)) {
1287 9530
            return $query;
1288
        }
1289
1290 9452
        $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 9530
            // 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 9530
            'WITH dctrn_cte AS (%s) '
1301 9530
            . '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 9452
     *
1314
     * @param string $query
1315 9452
     *
1316 9452
     * @return string
1317
     */
1318 9452
    private function scrubInnerOrderBy($query)
1319 9452
    {
1320 9452
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1321 3379
        $offset = 0;
1322
1323
        while ($count-- > 0) {
1324 9452
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1325 9452
            if ($orderByPos === false) {
1326 9452
                break;
1327
            }
1328 9452
1329 9452
            $qLen            = strlen($query);
1330 9104
            $parenCount      = 0;
1331 9452
            $currentPosition = $orderByPos;
1332 9278
1333
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1334
                if ($query[$currentPosition] === '(') {
1335 9452
                    $parenCount++;
1336
                } elseif ($query[$currentPosition] === ')') {
1337
                    $parenCount--;
1338 9452
                }
1339
1340
                $currentPosition++;
1341 9446
            }
1342 9446
1343
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1344
                // If the order by clause is in a TOP N subquery, do not remove
1345 9270
                // it and continue iteration from the current position.
1346
                $offset = $currentPosition;
1347
                continue;
1348
            }
1349 9270
1350 9270
            if ($currentPosition >= $qLen - 1) {
1351
                continue;
1352
            }
1353 9452
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 9452
     * @param string $query           The query
1365
     * @param int    $currentPosition Start position of ORDER BY clause
1366
     *
1367 9452
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
1368 9452
     */
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 9452
        $subQueryBuffer = '';
1373 9452
        $parenCount     = 0;
1374 9284
1375 9452
        // If $parenCount goes negative, we've exited the subquery we're examining.
1376 9203
        // 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 9452
            } elseif ($query[$currentPosition] === ')') {
1381
                $parenCount++;
1382 9452
            }
1383
1384
            // Only yank query text on the same nesting level as the ORDER BY clause.
1385 9452
            $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 3075
    }
1392
1393 3075
    /**
1394
     * {@inheritDoc}
1395
     */
1396
    public function supportsLimitOffset()
1397
    {
1398
        return false;
1399 7569
    }
1400
1401 7569
    /**
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 7569
                    continue;
1410 7569
                }
1411
1412
                $item[$key] = $value ? 1 : 0;
1413 7569
            }
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 3960
    }
1468
1469 3960
    /**
1470
     * {@inheritDoc}
1471
     */
1472
    public function getName()
1473
    {
1474
        return 'mssql';
1475 8998
    }
1476
1477 8998
    /**
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 8998
            'binary' => 'binary',
1505
            'varbinary' => 'binary',
1506
            'image' => 'blob',
1507
            'uniqueidentifier' => 'guid',
1508
        ];
1509 1133
    }
1510
1511 1133
    /**
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 1133
    }
1526
1527 1133
    /**
1528
     * {@inheritDoc}
1529
     */
1530
    public function rollbackSavePoint($savepoint)
1531
    {
1532
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1533 8324
    }
1534
1535
    /**
1536 8324
     * {@inheritdoc}
1537 7906
     */
1538
    public function getForeignKeyReferentialActionSQL($action)
1539
    {
1540 8318
        // 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 4109
    }
1547
1548 4109
    /**
1549
     * {@inheritDoc}
1550 3477
     */
1551
    public function appendLockHint($fromClause, $lockMode)
1552 4107
    {
1553 3427
        switch (true) {
1554
            case $lockMode === LockMode::NONE:
1555 4105
                return $fromClause . ' WITH (NOLOCK)';
1556 3982
1557
            case $lockMode === LockMode::PESSIMISTIC_READ:
1558
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1559 3533
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 2824
    }
1575
1576 2824
    /**
1577
     * {@inheritDoc}
1578
     */
1579
    protected function getReservedKeywordsClass()
1580
    {
1581
        return Keywords\SQLServerKeywords::class;
1582 9564
    }
1583
1584 9564
    /**
1585
     * {@inheritDoc}
1586
     */
1587
    public function quoteSingleIdentifier($str)
1588
    {
1589
        return '[' . str_replace(']', '][', $str) . ']';
1590 7279
    }
1591
1592 7279
    /**
1593
     * {@inheritDoc}
1594 7279
     */
1595
    public function getTruncateTableSQL($tableName, $cascade = false)
1596
    {
1597
        $tableIdentifier = new Identifier($tableName);
1598
1599
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1600 9063
    }
1601
1602 9063
    /**
1603
     * {@inheritDoc}
1604
     */
1605
    public function getBlobTypeDeclarationSQL(array $field)
1606
    {
1607
        return 'VARBINARY(MAX)';
1608 9013
    }
1609
1610 9013
    /**
1611
     * {@inheritDoc}
1612
     */
1613
    public function getDefaultValueDeclarationSQL($field)
1614 9013
    {
1615 8574
        if (! isset($field['default'])) {
1616
            return empty($field['notnull']) ? ' NULL' : '';
1617
        }
1618 8989
1619
        if (! isset($field['type'])) {
1620 8989
            return " DEFAULT '" . $field['default'] . "'";
1621 8917
        }
1622
1623
        $type = $field['type'];
1624 8975
1625 7379
        if ($type instanceof Types\PhpIntegerMappingType) {
1626
            return ' DEFAULT ' . $field['default'];
1627
        }
1628 8969
1629 7569
        if ($type instanceof Types\PhpDateTimeMappingType && $field['default'] === $this->getCurrentTimestampSQL()) {
1630
            return ' DEFAULT ' . $this->getCurrentTimestampSQL();
1631
        }
1632 8967
1633
        if ($type instanceof Types\BooleanType) {
1634
            return " DEFAULT '" . $this->convertBooleans($field['default']) . "'";
1635
        }
1636
1637
        return " DEFAULT '" . $field['default'] . "'";
1638
    }
1639
1640 10234
    /**
1641
     * {@inheritdoc}
1642 10234
     *
1643 7106
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1644
     */
1645 10228
    public function getColumnDeclarationSQL($name, array $field)
1646 10228
    {
1647
        if (isset($field['columnDefinition'])) {
1648 10228
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1649
        } else {
1650 10228
            $collation = isset($field['collation']) && $field['collation'] ?
1651 10228
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1652
1653 10228
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1654 10228
1655
            $unique = isset($field['unique']) && $field['unique'] ?
1656 10228
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1657 10228
1658
            $check = isset($field['check']) && $field['check'] ?
1659
                ' ' . $field['check'] : '';
1660 10234
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 8989
     * @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 8989
     *
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 8995
     *
1684
     * @param string $identifier Identifier to generate a hash value for.
1685
     *
1686 8995
     * @return string
1687
     */
1688 8995
    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
    public function getListTableMetadataSQL(string $table) : string
1697
    {
1698
        return sprintf(
1699
            <<<'SQL'
1700
SELECT      td.value AS [table_comment],
1701
FROM        sysobjects t
1702
INNER JOIN  sysusers u
1703
    ON      u.uid = t.uid
1704
LEFT OUTER JOIN sys.extended_properties td
1705
    ON      td.major_id = t.id
1706
    AND     td.minor_id = 0
1707
    AND     td.name = 'MS_Description'
1708
WHERE t.type = %s
1709
ORDER BY    t.name, c.colorder
1710
SQL
1711
            ,
1712
            $this->quoteStringLiteral($table)
1713
        );
1714
    }
1715
}
1716