Failed Conditions
Push — master ( 01143c...7811e4 )
by Sergei
21s queued 14s
created

SQLServer2012Platform::getColumnDeclarationSQL()   C

Complexity

Conditions 10
Paths 257

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 21
rs 6.1208
c 0
b 0
f 0
cc 10
nc 257
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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