Completed
Push — master ( 299ef4...a90152 )
by Sergei
14:43 queued 14:35
created

SQLServerPlatform::getSubstringExpression()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 3
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Platforms;
6
7
use Doctrine\DBAL\Exception\ColumnLengthRequired;
8
use Doctrine\DBAL\LockMode;
9
use Doctrine\DBAL\Schema\Column;
10
use Doctrine\DBAL\Schema\ColumnDiff;
11
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
12
use Doctrine\DBAL\Schema\Identifier;
13
use Doctrine\DBAL\Schema\Index;
14
use Doctrine\DBAL\Schema\Sequence;
15
use Doctrine\DBAL\Schema\Table;
16
use Doctrine\DBAL\Schema\TableDiff;
17
use InvalidArgumentException;
18
use const PREG_OFFSET_CAPTURE;
19
use function array_merge;
20
use function array_unique;
21
use function array_values;
22
use function count;
23
use function crc32;
24
use function dechex;
25
use function explode;
26
use function implode;
27
use function in_array;
28
use function is_array;
29
use function is_bool;
30
use function is_numeric;
31
use function is_string;
32
use function preg_match;
33
use function preg_match_all;
34
use function sprintf;
35
use function str_replace;
36
use function strpos;
37
use function strtoupper;
38
use function substr_count;
39
40
/**
41
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
42
 * Microsoft SQL Server database platform.
43
 */
44
class SQLServerPlatform extends AbstractPlatform
45
{
46
    /**
47 8446
     * {@inheritdoc}
48
     */
49 8446
    public function getCurrentDateSQL() : string
50
    {
51
        return $this->getConvertExpression('date', 'GETDATE()');
52
    }
53
54
    /**
55 8438
     * {@inheritdoc}
56
     */
57 8438
    public function getCurrentTimeSQL() : string
58
    {
59
        return $this->getConvertExpression('time', 'GETDATE()');
60
    }
61
62
    /**
63
     * Returns an expression that converts an expression of one data type to another.
64
     *
65
     * @param string $dataType   The target native data type. Alias data types cannot be used.
66
     * @param string $expression The SQL expression to convert.
67
     */
68 8446
    private function getConvertExpression(string $dataType, string $expression) : string
69
    {
70 8446
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76 961
    protected function getDateArithmeticIntervalExpression(string $date, string $operator, string $interval, string $unit) : string
77
    {
78 961
        $factorClause = '';
79
80 961
        if ($operator === '-') {
81 961
            $factorClause = '-1 * ';
82
        }
83
84 961
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
85
    }
86
87
    /**
88
     * {@inheritDoc}
89
     */
90 940
    public function getDateDiffExpression(string $date1, string $date2) : string
91
    {
92 940
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
93
    }
94
95
    /**
96
     * {@inheritDoc}
97
     *
98
     * Microsoft SQL Server prefers "autoincrement" identity columns
99
     * since sequences can only be emulated with a table.
100
     */
101 8059
    public function prefersIdentityColumns() : bool
102
    {
103 8059
        return true;
104
    }
105
106
    /**
107
     * {@inheritDoc}
108
     *
109
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
110
     */
111 843
    public function supportsIdentityColumns() : bool
112
    {
113 843
        return true;
114
    }
115
116
    /**
117
     * {@inheritDoc}
118
     */
119 1003
    public function supportsReleaseSavepoints() : bool
120
    {
121 1003
        return false;
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 873
    public function supportsSchemas() : bool
128
    {
129 873
        return true;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135 813
    public function getDefaultSchemaName() : string
136
    {
137 813
        return 'dbo';
138
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143 6950
    public function supportsColumnCollation() : bool
144
    {
145 6950
        return true;
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 8888
    public function supportsSequences() : bool
152
    {
153 8888
        return true;
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159 8475
    public function getAlterSequenceSQL(Sequence $sequence) : string
160
    {
161 8475
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
162
            ' INCREMENT BY ' . $sequence->getAllocationSize();
163
    }
164
165
    /**
166
     * {@inheritdoc}
167 8475
     */
168
    public function getCreateSequenceSQL(Sequence $sequence) : string
169 8475
    {
170
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
171
            ' START WITH ' . $sequence->getInitialValue() .
172
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
173
            ' MINVALUE ' . $sequence->getInitialValue();
174
    }
175 1023
176
    /**
177 1023
     * {@inheritdoc}
178
     */
179
    public function getDropSequenceSQL($sequence) : string
180
    {
181
        if ($sequence instanceof Sequence) {
182
            $sequence = $sequence->getQuotedName($this);
183 7658
        }
184
185 7658
        return 'DROP SEQUENCE ' . $sequence;
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191 5928
    public function getListSequencesSQL(string $database) : string
192
    {
193 5928
        return 'SELECT seq.name,
194 5531
                       CAST(
195
                           seq.increment AS VARCHAR(MAX)
196
                       ) AS increment, -- CAST avoids driver error for sql_variant type
197 5928
                       CAST(
198 5928
                           seq.start_value AS VARCHAR(MAX)
199
                       ) AS start_value -- CAST avoids driver error for sql_variant type
200
                FROM   sys.sequences AS seq';
201 5928
    }
202 5928
203
    /**
204 5928
     * {@inheritdoc}
205
     */
206
    public function getSequenceNextValSQL(string $sequenceName) : string
207
    {
208
        return 'SELECT NEXT VALUE FOR ' . $sequenceName;
209
    }
210 851
211
    /**
212 851
     * {@inheritDoc}
213 843
     */
214 851
    public function hasNativeGuidType() : bool
215
    {
216
        return true;
217
    }
218 851
219
    /**
220
     * {@inheritDoc}
221
     */
222 851
    public function getCreateDatabaseSQL(string $database) : string
223 851
    {
224
        return 'CREATE DATABASE ' . $database;
225
    }
226 851
227
    /**
228
     * {@inheritDoc}
229
     */
230
    public function getDropDatabaseSQL(string $database) : string
231
    {
232
        return 'DROP DATABASE ' . $database;
233
    }
234 851
235 851
    /**
236 851
     * {@inheritDoc}
237 851
     */
238 851
    public function supportsCreateDropDatabase() : bool
239
    {
240
        return true;
241
    }
242
243
    /**
244
     * {@inheritDoc}
245 8642
     */
246
    public function getCreateSchemaSQL(string $schemaName) : string
247 8642
    {
248 8642
        return 'CREATE SCHEMA ' . $schemaName;
249
    }
250 8642
251 8642
    /**
252 771
     * {@inheritDoc}
253
     */
254
    public function getDropForeignKeySQL($foreignKey, $table) : string
255
    {
256
        if (! $foreignKey instanceof ForeignKeyConstraint) {
257 8642
            $foreignKey = new Identifier($foreignKey);
258 8642
        }
259 7907
260
        if (! $table instanceof Table) {
261
            $table = new Identifier($table);
262
        }
263 8642
264 7400
        $foreignKey = $foreignKey->getQuotedName($this);
265 7400
        $table      = $table->getQuotedName($this);
266
267
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
268 8642
    }
269 8630
270
    /**
271
     * {@inheritDoc}
272 7665
     */
273
    public function getDropIndexSQL($index, $table = null) : string
274
    {
275 8642
        if ($index instanceof Index) {
276
            $index = $index->getQuotedName($this);
277 8642
        } elseif (! is_string($index)) {
278
            throw new InvalidArgumentException(sprintf(
279
                'AbstractPlatform::getDropIndexSQL() expects $index parameter to be a string or an instanceof %s.',
280
                Index::class
281
            ));
282
        }
283 8642
284 7913
        if (! isset($table)) {
285 7913
            return 'DROP INDEX ' . $index;
286 7456
        }
287
288 7913
        if ($table instanceof Table) {
289
            $table = $table->getQuotedName($this);
290
        }
291 8642
292
        return sprintf(
293 8642
            <<<SQL
294 8642
IF EXISTS (SELECT * FROM sysobjects WHERE name = '%s')
295
    ALTER TABLE %s DROP CONSTRAINT %s
296
ELSE
297 8642
    DROP INDEX %s ON %s
298
SQL
299 8642
            ,
300
            $index,
301 8642
            $table,
302 6556
            $index,
303 6556
            $index,
304
            $table
305
        );
306
    }
307 8642
308 6336
    /**
309 6168
     * {@inheritDoc}
310
     */
311
    protected function _getCreateTableSQL(string $tableName, array $columns, array $options = []) : array
312
    {
313 8642
        $defaultConstraintsSql = [];
314
        $commentsSql           = [];
315
316
        $tableComment = $options['comment'] ?? null;
317
        if ($tableComment !== null) {
318
            $commentsSql[] = $this->getCommentOnTableSQL($tableName, $tableComment);
319 7437
        }
320
321 7437
        // @todo does other code breaks because of this?
322
        // force primary keys to be not null
323
        foreach ($columns as &$column) {
324 7437
            if (isset($column['primary']) && $column['primary']) {
325
                $column['notnull'] = true;
326
            }
327 7437
328
            // Build default constraints SQL statements.
329 7437
            if (isset($column['default'])) {
330 7431
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
331
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
332
            }
333 7437
334
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
335
                continue;
336
            }
337
338
            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
339
        }
340
341
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
342
343
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
344
            foreach ($options['uniqueConstraints'] as $name => $definition) {
345
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
346
            }
347
        }
348
349
        if (isset($options['primary']) && ! empty($options['primary'])) {
350
            $flags = '';
351
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
352
                $flags = ' NONCLUSTERED';
353 7683
            }
354
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
355 7683
        }
356 7362
357 7362
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
358 7362
359
        $check = $this->getCheckDeclarationSQL($columns);
360 7579
        if (! empty($check)) {
361 7579
            $query .= ', ' . $check;
362
        }
363
        $query .= ')';
364 7683
365 7683
        $sql = [$query];
366 42
367 7683
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
368 42
            foreach ($options['indexes'] as $index) {
369 7683
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
370 42
            }
371 7683
        }
372 42
373
        if (isset($options['foreignKeys'])) {
374
            foreach ((array) $options['foreignKeys'] as $definition) {
375
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
376
            }
377
        }
378
379
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
380
    }
381
382
    /**
383
     * {@inheritDoc}
384
     */
385
    public function getCreatePrimaryKeySQL(Index $index, $table) : string
386 7581
    {
387
        if ($table instanceof Table) {
388 7581
            $identifier = $table->getQuotedName($this);
389
        } else {
390
            $identifier = $table;
391
        }
392 7581
393
        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';
394
395 7581
        if ($index->hasFlag('nonclustered')) {
396 7581
            $sql .= ' NONCLUSTERED';
397 7581
        }
398
399
        return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
400
    }
401
402
    /**
403 6112
     * Returns the SQL statement for creating a column comment.
404
     *
405 6112
     * SQL Server does not support native column comments,
406
     * therefore the extended properties functionality is used
407 6112
     * as a workaround to store them.
408
     * The property name used to store column comments is "MS_Description"
409 6112
     * which provides compatibility with SQL Server Management Studio,
410
     * as column comments are stored in the same property there when
411
     * specifying a column's "Description" attribute.
412
     *
413
     * @param string      $tableName  The quoted table name to which the column belongs.
414
     * @param string      $columnName The quoted column name to create the comment for.
415 7782
     * @param string|null $comment    The column's comment.
416
     */
417 7782
    protected function getCreateColumnCommentSQL(string $tableName, string $columnName, ?string $comment) : string
418
    {
419 7782
        if (strpos($tableName, '.') !== false) {
420 6550
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
421
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
422
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
423 7782
        } else {
424
            $schemaSQL = "'dbo'";
425
            $tableSQL  = $this->quoteStringLiteral($tableName);
426
        }
427
428
        return $this->getAddExtendedPropertySQL(
429 7782
            'MS_Description',
430
            $comment,
431 7782
            'SCHEMA',
432 7782
            $schemaSQL,
433 6550
            'TABLE',
434
            $tableSQL,
435
            'COLUMN',
436 7782
            $columnName
437 7481
        );
438 6580
    }
439
440
    /**
441
     * Returns the SQL snippet for declaring a default constraint.
442 7782
     *
443
     * @param string  $table  Name of the table to return the default constraint declaration for.
444
     * @param mixed[] $column Column definition.
445
     *
446
     * @throws InvalidArgumentException
447
     */
448
    public function getDefaultConstraintDeclarationSQL(string $table, array $column) : string
449
    {
450
        if (! isset($column['default'])) {
451
            throw new InvalidArgumentException('Incomplete column definition. "default" required.');
452 6562
        }
453
454 6562
        $columnName = new Identifier($column['name']);
455
456 6562
        return ' CONSTRAINT ' .
457 6562
            $this->generateDefaultConstraintName($table, $column['name']) .
458
            $this->getDefaultValueDeclarationSQL($column) .
459
            ' FOR ' . $columnName->getQuotedName($this);
460 6562
    }
461
462
    /**
463
     * {@inheritDoc}
464
     */
465
    public function getCreateIndexSQL(Index $index, $table) : string
466 7756
    {
467
        $constraint = parent::getCreateIndexSQL($index, $table);
468 7756
469 7756
        if ($index->isUnique() && ! $index->isPrimary()) {
470 7756
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
471 7756
        }
472
473 7756
        return $constraint;
474 7678
    }
475
476
    /**
477
     * {@inheritDoc}
478 7678
     */
479 7678
    protected function getCreateIndexSQLFlags(Index $index) : string
480
    {
481 7678
        $type = '';
482 6924
        if ($index->isUnique()) {
483
            $type .= 'UNIQUE ';
484
        }
485 7678
486
        if ($index->hasFlag('clustered')) {
487 7678
            $type .= 'CLUSTERED ';
488 7574
        } elseif ($index->hasFlag('nonclustered')) {
489
            $type .= 'NONCLUSTERED ';
490
        }
491 7636
492 7636
        return $type;
493 7636
    }
494 18
495
    /**
496
     * Extend unique key constraint with required filters
497
     */
498 7756
    private function _appendUniqueConstraintDefinition(string $sql, Index $index) : string
499 7580
    {
500
        $fields = [];
501
502
        foreach ($index->getQuotedColumns($this) as $field) {
503 7580
            $fields[] = $field . ' IS NOT NULL';
504
        }
505
506 7756
        return $sql . ' WHERE ' . implode(' AND ', $fields);
507 7681
    }
508
509
    /**
510
     * {@inheritDoc}
511 7681
     */
512 7681
    public function getAlterTableSQL(TableDiff $diff) : array
513 7681
    {
514
        $queryParts  = [];
515 7681
        $sql         = [];
516 7651
        $columnSql   = [];
517 7651
        $commentsSql = [];
518
519 7651
        foreach ($diff->addedColumns as $column) {
520 7584
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
521 7584
                continue;
522 7584
            }
523 12
524
            $columnDef    = $column->toArray();
525 7645
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
526 7607
527 7570
            if (isset($columnDef['default'])) {
528 7532
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
529 7532
            }
530 7532
531 6
            $comment = $this->getColumnComment($column);
532
533
            if (empty($comment) && ! is_numeric($comment)) {
534
                continue;
535
            }
536
537 7681
            $commentsSql[] = $this->getCreateColumnCommentSQL(
538 7619
                $diff->name,
539
                $column->getQuotedName($this),
540
                $comment
541 7594
            );
542
        }
543 7594
544 7466
        foreach ($diff->removedColumns as $column) {
545 7466
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
546 7466
                continue;
547
            }
548
549
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
550 7594
        }
551
552 7594
        foreach ($diff->changedColumns as $columnDiff) {
553 7594
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
554
                continue;
555 7594
            }
556 7558
557
            $column     = $columnDiff->column;
558
            $comment    = $this->getColumnComment($column);
559 7472
            $hasComment = ! empty($comment) || is_numeric($comment);
560
561
            if ($columnDiff->fromColumn instanceof Column) {
562 7756
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
563 7558
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
564
565
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
566
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
567 7558
                        $diff->name,
568
                        $column->getQuotedName($this),
569 7558
                        $comment
570 7558
                    );
571 7558
                } elseif ($hasFromComment && ! $hasComment) {
572
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
573
                } elseif ($hasComment) {
574 7558
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
575 7550
                        $diff->name,
576
                        $column->getQuotedName($this),
577
                        $comment
578 5901
                    );
579 5901
                }
580 5901
            }
581
582 5901
            // Do not add query part if only comment has changed.
583
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
584
                continue;
585 7756
            }
586
587 7756
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
588
589
            if ($requireDropDefaultConstraint) {
590
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
591 7756
                    $diff->name,
592 7708
                    $columnDiff->oldColumnName
593
                );
594
            }
595 7756
596
            $columnDef = $column->toArray();
597 7756
598
            $queryParts[] = 'ALTER COLUMN ' .
599 7756
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
600 6436
601
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
602
                continue;
603
            }
604
605
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
606
        }
607
608
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
609
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
610 6436
                continue;
611
            }
612 6436
613 6436
            $oldColumnName = new Identifier($oldColumnName);
614 6436
615 6436
            $sql[] = "sp_RENAME '" .
616 6436
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
617 6436
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
618
619
            // Recreate default constraint with new column name if necessary (for future reference).
620 7756
            if ($column->getDefault() === null) {
621 7756
                continue;
622 7756
            }
623 7756
624
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
625
                $diff->name,
626 7756
                $oldColumnName->getQuotedName($this)
627
            );
628
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
629
        }
630
631
        $tableSql = [];
632
633
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
634
            return array_merge($tableSql, $columnSql);
635
        }
636
637 7478
        foreach ($queryParts as $query) {
638
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
639 7478
        }
640 7478
641
        $sql = array_merge($sql, $commentsSql);
642 7478
643
        $newName = $diff->getNewName();
644
645
        if ($newName !== null) {
646
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";
647
648
            /**
649
             * Rename table's default constraints names
650
             * to match the new table name.
651
             * This is necessary to ensure that the default
652
             * constraints can be referenced in future table
653 7472
             * alterations as the table name is encoded in
654
             * default constraints' names.
655 7472
             */
656
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
657
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
658
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
659
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
660
                'FROM sys.default_constraints dc ' .
661
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
662
                "WHERE tbl.name = '" . $newName->getName() . "';" .
663
                'EXEC sp_executesql @sql';
664
        }
665
666
        $sql = array_merge(
667
            $this->getPreAlterTableIndexForeignKeySQL($diff),
668
            $sql,
669
            $this->getPostAlterTableIndexForeignKeySQL($diff)
670 7594
        );
671
672
        return array_merge($sql, $tableSql, $columnSql);
673
    }
674 7594
675 6049
    /**
676
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
677
     *
678
     * @param string $tableName The name of the table to generate the clause for.
679
     * @param Column $column    The column to generate the clause for.
680 7570
     */
681 7538
    private function getAlterTableAddDefaultConstraintClause(string $tableName, Column $column) : string
682
    {
683
        $columnDef         = $column->toArray();
684
        $columnDef['name'] = $column->getQuotedName($this);
685
686 7466
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
687 7253
    }
688
689
    /**
690
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
691
     *
692 7442
     * @param string $tableName  The name of the table to generate the clause for.
693
     * @param string $columnName The name of the column to generate the clause for.
694
     */
695
    private function getAlterTableDropDefaultConstraintClause(string $tableName, string $columnName) : string
696
    {
697
        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 7584
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff) : bool
713
    {
714 7584
        // We can only decide whether to drop an existing default constraint
715 7281
        // if we know the original default value.
716 7281
        if (! $columnDiff->fromColumn instanceof Column) {
717 7281
            return false;
718
        }
719 7532
720 7532
        // We only need to drop an existing default constraint if we know the
721
        // column was defined with a default value before.
722
        if ($columnDiff->fromColumn->getDefault() === null) {
723 7584
            return false;
724 7584
        }
725 12
726 7584
        // We need to drop an existing default constraint if the column was
727 12
        // defined with a default value before and it has changed.
728 7584
        if ($columnDiff->hasChanged('default')) {
729 12
            return true;
730 7584
        }
731 12
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
        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 7607
     */
752
    protected function getAlterColumnCommentSQL(string $tableName, string $columnName, ?string $comment) : string
753 7607
    {
754 7306
        if (strpos($tableName, '.') !== false) {
755 7306
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
756 7306
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
757
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
758 7532
        } else {
759 7532
            $schemaSQL = "'dbo'";
760
            $tableSQL  = $this->quoteStringLiteral($tableName);
761
        }
762 7607
763 7607
        return $this->getUpdateExtendedPropertySQL(
764 7607
            'MS_Description',
765 12
            $comment,
766 7607
            'SCHEMA',
767 12
            $schemaSQL,
768 7607
            'TABLE',
769 12
            $tableSQL,
770
            'COLUMN',
771
            $columnName
772
        );
773
    }
774
775
    /**
776 6071
     * Returns the SQL statement for dropping a column comment.
777
     *
778 6071
     * SQL Server does not support native column comments,
779 30
     * therefore the extended properties functionality is used
780 6071
     * as a workaround to store them.
781 6071
     * The property name used to store column comments is "MS_Description"
782 6071
     * which provides compatibility with SQL Server Management Studio,
783
     * as column comments are stored in the same property there when
784
     * specifying a column's "Description" attribute.
785
     *
786
     * @param string $tableName  The quoted table name to which the column belongs.
787
     * @param string $columnName The quoted column name to drop the comment for.
788
     */
789
    protected function getDropColumnCommentSQL(string $tableName, string $columnName) : string
790
    {
791
        if (strpos($tableName, '.') !== false) {
792
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
793
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
794
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
795
        } else {
796
            $schemaSQL = "'dbo'";
797
            $tableSQL  = $this->quoteStringLiteral($tableName);
798
        }
799
800
        return $this->getDropExtendedPropertySQL(
801
            'MS_Description',
802
            'SCHEMA',
803 7683
            $schemaSQL,
804
            'TABLE',
805
            $tableSQL,
806
            'COLUMN',
807
            $columnName
808
        );
809
    }
810
811
    /**
812
     * {@inheritdoc}
813
     */
814 7683
    protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName) : array
815 7683
    {
816 7683
        return [sprintf(
817 7683
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
818
            $tableName,
819
            $oldIndexName,
820
            $index->getQuotedName($this)
821
        ),
822
        ];
823
    }
824
825
    /**
826
     * Returns the SQL statement for adding an extended property to a database object.
827
     *
828
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
829
     *
830
     * @param string      $name       The name of the property to add.
831
     * @param string|null $value      The value of the property to add.
832
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
833
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
834
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
835 7607
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
836
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
837
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
838
     */
839
    public function getAddExtendedPropertySQL(
840
        string $name,
841
        ?string $value = null,
842
        ?string $level0Type = null,
843
        ?string $level0Name = null,
844
        ?string $level1Type = null,
845 7607
        ?string $level1Name = null,
846 7607
        ?string $level2Type = null,
847 7607
        ?string $level2Name = null
848 7607
    ) : string {
849
        return 'EXEC sp_addextendedproperty ' .
850
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
851
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
852
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
853
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
854
    }
855
856
    /**
857
     * Returns the SQL statement for dropping an extended property from a database object.
858
     *
859
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
860
     *
861
     * @param string      $name       The name of the property to drop.
862
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
863
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
864
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
865
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
866
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
867 7584
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
868
     */
869
    public function getDropExtendedPropertySQL(
870
        string $name,
871
        ?string $level0Type = null,
872
        ?string $level0Name = null,
873
        ?string $level1Type = null,
874
        ?string $level1Name = null,
875
        ?string $level2Type = null,
876
        ?string $level2Name = null
877
    ) : string {
878 7584
        return 'EXEC sp_dropextendedproperty ' .
879 7584
            'N' . $this->quoteStringLiteral($name) . ', ' .
880 7584
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
881 7584
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
882
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
883
    }
884
885
    /**
886
     * Returns the SQL statement for updating an extended property of a database object.
887 660
     *
888
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
889 660
     *
890
     * @param string      $name       The name of the property to update.
891
     * @param string|null $value      The value of the property to update.
892
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
893
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
894
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
895
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
896
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
897
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
898
     */
899
    public function getUpdateExtendedPropertySQL(
900
        string $name,
901
        ?string $value = null,
902
        ?string $level0Type = null,
903
        ?string $level0Name = null,
904
        ?string $level1Type = null,
905 7118
        ?string $level1Name = null,
906
        ?string $level2Type = null,
907
        ?string $level2Name = null
908
    ) : string {
909
        return 'EXEC sp_updateextendedproperty ' .
910
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
911
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
912
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
913
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
914
    }
915
916
    /**
917
     * {@inheritDoc}
918
     */
919
    public function getEmptyIdentityInsertSQL(string $tableName, string $identifierColumnName) : string
920
    {
921
        return 'INSERT INTO ' . $tableName . ' DEFAULT VALUES';
922
    }
923
924
    /**
925
     * {@inheritDoc}
926
     */
927
    public function getListTablesSQL() : string
928
    {
929
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
930
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
931
        return "SELECT name, SCHEMA_NAME (uid) AS schema_name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
932 7118
    }
933
934
    /**
935
     * {@inheritDoc}
936
     */
937
    public function getListTableColumnsSQL(string $table, ?string $database = null) : string
938 7072
    {
939
        return "SELECT    col.name,
940
                          type.name AS type,
941
                          col.max_length AS length,
942
                          ~col.is_nullable AS notnull,
943
                          def.definition AS [default],
944
                          col.scale,
945
                          col.precision,
946
                          col.is_identity AS autoincrement,
947
                          col.collation_name AS collation,
948
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
949
                FROM      sys.columns AS col
950
                JOIN      sys.types AS type
951
                ON        col.user_type_id = type.user_type_id
952
                JOIN      sys.objects AS obj
953
                ON        col.object_id = obj.object_id
954 7072
                JOIN      sys.schemas AS scm
955
                ON        obj.schema_id = scm.schema_id
956
                LEFT JOIN sys.default_constraints def
957
                ON        col.default_object_id = def.object_id
958
                AND       col.object_id = def.parent_object_id
959
                LEFT JOIN sys.extended_properties AS prop
960 7026
                ON        obj.object_id = prop.major_id
961
                AND       col.column_id = prop.minor_id
962
                AND       prop.name = 'MS_Description'
963
                WHERE     obj.type = 'U'
964
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
965
    }
966
967
    /**
968
     * {@inheritDoc}
969
     */
970
    public function getListTableForeignKeysSQL(string $table, ?string $database = null) : string
971
    {
972
        return 'SELECT f.name AS ForeignKey,
973
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
974
                OBJECT_NAME (f.parent_object_id) AS TableName,
975
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
976 7026
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
977
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
978
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
979
                f.delete_referential_action_desc,
980
                f.update_referential_action_desc
981
                FROM sys.foreign_keys AS f
982
                INNER JOIN sys.foreign_key_columns AS fc
983 839
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
984
                ON f.OBJECT_ID = fc.constraint_object_id
985 839
                WHERE ' .
986
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
987
    }
988
989
    /**
990
     * {@inheritDoc}
991 839
     */
992
    public function getListTableIndexesSQL(string $table, ?string $currentDatabase = null) : string
993 839
    {
994
        return "SELECT idx.name AS key_name,
995
                       col.name AS column_name,
996
                       ~idx.is_unique AS non_unique,
997
                       idx.is_primary_key AS [primary],
998
                       CASE idx.type
999
                           WHEN '1' THEN 'clustered'
1000
                           WHEN '2' THEN 'nonclustered'
1001
                           ELSE NULL
1002
                       END AS flags
1003
                FROM sys.tables AS tbl
1004
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
1005 7142
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
1006
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
1007 7142
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
1008 7018
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
1009 7018
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
1010 7018
    }
1011
1012 7124
    /**
1013 7124
     * {@inheritDoc}
1014
     */
1015
    public function getCreateViewSQL(string $name, string $sql) : string
1016 7142
    {
1017
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
1018
    }
1019
1020
    /**
1021
     * {@inheritDoc}
1022 839
     */
1023
    public function getListViewsSQL(string $database) : string
1024 839
    {
1025
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
1026
    }
1027
1028
    /**
1029
     * Returns the where clause to filter schema and table name in a query.
1030
     *
1031
     * @param string $table        The full qualified name of the table.
1032
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
1033
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
1034
     */
1035
    private function getTableWhereClause(string $table, string $schemaColumn, string $tableColumn) : string
1036
    {
1037
        if (strpos($table, '.') !== false) {
1038
            [$schema, $table] = explode('.', $table);
1039
            $schema           = $this->quoteStringLiteral($schema);
1040 959
            $table            = $this->quoteStringLiteral($table);
1041
        } else {
1042 959
            $schema = 'SCHEMA_NAME()';
1043 959
            $table  = $this->quoteStringLiteral($table);
1044
        }
1045
1046 959
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1047
    }
1048
1049
    /**
1050
     * {@inheritDoc}
1051
     */
1052
    public function getDropViewSQL(string $name) : string
1053
    {
1054
        return 'DROP VIEW ' . $name;
1055
    }
1056
1057
    /**
1058
     * {@inheritDoc}
1059
     */
1060 993
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
1061
    {
1062 993
        if ($start === null) {
1063 993
            return sprintf('CHARINDEX(%s, %s)', $substring, $string);
1064
        }
1065 991
1066 991
        return sprintf('CHARINDEX(%s, %s, %s)', $substring, $string, $start);
1067
    }
1068
1069 989
    /**
1070 989
     * {@inheritDoc}
1071
     */
1072
    public function getModExpression(string $dividend, string $divisor) : string
1073 993
    {
1074
        return $dividend . ' % ' . $divisor;
1075
    }
1076 991
1077
    /**
1078
     * {@inheritDoc}
1079
     */
1080
    public function getTrimExpression(string $str, int $mode = TrimMode::UNSPECIFIED, ?string $char = null) : string
1081
    {
1082
        if (! in_array($mode, [TrimMode::UNSPECIFIED, TrimMode::LEADING, TrimMode::TRAILING, TrimMode::BOTH], true)) {
1083
            throw new InvalidArgumentException(
1084
                sprintf('The value of $mode is expected to be one of the TrimMode constants, %d given', $mode)
1085
            );
1086
        }
1087
1088 985
        if ($char === null) {
1089
            switch ($mode) {
1090 985
                case TrimMode::LEADING:
1091 983
                    return 'LTRIM(' . $str . ')';
1092
1093
                case TrimMode::TRAILING:
1094 985
                    return 'RTRIM(' . $str . ')';
1095 981
1096
                default:
1097
                    return 'LTRIM(RTRIM(' . $str . '))';
1098 985
            }
1099
        }
1100
1101
        /** Original query used to get those expressions
1102
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1103
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1104 8156
          select @c as string
1105
          , @trim_char as trim_char
1106 8156
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1107
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1108 8156
          , 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;
1109
         */
1110
        $pattern = "'%[^' + " . $char . " + ']%'";
1111
1112
        if ($mode === TrimMode::LEADING) {
1113
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1114 8333
        }
1115
1116 8333
        if ($mode === TrimMode::TRAILING) {
1117
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1118
        }
1119
1120
        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))';
1121
    }
1122 867
1123
    /**
1124 867
     * {@inheritDoc}
1125
     */
1126
    public function getConcatExpression(string ...$string) : string
1127
    {
1128
        return '(' . implode(' + ', $string) . ')';
1129
    }
1130
1131
    /**
1132
     * {@inheritDoc}
1133
     */
1134
    public function getListDatabasesSQL() : string
1135
    {
1136
        return 'SELECT * FROM sys.databases';
1137
    }
1138
1139
    /**
1140
     * {@inheritDoc}
1141
     */
1142
    public function getListNamespacesSQL() : string
1143
    {
1144
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1145
    }
1146
1147
    /**
1148
     * {@inheritDoc}
1149
     */
1150 8131
    public function getSubstringExpression(string $string, string $start, ?string $length = null) : string
1151
    {
1152 8131
        if ($length === null) {
1153
            return sprintf('SUBSTRING(%s, %s, LEN(%s) - %s + 1)', $string, $start, $string, $start);
1154
        }
1155
1156
        return sprintf('SUBSTRING(%s, %s, %s)', $string, $start, $length);
1157
    }
1158 8624
1159
    /**
1160 8624
     * {@inheritDoc}
1161
     */
1162
    public function getLengthExpression(string $string) : string
1163
    {
1164
        return 'LEN(' . $string . ')';
1165
    }
1166 721
1167
    /**
1168 721
     * {@inheritDoc}
1169
     */
1170
    public function getCurrentDatabaseExpression() : string
1171
    {
1172
        return 'DB_NAME()';
1173
    }
1174 823
1175
    /**
1176 823
     * {@inheritDoc}
1177
     */
1178
    public function getSetTransactionIsolationSQL(int $level) : string
1179
    {
1180
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1181
    }
1182 6806
1183
    /**
1184 6806
     * {@inheritDoc}
1185
     */
1186
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
1187
    {
1188
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1189
    }
1190 8547
1191
    /**
1192 8547
     * {@inheritDoc}
1193
     */
1194
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
1195
    {
1196
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1197
    }
1198 7422
1199
    /**
1200 7422
     * {@inheritDoc}
1201
     */
1202
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
1203
    {
1204
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
1205
    }
1206 7434
1207
    /**
1208 7434
     * {@inheritDoc}
1209
     */
1210
    public function getGuidTypeDeclarationSQL(array $column) : string
1211
    {
1212
        return 'UNIQUEIDENTIFIER';
1213
    }
1214 2760
1215
    /**
1216 2760
     * {@inheritDoc}
1217
     */
1218
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) : string
1219
    {
1220
        return 'DATETIMEOFFSET(6)';
1221
    }
1222 8624
1223
    /**
1224 8624
     * {@inheritDoc}
1225
     */
1226
    protected function getCharTypeDeclarationSQLSnippet(?int $length) : string
1227
    {
1228
        $sql = 'NCHAR';
1229
1230
        if ($length !== null) {
1231
            $sql .= sprintf('(%d)', $length);
1232
        }
1233
1234
        return $sql;
1235
    }
1236
1237
    /**
1238
     * {@inheritDoc}
1239
     */
1240
    protected function getVarcharTypeDeclarationSQLSnippet(?int $length) : string
1241
    {
1242
        if ($length === null) {
1243
            throw ColumnLengthRequired::new($this, 'NVARCHAR');
1244
        }
1245
1246
        return sprintf('NVARCHAR(%d)', $length);
1247
    }
1248
1249
    /**
1250
     * {@inheritDoc}
1251
     */
1252
    public function getClobTypeDeclarationSQL(array $field) : string
1253
    {
1254 6443
        return 'VARCHAR(MAX)';
1255
    }
1256 6443
1257
    /**
1258
     * {@inheritDoc}
1259
     */
1260
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : string
1261
    {
1262 8084
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1263
    }
1264 8084
1265
    /**
1266 8084
     * {@inheritDoc}
1267 7958
     */
1268
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
1269
    {
1270 8084
        // 3 - microseconds precision length
1271 8080
        // http://msdn.microsoft.com/en-us/library/ms187819.aspx
1272 8080
        return 'DATETIME2(6)';
1273
    }
1274 5354
1275
    /**
1276
     * {@inheritDoc}
1277 8084
     */
1278 5354
    public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string
1279
    {
1280
        return 'DATE';
1281
    }
1282
1283
    /**
1284
     * {@inheritDoc}
1285 8080
     */
1286
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string
1287
    {
1288
        return 'TIME(0)';
1289 8080
    }
1290
1291 8080
    /**
1292
     * {@inheritDoc}
1293
     */
1294 8002
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
1295
    {
1296
        return 'BIT';
1297
    }
1298 8080
1299
    /**
1300
     * {@inheritDoc}
1301
     */
1302
    protected function doModifyLimitQuery(string $query, ?int $limit, int $offset) : string
1303 80
    {
1304 8080
        if ($limit === null && $offset <= 0) {
1305 8080
            return $query;
1306
        }
1307
1308
        // Queries using OFFSET... FETCH MUST have an ORDER BY clause
1309
        // Find the position of the last instance of ORDER BY and ensure it is not within a parenthetical statement
1310
        // but can be in a newline
1311
        $matches      = [];
1312
        $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE);
1313
        $orderByPos   = false;
1314
        if ($matchesCount > 0) {
1315
            $orderByPos = $matches[0][($matchesCount - 1)][1];
1316
        }
1317 8002
1318
        if ($orderByPos === false
1319 8002
            || substr_count($query, '(', $orderByPos) - substr_count($query, ')', $orderByPos)
1320 8002
        ) {
1321
            if (preg_match('/^SELECT\s+DISTINCT/im', $query)) {
1322 8002
                // SQL Server won't let us order by a non-selected column in a DISTINCT query,
1323 8002
                // so we have to do this madness. This says, order by the first column in the
1324 8002
                // result. SQL Server's docs say that a nonordered query's result order is non-
1325 2904
                // deterministic anyway, so this won't do anything that a bunch of update and
1326
                // deletes to the table wouldn't do anyway.
1327
                $query .= ' ORDER BY 1';
1328 8002
            } else {
1329 8002
                // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you
1330 8002
                // use constant expressions in the order by list.
1331
                $query .= ' ORDER BY (SELECT 0)';
1332 8002
            }
1333 8002
        }
1334 7654
1335 8002
        // This looks somewhat like MYSQL, but limit/offset are in inverse positions
1336 7828
        // Supposedly SQL:2008 core standard.
1337
        // Per TSQL spec, FETCH NEXT n ROWS ONLY is not valid without OFFSET n ROWS.
1338
        $query .= sprintf(' OFFSET %d ROWS', $offset);
1339 8002
1340
        if ($limit !== null) {
1341
            $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit);
1342 8002
        }
1343
1344
        return $query;
1345 7996
    }
1346 7996
1347
    /**
1348
     * {@inheritDoc}
1349 7820
     */
1350
    public function supportsLimitOffset() : bool
1351
    {
1352
        return true;
1353 7820
    }
1354 7820
1355
    /**
1356
     * {@inheritDoc}
1357 8002
     */
1358
    public function convertBooleans($item)
1359
    {
1360
        if (is_array($item)) {
1361
            foreach ($item as $key => $value) {
1362
                if (! is_bool($value) && ! is_numeric($value)) {
1363
                    continue;
1364
                }
1365
1366
                $item[$key] = $value ? 1 : 0;
1367
            }
1368 8002
        } elseif (is_bool($item) || is_numeric($item)) {
1369
            $item = $item ? 1 : 0;
1370
        }
1371 8002
1372 8002
        return $item;
1373
    }
1374
1375
    /**
1376 8002
     * {@inheritDoc}
1377 8002
     */
1378 7834
    public function getCreateTemporaryTableSnippetSQL() : string
1379 8002
    {
1380 7753
        return 'CREATE TABLE';
1381
    }
1382
1383
    /**
1384 8002
     * {@inheritDoc}
1385
     */
1386 8002
    public function getTemporaryTableName(string $tableName) : string
1387
    {
1388
        return '#' . $tableName;
1389 8002
    }
1390
1391
    /**
1392
     * {@inheritDoc}
1393
     */
1394
    public function getDateTimeFormatString() : string
1395 2650
    {
1396
        return 'Y-m-d H:i:s.u';
1397 2650
    }
1398
1399
    /**
1400
     * {@inheritDoc}
1401
     */
1402
    public function getDateFormatString() : string
1403 6443
    {
1404
        return 'Y-m-d';
1405 6443
    }
1406
1407
    /**
1408
     * {@inheritDoc}
1409
     */
1410
    public function getTimeFormatString() : string
1411
    {
1412
        return 'H:i:s';
1413 6443
    }
1414 6443
1415
    /**
1416
     * {@inheritDoc}
1417 6443
     */
1418
    public function getDateTimeTzFormatString() : string
1419
    {
1420
        return 'Y-m-d H:i:s.u P';
1421
    }
1422
1423 725
    /**
1424
     * {@inheritDoc}
1425 725
     */
1426
    public function getName() : string
1427
    {
1428
        return 'mssql';
1429
    }
1430
1431 725
    /**
1432
     * {@inheritDoc}
1433 725
     */
1434
    protected function initializeDoctrineTypeMappings() : void
1435
    {
1436
        $this->doctrineTypeMapping = [
1437
            'bigint'           => 'bigint',
1438
            'binary'           => 'binary',
1439
            'bit'              => 'boolean',
1440
            'char'             => 'string',
1441
            'date'             => 'date',
1442
            'datetime'         => 'datetime',
1443
            'datetime2'        => 'datetime',
1444
            'datetimeoffset'   => 'datetimetz',
1445
            'decimal'          => 'decimal',
1446
            'double'           => 'float',
1447
            'double precision' => 'float',
1448
            'float'            => 'float',
1449
            'image'            => 'blob',
1450
            'int'              => 'integer',
1451
            'money'            => 'integer',
1452
            'nchar'            => 'string',
1453
            'ntext'            => 'text',
1454
            'numeric'          => 'decimal',
1455
            'nvarchar'         => 'string',
1456
            'real'             => 'float',
1457
            'smalldatetime'    => 'datetime',
1458
            'smallint'         => 'smallint',
1459
            'smallmoney'       => 'integer',
1460
            'text'             => 'text',
1461
            'time'             => 'time',
1462
            'tinyint'          => 'smallint',
1463
            'uniqueidentifier' => 'guid',
1464
            'varbinary'        => 'binary',
1465
            'varchar'          => 'string',
1466
        ];
1467
    }
1468
1469
    /**
1470
     * {@inheritDoc}
1471 3443
     */
1472
    public function createSavePoint(string $savepoint) : string
1473 3443
    {
1474
        return 'SAVE TRANSACTION ' . $savepoint;
1475
    }
1476
1477
    /**
1478
     * {@inheritDoc}
1479 7550
     */
1480
    public function releaseSavePoint(string $savepoint) : string
1481 7550
    {
1482
        return '';
1483
    }
1484
1485
    /**
1486
     * {@inheritDoc}
1487
     */
1488
    public function rollbackSavePoint(string $savepoint) : string
1489
    {
1490
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1491
    }
1492
1493
    /**
1494
     * {@inheritdoc}
1495
     */
1496
    public function getForeignKeyReferentialActionSQL(string $action) : string
1497
    {
1498
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1499
        if (strtoupper($action) === 'RESTRICT') {
1500
            return 'NO ACTION';
1501
        }
1502
1503
        return parent::getForeignKeyReferentialActionSQL($action);
1504
    }
1505
1506
    /**
1507
     * {@inheritDoc}
1508 7550
     */
1509
    public function appendLockHint(string $fromClause, ?int $lockMode) : string
1510
    {
1511
        switch (true) {
1512
            case $lockMode === LockMode::NONE:
1513 1003
                return $fromClause . ' WITH (NOLOCK)';
1514
1515 1003
            case $lockMode === LockMode::PESSIMISTIC_READ:
1516
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1517
1518
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1519
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1520
1521
            default:
1522
                return $fromClause;
1523
        }
1524
    }
1525
1526
    /**
1527
     * {@inheritDoc}
1528
     */
1529 1003
    public function getForUpdateSQL() : string
1530
    {
1531 1003
        return ' ';
1532
    }
1533
1534
    /**
1535
     * {@inheritDoc}
1536
     */
1537 6869
    protected function getReservedKeywordsClass() : string
1538
    {
1539
        return Keywords\SQLServerKeywords::class;
1540 6869
    }
1541 6431
1542
    /**
1543
     * {@inheritDoc}
1544 6863
     */
1545
    public function quoteSingleIdentifier(string $str) : string
1546
    {
1547
        return '[' . str_replace(']', '][', $str) . ']';
1548
    }
1549
1550 3526
    /**
1551
     * {@inheritDoc}
1552 3526
     */
1553
    public function getTruncateTableSQL(string $tableName, bool $cascade = false) : string
1554 3002
    {
1555
        $tableIdentifier = new Identifier($tableName);
1556 3524
1557 2952
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1558
    }
1559 3522
1560 3422
    /**
1561
     * {@inheritDoc}
1562
     */
1563 3033
    public function getBlobTypeDeclarationSQL(array $field) : string
1564
    {
1565
        return 'VARBINARY(MAX)';
1566
    }
1567
1568
    /**
1569
     * {@inheritdoc}
1570 729
     *
1571
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1572 729
     */
1573
    public function getColumnDeclarationSQL(string $name, array $field) : string
1574
    {
1575
        if (isset($field['columnDefinition'])) {
1576
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1577
        } else {
1578 2399
            $collation = isset($field['collation']) && $field['collation'] ?
1579
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1580 2399
1581
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1582
1583
            $unique = isset($field['unique']) && $field['unique'] ?
1584
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1585
1586 8100
            $check = isset($field['check']) && $field['check'] ?
1587
                ' ' . $field['check'] : '';
1588 8100
1589
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1590
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1591
        }
1592
1593
        return $name . ' ' . $columnDef;
1594 6230
    }
1595
1596 6230
    /**
1597
     * {@inheritdoc}
1598 6230
     */
1599
    protected function getLikeWildcardCharacters() : string
1600
    {
1601
        return parent::getLikeWildcardCharacters() . '[]^';
1602
    }
1603
1604 7601
    /**
1605
     * Returns a unique default constraint name for a table and column.
1606 7601
     *
1607
     * @param string $table  Name of the table to generate the unique default constraint name for.
1608
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1609
     */
1610
    private function generateDefaultConstraintName(string $table, string $column) : string
1611
    {
1612
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1613
    }
1614 8726
1615
    /**
1616 8726
     * Returns a hash value for a given identifier.
1617 6006
     *
1618
     * @param string $identifier Identifier to generate a hash value for.
1619 8720
     */
1620 8720
    private function generateIdentifierName(string $identifier) : string
1621
    {
1622 8720
        // Always generate name for unquoted identifiers to ensure consistency.
1623
        $identifier = new Identifier($identifier);
1624 8720
1625 8720
        return strtoupper(dechex(crc32($identifier->getName())));
1626
    }
1627 8720
1628 8720
    protected function getCommentOnTableSQL(string $tableName, ?string $comment) : string
1629
    {
1630 8720
        return sprintf(
1631 8720
            <<<'SQL'
1632
EXEC sys.sp_addextendedproperty @name=N'MS_Description', 
1633
  @value=N%s, @level0type=N'SCHEMA', @level0name=N'dbo', 
1634 8726
  @level1type=N'TABLE', @level1name=N%s
1635
SQL
1636
            ,
1637
            $this->quoteStringLiteral((string) $comment),
1638
            $this->quoteStringLiteral($tableName)
1639
        );
1640
    }
1641
1642
    public function getListTableMetadataSQL(string $table) : string
1643
    {
1644
        return sprintf(
1645 7581
            <<<'SQL'
1646
SELECT
1647 7581
  p.value AS [table_comment]
1648
FROM
1649
  sys.tables AS tbl
1650
  INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1
1651
WHERE
1652
  (tbl.name=N%s and SCHEMA_NAME(tbl.schema_id)=N'dbo' and p.name=N'MS_Description')
1653
SQL
1654
            ,
1655
            $this->quoteStringLiteral($table)
1656
        );
1657 7587
    }
1658
}
1659