Completed
Pull Request — develop (#3348)
by Sergei
62:55
created

SQLServerPlatform::getDefaultValueDeclarationSQL()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 8.048

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 25
ccs 10
cts 11
cp 0.9091
rs 8.4444
c 0
b 0
f 0
cc 8
nc 7
nop 1
crap 8.048

2 Methods

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