Completed
Pull Request — develop (#3524)
by Jonathan
95:41 queued 92:25
created

SQLServerPlatform::getLocateExpression()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

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