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

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