Completed
Push — 3.0.x ( 0d9a8c...f82f5c )
by Sergei
25s queued 17s
created

SQLServerPlatform::getDropSequenceSQL()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

1000
    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...
1001
    {
1002
        return 'SELECT f.name AS ForeignKey,
1003
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
1004
                OBJECT_NAME (f.parent_object_id) AS TableName,
1005
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
1006
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
1007
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
1008
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
1009
                f.delete_referential_action_desc,
1010
                f.update_referential_action_desc
1011
                FROM sys.foreign_keys AS f
1012
                INNER JOIN sys.foreign_key_columns AS fc
1013
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
1014
                ON f.OBJECT_ID = fc.constraint_object_id
1015
                WHERE ' .
1016
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
1017
    }
1018
1019
    /**
1020
     * {@inheritDoc}
1021
     */
1022
    public function getListTableIndexesSQL($table, $currentDatabase = null)
1023
    {
1024
        return "SELECT idx.name AS key_name,
1025
                       col.name AS column_name,
1026
                       ~idx.is_unique AS non_unique,
1027
                       idx.is_primary_key AS [primary],
1028
                       CASE idx.type
1029
                           WHEN '1' THEN 'clustered'
1030
                           WHEN '2' THEN 'nonclustered'
1031
                           ELSE NULL
1032
                       END AS flags
1033
                FROM sys.tables AS tbl
1034
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
1035
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
1036
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
1037
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
1038
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
1039
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
1040
    }
1041
1042
    /**
1043
     * {@inheritDoc}
1044
     */
1045
    public function getCreateViewSQL($name, $sql)
1046
    {
1047
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
1048
    }
1049
1050
    /**
1051
     * {@inheritDoc}
1052
     */
1053
    public function getListViewsSQL($database)
1054
    {
1055
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
1056
    }
1057
1058
    /**
1059
     * Returns the where clause to filter schema and table name in a query.
1060
     *
1061
     * @param string $table        The full qualified name of the table.
1062
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
1063
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
1064
     *
1065
     * @return string
1066
     */
1067
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
1068
    {
1069
        if (strpos($table, '.') !== false) {
1070
            [$schema, $table] = explode('.', $table);
1071
            $schema           = $this->quoteStringLiteral($schema);
1072
            $table            = $this->quoteStringLiteral($table);
1073
        } else {
1074
            $schema = 'SCHEMA_NAME()';
1075
            $table  = $this->quoteStringLiteral($table);
1076
        }
1077
1078
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1079
    }
1080
1081
    /**
1082
     * {@inheritDoc}
1083
     */
1084
    public function getDropViewSQL($name)
1085
    {
1086
        return 'DROP VIEW ' . $name;
1087
    }
1088
1089
    /**
1090
     * {@inheritDoc}
1091
     *
1092
     * @deprecated Use application-generated UUIDs instead
1093
     */
1094
    public function getGuidExpression()
1095
    {
1096
        return 'NEWID()';
1097
    }
1098
1099
    /**
1100
     * {@inheritDoc}
1101
     */
1102
    public function getLocateExpression($str, $substr, $startPos = false)
1103
    {
1104
        if ($startPos === false) {
1105
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1106
        }
1107
1108
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1109
    }
1110
1111
    /**
1112
     * {@inheritDoc}
1113
     */
1114
    public function getModExpression($expression1, $expression2)
1115
    {
1116
        return $expression1 . ' % ' . $expression2;
1117
    }
1118
1119
    /**
1120
     * {@inheritDoc}
1121
     */
1122
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1123
    {
1124
        if (! $char) {
1125
            switch ($pos) {
1126
                case TrimMode::LEADING:
1127
                    $trimFn = 'LTRIM';
1128
                    break;
1129
1130
                case TrimMode::TRAILING:
1131
                    $trimFn = 'RTRIM';
1132
                    break;
1133
1134
                default:
1135
                    return 'LTRIM(RTRIM(' . $str . '))';
1136
            }
1137
1138
            return $trimFn . '(' . $str . ')';
1139
        }
1140
1141
        /** Original query used to get those expressions
1142
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1143
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1144
          select @c as string
1145
          , @trim_char as trim_char
1146
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1147
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1148
          , 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;
1149
         */
1150
        $pattern = "'%[^' + " . $char . " + ']%'";
1151
1152
        if ($pos === TrimMode::LEADING) {
1153
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1154
        }
1155
1156
        if ($pos === TrimMode::TRAILING) {
1157
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1158
        }
1159
1160
        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))';
1161
    }
1162
1163
    /**
1164
     * {@inheritDoc}
1165
     */
1166
    public function getConcatExpression()
1167
    {
1168
        $args = func_get_args();
1169
1170
        return '(' . implode(' + ', $args) . ')';
1171
    }
1172
1173
    /**
1174
     * {@inheritDoc}
1175
     */
1176
    public function getListDatabasesSQL()
1177
    {
1178
        return 'SELECT * FROM sys.databases';
1179
    }
1180
1181
    /**
1182
     * {@inheritDoc}
1183
     */
1184
    public function getListNamespacesSQL()
1185
    {
1186
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1187
    }
1188
1189
    /**
1190
     * {@inheritDoc}
1191
     */
1192
    public function getSubstringExpression($value, $from, $length = null)
1193
    {
1194
        if ($length !== null) {
1195
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1196
        }
1197
1198
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1199
    }
1200
1201
    /**
1202
     * {@inheritDoc}
1203
     */
1204
    public function getLengthExpression($column)
1205
    {
1206
        return 'LEN(' . $column . ')';
1207
    }
1208
1209
    /**
1210
     * {@inheritDoc}
1211
     */
1212
    public function getSetTransactionIsolationSQL($level)
1213
    {
1214
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1215
    }
1216
1217
    /**
1218
     * {@inheritDoc}
1219
     */
1220
    public function getIntegerTypeDeclarationSQL(array $field)
1221
    {
1222
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1223
    }
1224
1225
    /**
1226
     * {@inheritDoc}
1227
     */
1228
    public function getBigIntTypeDeclarationSQL(array $field)
1229
    {
1230
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1231
    }
1232
1233
    /**
1234
     * {@inheritDoc}
1235
     */
1236
    public function getSmallIntTypeDeclarationSQL(array $field)
1237
    {
1238
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1239
    }
1240
1241
    /**
1242
     * {@inheritDoc}
1243
     */
1244
    public function getGuidTypeDeclarationSQL(array $field)
1245
    {
1246
        return 'UNIQUEIDENTIFIER';
1247
    }
1248
1249
    /**
1250
     * {@inheritDoc}
1251
     */
1252
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
1253
    {
1254
        return 'DATETIMEOFFSET(6)';
1255
    }
1256
1257
    /**
1258
     * {@inheritDoc}
1259
     */
1260
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1261
    {
1262
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1263
    }
1264
1265
    /**
1266
     * {@inheritdoc}
1267
     */
1268
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1269
    {
1270
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1271
    }
1272
1273
    /**
1274
     * {@inheritdoc}
1275
     */
1276
    public function getBinaryMaxLength()
1277
    {
1278
        return 8000;
1279
    }
1280
1281
    /**
1282
     * {@inheritDoc}
1283
     */
1284
    public function getClobTypeDeclarationSQL(array $field)
1285
    {
1286
        return 'VARCHAR(MAX)';
1287
    }
1288
1289
    /**
1290
     * {@inheritDoc}
1291
     */
1292
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1293
    {
1294
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1295
    }
1296
1297
    /**
1298
     * {@inheritDoc}
1299
     */
1300
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1301
    {
1302
        // 3 - microseconds precision length
1303
        // http://msdn.microsoft.com/en-us/library/ms187819.aspx
1304
        return 'DATETIME2(6)';
1305
    }
1306
1307
    /**
1308
     * {@inheritDoc}
1309
     */
1310
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1311
    {
1312
        return 'DATE';
1313
    }
1314
1315
    /**
1316
     * {@inheritDoc}
1317
     */
1318
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1319
    {
1320
        return 'TIME(0)';
1321
    }
1322
1323
    /**
1324
     * {@inheritDoc}
1325
     */
1326
    public function getBooleanTypeDeclarationSQL(array $field)
1327
    {
1328
        return 'BIT';
1329
    }
1330
1331
    /**
1332
     * {@inheritDoc}
1333
     */
1334
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1335
    {
1336
        if ($limit === null && $offset <= 0) {
1337
            return $query;
1338
        }
1339
1340
        // Queries using OFFSET... FETCH MUST have an ORDER BY clause
1341
        // Find the position of the last instance of ORDER BY and ensure it is not within a parenthetical statement
1342
        // but can be in a newline
1343
        $matches      = [];
1344
        $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE);
1345
        $orderByPos   = false;
1346
        if ($matchesCount > 0) {
1347
            $orderByPos = $matches[0][($matchesCount - 1)][1];
1348
        }
1349
1350
        if ($orderByPos === false
1351
            || substr_count($query, '(', $orderByPos) - substr_count($query, ')', $orderByPos)
1352
        ) {
1353
            if (preg_match('/^SELECT\s+DISTINCT/im', $query)) {
1354
                // SQL Server won't let us order by a non-selected column in a DISTINCT query,
1355
                // so we have to do this madness. This says, order by the first column in the
1356
                // result. SQL Server's docs say that a nonordered query's result order is non-
1357
                // deterministic anyway, so this won't do anything that a bunch of update and
1358
                // deletes to the table wouldn't do anyway.
1359
                $query .= ' ORDER BY 1';
1360
            } else {
1361
                // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you
1362
                // use constant expressions in the order by list.
1363
                $query .= ' ORDER BY (SELECT 0)';
1364
            }
1365
        }
1366
1367
        // This looks somewhat like MYSQL, but limit/offset are in inverse positions
1368
        // Supposedly SQL:2008 core standard.
1369
        // Per TSQL spec, FETCH NEXT n ROWS ONLY is not valid without OFFSET n ROWS.
1370
        $query .= sprintf(' OFFSET %d ROWS', $offset);
1371
1372
        if ($limit !== null) {
1373
            $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit);
1374
        }
1375
1376
        return $query;
1377
    }
1378
1379
    /**
1380
     * {@inheritDoc}
1381
     */
1382
    public function supportsLimitOffset()
1383
    {
1384
        return true;
1385
    }
1386
1387
    /**
1388
     * {@inheritDoc}
1389
     */
1390
    public function convertBooleans($item)
1391
    {
1392
        if (is_array($item)) {
1393
            foreach ($item as $key => $value) {
1394
                if (! is_bool($value) && ! is_numeric($value)) {
1395
                    continue;
1396
                }
1397
1398
                $item[$key] = $value ? 1 : 0;
1399
            }
1400
        } elseif (is_bool($item) || is_numeric($item)) {
1401
            $item = $item ? 1 : 0;
1402
        }
1403
1404
        return $item;
1405
    }
1406
1407
    /**
1408
     * {@inheritDoc}
1409
     */
1410
    public function getCreateTemporaryTableSnippetSQL()
1411
    {
1412
        return 'CREATE TABLE';
1413
    }
1414
1415
    /**
1416
     * {@inheritDoc}
1417
     */
1418
    public function getTemporaryTableName($tableName)
1419
    {
1420
        return '#' . $tableName;
1421
    }
1422
1423
    /**
1424
     * {@inheritDoc}
1425
     */
1426
    public function getDateTimeFormatString()
1427
    {
1428
        return 'Y-m-d H:i:s.u';
1429
    }
1430
1431
    /**
1432
     * {@inheritDoc}
1433
     */
1434
    public function getDateFormatString()
1435
    {
1436
        return 'Y-m-d';
1437
    }
1438
1439
    /**
1440
     * {@inheritDoc}
1441
     */
1442
    public function getTimeFormatString()
1443
    {
1444
        return 'H:i:s';
1445
    }
1446
1447
    /**
1448
     * {@inheritDoc}
1449
     */
1450
    public function getDateTimeTzFormatString()
1451
    {
1452
        return 'Y-m-d H:i:s.u P';
1453
    }
1454
1455
    /**
1456
     * {@inheritDoc}
1457
     */
1458
    public function getName()
1459
    {
1460
        return 'mssql';
1461
    }
1462
1463
    /**
1464
     * {@inheritDoc}
1465
     */
1466
    protected function initializeDoctrineTypeMappings()
1467
    {
1468
        $this->doctrineTypeMapping = [
1469
            'bigint'           => 'bigint',
1470
            'binary'           => 'binary',
1471
            'bit'              => 'boolean',
1472
            'char'             => 'string',
1473
            'date'             => 'date',
1474
            'datetime'         => 'datetime',
1475
            'datetime2'        => 'datetime',
1476
            'datetimeoffset'   => 'datetimetz',
1477
            'decimal'          => 'decimal',
1478
            'double'           => 'float',
1479
            'double precision' => 'float',
1480
            'float'            => 'float',
1481
            'image'            => 'blob',
1482
            'int'              => 'integer',
1483
            'money'            => 'integer',
1484
            'nchar'            => 'string',
1485
            'ntext'            => 'text',
1486
            'numeric'          => 'decimal',
1487
            'nvarchar'         => 'string',
1488
            'real'             => 'float',
1489
            'smalldatetime'    => 'datetime',
1490
            'smallint'         => 'smallint',
1491
            'smallmoney'       => 'integer',
1492
            'text'             => 'text',
1493
            'time'             => 'time',
1494
            'tinyint'          => 'smallint',
1495
            'uniqueidentifier' => 'guid',
1496
            'varbinary'        => 'binary',
1497
            'varchar'          => 'string',
1498
        ];
1499
    }
1500
1501
    /**
1502
     * {@inheritDoc}
1503
     */
1504
    public function createSavePoint($savepoint)
1505
    {
1506
        return 'SAVE TRANSACTION ' . $savepoint;
1507
    }
1508
1509
    /**
1510
     * {@inheritDoc}
1511
     */
1512
    public function releaseSavePoint($savepoint)
1513
    {
1514
        return '';
1515
    }
1516
1517
    /**
1518
     * {@inheritDoc}
1519
     */
1520
    public function rollbackSavePoint($savepoint)
1521
    {
1522
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1523
    }
1524
1525
    /**
1526
     * {@inheritdoc}
1527
     */
1528
    public function getForeignKeyReferentialActionSQL($action)
1529
    {
1530
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1531
        if (strtoupper($action) === 'RESTRICT') {
1532
            return 'NO ACTION';
1533
        }
1534
1535
        return parent::getForeignKeyReferentialActionSQL($action);
1536
    }
1537
1538
    /**
1539
     * {@inheritDoc}
1540
     */
1541
    public function appendLockHint($fromClause, $lockMode)
1542
    {
1543
        switch (true) {
1544
            case $lockMode === LockMode::NONE:
1545
                return $fromClause . ' WITH (NOLOCK)';
1546
1547
            case $lockMode === LockMode::PESSIMISTIC_READ:
1548
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1549
1550
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1551
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1552
1553
            default:
1554
                return $fromClause;
1555
        }
1556
    }
1557
1558
    /**
1559
     * {@inheritDoc}
1560
     */
1561
    public function getForUpdateSQL()
1562
    {
1563
        return ' ';
1564
    }
1565
1566
    /**
1567
     * {@inheritDoc}
1568
     */
1569
    protected function getReservedKeywordsClass()
1570
    {
1571
        return Keywords\SQLServerKeywords::class;
1572
    }
1573
1574
    /**
1575
     * {@inheritDoc}
1576
     */
1577
    public function quoteSingleIdentifier($str)
1578
    {
1579
        return '[' . str_replace(']', '][', $str) . ']';
1580
    }
1581
1582
    /**
1583
     * {@inheritDoc}
1584
     */
1585
    public function getTruncateTableSQL($tableName, $cascade = false)
1586
    {
1587
        $tableIdentifier = new Identifier($tableName);
1588
1589
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1590
    }
1591
1592
    /**
1593
     * {@inheritDoc}
1594
     */
1595
    public function getBlobTypeDeclarationSQL(array $field)
1596
    {
1597
        return 'VARBINARY(MAX)';
1598
    }
1599
1600
    /**
1601
     * {@inheritdoc}
1602
     *
1603
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1604
     */
1605
    public function getColumnDeclarationSQL($name, array $field)
1606
    {
1607
        if (isset($field['columnDefinition'])) {
1608
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1609
        } else {
1610
            $collation = isset($field['collation']) && $field['collation'] ?
1611
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1612
1613
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1614
1615
            $unique = isset($field['unique']) && $field['unique'] ?
1616
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1617
1618
            $check = isset($field['check']) && $field['check'] ?
1619
                ' ' . $field['check'] : '';
1620
1621
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1622
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1623
        }
1624
1625
        return $name . ' ' . $columnDef;
1626
    }
1627
1628
    /**
1629
     * {@inheritdoc}
1630
     */
1631
    protected function getLikeWildcardCharacters() : string
1632
    {
1633
        return parent::getLikeWildcardCharacters() . '[]^';
1634
    }
1635
1636
    /**
1637
     * Returns a unique default constraint name for a table and column.
1638
     *
1639
     * @param string $table  Name of the table to generate the unique default constraint name for.
1640
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1641
     *
1642
     * @return string
1643
     */
1644
    private function generateDefaultConstraintName($table, $column)
1645
    {
1646
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1647
    }
1648
1649
    /**
1650
     * Returns a hash value for a given identifier.
1651
     *
1652
     * @param string $identifier Identifier to generate a hash value for.
1653
     *
1654
     * @return string
1655
     */
1656
    private function generateIdentifierName($identifier)
1657
    {
1658
        // Always generate name for unquoted identifiers to ensure consistency.
1659
        $identifier = new Identifier($identifier);
1660
1661
        return strtoupper(dechex(crc32($identifier->getName())));
1662
    }
1663
1664
    protected function getCommentOnTableSQL(string $tableName, ?string $comment) : string
1665
    {
1666
        return sprintf(
1667
            <<<'SQL'
1668
EXEC sys.sp_addextendedproperty @name=N'MS_Description', 
1669
  @value=N%s, @level0type=N'SCHEMA', @level0name=N'dbo', 
1670
  @level1type=N'TABLE', @level1name=N%s
1671
SQL
1672
            ,
1673
            $this->quoteStringLiteral((string) $comment),
1674
            $this->quoteStringLiteral($tableName)
1675
        );
1676
    }
1677
1678
    public function getListTableMetadataSQL(string $table) : string
1679
    {
1680
        return sprintf(
1681
            <<<'SQL'
1682
SELECT
1683
  p.value AS [table_comment]
1684
FROM
1685
  sys.tables AS tbl
1686
  INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1
1687
WHERE
1688
  (tbl.name=N%s and SCHEMA_NAME(tbl.schema_id)=N'dbo' and p.name=N'MS_Description')
1689
SQL
1690
            ,
1691
            $this->quoteStringLiteral($table)
1692
        );
1693
    }
1694
}
1695