Failed Conditions
Pull Request — master (#3260)
by Michael
61:30
created

SQLServerPlatform::getTrimExpression()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 41
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7.116

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

924
    public function getListTableForeignKeysSQL($table, /** @scrutinizer ignore-unused */ $database = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

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