Completed
Pull Request — develop (#3348)
by Sergei
65:02
created

SQLServerPlatform::getTimeFormatString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 2
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 6458
    public function getCurrentDateSQL() : string
51
    {
52 6458
        return $this->getConvertExpression('date', 'GETDATE()');
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58 6454
    public function getCurrentTimeSQL() : string
59
    {
60 6454
        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
    private function getConvertExpression(string $dataType, string $expression) : string
70
    {
71 6458
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
72
    }
73 6458
74
    /**
75
     * {@inheritdoc}
76
     */
77
    protected function getDateArithmeticIntervalExpression(string $date, string $operator, string $interval, string $unit) : string
78
    {
79
        $factorClause = '';
80
81
        if ($operator === '-') {
82
            $factorClause = '-1 * ';
83
        }
84
85
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
86
    }
87
88
    /**
89
     * {@inheritDoc}
90
     */
91
    public function getDateDiffExpression(string $date1, string $date2) : string
92
    {
93
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
94
    }
95
96
    /**
97
     * {@inheritDoc}
98
     *
99
     * Microsoft SQL Server prefers "autoincrement" identity columns
100
     * since sequences can only be emulated with a table.
101
     */
102
    public function prefersIdentityColumns() : bool
103
    {
104 6329
        return true;
105
    }
106 6329
107
    /**
108
     * {@inheritDoc}
109
     *
110
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
111
     */
112
    public function supportsIdentityColumns() : bool
113
    {
114 6304
        return true;
115
    }
116 6304
117
    /**
118
     * {@inheritDoc}
119
     */
120
    public function supportsReleaseSavepoints() : bool
121
    {
122
        return false;
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128
    public function supportsSchemas() : bool
129
    {
130 6279
        return true;
131
    }
132 6279
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function getDefaultSchemaName() : string
137
    {
138
        return 'dbo';
139
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144
    public function supportsColumnCollation() : bool
145
    {
146 5362
        return true;
147
    }
148 5362
149
    /**
150
     * {@inheritDoc}
151
     */
152
    public function hasNativeGuidType() : bool
153
    {
154 6809
        return true;
155
    }
156 6809
157
    /**
158
     * {@inheritDoc}
159
     */
160
    public function getCreateDatabaseSQL(string $database) : string
161
    {
162 6404
        return 'CREATE DATABASE ' . $database;
163
    }
164 6404
165
    /**
166
     * {@inheritDoc}
167
     */
168
    public function getDropDatabaseSQL(string $database) : string
169
    {
170 6404
        return 'DROP DATABASE ' . $database;
171
    }
172 6404
173
    /**
174
     * {@inheritDoc}
175
     */
176
    public function supportsCreateDropDatabase() : bool
177
    {
178 6304
        return true;
179
    }
180 6304
181
    /**
182
     * {@inheritDoc}
183
     */
184
    public function getCreateSchemaSQL(string $schemaName) : string
185
    {
186 6104
        return 'CREATE SCHEMA ' . $schemaName;
187
    }
188 6104
189
    /**
190
     * {@inheritDoc}
191
     */
192
    public function getDropForeignKeySQL($foreignKey, $table) : string
193
    {
194 3858
        if (! $foreignKey instanceof ForeignKeyConstraint) {
195
            $foreignKey = new Identifier($foreignKey);
196 3858
        }
197 3854
198
        if (! $table instanceof Table) {
199
            $table = new Identifier($table);
200 3858
        }
201 3858
202
        $foreignKey = $foreignKey->getQuotedName($this);
203
        $table      = $table->getQuotedName($this);
204 3858
205 3858
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
206
    }
207 3858
208
    /**
209
     * {@inheritDoc}
210
     */
211
    public function getDropIndexSQL($index, $table = null) : string
212
    {
213
        if ($index instanceof Index) {
214
            $index = $index->getQuotedName($this);
215
        } elseif (! is_string($index)) {
216
            throw new InvalidArgumentException(sprintf(
217
                'AbstractPlatform::getDropIndexSQL() expects $index parameter to be a string or an instanceof %s.',
218
                Index::class
219
            ));
220
        }
221
222
        if (! isset($table)) {
223
            return 'DROP INDEX ' . $index;
224
        }
225
226
        if ($table instanceof Table) {
227
            $table = $table->getQuotedName($this);
228
        }
229
230
        return sprintf(
231
            <<<SQL
232
IF EXISTS (SELECT * FROM sysobjects WHERE name = '%s')
233
    ALTER TABLE %s DROP CONSTRAINT %s
234
ELSE
235
    DROP INDEX %s ON %s
236
SQL
237
            ,
238
            $index,
239
            $table,
240
            $index,
241
            $index,
242
            $table
243
        );
244
    }
245
246
    /**
247
     * {@inheritDoc}
248 6645
     */
249
    protected function _getCreateTableSQL(string $tableName, array $columns, array $options = []) : array
250 6645
    {
251 6645
        $defaultConstraintsSql = [];
252
        $commentsSql           = [];
253
254
        // @todo does other code breaks because of this?
255 6645
        // force primary keys to be not null
256 6645
        foreach ($columns as &$column) {
257 6199
            if (isset($column['primary']) && $column['primary']) {
258
                $column['notnull'] = true;
259
            }
260
261 6645
            // Build default constraints SQL statements.
262 5766
            if (isset($column['default'])) {
263 5766
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
264
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
265
            }
266 6645
267 6637
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
268
                continue;
269
            }
270 6091
271
            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
272
        }
273 6645
274
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
275 6645
276
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
277
            foreach ($options['uniqueConstraints'] as $name => $definition) {
278
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
279
            }
280
        }
281 6645
282 6203
        if (isset($options['primary']) && ! empty($options['primary'])) {
283 6203
            $flags = '';
284 6179
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
285
                $flags = ' NONCLUSTERED';
286 6203
            }
287
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
288
        }
289 6645
290
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
291 6645
292 6645
        $check = $this->getCheckDeclarationSQL($columns);
293
        if (! empty($check)) {
294
            $query .= ', ' . $check;
295 6645
        }
296
        $query .= ')';
297 6645
298
        $sql = [$query];
299 6645
300 4612
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
301 4612
            foreach ($options['indexes'] as $index) {
302
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
303
            }
304
        }
305 6645
306 4129
        if (isset($options['foreignKeys'])) {
307 4129
            foreach ((array) $options['foreignKeys'] as $definition) {
308
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
309
            }
310
        }
311 6645
312
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
313
    }
314
315
    /**
316
     * {@inheritDoc}
317 6158
     */
318
    public function getCreatePrimaryKeySQL(Index $index, $table) : string
319 6158
    {
320
        if ($table instanceof Table) {
321
            $identifier = $table->getQuotedName($this);
322 6158
        } else {
323
            $identifier = $table;
324
        }
325 6158
326
        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';
327 6158
328 6154
        if ($index->hasFlag('nonclustered')) {
329
            $sql .= ' NONCLUSTERED';
330
        }
331 6158
332
        return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
333
    }
334
335
    /**
336
     * Returns the SQL statement for creating a column comment.
337
     *
338
     * SQL Server does not support native column comments,
339
     * therefore the extended properties functionality is used
340
     * as a workaround to store them.
341
     * The property name used to store column comments is "MS_Description"
342
     * which provides compatibility with SQL Server Management Studio,
343
     * as column comments are stored in the same property there when
344
     * specifying a column's "Description" attribute.
345
     *
346
     * @param string      $tableName  The quoted table name to which the column belongs.
347
     * @param string      $columnName The quoted column name to create the comment for.
348
     * @param string|null $comment    The column's comment.
349
     */
350
    protected function getCreateColumnCommentSQL(string $tableName, string $columnName, ?string $comment) : string
351 6103
    {
352
        if (strpos($tableName, '.') !== false) {
353 6103
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
354 6083
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
355 6083
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
356 6083
        } else {
357
            $schemaSQL = "'dbo'";
358 5995
            $tableSQL  = $this->quoteStringLiteral($tableName);
359 5995
        }
360
361
        return $this->getAddExtendedPropertySQL(
362 6103
            'MS_Description',
363 6103
            $comment,
364 28
            'SCHEMA',
365 6103
            $schemaSQL,
366 28
            'TABLE',
367 6103
            $tableSQL,
368 28
            'COLUMN',
369 6103
            $columnName
370 28
        );
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
     * @throws InvalidArgumentException
380
     */
381
    public function getDefaultConstraintDeclarationSQL(string $table, array $column) : string
382
    {
383
        if (! isset($column['default'])) {
384 5935
            throw new InvalidArgumentException('Incomplete column definition. "default" required.');
385
        }
386 5935
387
        $columnName = new Identifier($column['name']);
388
389
        return ' CONSTRAINT ' .
390 5935
            $this->generateDefaultConstraintName($table, $column['name']) .
391
            $this->getDefaultValueDeclarationSQL($column) .
392
            ' FOR ' . $columnName->getQuotedName($this);
393 5935
    }
394 5935
395 5935
    /**
396
     * {@inheritDoc}
397
     */
398
    public function getCreateIndexSQL(Index $index, $table) : string
399
    {
400
        $constraint = parent::getCreateIndexSQL($index, $table);
401 6232
402
        if ($index->isUnique() && ! $index->isPrimary()) {
403 6232
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
404
        }
405 6232
406 4608
        return $constraint;
407
    }
408
409 6232
    /**
410
     * {@inheritDoc}
411
     */
412
    protected function getCreateIndexSQLFlags(Index $index) : string
413
    {
414
        $type = '';
415 6232
        if ($index->isUnique()) {
416
            $type .= 'UNIQUE ';
417 6232
        }
418 6232
419 4608
        if ($index->hasFlag('clustered')) {
420
            $type .= 'CLUSTERED ';
421
        } elseif ($index->hasFlag('nonclustered')) {
422 6232
            $type .= 'NONCLUSTERED ';
423 6204
        }
424 4628
425
        return $type;
426
    }
427
428 6232
    /**
429
     * Extend unique key constraint with required filters
430
     */
431
    private function _appendUniqueConstraintDefinition(string $sql, Index $index) : string
432
    {
433
        $fields = [];
434
435
        foreach ($index->getQuotedColumns($this) as $field) {
436
            $fields[] = $field . ' IS NOT NULL';
437
        }
438 4608
439
        return $sql . ' WHERE ' . implode(' AND ', $fields);
440 4608
    }
441
442 4608
    /**
443 4608
     * {@inheritDoc}
444
     */
445
    public function getAlterTableSQL(TableDiff $diff) : array
446 4608
    {
447
        $queryParts  = [];
448
        $sql         = [];
449
        $columnSql   = [];
450
        $commentsSql = [];
451
452 6138
        foreach ($diff->addedColumns as $column) {
453
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
454 6138
                continue;
455 6138
            }
456 6138
457 6138
            $columnDef    = $column->toArray();
458
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
459 6138
460 6090
            if (isset($columnDef['default'])) {
461
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
462
            }
463
464 6090
            $comment = $this->getColumnComment($column);
465 6090
466
            if (empty($comment) && ! is_numeric($comment)) {
467 6090
                continue;
468 5666
            }
469
470
            $commentsSql[] = $this->getCreateColumnCommentSQL(
471 6090
                $diff->name,
472
                $column->getQuotedName($this),
473 6090
                $comment
474 5982
            );
475
        }
476
477 6062
        foreach ($diff->removedColumns as $column) {
478 6062
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
479 6062
                continue;
480 12
            }
481
482
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
483
        }
484 6138
485 5678
        foreach ($diff->changedColumns as $columnDiff) {
486
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
487
                continue;
488
            }
489 5678
490
            $column     = $columnDiff->column;
491
            $comment    = $this->getColumnComment($column);
492 6138
            $hasComment = ! empty($comment) || is_numeric($comment);
493 6081
494
            if ($columnDiff->fromColumn instanceof Column) {
495
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
496
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
497 6081
498 6081
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
499 6081
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
500
                        $diff->name,
501 6081
                        $column->getQuotedName($this),
502 6061
                        $comment
503 6061
                    );
504
                } elseif ($hasFromComment && ! $hasComment) {
505 6061
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
506 6008
                } elseif ($hasComment) {
507 6008
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
508 6008
                        $diff->name,
509 8
                        $column->getQuotedName($this),
510
                        $comment
511 6057
                    );
512 6033
                }
513 5978
            }
514 5954
515 5954
            // Do not add query part if only comment has changed.
516 5954
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
517 4
                continue;
518
            }
519
520
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
521
522
            if ($requireDropDefaultConstraint) {
523 6081
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
524 6041
                    $diff->name,
525
                    $columnDiff->oldColumnName
526
                );
527 5994
            }
528
529 5994
            $columnDef = $column->toArray();
530 5895
531 5895
            $queryParts[] = 'ALTER COLUMN ' .
532 5895
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
533
534
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
535
                continue;
536 5994
            }
537
538 5994
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
539 5994
        }
540
541 5994
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
542 5970
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
543
                continue;
544
            }
545 5899
546
            $oldColumnName = new Identifier($oldColumnName);
547
548 6138
            $sql[] = "sp_RENAME '" .
549 4391
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
550
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
551
552
            // Recreate default constraint with new column name if necessary (for future reference).
553 4391
            if ($column->getDefault() === null) {
554
                continue;
555 4391
            }
556 4391
557 4391
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
558
                $diff->name,
559
                $oldColumnName->getQuotedName($this)
560 4391
            );
561 4387
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
562
        }
563
564 3779
        $tableSql = [];
565 3779
566 3779
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
567
            return array_merge($tableSql, $columnSql);
568 3779
        }
569
570
        foreach ($queryParts as $query) {
571 6138
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
572
        }
573 6138
574
        $sql = array_merge($sql, $commentsSql);
575
576
        $newName = $diff->getNewName();
577 6138
578 6106
        if ($newName !== false) {
579
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";
580
581 6138
            /**
582
             * Rename table's default constraints names
583 6138
             * to match the new table name.
584
             * This is necessary to ensure that the default
585 6138
             * constraints can be referenced in future table
586 4458
             * alterations as the table name is encoded in
587
             * default constraints' names.
588
             */
589
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
590
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
591
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
592
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
593
                'FROM sys.default_constraints dc ' .
594
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
595
                "WHERE tbl.name = '" . $newName->getName() . "';" .
596 4458
                'EXEC sp_executesql @sql';
597
        }
598 4458
599 4458
        $sql = array_merge(
600 4458
            $this->getPreAlterTableIndexForeignKeySQL($diff),
601 4458
            $sql,
602 4458
            $this->getPostAlterTableIndexForeignKeySQL($diff)
603 4458
        );
604
605
        return array_merge($sql, $tableSql, $columnSql);
606 6138
    }
607 6138
608 6138
    /**
609 6138
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
610
     *
611
     * @param string $tableName The name of the table to generate the clause for.
612 6138
     * @param Column $column    The column to generate the clause for.
613
     */
614
    private function getAlterTableAddDefaultConstraintClause(string $tableName, Column $column) : string
615
    {
616
        $columnDef         = $column->toArray();
617
        $columnDef['name'] = $column->getQuotedName($this);
618
619
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
620
    }
621
622
    /**
623 5903
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
624
     *
625 5903
     * @param string $tableName  The name of the table to generate the clause for.
626 5903
     * @param string $columnName The name of the column to generate the clause for.
627
     */
628 5903
    private function getAlterTableDropDefaultConstraintClause(string $tableName, string $columnName) : string
629
    {
630
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
631
    }
632
633
    /**
634
     * Checks whether a column alteration requires dropping its default constraint first.
635
     *
636
     * Different to other database vendors SQL Server implements column default values
637
     * as constraints and therefore changes in a column's default value as well as changes
638
     * in a column's type require dropping the default constraint first before being to
639 5899
     * alter the particular column to the new definition.
640
     *
641 5899
     * @param ColumnDiff $columnDiff The column diff to evaluate.
642
     *
643
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
644
     */
645
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff) : bool
646
    {
647
        // We can only decide whether to drop an existing default constraint
648
        // if we know the original default value.
649
        if (! $columnDiff->fromColumn instanceof Column) {
650
            return false;
651
        }
652
653
        // We only need to drop an existing default constraint if we know the
654
        // column was defined with a default value before.
655
        if ($columnDiff->fromColumn->getDefault() === null) {
656 5994
            return false;
657
        }
658
659
        // We need to drop an existing default constraint if the column was
660 5994
        // defined with a default value before and it has changed.
661 4466
        if ($columnDiff->hasChanged('default')) {
662
            return true;
663
        }
664
665
        // We need to drop an existing default constraint if the column was
666 5978
        // defined with a default value before and the native column type has changed.
667 5958
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
668
    }
669
670
    /**
671
     * Returns the SQL statement for altering a column comment.
672 5895
     *
673 5666
     * SQL Server does not support native column comments,
674
     * therefore the extended properties functionality is used
675
     * as a workaround to store them.
676
     * The property name used to store column comments is "MS_Description"
677
     * which provides compatibility with SQL Server Management Studio,
678 5879
     * as column comments are stored in the same property there when
679
     * specifying a column's "Description" attribute.
680
     *
681
     * @param string      $tableName  The quoted table name to which the column belongs.
682
     * @param string      $columnName The quoted column name to alter the comment for.
683
     * @param string|null $comment    The column's comment.
684
     */
685
    protected function getAlterColumnCommentSQL(string $tableName, string $columnName, ?string $comment) : string
686
    {
687
        if (strpos($tableName, '.') !== false) {
688
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
689
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
690
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
691
        } else {
692
            $schemaSQL = "'dbo'";
693
            $tableSQL  = $this->quoteStringLiteral($tableName);
694
        }
695
696
        return $this->getUpdateExtendedPropertySQL(
697
            'MS_Description',
698 6008
            $comment,
699
            'SCHEMA',
700 6008
            $schemaSQL,
701 6004
            'TABLE',
702 6004
            $tableSQL,
703 6004
            'COLUMN',
704
            $columnName
705 5954
        );
706 5954
    }
707
708
    /**
709 6008
     * Returns the SQL statement for dropping a column comment.
710 6008
     *
711 8
     * SQL Server does not support native column comments,
712 6008
     * therefore the extended properties functionality is used
713 8
     * as a workaround to store them.
714 6008
     * The property name used to store column comments is "MS_Description"
715 8
     * which provides compatibility with SQL Server Management Studio,
716 6008
     * as column comments are stored in the same property there when
717 8
     * specifying a column's "Description" attribute.
718
     *
719
     * @param string $tableName  The quoted table name to which the column belongs.
720
     * @param string $columnName The quoted column name to drop the comment for.
721
     */
722
    protected function getDropColumnCommentSQL(string $tableName, string $columnName) : string
723
    {
724
        if (strpos($tableName, '.') !== false) {
725
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
726
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
727
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
728
        } else {
729
            $schemaSQL = "'dbo'";
730
            $tableSQL  = $this->quoteStringLiteral($tableName);
731
        }
732
733
        return $this->getDropExtendedPropertySQL(
734
            'MS_Description',
735
            'SCHEMA',
736
            $schemaSQL,
737 6033
            'TABLE',
738
            $tableSQL,
739 6033
            'COLUMN',
740 6029
            $columnName
741 6029
        );
742 6029
    }
743
744 5954
    /**
745 5954
     * {@inheritdoc}
746
     */
747
    protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName) : array
748 6033
    {
749 6033
        return [sprintf(
750 6033
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
751 8
            $tableName,
752 6033
            $oldIndexName,
753 8
            $index->getQuotedName($this)
754 6033
        ),
755 8
        ];
756
    }
757
758
    /**
759
     * Returns the SQL statement for adding an extended property to a database object.
760
     *
761
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
762 3995
     *
763
     * @param string      $name       The name of the property to add.
764 3995
     * @param string|null $value      The value of the property to add.
765 20
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
766 3995
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
767 3995
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
768 3995
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
769
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
770
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
771
     */
772
    public function getAddExtendedPropertySQL(
773
        string $name,
774
        ?string $value = null,
775
        ?string $level0Type = null,
776
        ?string $level0Name = null,
777
        ?string $level1Type = null,
778
        ?string $level1Name = null,
779
        ?string $level2Type = null,
780
        ?string $level2Name = null
781
    ) : string {
782
        return 'EXEC sp_addextendedproperty ' .
783
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
784
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
785
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
786
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
787
    }
788
789 6103
    /**
790
     * Returns the SQL statement for dropping an extended property from a database object.
791
     *
792
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
793
     *
794
     * @param string      $name       The name of the property to drop.
795
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
796
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
797
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
798
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
799
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
800 6103
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
801 6103
     */
802 6103
    public function getDropExtendedPropertySQL(
803 6103
        string $name,
804
        ?string $level0Type = null,
805
        ?string $level0Name = null,
806
        ?string $level1Type = null,
807
        ?string $level1Name = null,
808
        ?string $level2Type = null,
809
        ?string $level2Name = null
810
    ) : string {
811
        return 'EXEC sp_dropextendedproperty ' .
812
            'N' . $this->quoteStringLiteral($name) . ', ' .
813
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
814
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
815
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
816
    }
817
818
    /**
819
     * Returns the SQL statement for updating an extended property of a database object.
820
     *
821 6033
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
822
     *
823
     * @param string      $name       The name of the property to update.
824
     * @param string|null $value      The value of the property to update.
825
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
826
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
827
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
828
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
829
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
830
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
831 6033
     */
832 6033
    public function getUpdateExtendedPropertySQL(
833 6033
        string $name,
834 6033
        ?string $value = null,
835
        ?string $level0Type = null,
836
        ?string $level0Name = null,
837
        ?string $level1Type = null,
838
        ?string $level1Name = null,
839
        ?string $level2Type = null,
840
        ?string $level2Name = null
841
    ) : string {
842
        return 'EXEC sp_updateextendedproperty ' .
843
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
844
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
845
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
846
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
847
    }
848
849
    /**
850
     * {@inheritDoc}
851
     */
852
    public function getEmptyIdentityInsertSQL(string $tableName, string $identifierColumnName) : string
853 6008
    {
854
        return 'INSERT INTO ' . $tableName . ' DEFAULT VALUES';
855
    }
856
857
    /**
858
     * {@inheritDoc}
859
     */
860
    public function getListTablesSQL() : string
861
    {
862
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
863
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
864 6008
        return "SELECT name, SCHEMA_NAME (uid) AS schema_name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
865 6008
    }
866 6008
867 6008
    /**
868
     * {@inheritDoc}
869
     */
870
    public function getListTableColumnsSQL(string $table, ?string $database = null) : string
871
    {
872
        return "SELECT    col.name,
873
                          type.name AS type,
874
                          col.max_length AS length,
875
                          ~col.is_nullable AS notnull,
876
                          def.definition AS [default],
877
                          col.scale,
878
                          col.precision,
879
                          col.is_identity AS autoincrement,
880
                          col.collation_name AS collation,
881
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
882
                FROM      sys.columns AS col
883
                JOIN      sys.types AS type
884
                ON        col.user_type_id = type.user_type_id
885
                JOIN      sys.objects AS obj
886
                ON        col.object_id = obj.object_id
887
                JOIN      sys.schemas AS scm
888
                ON        obj.schema_id = scm.schema_id
889
                LEFT JOIN sys.default_constraints def
890
                ON        col.default_object_id = def.object_id
891 5533
                AND       col.object_id = def.parent_object_id
892
                LEFT JOIN sys.extended_properties AS prop
893
                ON        obj.object_id = prop.major_id
894
                AND       col.column_id = prop.minor_id
895
                AND       prop.name = 'MS_Description'
896
                WHERE     obj.type = 'U'
897
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
898
    }
899
900
    /**
901
     * {@inheritDoc}
902
     */
903
    public function getListTableForeignKeysSQL(string $table, ?string $database = null) : string
904
    {
905
        return 'SELECT f.name AS ForeignKey,
906
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
907
                OBJECT_NAME (f.parent_object_id) AS TableName,
908
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
909
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
910
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
911
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
912
                f.delete_referential_action_desc,
913
                f.update_referential_action_desc
914
                FROM sys.foreign_keys AS f
915
                INNER JOIN sys.foreign_key_columns AS fc
916
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
917
                ON f.OBJECT_ID = fc.constraint_object_id
918 5533
                WHERE ' .
919
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
920
    }
921
922
    /**
923
     * {@inheritDoc}
924 5483
     */
925
    public function getListTableIndexesSQL(string $table, ?string $currentDatabase = null) : string
926
    {
927
        return "SELECT idx.name AS key_name,
928
                       col.name AS column_name,
929
                       ~idx.is_unique AS non_unique,
930
                       idx.is_primary_key AS [primary],
931
                       CASE idx.type
932
                           WHEN '1' THEN 'clustered'
933
                           WHEN '2' THEN 'nonclustered'
934
                           ELSE NULL
935
                       END AS flags
936
                FROM sys.tables AS tbl
937
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
938
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
939
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
940 5483
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
941
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
942
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
943
    }
944
945
    /**
946 5433
     * {@inheritDoc}
947
     */
948
    public function getCreateViewSQL(string $name, string $sql) : string
949
    {
950
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
951
    }
952
953
    /**
954
     * {@inheritDoc}
955
     */
956
    public function getListViewsSQL(?string $database) : string
957
    {
958
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
959
    }
960
961
    /**
962 5433
     * Returns the where clause to filter schema and table name in a query.
963
     *
964
     * @param string $table        The full qualified name of the table.
965
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
966
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
967
     */
968
    private function getTableWhereClause(string $table, string $schemaColumn, string $tableColumn) : string
969
    {
970
        if (strpos($table, '.') !== false) {
971
            [$schema, $table] = explode('.', $table);
972
            $schema           = $this->quoteStringLiteral($schema);
973
            $table            = $this->quoteStringLiteral($table);
974
        } else {
975
            $schema = 'SCHEMA_NAME()';
976
            $table  = $this->quoteStringLiteral($table);
977
        }
978
979
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
980
    }
981
982
    /**
983
     * {@inheritDoc}
984
     */
985
    public function getDropViewSQL(string $name) : string
986
    {
987
        return 'DROP VIEW ' . $name;
988
    }
989
990
    /**
991 5549
     * {@inheritDoc}
992
     */
993 5549
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
994 5512
    {
995 5512
        if ($start === null) {
996 5512
            return sprintf('CHARINDEX(%s, %s)', $substring, $string);
997
        }
998 5537
999 5537
        return sprintf('CHARINDEX(%s, %s, %s)', $substring, $string, $start);
1000
    }
1001
1002 5549
    /**
1003
     * {@inheritDoc}
1004
     */
1005
    public function getModExpression(string $dividend, string $divisor) : string
1006
    {
1007
        return $dividend . ' % ' . $divisor;
1008
    }
1009
1010
    /**
1011
     * {@inheritDoc}
1012
     */
1013
    public function getTrimExpression(string $str, int $mode = TrimMode::UNSPECIFIED, ?string $char = null) : string
1014
    {
1015
        if (! in_array($mode, [TrimMode::UNSPECIFIED, TrimMode::LEADING, TrimMode::TRAILING, TrimMode::BOTH], true)) {
1016
            throw new InvalidArgumentException(
1017
                sprintf('The value of $mode is expected to be one of the TrimMode constants, %d given', $mode)
1018
            );
1019
        }
1020
1021
        if ($char === null) {
1022
            switch ($mode) {
1023
                case TrimMode::LEADING:
1024
                    return 'LTRIM(' . $str . ')';
1025
1026
                case TrimMode::TRAILING:
1027
                    return 'RTRIM(' . $str . ')';
1028
1029
                default:
1030
                    return 'LTRIM(RTRIM(' . $str . '))';
1031
            }
1032
        }
1033
1034
        /** Original query used to get those expressions
1035
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1036
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1037
          select @c as string
1038
          , @trim_char as trim_char
1039
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1040
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1041
          , 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;
1042
         */
1043
        $pattern = "'%[^' + " . $char . " + ']%'";
1044
1045
        if ($mode === TrimMode::LEADING) {
1046
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1047
        }
1048
1049
        if ($mode === TrimMode::TRAILING) {
1050
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1051
        }
1052
1053
        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))';
1054
    }
1055
1056
    /**
1057
     * {@inheritDoc}
1058
     */
1059
    public function getConcatExpression(string ...$string) : string
1060
    {
1061
        return '(' . implode(' + ', $string) . ')';
1062
    }
1063
1064
    /**
1065
     * {@inheritDoc}
1066
     */
1067
    public function getListDatabasesSQL() : string
1068
    {
1069
        return 'SELECT * FROM sys.databases';
1070
    }
1071
1072
    /**
1073
     * {@inheritDoc}
1074
     */
1075
    public function getListNamespacesSQL() : string
1076
    {
1077
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1078
    }
1079
1080
    /**
1081
     * {@inheritDoc}
1082 6454
     */
1083
    public function getSubstringExpression(string $string, string $start, ?string $length = null) : string
1084 6454
    {
1085
        if ($length === null) {
1086
            return sprintf('SUBSTRING(%s, %s, LEN(%s) - %s + 1)', $string, $start, $string, $start);
1087
        }
1088
1089
        return sprintf('SUBSTRING(%s, %s, %s)', $string, $start, $length);
1090 6404
    }
1091
1092 6404
    /**
1093
     * {@inheritDoc}
1094
     */
1095
    public function getLengthExpression(string $string) : string
1096
    {
1097
        return 'LEN(' . $string . ')';
1098
    }
1099
1100
    /**
1101
     * {@inheritDoc}
1102
     */
1103
    public function getSetTransactionIsolationSQL(int $level) : string
1104
    {
1105
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1106
    }
1107
1108
    /**
1109
     * {@inheritDoc}
1110
     */
1111
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
1112
    {
1113
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1114
    }
1115
1116
    /**
1117
     * {@inheritDoc}
1118
     */
1119
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
1120
    {
1121
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1122
    }
1123
1124
    /**
1125
     * {@inheritDoc}
1126 6429
     */
1127
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
1128 6429
    {
1129
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1130
    }
1131
1132
    /**
1133
     * {@inheritDoc}
1134 6633
     */
1135
    public function getGuidTypeDeclarationSQL(array $field) : string
1136 6633
    {
1137
        return 'UNIQUEIDENTIFIER';
1138
    }
1139
1140
    /**
1141
     * {@inheritDoc}
1142
     */
1143
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) : string
1144
    {
1145
        return 'DATETIMEOFFSET(6)';
1146
    }
1147
1148
    /**
1149
     * {@inheritDoc}
1150
     */
1151
    protected function getVarcharTypeDeclarationSQLSnippet(?int $length, bool $fixed) : string
1152
    {
1153
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1154
    }
1155
1156
    /**
1157
     * {@inheritdoc}
1158 5554
     */
1159
    protected function getBinaryTypeDeclarationSQLSnippet(?int $length, bool $fixed) : string
1160 5554
    {
1161
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1162
    }
1163
1164
    /**
1165
     * {@inheritdoc}
1166 3452
     */
1167
    public function getBinaryMaxLength() : int
1168 3452
    {
1169
        return 8000;
1170
    }
1171
1172
    /**
1173
     * {@inheritDoc}
1174 6446
     */
1175
    public function getClobTypeDeclarationSQL(array $field) : string
1176 6446
    {
1177
        return 'VARCHAR(MAX)';
1178
    }
1179
1180
    /**
1181
     * {@inheritDoc}
1182 5904
     */
1183
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : string
1184 5904
    {
1185
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1186
    }
1187
1188
    /**
1189
     * {@inheritDoc}
1190 4004
     */
1191
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
1192 4004
    {
1193
        // 3 - microseconds precision length
1194
        // http://msdn.microsoft.com/en-us/library/ms187819.aspx
1195
        return 'DATETIME2(6)';
1196
    }
1197
1198 6370
    /**
1199
     * {@inheritDoc}
1200 6370
     */
1201
    public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string
1202
    {
1203
        return 'DATE';
1204
    }
1205
1206 6633
    /**
1207
     * {@inheritDoc}
1208 6633
     */
1209
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string
1210
    {
1211
        return 'TIME(0)';
1212
    }
1213
1214
    /**
1215
     * {@inheritDoc}
1216
     */
1217
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
1218
    {
1219
        return 'BIT';
1220
    }
1221
1222
    /**
1223
     * {@inheritDoc}
1224
     */
1225
    protected function doModifyLimitQuery(string $query, ?int $limit, int $offset) : string
1226
    {
1227
        $where = [];
1228
1229
        if ($offset > 0) {
1230
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1231
        }
1232
1233
        if ($limit !== null) {
1234
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1235
            $top     = sprintf('TOP %d', $offset + $limit);
1236
        } else {
1237
            $top = 'TOP 9223372036854775807';
1238
        }
1239
1240 4454
        if (empty($where)) {
1241
            return $query;
1242 4454
        }
1243
1244
        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
1245
        // Even if the TOP n is very large, the use of a CTE will
1246
        // allow the SQL Server query planner to optimize it so it doesn't
1247
        // actually scan the entire range covered by the TOP clause.
1248 3548
        if (! preg_match('/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/is', $query, $matches)) {
1249
            return $query;
1250 3548
        }
1251
1252 3548
        $query = $matches[1] . $top . ' ' . $matches[2];
1253 3183
1254
        if (stristr($query, 'ORDER BY')) {
1255
            // Inner order by is not valid in SQL Server for our purposes
1256 3548
            // unless it's in a TOP N subquery.
1257 3546
            $query = $this->scrubInnerOrderBy($query);
1258 3546
        }
1259
1260 52
        // Build a new limited query around the original, using a CTE
1261
        return sprintf(
1262
            'WITH dctrn_cte AS (%s) '
1263 3548
            . 'SELECT * FROM ('
1264 52
            . 'SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte'
1265
            . ') AS doctrine_tbl '
1266
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1267
            $query,
1268
            implode(' AND ', $where)
1269
        );
1270
    }
1271 3546
1272
    /**
1273
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
1274
     * Caveat: will leave ORDER BY in TOP N subqueries.
1275 3546
     */
1276
    private function scrubInnerOrderBy(string $query) : string
1277 3546
    {
1278
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1279
        $offset = 0;
1280 3532
1281
        while ($count-- > 0) {
1282
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1283
            if ($orderByPos === false) {
1284 3546
                break;
1285
            }
1286
1287
            $qLen            = strlen($query);
1288
            $parenCount      = 0;
1289 46
            $currentPosition = $orderByPos;
1290 3546
1291 3546
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1292
                if ($query[$currentPosition] === '(') {
1293
                    $parenCount++;
1294
                } elseif ($query[$currentPosition] === ')') {
1295
                    $parenCount--;
1296
                }
1297
1298
                $currentPosition++;
1299
            }
1300
1301
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1302
                // If the order by clause is in a TOP N subquery, do not remove
1303 3532
                // it and continue iteration from the current position.
1304
                $offset = $currentPosition;
1305 3532
                continue;
1306 3532
            }
1307
1308 3532
            if ($currentPosition >= $qLen - 1) {
1309 3532
                continue;
1310 3532
            }
1311 3504
1312
            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1313
            $offset = $orderByPos;
1314 3532
        }
1315 3532
1316 3532
        return $query;
1317
    }
1318 3532
1319 3532
    /**
1320 2877
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
1321 3532
     *
1322 3516
     * @param string $query           The query
1323
     * @param int    $currentPosition Start position of ORDER BY clause
1324
     *
1325 3532
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
1326
     */
1327
    private function isOrderByInTopNSubquery(string $query, int $currentPosition) : bool
1328 3532
    {
1329
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1330
        $subQueryBuffer = '';
1331 3528
        $parenCount     = 0;
1332 3528
1333
        // If $parenCount goes negative, we've exited the subquery we're examining.
1334
        // If $currentPosition goes negative, we've reached the beginning of the query.
1335 3512
        while ($parenCount >= 0 && $currentPosition >= 0) {
1336
            if ($query[$currentPosition] === '(') {
1337
                $parenCount--;
1338
            } elseif ($query[$currentPosition] === ')') {
1339 3512
                $parenCount++;
1340 3512
            }
1341
1342
            // Only yank query text on the same nesting level as the ORDER BY clause.
1343 3532
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1344
1345
            $currentPosition--;
1346
        }
1347
1348
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1349
    }
1350
1351
    /**
1352
     * {@inheritDoc}
1353
     */
1354 3532
    public function supportsLimitOffset() : bool
1355
    {
1356
        return true;
1357 3532
    }
1358 3532
1359
    /**
1360
     * {@inheritDoc}
1361
     */
1362 3532
    public function convertBooleans($item)
1363 3532
    {
1364 3522
        if (is_array($item)) {
1365 3532
            foreach ($item as $key => $value) {
1366 3518
                if (! is_bool($value) && ! is_numeric($value)) {
1367
                    continue;
1368
                }
1369
1370 3532
                $item[$key] = $value ? 1 : 0;
1371
            }
1372 3532
        } elseif (is_bool($item) || is_numeric($item)) {
1373
            $item = $item ? 1 : 0;
1374
        }
1375 3532
1376
        return $item;
1377
    }
1378
1379
    /**
1380
     * {@inheritDoc}
1381 6566
     */
1382
    public function getCreateTemporaryTableSnippetSQL() : string
1383 6566
    {
1384
        return 'CREATE TABLE';
1385
    }
1386
1387
    /**
1388
     * {@inheritDoc}
1389 4454
     */
1390
    public function getTemporaryTableName(string $tableName) : string
1391 4454
    {
1392
        return '#' . $tableName;
1393
    }
1394
1395
    /**
1396
     * {@inheritDoc}
1397
     */
1398
    public function getDateTimeFormatString() : string
1399 4454
    {
1400 4454
        return 'Y-m-d H:i:s.u';
1401
    }
1402
1403 4454
    /**
1404
     * {@inheritDoc}
1405
     */
1406
    public function getDateFormatString() : string
1407
    {
1408
        return 'Y-m-d';
1409
    }
1410
1411
    /**
1412
     * {@inheritDoc}
1413
     */
1414
    public function getTimeFormatString() : string
1415
    {
1416
        return 'H:i:s';
1417
    }
1418
1419
    /**
1420
     * {@inheritDoc}
1421
     */
1422
    public function getDateTimeTzFormatString() : string
1423
    {
1424
        return 'Y-m-d H:i:s.u P';
1425
    }
1426
1427
    /**
1428
     * {@inheritDoc}
1429
     */
1430
    public function getName() : string
1431
    {
1432
        return 'mssql';
1433
    }
1434
1435
    /**
1436
     * {@inheritDoc}
1437
     */
1438
    protected function initializeDoctrineTypeMappings() : void
1439
    {
1440
        $this->doctrineTypeMapping = [
1441
            'bigint'           => 'bigint',
1442
            'binary'           => 'binary',
1443
            'bit'              => 'boolean',
1444
            'char'             => 'string',
1445
            'date'             => 'date',
1446
            'datetime'         => 'datetime',
1447
            'datetime2'        => 'datetime',
1448
            'datetimeoffset'   => 'datetimetz',
1449
            'decimal'          => 'decimal',
1450
            'double'           => 'float',
1451
            'double precision' => 'float',
1452
            'float'            => 'float',
1453
            'image'            => 'blob',
1454
            'int'              => 'integer',
1455
            'money'            => 'integer',
1456
            'nchar'            => 'string',
1457
            'ntext'            => 'text',
1458
            'numeric'          => 'decimal',
1459
            'nvarchar'         => 'string',
1460
            'real'             => 'float',
1461
            'smalldatetime'    => 'datetime',
1462
            'smallint'         => 'smallint',
1463
            'smallmoney'       => 'integer',
1464
            'text'             => 'text',
1465 5945
            'time'             => 'time',
1466
            'tinyint'          => 'smallint',
1467 5945
            'uniqueidentifier' => 'guid',
1468
            'varbinary'        => 'binary',
1469
            'varchar'          => 'string',
1470
        ];
1471
    }
1472
1473
    /**
1474
     * {@inheritDoc}
1475
     */
1476
    public function createSavePoint(string $savepoint) : string
1477
    {
1478
        return 'SAVE TRANSACTION ' . $savepoint;
1479
    }
1480
1481
    /**
1482
     * {@inheritDoc}
1483
     */
1484
    public function releaseSavePoint(string $savepoint) : string
1485
    {
1486
        return '';
1487
    }
1488
1489
    /**
1490
     * {@inheritDoc}
1491
     */
1492
    public function rollbackSavePoint(string $savepoint) : string
1493
    {
1494
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1495
    }
1496
1497
    /**
1498 5945
     * {@inheritdoc}
1499
     */
1500
    public function getForeignKeyReferentialActionSQL(string $action) : string
1501
    {
1502
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1503
        if (strtoupper($action) === 'RESTRICT') {
1504
            return 'NO ACTION';
1505
        }
1506
1507
        return parent::getForeignKeyReferentialActionSQL($action);
1508
    }
1509
1510
    /**
1511
     * {@inheritDoc}
1512
     */
1513
    public function appendLockHint(string $fromClause, ?int $lockMode) : string
1514
    {
1515
        switch (true) {
1516
            case $lockMode === LockMode::NONE:
1517
                return $fromClause . ' WITH (NOLOCK)';
1518
1519
            case $lockMode === LockMode::PESSIMISTIC_READ:
1520
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1521
1522
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1523
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1524
1525
            default:
1526
                return $fromClause;
1527 5303
        }
1528
    }
1529
1530 5303
    /**
1531 5229
     * {@inheritDoc}
1532
     */
1533
    public function getForUpdateSQL() : string
1534 5299
    {
1535
        return ' ';
1536
    }
1537
1538
    /**
1539
     * {@inheritDoc}
1540 3664
     */
1541
    protected function getReservedKeywordsClass() : string
1542 3664
    {
1543
        return Keywords\SQLServerKeywords::class;
1544 3602
    }
1545
1546 3662
    /**
1547 3552
     * {@inheritDoc}
1548
     */
1549 3660
    public function quoteSingleIdentifier(string $str) : string
1550 3527
    {
1551
        return '[' . str_replace(']', '][', $str) . ']';
1552
    }
1553 3658
1554
    /**
1555
     * {@inheritDoc}
1556
     */
1557
    public function getTruncateTableSQL(string $tableName, bool $cascade = false) : string
1558
    {
1559
        $tableIdentifier = new Identifier($tableName);
1560
1561
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1562
    }
1563
1564
    /**
1565
     * {@inheritDoc}
1566
     */
1567
    public function getBlobTypeDeclarationSQL(array $field) : string
1568 6701
    {
1569
        return 'VARBINARY(MAX)';
1570 6701
    }
1571
1572
    /**
1573
     * {@inheritdoc}
1574
     *
1575
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1576 6372
     */
1577
    public function getColumnDeclarationSQL(string $name, array $field) : string
1578 6372
    {
1579
        if (isset($field['columnDefinition'])) {
1580
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1581
        } else {
1582
            $collation = isset($field['collation']) && $field['collation'] ?
1583
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1584 4079
1585
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1586 4079
1587
            $unique = isset($field['unique']) && $field['unique'] ?
1588 4079
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1589
1590
            $check = isset($field['check']) && $field['check'] ?
1591
                ' ' . $field['check'] : '';
1592
1593
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1594
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1595
        }
1596
1597
        return $name . ' ' . $columnDef;
1598
    }
1599
1600
    /**
1601
     * {@inheritdoc}
1602 5951
     */
1603
    protected function getLikeWildcardCharacters() : string
1604 5951
    {
1605
        return parent::getLikeWildcardCharacters() . '[]^';
1606
    }
1607
1608 5951
    /**
1609 5866
     * Returns a unique default constraint name for a table and column.
1610
     *
1611
     * @param string $table  Name of the table to generate the unique default constraint name for.
1612 5935
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1613
     */
1614 5935
    private function generateDefaultConstraintName(string $table, string $column) : string
1615 5887
    {
1616
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1617
    }
1618 5927
1619 4254
    /**
1620
     * Returns a hash value for a given identifier.
1621
     *
1622 5923
     * @param string $identifier Identifier to generate a hash value for.
1623 4454
     */
1624
    private function generateIdentifierName(string $identifier) : string
1625
    {
1626 5923
        // Always generate name for unquoted identifiers to ensure consistency.
1627
        $identifier = new Identifier($identifier);
1628
1629
        return strtoupper(dechex(crc32($identifier->getName())));
1630
    }
1631
}
1632