Completed
Pull Request — 3.0.x (#3980)
by Guilherme
65:33
created

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

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