Failed Conditions
Pull Request — 3.0.x (#3980)
by Guilherme
06:55
created

getUniqueConstraintDeclarationSQL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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

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