Failed Conditions
Push — 3.0.x ( 66f057...14f5f1 )
by Sergei
36s queued 17s
created

SQLServer2012Platform::getDefaultSchemaName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Doctrine\DBAL\Platforms;
4
5
use Doctrine\DBAL\LockMode;
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\ColumnDiff;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Doctrine\DBAL\Schema\Identifier;
10
use Doctrine\DBAL\Schema\Index;
11
use Doctrine\DBAL\Schema\Sequence;
12
use Doctrine\DBAL\Schema\Table;
13
use Doctrine\DBAL\Schema\TableDiff;
14
use InvalidArgumentException;
15
use const PREG_OFFSET_CAPTURE;
16
use function array_merge;
17
use function array_unique;
18
use function array_values;
19
use function count;
20
use function crc32;
21
use function dechex;
22
use function explode;
23
use function func_get_args;
24
use function implode;
25
use function is_array;
26
use function is_bool;
27
use function is_numeric;
28
use function is_string;
29
use function preg_match;
30
use function preg_match_all;
31
use function sprintf;
32
use function str_replace;
33
use function strpos;
34
use function strtoupper;
35
use function substr_count;
36
37
/**
38
 * Provides the behavior, features and SQL dialect of the Microsoft SQL Server 2012 database platform.
39
 */
40
class SQLServer2012Platform extends AbstractPlatform
41
{
42
    /**
43
     * {@inheritdoc}
44
     */
45
    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 (! empty($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
     * @param string      $table
998
     * @param string|null $database
999
     *
1000
     * @return string
1001
     */
1002
    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

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