Completed
Pull Request — develop (#3491)
by Sergei
64:09
created

getDefaultConstraintDeclarationSQL()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2.0116

Importance

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

922
    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...
923
    {
924
        return 'SELECT f.name AS ForeignKey,
925
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
926
                OBJECT_NAME (f.parent_object_id) AS TableName,
927
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
928
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
929
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
930
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
931
                f.delete_referential_action_desc,
932
                f.update_referential_action_desc
933
                FROM sys.foreign_keys AS f
934
                INNER JOIN sys.foreign_key_columns AS fc
935
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
936
                ON f.OBJECT_ID = fc.constraint_object_id
937
                WHERE ' .
938 5581
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
939
    }
940
941
    /**
942
     * {@inheritDoc}
943
     */
944 5537
    public function getListTableIndexesSQL($table, $currentDatabase = null)
945
    {
946
        return "SELECT idx.name AS key_name,
947
                       col.name AS column_name,
948
                       ~idx.is_unique AS non_unique,
949
                       idx.is_primary_key AS [primary],
950
                       CASE idx.type
951
                           WHEN '1' THEN 'clustered'
952
                           WHEN '2' THEN 'nonclustered'
953
                           ELSE NULL
954
                       END AS flags
955
                FROM sys.tables AS tbl
956
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
957
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
958
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
959
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
960 5537
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
961
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
962
    }
963
964
    /**
965
     * {@inheritDoc}
966
     */
967 707
    public function getCreateViewSQL($name, $sql)
968
    {
969 707
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
970
    }
971
972
    /**
973
     * {@inheritDoc}
974
     */
975 707
    public function getListViewsSQL($database)
976
    {
977 707
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
978
    }
979
980
    /**
981
     * Returns the where clause to filter schema and table name in a query.
982
     *
983
     * @param string $table        The full qualified name of the table.
984
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
985
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
986
     *
987
     * @return string
988
     */
989 5633
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
990
    {
991 5633
        if (strpos($table, '.') !== false) {
992 5529
            [$schema, $table] = explode('.', $table);
993 5529
            $schema           = $this->quoteStringLiteral($schema);
994 5529
            $table            = $this->quoteStringLiteral($table);
995
        } else {
996 5627
            $schema = 'SCHEMA_NAME()';
997 5627
            $table  = $this->quoteStringLiteral($table);
998
        }
999
1000 5633
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1001
    }
1002
1003
    /**
1004
     * {@inheritDoc}
1005
     */
1006 707
    public function getDropViewSQL($name)
1007
    {
1008 707
        return 'DROP VIEW ' . $name;
1009
    }
1010
1011
    /**
1012
     * {@inheritDoc}
1013
     */
1014 825
    public function getLocateExpression($str, $substr, $startPos = false)
1015
    {
1016 825
        if ($startPos === false) {
1017 825
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1018
        }
1019
1020 825
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1021
    }
1022
1023
    /**
1024
     * {@inheritDoc}
1025
     */
1026
    public function getModExpression($expression1, $expression2)
1027
    {
1028
        return $expression1 . ' % ' . $expression2;
1029
    }
1030
1031
    /**
1032
     * {@inheritDoc}
1033
     */
1034 883
    public function getTrimExpression(string $str, int $mode = TrimMode::UNSPECIFIED, ?string $char = null) : string
1035
    {
1036 883
        switch ($mode) {
1037 883
            case TrimMode::UNSPECIFIED:
1038
            case TrimMode::LEADING:
1039 881
            case TrimMode::TRAILING:
1040 881
            case TrimMode::BOTH:
1041
                break;
1042
1043 879
            default:
1044 879
                throw new InvalidArgumentException(
1045
                    sprintf(
1046
                        'The value of $mode is expected to be one of the TrimMode constants, %d given',
1047 883
                        $mode
1048
                    )
1049
                );
1050 881
        }
1051
1052
        if ($char === null) {
1053
            switch ($mode) {
1054
                case TrimMode::LEADING:
1055
                    return 'LTRIM(' . $str . ')';
1056
1057
                case TrimMode::TRAILING:
1058
                    return 'RTRIM(' . $str . ')';
1059
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
1060
1061
                default:
1062 875
                    return 'LTRIM(RTRIM(' . $str . '))';
1063
            }
1064 875
        }
1065 871
1066
        /** Original query used to get those expressions
1067
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1068 875
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1069 867
          select @c as string
1070
          , @trim_char as trim_char
1071
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1072 875
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1073
          , 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;
1074
         */
1075
        $pattern = "'%[^' + " . $char . " + ']%'";
1076
1077
        if ($mode === TrimMode::LEADING) {
1078 6196
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1079
        }
1080 6196
1081
        if ($mode === TrimMode::TRAILING) {
1082 6196
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1083
        }
1084
1085
        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))';
1086
    }
1087
1088 6379
    /**
1089
     * {@inheritDoc}
1090 6379
     */
1091
    public function getConcatExpression()
1092
    {
1093
        $args = func_get_args();
1094
1095
        return '(' . implode(' + ', $args) . ')';
1096 735
    }
1097
1098 735
    /**
1099
     * {@inheritDoc}
1100
     */
1101
    public function getListDatabasesSQL()
1102
    {
1103
        return 'SELECT * FROM sys.databases';
1104
    }
1105
1106
    /**
1107
     * {@inheritDoc}
1108
     */
1109
    public function getListNamespacesSQL()
1110
    {
1111
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1112
    }
1113
1114
    /**
1115
     * {@inheritDoc}
1116
     */
1117
    public function getSubstringExpression($value, $from, $length = null)
1118
    {
1119
        if ($length !== null) {
1120
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1121
        }
1122
1123
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1124 6172
    }
1125
1126 6172
    /**
1127
     * {@inheritDoc}
1128
     */
1129
    public function getLengthExpression($column)
1130
    {
1131
        return 'LEN(' . $column . ')';
1132 6736
    }
1133
1134 6736
    /**
1135
     * {@inheritDoc}
1136
     */
1137
    public function getSetTransactionIsolationSQL($level)
1138
    {
1139
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1140 594
    }
1141
1142 594
    /**
1143
     * {@inheritDoc}
1144
     */
1145
    public function getIntegerTypeDeclarationSQL(array $field)
1146
    {
1147
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1148 691
    }
1149
1150 691
    /**
1151
     * {@inheritDoc}
1152
     */
1153
    public function getBigIntTypeDeclarationSQL(array $field)
1154
    {
1155
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1156 5332
    }
1157
1158 5332
    /**
1159
     * {@inheritDoc}
1160
     */
1161
    public function getSmallIntTypeDeclarationSQL(array $field)
1162
    {
1163
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1164 3632
    }
1165
1166 3632
    /**
1167
     * {@inheritDoc}
1168
     */
1169
    public function getGuidTypeDeclarationSQL(array $field)
1170
    {
1171
        return 'UNIQUEIDENTIFIER';
1172 6539
    }
1173
1174 6539
    /**
1175
     * {@inheritDoc}
1176
     */
1177
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
1178
    {
1179
        return 'DATETIMEOFFSET(6)';
1180 5879
    }
1181
1182 5879
    /**
1183
     * {@inheritDoc}
1184
     */
1185
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1186
    {
1187
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1188 3843
    }
1189
1190 3843
    /**
1191
     * {@inheritdoc}
1192
     */
1193
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1194
    {
1195
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1196 6519
    }
1197
1198 6519
    /**
1199
     * {@inheritdoc}
1200
     */
1201
    public function getBinaryMaxLength()
1202
    {
1203
        return 8000;
1204 6736
    }
1205
1206 6736
    /**
1207
     * {@inheritDoc}
1208
     */
1209
    public function getClobTypeDeclarationSQL(array $field)
1210
    {
1211
        return 'VARCHAR(MAX)';
1212 901
    }
1213
1214
    /**
1215
     * {@inheritDoc}
1216 901
     */
1217
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1218
    {
1219
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1220
    }
1221
1222 749
    /**
1223
     * {@inheritDoc}
1224 749
     */
1225
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1226
    {
1227
        // 3 - microseconds precision length
1228
        // http://msdn.microsoft.com/en-us/library/ms187819.aspx
1229
        return 'DATETIME2(6)';
1230 749
    }
1231
1232 749
    /**
1233
     * {@inheritDoc}
1234
     */
1235
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1236
    {
1237
        return 'DATE';
1238 4673
    }
1239
1240 4673
    /**
1241
     * {@inheritDoc}
1242
     */
1243
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1244
    {
1245
        return 'TIME(0)';
1246 3385
    }
1247
1248 3385
    /**
1249
     * {@inheritDoc}
1250 3385
     */
1251 3053
    public function getBooleanTypeDeclarationSQL(array $field)
1252
    {
1253
        return 'BIT';
1254 3385
    }
1255 3384
1256 3384
    /**
1257
     * {@inheritDoc}
1258 49
     */
1259
    protected function doModifyLimitQuery(string $query, ?int $limit, int $offset) : string
1260
    {
1261 3385
        $where = [];
1262 49
1263
        if ($offset > 0) {
1264
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1265
        }
1266
1267
        if ($limit !== null) {
1268
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1269 3384
            $top     = sprintf('TOP %d', $offset + $limit);
1270
        } else {
1271
            $top = 'TOP 9223372036854775807';
1272
        }
1273 3384
1274
        if (empty($where)) {
1275 3384
            return $query;
1276
        }
1277
1278 3377
        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
1279
        // Even if the TOP n is very large, the use of a CTE will
1280
        // allow the SQL Server query planner to optimize it so it doesn't
1281
        // actually scan the entire range covered by the TOP clause.
1282 3384
        if (! preg_match('/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/is', $query, $matches)) {
1283
            return $query;
1284
        }
1285
1286
        $query = $matches[1] . $top . ' ' . $matches[2];
1287 23
1288 3384
        if (stristr($query, 'ORDER BY')) {
1289 3384
            // Inner order by is not valid in SQL Server for our purposes
1290
            // unless it's in a TOP N subquery.
1291
            $query = $this->scrubInnerOrderBy($query);
1292
        }
1293
1294
        // Build a new limited query around the original, using a CTE
1295
        return sprintf(
1296
            'WITH dctrn_cte AS (%s) '
1297
            . 'SELECT * FROM ('
1298
            . 'SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte'
1299
            . ') AS doctrine_tbl '
1300
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1301 3377
            $query,
1302
            implode(' AND ', $where)
1303 3377
        );
1304 3377
    }
1305
1306 3377
    /**
1307 3377
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
1308 3377
     * Caveat: will leave ORDER BY in TOP N subqueries.
1309 3363
     *
1310
     * @param string $query
1311
     *
1312 3377
     * @return string
1313 3377
     */
1314 3377
    private function scrubInnerOrderBy($query)
1315
    {
1316 3377
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1317 3377
        $offset = 0;
1318 2762
1319 3377
        while ($count-- > 0) {
1320 3369
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1321
            if ($orderByPos === false) {
1322
                break;
1323 3377
            }
1324
1325
            $qLen            = strlen($query);
1326 3377
            $parenCount      = 0;
1327
            $currentPosition = $orderByPos;
1328
1329 3375
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1330 3375
                if ($query[$currentPosition] === '(') {
1331
                    $parenCount++;
1332
                } elseif ($query[$currentPosition] === ')') {
1333 3367
                    $parenCount--;
1334
                }
1335
1336
                $currentPosition++;
1337 3367
            }
1338 3367
1339
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1340 3377
                // If the order by clause is in a TOP N subquery, do not remove
1341
                // it and continue iteration from the current position.
1342
                $offset = $currentPosition;
1343
                continue;
1344
            }
1345
1346
            if ($currentPosition >= $qLen - 1) {
1347
                continue;
1348
            }
1349
1350
            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1351 3377
            $offset = $orderByPos;
1352
        }
1353
        return $query;
1354 3377
    }
1355 3377
1356
    /**
1357
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
1358
     *
1359 3377
     * @param string $query           The query
1360 3377
     * @param int    $currentPosition Start position of ORDER BY clause
1361 3372
     *
1362 3377
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
1363 3370
     */
1364
    private function isOrderByInTopNSubquery($query, $currentPosition)
1365
    {
1366
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1367 3377
        $subQueryBuffer = '';
1368
        $parenCount     = 0;
1369 3377
1370
        // If $parenCount goes negative, we've exited the subquery we're examining.
1371
        // If $currentPosition goes negative, we've reached the beginning of the query.
1372 3377
        while ($parenCount >= 0 && $currentPosition >= 0) {
1373
            if ($query[$currentPosition] === '(') {
1374
                $parenCount--;
1375
            } elseif ($query[$currentPosition] === ')') {
1376
                $parenCount++;
1377
            }
1378 6595
1379
            // Only yank query text on the same nesting level as the ORDER BY clause.
1380 6595
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1381
1382
            $currentPosition--;
1383
        }
1384
1385
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1386 4673
    }
1387
1388 4673
    /**
1389
     * {@inheritDoc}
1390
     */
1391
    public function supportsLimitOffset()
1392
    {
1393
        return true;
1394
    }
1395
1396 4673
    /**
1397 4673
     * {@inheritDoc}
1398
     */
1399
    public function convertBooleans($item)
1400 4673
    {
1401
        if (is_array($item)) {
1402
            foreach ($item as $key => $value) {
1403
                if (! is_bool($value) && ! is_numeric($value)) {
1404
                    continue;
1405
                }
1406 598
1407
                $item[$key] = $value ? 1 : 0;
1408 598
            }
1409
        } elseif (is_bool($item) || is_numeric($item)) {
1410
            $item = $item ? 1 : 0;
1411
        }
1412
1413
        return $item;
1414 598
    }
1415
1416 598
    /**
1417
     * {@inheritDoc}
1418
     */
1419
    public function getCreateTemporaryTableSnippetSQL()
1420
    {
1421
        return 'CREATE TABLE';
1422 897
    }
1423
1424 897
    /**
1425
     * {@inheritDoc}
1426
     */
1427
    public function getTemporaryTableName($tableName)
1428
    {
1429
        return '#' . $tableName;
1430 568
    }
1431
1432 568
    /**
1433
     * {@inheritDoc}
1434
     */
1435
    public function getDateTimeFormatString()
1436
    {
1437
        return 'Y-m-d H:i:s.u';
1438 566
    }
1439
1440 566
    /**
1441
     * {@inheritDoc}
1442
     */
1443
    public function getDateFormatString()
1444
    {
1445
        return 'Y-m-d';
1446 570
    }
1447
1448 570
    /**
1449
     * {@inheritDoc}
1450
     */
1451
    public function getTimeFormatString()
1452
    {
1453
        return 'H:i:s';
1454 905
    }
1455
1456 905
    /**
1457
     * {@inheritDoc}
1458
     */
1459
    public function getDateTimeTzFormatString()
1460
    {
1461
        return 'Y-m-d H:i:s.u P';
1462 5983
    }
1463
1464 5983
    /**
1465
     * {@inheritDoc}
1466
     */
1467
    public function getName()
1468
    {
1469
        return 'mssql';
1470
    }
1471
1472
    /**
1473
     * {@inheritDoc}
1474
     */
1475
    protected function initializeDoctrineTypeMappings()
1476
    {
1477
        $this->doctrineTypeMapping = [
1478
            'bigint'           => 'bigint',
1479
            'binary'           => 'binary',
1480
            'bit'              => 'boolean',
1481
            'char'             => 'string',
1482
            'date'             => 'date',
1483
            'datetime'         => 'datetime',
1484
            'datetime2'        => 'datetime',
1485
            'datetimeoffset'   => 'datetimetz',
1486
            'decimal'          => 'decimal',
1487
            'double'           => 'float',
1488
            'double precision' => 'float',
1489
            'float'            => 'float',
1490
            'image'            => 'blob',
1491
            'int'              => 'integer',
1492
            'money'            => 'integer',
1493
            'nchar'            => 'string',
1494
            'ntext'            => 'text',
1495 5983
            'numeric'          => 'decimal',
1496
            'nvarchar'         => 'string',
1497
            'real'             => 'float',
1498
            'smalldatetime'    => 'datetime',
1499
            'smallint'         => 'smallint',
1500 907
            'smallmoney'       => 'integer',
1501
            'text'             => 'text',
1502 907
            'time'             => 'time',
1503
            'tinyint'          => 'smallint',
1504
            'uniqueidentifier' => 'guid',
1505
            'varbinary'        => 'binary',
1506
            'varchar'          => 'string',
1507
        ];
1508
    }
1509
1510
    /**
1511
     * {@inheritDoc}
1512
     */
1513
    public function createSavePoint($savepoint)
1514
    {
1515
        return 'SAVE TRANSACTION ' . $savepoint;
1516 907
    }
1517
1518 907
    /**
1519
     * {@inheritDoc}
1520
     */
1521
    public function releaseSavePoint($savepoint)
1522
    {
1523
        return '';
1524 5373
    }
1525
1526
    /**
1527 5373
     * {@inheritDoc}
1528 5019
     */
1529
    public function rollbackSavePoint($savepoint)
1530
    {
1531 5371
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1532
    }
1533
1534
    /**
1535
     * {@inheritdoc}
1536
     */
1537 3822
    public function getForeignKeyReferentialActionSQL($action)
1538
    {
1539 3822
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1540
        if (strtoupper($action) === 'RESTRICT') {
1541 3458
            return 'NO ACTION';
1542
        }
1543 3821
1544 3410
        return parent::getForeignKeyReferentialActionSQL($action);
1545
    }
1546 3820
1547 3706
    /**
1548
     * {@inheritDoc}
1549
     */
1550 3509
    public function appendLockHint($fromClause, $lockMode)
1551
    {
1552
        switch (true) {
1553
            case $lockMode === LockMode::NONE:
1554
                return $fromClause . ' WITH (NOLOCK)';
1555
1556
            case $lockMode === LockMode::PESSIMISTIC_READ:
1557 602
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1558
1559 602
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1560
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1561
1562
            default:
1563
                return $fromClause;
1564
        }
1565 6377
    }
1566
1567 6377
    /**
1568
     * {@inheritDoc}
1569
     */
1570
    public function getForUpdateSQL()
1571
    {
1572
        return ' ';
1573 6462
    }
1574
1575 6462
    /**
1576
     * {@inheritDoc}
1577
     */
1578
    protected function getReservedKeywordsClass()
1579
    {
1580
        return Keywords\SQLServerKeywords::class;
1581 4410
    }
1582
1583 4410
    /**
1584
     * {@inheritDoc}
1585 4410
     */
1586
    public function quoteSingleIdentifier($str)
1587
    {
1588
        return '[' . str_replace(']', '][', $str) . ']';
1589
    }
1590
1591 919
    /**
1592
     * {@inheritDoc}
1593 919
     */
1594
    public function getTruncateTableSQL($tableName, $cascade = false)
1595
    {
1596
        $tableIdentifier = new Identifier($tableName);
1597
1598
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1599 5967
    }
1600
1601 5967
    /**
1602
     * {@inheritDoc}
1603
     */
1604
    public function getBlobTypeDeclarationSQL(array $field)
1605 5967
    {
1606 5626
        return 'VARBINARY(MAX)';
1607
    }
1608
1609 5959
    /**
1610
     * {@inheritDoc}
1611 5959
     */
1612 5935
    public function getDefaultValueDeclarationSQL($field)
1613
    {
1614
        if (! isset($field['default'])) {
1615 5953
            return empty($field['notnull']) ? ' NULL' : '';
1616 4491
        }
1617
1618
        if (! isset($field['type'])) {
1619 5951
            return " DEFAULT '" . $field['default'] . "'";
1620 4673
        }
1621
1622
        $type = $field['type'];
1623 5949
1624
        if ($type instanceof Types\PhpIntegerMappingType) {
1625
            return ' DEFAULT ' . $field['default'];
1626
        }
1627
1628
        if ($type instanceof Types\PhpDateTimeMappingType && $field['default'] === $this->getCurrentTimestampSQL()) {
1629
            return ' DEFAULT ' . $this->getCurrentTimestampSQL();
1630
        }
1631 6770
1632
        if ($type instanceof Types\BooleanType) {
1633 6770
            return " DEFAULT '" . $this->convertBooleans($field['default']) . "'";
1634 4250
        }
1635
1636 6768
        return " DEFAULT '" . $field['default'] . "'";
1637 6768
    }
1638
1639 6768
    /**
1640
     * {@inheritdoc}
1641 6768
     *
1642 6768
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1643
     */
1644 6768
    public function getColumnDeclarationSQL($name, array $field)
1645 6768
    {
1646
        if (isset($field['columnDefinition'])) {
1647 6768
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1648 6768
        } else {
1649
            $collation = isset($field['collation']) && $field['collation'] ?
1650
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1651 6770
1652
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1653
1654
            $unique = isset($field['unique']) && $field['unique'] ?
1655
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1656
1657 4060
            $check = isset($field['check']) && $field['check'] ?
1658
                ' ' . $field['check'] : '';
1659 4060
1660
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1661
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1662
        }
1663
1664
        return $name . ' ' . $columnDef;
1665
    }
1666
1667
    /**
1668
     * {@inheritdoc}
1669
     */
1670 5959
    protected function getLikeWildcardCharacters() : string
1671
    {
1672 5959
        return parent::getLikeWildcardCharacters() . '[]^';
1673
    }
1674
1675
    /**
1676
     * Returns a unique default constraint name for a table and column.
1677
     *
1678
     * @param string $table  Name of the table to generate the unique default constraint name for.
1679
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1680
     *
1681
     * @return string
1682 5961
     */
1683
    private function generateDefaultConstraintName($table, $column)
1684
    {
1685 5961
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1686
    }
1687 5961
1688
    /**
1689
     * Returns a hash value for a given identifier.
1690
     *
1691
     * @param string $identifier Identifier to generate a hash value for.
1692
     *
1693
     * @return string
1694
     */
1695
    private function generateIdentifierName($identifier)
1696
    {
1697
        // Always generate name for unquoted identifiers to ensure consistency.
1698
        $identifier = new Identifier($identifier);
1699
1700
        return strtoupper(dechex(crc32($identifier->getName())));
1701
    }
1702
}
1703