Failed Conditions
Pull Request — develop (#3518)
by Michael
29:00 queued 25:29
created

SQLServerPlatform::_getCreateTableSQL()   F

Complexity

Conditions 20
Paths 432

Size

Total Lines 64
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 20.2744

Importance

Changes 0
Metric Value
eloc 33
dl 0
loc 64
ccs 31
cts 34
cp 0.9118
rs 0.7887
c 0
b 0
f 0
cc 20
nc 432
nop 3
crap 20.2744

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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

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