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

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

999
    public function getListTableForeignKeysSQL($table, /** @scrutinizer ignore-unused */ $database = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

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