Failed Conditions
Pull Request — 3.0.x (#3980)
by Guilherme
38:44
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 52
    public function supportsSchemas()
126
    {
127 52
        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 194
    public function getCreateIndexSQL(Index $index, $table)
465
    {
466 194
        $constraint = parent::getCreateIndexSQL($index, $table);
467
468 194
        if ($index->isUnique() && ! $index->isPrimary()) {
469 50
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
470
        }
471
472 194
        return $constraint;
473
    }
474
475
    /**
476
     * {@inheritDoc}
477
     */
478 194
    protected function getCreateIndexSQLFlags(Index $index)
479
    {
480 194
        $type = '';
481 194
        if ($index->isUnique()) {
482 50
            $type .= 'UNIQUE ';
483
        }
484
485 194
        if ($index->hasFlag('clustered')) {
486 22
            $type .= 'CLUSTERED ';
487 172
        } elseif ($index->hasFlag('nonclustered')) {
488
            $type .= 'NONCLUSTERED ';
489
        }
490
491 194
        return $type;
492
    }
493
494
    /**
495
     * Extend unique key constraint with required filters
496
     *
497
     * @param string $sql
498
     *
499
     * @return string
500
     */
501 50
    private function _appendUniqueConstraintDefinition($sql, Index $index)
502
    {
503 50
        $fields = [];
504
505 50
        foreach ($index->getQuotedColumns($this) as $field) {
506 50
            $fields[] = $field . ' IS NOT NULL';
507
        }
508
509 50
        return $sql . ' WHERE ' . implode(' AND ', $fields);
510
    }
511
512
    /**
513
     * {@inheritDoc}
514
     */
515 520
    public function getAlterTableSQL(TableDiff $diff)
516
    {
517 520
        $queryParts  = [];
518 520
        $sql         = [];
519 520
        $columnSql   = [];
520 520
        $commentsSql = [];
521
522 520
        foreach ($diff->addedColumns as $column) {
523 224
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
524
                continue;
525
            }
526
527 224
            $columnDef    = $column->toArray();
528 224
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
529
530 224
            if (isset($columnDef['default'])) {
531 88
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
532
            }
533
534 224
            $comment = $this->getColumnComment($column);
535
536 224
            if (empty($comment) && ! is_numeric($comment)) {
537 180
                continue;
538
            }
539
540 68
            $commentsSql[] = $this->getCreateColumnCommentSQL(
541 68
                $diff->name,
542 68
                $column->getQuotedName($this),
543
                $comment
544
            );
545
        }
546
547 520
        foreach ($diff->removedColumns as $column) {
548 184
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
549
                continue;
550
            }
551
552 184
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
553
        }
554
555 520
        foreach ($diff->changedColumns as $columnDiff) {
556 336
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
557
                continue;
558
            }
559
560 336
            $column     = $columnDiff->column;
561 336
            $comment    = $this->getColumnComment($column);
562 336
            $hasComment = ! empty($comment) || is_numeric($comment);
563
564 336
            if ($columnDiff->fromColumn instanceof Column) {
565 226
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
566 226
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
567
568 226
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
569 52
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
570 52
                        $diff->name,
571 52
                        $column->getQuotedName($this),
572
                        $comment
573
                    );
574 204
                } elseif ($hasFromComment && ! $hasComment) {
575 60
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
576 180
                } elseif ($hasComment) {
577 42
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
578 42
                        $diff->name,
579 42
                        $column->getQuotedName($this),
580
                        $comment
581
                    );
582
                }
583
            }
584
585
            // Do not add query part if only comment has changed.
586 336
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
587 110
                continue;
588
            }
589
590 250
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
591
592 250
            if ($requireDropDefaultConstraint) {
593 116
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
594 116
                    $diff->name,
595 116
                    $columnDiff->oldColumnName
596
                );
597
            }
598
599 250
            $columnDef = $column->toArray();
600
601 250
            $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
            }
607
608 138
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
609
        }
610
611 520
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
612 116
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
613
                continue;
614
            }
615
616 116
            $oldColumnName = new Identifier($oldColumnName);
617
618 116
            $sql[] = "sp_RENAME '" .
619 116
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
620 116
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
621
622
            // Recreate default constraint with new column name if necessary (for future reference).
623 116
            if ($column->getDefault() === null) {
624 92
                continue;
625
            }
626
627 24
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
628 24
                $diff->name,
629 24
                $oldColumnName->getQuotedName($this)
630
            );
631 24
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
632
        }
633
634 520
        $tableSql = [];
635
636 520
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
637
            return array_merge($tableSql, $columnSql);
638
        }
639
640 520
        foreach ($queryParts as $query) {
641 320
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
642
        }
643
644 520
        $sql = array_merge($sql, $commentsSql);
645
646 520
        $newName = $diff->getNewName();
647
648 520
        if ($newName !== false) {
649 46
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";
650
651
            /**
652
             * Rename table's default constraints names
653
             * to match the new table name.
654
             * This is necessary to ensure that the default
655
             * constraints can be referenced in future table
656
             * alterations as the table name is encoded in
657
             * default constraints' names.
658
             */
659 46
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
660
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
661 46
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
662 46
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
663 46
                'FROM sys.default_constraints dc ' .
664 46
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
665 46
                "WHERE tbl.name = '" . $newName->getName() . "';" .
666 46
                'EXEC sp_executesql @sql';
667
        }
668
669 520
        $sql = array_merge(
670 520
            $this->getPreAlterTableIndexForeignKeySQL($diff),
671 520
            $sql,
672 520
            $this->getPostAlterTableIndexForeignKeySQL($diff)
673
        );
674
675 520
        return array_merge($sql, $tableSql, $columnSql);
676
    }
677
678
    /**
679
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
680
     *
681
     * @param string $tableName The name of the table to generate the clause for.
682
     * @param Column $column    The column to generate the clause for.
683
     *
684
     * @return string
685
     */
686 160
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
687
    {
688 160
        $columnDef         = $column->toArray();
689 160
        $columnDef['name'] = $column->getQuotedName($this);
690
691 160
        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
     * @param string $columnName The name of the column to generate the clause for.
699
     *
700
     * @return string
701
     */
702 138
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
703
    {
704 138
        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
     *
715
     * @param ColumnDiff $columnDiff The column diff to evaluate.
716
     *
717
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
718
     */
719 250
    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 250
        if (! $columnDiff->fromColumn instanceof Column) {
724 88
            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 162
        if ($columnDiff->fromColumn->getDefault() === null) {
730 48
            return false;
731
        }
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 116
        if ($columnDiff->hasChanged('default')) {
736 92
            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 26
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
742
    }
743
744
    /**
745
     * Returns the SQL statement for altering a column comment.
746
     *
747
     * SQL Server does not support native column comments,
748
     * 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
     * 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 52
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
762
    {
763 52
        if (strpos($tableName, '.') !== false) {
764 22
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
765 22
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
766 22
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
767
        } else {
768 30
            $schemaSQL = "'dbo'";
769 30
            $tableSQL  = $this->quoteStringLiteral($tableName);
770
        }
771
772 52
        return $this->getUpdateExtendedPropertySQL(
773 52
            'MS_Description',
774
            $comment,
775 52
            'SCHEMA',
776
            $schemaSQL,
777 52
            'TABLE',
778
            $tableSQL,
779 52
            'COLUMN',
780
            $columnName
781
        );
782
    }
783
784
    /**
785
     * Returns the SQL statement for dropping a column comment.
786
     *
787
     * SQL Server does not support native column comments,
788
     * therefore the extended properties functionality is used
789
     * as a workaround to store them.
790
     * The property name used to store column comments is "MS_Description"
791
     * 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 60
    protected function getDropColumnCommentSQL($tableName, $columnName)
801
    {
802 60
        if (strpos($tableName, '.') !== false) {
803 22
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
804 22
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
805 22
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
806
        } else {
807 38
            $schemaSQL = "'dbo'";
808 38
            $tableSQL  = $this->quoteStringLiteral($tableName);
809
        }
810
811 60
        return $this->getDropExtendedPropertySQL(
812 60
            'MS_Description',
813 60
            'SCHEMA',
814
            $schemaSQL,
815 60
            'TABLE',
816
            $tableSQL,
817 60
            'COLUMN',
818
            $columnName
819
        );
820
    }
821
822
    /**
823
     * {@inheritdoc}
824
     */
825 114
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
826
    {
827 114
        return [sprintf(
828 5
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
829 114
            $tableName,
830 114
            $oldIndexName,
831 114
            $index->getQuotedName($this)
832
        ),
833
        ];
834
    }
835
836
    /**
837
     * Returns the SQL statement for adding an extended property to a database object.
838
     *
839
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
840
     *
841
     * @param string      $name       The name of the property to add.
842
     * @param string|null $value      The value of the property to add.
843
     * @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 218
    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 218
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
864 218
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
865 218
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
866 218
            '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
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
876
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
877
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
878
     * @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 60
    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 60
            'N' . $this->quoteStringLiteral($name) . ', ' .
895 60
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
896 60
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
897 60
            '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
     * @param string|null $value      The value of the property to update.
907
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
908
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
909
     * @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 52
    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 52
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
928 52
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
929 52
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
930 52
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
931
    }
932
933
    /**
934
     * {@inheritDoc}
935
     */
936 2
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
937
    {
938 2
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
939
    }
940
941
    /**
942
     * {@inheritDoc}
943
     */
944 136
    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 136
        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
951
    /**
952
     * {@inheritDoc}
953
     */
954 156
    public function getListTableColumnsSQL($table, $database = null)
955
    {
956
        return "SELECT    col.name,
957
                          type.name AS type,
958
                          col.max_length AS length,
959
                          ~col.is_nullable AS notnull,
960
                          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
                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 156
                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 134
    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
                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
                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 134
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
1007
    }
1008
1009
    /**
1010
     * {@inheritDoc}
1011
     */
1012 140
    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
                       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
                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 140
                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 2
    public function getCreateViewSQL($name, $sql)
1036
    {
1037 2
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
1038
    }
1039
1040
    /**
1041
     * {@inheritDoc}
1042
     */
1043 2
    public function getListViewsSQL($database)
1044
    {
1045 2
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
1046
    }
1047
1048
    /**
1049
     * 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
     * @return string
1056
     */
1057 258
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
1058
    {
1059 258
        if (strpos($table, '.') !== false) {
1060 68
            [$schema, $table] = explode('.', $table);
1061 68
            $schema           = $this->quoteStringLiteral($schema);
1062 68
            $table            = $this->quoteStringLiteral($table);
1063
        } else {
1064 192
            $schema = 'SCHEMA_NAME()';
1065 192
            $table  = $this->quoteStringLiteral($table);
1066
        }
1067
1068 258
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1069
    }
1070
1071
    /**
1072
     * {@inheritDoc}
1073
     */
1074 2
    public function getDropViewSQL($name)
1075
    {
1076 2
        return 'DROP VIEW ' . $name;
1077
    }
1078
1079
    /**
1080
     * {@inheritDoc}
1081
     *
1082
     * @deprecated Use application-generated UUIDs instead
1083
     */
1084
    public function getGuidExpression()
1085
    {
1086
        return 'NEWID()';
1087
    }
1088
1089
    /**
1090
     * {@inheritDoc}
1091
     */
1092 2
    public function getLocateExpression($str, $substr, $startPos = false)
1093
    {
1094 2
        if ($startPos === false) {
1095 2
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1096
        }
1097
1098 2
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1099
    }
1100
1101
    /**
1102
     * {@inheritDoc}
1103
     */
1104
    public function getModExpression($expression1, $expression2)
1105
    {
1106
        return $expression1 . ' % ' . $expression2;
1107
    }
1108
1109
    /**
1110
     * {@inheritDoc}
1111
     */
1112 72
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1113
    {
1114 72
        if ($char === false) {
1115 16
            switch ($pos) {
1116
                case TrimMode::LEADING:
1117 4
                    $trimFn = 'LTRIM';
1118 4
                    break;
1119
1120
                case TrimMode::TRAILING:
1121 4
                    $trimFn = 'RTRIM';
1122 4
                    break;
1123
1124
                default:
1125 8
                    return 'LTRIM(RTRIM(' . $str . '))';
1126
            }
1127
1128 8
            return $trimFn . '(' . $str . ')';
1129
        }
1130
1131
        /** Original query used to get those expressions
1132
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1133
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1134
          select @c as string
1135
          , @trim_char as trim_char
1136
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1137
          , 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 56
        $pattern = "'%[^' + " . $char . " + ']%'";
1141
1142 56
        if ($pos === TrimMode::LEADING) {
1143 14
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1144
        }
1145
1146 42
        if ($pos === TrimMode::TRAILING) {
1147 14
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1148
        }
1149
1150 28
        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
1153
    /**
1154
     * {@inheritDoc}
1155
     */
1156 22
    public function getConcatExpression()
1157
    {
1158 22
        $args = func_get_args();
1159
1160 22
        return '(' . implode(' + ', $args) . ')';
1161
    }
1162
1163
    /**
1164
     * {@inheritDoc}
1165
     */
1166 26
    public function getListDatabasesSQL()
1167
    {
1168 26
        return 'SELECT * FROM sys.databases';
1169
    }
1170
1171
    /**
1172
     * {@inheritDoc}
1173
     */
1174 4
    public function getListNamespacesSQL()
1175
    {
1176 4
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1177
    }
1178
1179
    /**
1180
     * {@inheritDoc}
1181
     */
1182
    public function getSubstringExpression($value, $from, $length = null)
1183
    {
1184
        if ($length !== null) {
1185
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1186
        }
1187
1188
        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 22
    public function getSetTransactionIsolationSQL($level)
1203
    {
1204 22
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1205
    }
1206
1207
    /**
1208
     * {@inheritDoc}
1209
     */
1210 564
    public function getIntegerTypeDeclarationSQL(array $field)
1211
    {
1212 564
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1213
    }
1214
1215
    /**
1216
     * {@inheritDoc}
1217
     */
1218 30
    public function getBigIntTypeDeclarationSQL(array $field)
1219
    {
1220 30
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1221
    }
1222
1223
    /**
1224
     * {@inheritDoc}
1225
     */
1226 2
    public function getSmallIntTypeDeclarationSQL(array $field)
1227
    {
1228 2
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1229
    }
1230
1231
    /**
1232
     * {@inheritDoc}
1233
     */
1234 22
    public function getGuidTypeDeclarationSQL(array $field)
1235
    {
1236 22
        return 'UNIQUEIDENTIFIER';
1237
    }
1238
1239
    /**
1240
     * {@inheritDoc}
1241
     */
1242 52
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
1243
    {
1244 52
        return 'DATETIMEOFFSET(6)';
1245
    }
1246
1247
    /**
1248
     * {@inheritDoc}
1249
     */
1250 686
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1251
    {
1252 686
        return $fixed
1253 80
            ? ($length > 0 ? 'NCHAR(' . $length . ')' : 'CHAR(255)')
1254 686
            : ($length > 0 ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1255
    }
1256
1257
    /**
1258
     * {@inheritdoc}
1259
     */
1260 26
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1261
    {
1262 26
        return $fixed
1263 26
            ? 'BINARY(' . ($length > 0 ? $length : 255) . ')'
1264 26
            : 'VARBINARY(' . ($length > 0 ? $length : 255) . ')';
1265
    }
1266
1267
    /**
1268
     * {@inheritdoc}
1269
     */
1270 70
    public function getBinaryMaxLength()
1271
    {
1272 70
        return 8000;
1273
    }
1274
1275
    /**
1276
     * {@inheritDoc}
1277
     */
1278 206
    public function getClobTypeDeclarationSQL(array $field)
1279
    {
1280 206
        return 'VARCHAR(MAX)';
1281
    }
1282
1283
    /**
1284
     * {@inheritDoc}
1285
     */
1286 564
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1287
    {
1288 564
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1289
    }
1290
1291
    /**
1292
     * {@inheritDoc}
1293
     */
1294 46
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1295
    {
1296
        // 3 - microseconds precision length
1297
        // http://msdn.microsoft.com/en-us/library/ms187819.aspx
1298 46
        return 'DATETIME2(6)';
1299
    }
1300
1301
    /**
1302
     * {@inheritDoc}
1303
     */
1304 38
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1305
    {
1306 38
        return 'DATE';
1307
    }
1308
1309
    /**
1310
     * {@inheritDoc}
1311
     */
1312 38
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1313
    {
1314 38
        return 'TIME(0)';
1315
    }
1316
1317
    /**
1318
     * {@inheritDoc}
1319
     */
1320 56
    public function getBooleanTypeDeclarationSQL(array $field)
1321
    {
1322 56
        return 'BIT';
1323
    }
1324
1325
    /**
1326
     * {@inheritDoc}
1327
     */
1328 588
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1329
    {
1330 588
        if ($limit === null && $offset <= 0) {
1331 24
            return $query;
1332
        }
1333
1334
        // 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 564
        $matches      = [];
1338 564
        $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE);
1339 564
        $orderByPos   = false;
1340 564
        if ($matchesCount > 0) {
1341 386
            $orderByPos = $matches[0][($matchesCount - 1)][1];
1342
        }
1343
1344 564
        if ($orderByPos === false
1345 564
            || substr_count($query, '(', $orderByPos) !== substr_count($query, ')', $orderByPos)
1346
        ) {
1347 200
            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
                // so we have to do this madness. This says, order by the first column in the
1350
                // result. SQL Server's docs say that a nonordered query's result order is non-
1351
                // deterministic anyway, so this won't do anything that a bunch of update and
1352
                // deletes to the table wouldn't do anyway.
1353 44
                $query .= ' ORDER BY 1';
1354
            } else {
1355
                // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you
1356
                // use constant expressions in the order by list.
1357 156
                $query .= ' ORDER BY (SELECT 0)';
1358
            }
1359
        }
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 564
        $query .= sprintf(' OFFSET %d ROWS', $offset);
1365
1366 564
        if ($limit !== null) {
1367 564
            $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit);
1368
        }
1369
1370 564
        return $query;
1371
    }
1372
1373
    /**
1374
     * {@inheritDoc}
1375
     */
1376 102
    public function supportsLimitOffset()
1377
    {
1378 102
        return true;
1379
    }
1380
1381
    /**
1382
     * {@inheritDoc}
1383
     */
1384 30
    public function convertBooleans($item)
1385
    {
1386 30
        if (is_array($item)) {
1387
            foreach ($item as $key => $value) {
1388
                if (! is_bool($value) && ! is_numeric($value)) {
1389
                    continue;
1390
                }
1391
1392
                $item[$key] = (int) (bool) $value;
1393
            }
1394 30
        } elseif (is_bool($item) || is_numeric($item)) {
1395 30
            $item = (int) (bool) $item;
1396
        }
1397
1398 30
        return $item;
1399
    }
1400
1401
    /**
1402
     * {@inheritDoc}
1403
     */
1404 4
    public function getCreateTemporaryTableSnippetSQL()
1405
    {
1406 4
        return 'CREATE TABLE';
1407
    }
1408
1409
    /**
1410
     * {@inheritDoc}
1411
     */
1412 4
    public function getTemporaryTableName($tableName)
1413
    {
1414 4
        return '#' . $tableName;
1415
    }
1416
1417
    /**
1418
     * {@inheritDoc}
1419
     */
1420 22
    public function getDateTimeFormatString()
1421
    {
1422 22
        return 'Y-m-d H:i:s.u';
1423
    }
1424
1425
    /**
1426
     * {@inheritDoc}
1427
     */
1428 2
    public function getDateFormatString()
1429
    {
1430 2
        return 'Y-m-d';
1431
    }
1432
1433
    /**
1434
     * {@inheritDoc}
1435
     */
1436 2
    public function getTimeFormatString()
1437
    {
1438 2
        return 'H:i:s';
1439
    }
1440
1441
    /**
1442
     * {@inheritDoc}
1443
     */
1444 2
    public function getDateTimeTzFormatString()
1445
    {
1446 2
        return 'Y-m-d H:i:s.u P';
1447
    }
1448
1449
    /**
1450
     * {@inheritDoc}
1451
     */
1452 144
    public function getName()
1453
    {
1454 144
        return 'mssql';
1455
    }
1456
1457
    /**
1458
     * {@inheritDoc}
1459
     */
1460 112
    protected function initializeDoctrineTypeMappings()
1461
    {
1462 112
        $this->doctrineTypeMapping = [
1463
            'bigint'           => 'bigint',
1464
            'binary'           => 'binary',
1465
            'bit'              => 'boolean',
1466
            'char'             => 'string',
1467
            'date'             => 'date',
1468
            'datetime'         => 'datetime',
1469
            'datetime2'        => 'datetime',
1470
            'datetimeoffset'   => 'datetimetz',
1471
            'decimal'          => 'decimal',
1472
            'double'           => 'float',
1473
            'double precision' => 'float',
1474
            '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 112
    }
1494
1495
    /**
1496
     * {@inheritDoc}
1497
     */
1498 2
    public function createSavePoint($savepoint)
1499
    {
1500 2
        return 'SAVE TRANSACTION ' . $savepoint;
1501
    }
1502
1503
    /**
1504
     * {@inheritDoc}
1505
     */
1506
    public function releaseSavePoint($savepoint)
1507
    {
1508
        return '';
1509
    }
1510
1511
    /**
1512
     * {@inheritDoc}
1513
     */
1514 2
    public function rollbackSavePoint($savepoint)
1515
    {
1516 2
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1517
    }
1518
1519
    /**
1520
     * {@inheritdoc}
1521
     */
1522 156
    public function getForeignKeyReferentialActionSQL($action)
1523
    {
1524
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1525 156
        if (strtoupper($action) === 'RESTRICT') {
1526 22
            return 'NO ACTION';
1527
        }
1528
1529 134
        return parent::getForeignKeyReferentialActionSQL($action);
1530
    }
1531
1532
    /**
1533
     * {@inheritDoc}
1534
     */
1535 158
    public function appendLockHint($fromClause, $lockMode)
1536
    {
1537 158
        switch (true) {
1538
            case $lockMode === LockMode::NONE:
1539 22
                return $fromClause . ' WITH (NOLOCK)';
1540
1541 136
            case $lockMode === LockMode::PESSIMISTIC_READ:
1542 22
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1543
1544 114
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1545 26
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1546
1547
            default:
1548 88
                return $fromClause;
1549
        }
1550
    }
1551
1552
    /**
1553
     * {@inheritDoc}
1554
     */
1555 4
    public function getForUpdateSQL()
1556
    {
1557 4
        return ' ';
1558
    }
1559
1560
    /**
1561
     * {@inheritDoc}
1562
     */
1563 1404
    protected function getReservedKeywordsClass()
1564
    {
1565 1404
        return Keywords\SQLServer2012Keywords::class;
1566
    }
1567
1568
    /**
1569
     * {@inheritDoc}
1570
     */
1571 690
    public function quoteSingleIdentifier($str)
1572
    {
1573 690
        return '[' . str_replace(']', '][', $str) . ']';
1574
    }
1575
1576
    /**
1577
     * {@inheritDoc}
1578
     */
1579 38
    public function getTruncateTableSQL($tableName, $cascade = false)
1580
    {
1581 38
        $tableIdentifier = new Identifier($tableName);
1582
1583 38
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1584
    }
1585
1586
    /**
1587
     * {@inheritDoc}
1588
     */
1589 38
    public function getBlobTypeDeclarationSQL(array $field)
1590
    {
1591 38
        return 'VARBINARY(MAX)';
1592
    }
1593
1594
    /**
1595
     * {@inheritdoc}
1596
     *
1597
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1598
     */
1599 962
    public function getColumnDeclarationSQL($name, array $field)
1600
    {
1601 962
        if (isset($field['columnDefinition'])) {
1602 22
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1603
        } else {
1604 940
            $collation = ! empty($field['collation']) ?
1605 940
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1606
1607 940
            $notnull = ! empty($field['notnull']) ? ' NOT NULL' : '';
1608
1609 940
            $unique = ! empty($field['unique']) ?
1610 940
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1611
1612 940
            $check = ! empty($field['check']) ?
1613 940
                ' ' . $field['check'] : '';
1614
1615 940
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1616 940
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1617
        }
1618
1619 962
        return $name . ' ' . $columnDef;
1620
    }
1621
1622
    /**
1623
     * {@inheritdoc}
1624
     */
1625 24
    protected function getLikeWildcardCharacters() : string
1626
    {
1627 24
        return parent::getLikeWildcardCharacters() . '[]^';
1628
    }
1629
1630
    /**
1631
     * 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
     */
1638 360
    private function generateDefaultConstraintName($table, $column)
1639
    {
1640 360
        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 382
    private function generateIdentifierName($identifier)
1651
    {
1652
        // Always generate name for unquoted identifiers to ensure consistency.
1653 382
        $identifier = new Identifier($identifier);
1654
1655 382
        return strtoupper(dechex(crc32($identifier->getName())));
1656
    }
1657
1658 2
    protected function getCommentOnTableSQL(string $tableName, ?string $comment) : string
1659
    {
1660 2
        return sprintf(
1661
            <<<'SQL'
1662
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
SQL
1666
            ,
1667 2
            $this->quoteStringLiteral((string) $comment),
1668 2
            $this->quoteStringLiteral($tableName)
1669
        );
1670
    }
1671
1672 84
    public function getListTableMetadataSQL(string $table) : string
1673
    {
1674 84
        return sprintf(
1675
            <<<'SQL'
1676
SELECT
1677
  p.value AS [table_comment]
1678
FROM
1679
  sys.tables AS tbl
1680
  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
            ,
1685 84
            $this->quoteStringLiteral($table)
1686
        );
1687
    }
1688
}
1689