Completed
Push — develop ( de019a...a640b8 )
by Marco
19s queued 13s
created

SQLServerPlatform::getTrimExpression()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 42
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7

Importance

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

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