Completed
Pull Request — 2.10.x (#3936)
by Asmir
65:42
created

SQLServerPlatform::getCreateIndexSQL()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.072

Importance

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

915
    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...
916
    {
917
        return 'SELECT f.name AS ForeignKey,
918
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
919
                OBJECT_NAME (f.parent_object_id) AS TableName,
920
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
921
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
922
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
923
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
924
                f.delete_referential_action_desc,
925
                f.update_referential_action_desc
926
                FROM sys.foreign_keys AS f
927
                INNER JOIN sys.foreign_key_columns AS fc
928
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
929
                ON f.OBJECT_ID = fc.constraint_object_id
930
                WHERE ' .
931
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
932 6575
    }
933
934
    /**
935
     * {@inheritDoc}
936
     */
937
    public function getListTableIndexesSQL($table, $currentDatabase = null)
938 6533
    {
939
        return "SELECT idx.name AS key_name,
940
                       col.name AS column_name,
941
                       ~idx.is_unique AS non_unique,
942
                       idx.is_primary_key AS [primary],
943
                       CASE idx.type
944
                           WHEN '1' THEN 'clustered'
945
                           WHEN '2' THEN 'nonclustered'
946
                           ELSE NULL
947
                       END AS flags
948
                FROM sys.tables AS tbl
949
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
950
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
951
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
952
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
953
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
954 6533
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
955
    }
956
957
    /**
958
     * {@inheritDoc}
959
     */
960 6491
    public function getCreateViewSQL($name, $sql)
961
    {
962
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
963
    }
964
965
    /**
966
     * {@inheritDoc}
967
     */
968
    public function getListViewsSQL($database)
969
    {
970
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
971
    }
972
973
    /**
974
     * Returns the where clause to filter schema and table name in a query.
975
     *
976 6491
     * @param string $table        The full qualified name of the table.
977
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
978
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
979
     *
980
     * @return string
981
     */
982
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
983 836
    {
984
        if (strpos($table, '.') !== false) {
985 836
            [$schema, $table] = explode('.', $table);
986
            $schema           = $this->quoteStringLiteral($schema);
987
            $table            = $this->quoteStringLiteral($table);
988
        } else {
989
            $schema = 'SCHEMA_NAME()';
990
            $table  = $this->quoteStringLiteral($table);
991 836
        }
992
993 836
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
994
    }
995
996
    /**
997
     * {@inheritDoc}
998
     */
999
    public function getDropViewSQL($name)
1000
    {
1001
        return 'DROP VIEW ' . $name;
1002
    }
1003
1004
    /**
1005 6599
     * {@inheritDoc}
1006
     *
1007 6599
     * @deprecated Use application-generated UUIDs instead
1008 6477
     */
1009 6477
    public function getGuidExpression()
1010 6477
    {
1011
        return 'NEWID()';
1012 6581
    }
1013 6581
1014
    /**
1015
     * {@inheritDoc}
1016 6599
     */
1017
    public function getLocateExpression($str, $substr, $startPos = false)
1018
    {
1019
        if ($startPos === false) {
1020
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1021
        }
1022 836
1023
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1024 836
    }
1025
1026
    /**
1027
     * {@inheritDoc}
1028
     */
1029
    public function getModExpression($expression1, $expression2)
1030
    {
1031
        return $expression1 . ' % ' . $expression2;
1032
    }
1033
1034
    /**
1035
     * {@inheritDoc}
1036
     */
1037
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1038
    {
1039
        if (! $char) {
1040 955
            switch ($pos) {
1041
                case TrimMode::LEADING:
1042 955
                    $trimFn = 'LTRIM';
1043 955
                    break;
1044
1045
                case TrimMode::TRAILING:
1046 955
                    $trimFn = 'RTRIM';
1047
                    break;
1048
1049
                default:
1050
                    return 'LTRIM(RTRIM(' . $str . '))';
1051
            }
1052
1053
            return $trimFn . '(' . $str . ')';
1054
        }
1055
1056
        /** Original query used to get those expressions
1057
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1058
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1059
          select @c as string
1060 989
          , @trim_char as trim_char
1061
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1062 989
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1063 989
          , 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;
1064
         */
1065 987
        $pattern = "'%[^' + " . $char . " + ']%'";
1066 987
1067
        if ($pos === TrimMode::LEADING) {
1068
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1069 985
        }
1070 985
1071
        if ($pos === TrimMode::TRAILING) {
1072
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1073 989
        }
1074
1075
        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))';
1076 987
    }
1077
1078
    /**
1079
     * {@inheritDoc}
1080
     */
1081
    public function getConcatExpression()
1082
    {
1083
        $args = func_get_args();
1084
1085
        return '(' . implode(' + ', $args) . ')';
1086
    }
1087
1088 981
    /**
1089
     * {@inheritDoc}
1090 981
     */
1091 979
    public function getListDatabasesSQL()
1092
    {
1093
        return 'SELECT * FROM sys.databases';
1094 981
    }
1095 977
1096
    /**
1097
     * {@inheritDoc}
1098 981
     */
1099
    public function getListNamespacesSQL()
1100
    {
1101
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1102
    }
1103
1104 7504
    /**
1105
     * {@inheritDoc}
1106 7504
     */
1107
    public function getSubstringExpression($value, $from, $length = null)
1108 7504
    {
1109
        if ($length !== null) {
1110
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1111
        }
1112
1113
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1114 7682
    }
1115
1116 7682
    /**
1117
     * {@inheritDoc}
1118
     */
1119
    public function getLengthExpression($column)
1120
    {
1121
        return 'LEN(' . $column . ')';
1122 864
    }
1123
1124 864
    /**
1125
     * {@inheritDoc}
1126
     */
1127
    public function getSetTransactionIsolationSQL($level)
1128
    {
1129
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1130
    }
1131
1132
    /**
1133
     * {@inheritDoc}
1134
     */
1135
    public function getIntegerTypeDeclarationSQL(array $field)
1136
    {
1137
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1138
    }
1139
1140
    /**
1141
     * {@inheritDoc}
1142
     */
1143
    public function getBigIntTypeDeclarationSQL(array $field)
1144
    {
1145
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1146
    }
1147
1148
    /**
1149
     * {@inheritDoc}
1150 7481
     */
1151
    public function getSmallIntTypeDeclarationSQL(array $field)
1152 7481
    {
1153
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1154
    }
1155
1156
    /**
1157
     * {@inheritDoc}
1158 7966
     */
1159
    public function getGuidTypeDeclarationSQL(array $field)
1160 7966
    {
1161
        return 'UNIQUEIDENTIFIER';
1162
    }
1163
1164
    /**
1165
     * {@inheritDoc}
1166 722
     */
1167
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1168 722
    {
1169
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1170
    }
1171
1172
    /**
1173
     * {@inheritdoc}
1174 820
     */
1175
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1176 820
    {
1177
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1178
    }
1179
1180
    /**
1181
     * {@inheritdoc}
1182 6262
     */
1183
    public function getBinaryMaxLength()
1184 6262
    {
1185
        return 8000;
1186
    }
1187
1188
    /**
1189
     * {@inheritDoc}
1190 7899
     */
1191
    public function getClobTypeDeclarationSQL(array $field)
1192 7899
    {
1193
        return 'VARCHAR(MAX)';
1194
    }
1195
1196
    /**
1197
     * {@inheritDoc}
1198 6845
     */
1199
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1200 6845
    {
1201
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1202
    }
1203
1204
    /**
1205
     * {@inheritDoc}
1206 6857
     */
1207
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1208 6857
    {
1209
        return 'DATETIME';
1210
    }
1211
1212
    /**
1213
     * {@inheritDoc}
1214 2540
     */
1215
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1216 2540
    {
1217
        return 'DATETIME';
1218
    }
1219
1220
    /**
1221
     * {@inheritDoc}
1222 7966
     */
1223
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1224 7966
    {
1225
        return 'DATETIME';
1226
    }
1227
1228
    /**
1229
     * {@inheritDoc}
1230
     */
1231
    public function getBooleanTypeDeclarationSQL(array $field)
1232
    {
1233
        return 'BIT';
1234
    }
1235
1236
    /**
1237
     * {@inheritDoc}
1238
     */
1239
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1240
    {
1241
        $where = [];
1242
1243
        if ($offset > 0) {
1244
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1245
        }
1246
1247
        if ($limit !== null) {
1248
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1249
            $top     = sprintf('TOP %d', $offset + $limit);
1250
        } else {
1251
            $top = 'TOP 9223372036854775807';
1252
        }
1253
1254 5958
        if (empty($where)) {
1255
            return $query;
1256 5958
        }
1257
1258
        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
1259
        // Even if the TOP n is very large, the use of a CTE will
1260
        // allow the SQL Server query planner to optimize it so it doesn't
1261
        // actually scan the entire range covered by the TOP clause.
1262 7444
        if (! preg_match('/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/is', $query, $matches)) {
1263
            return $query;
1264 7444
        }
1265
1266 7444
        $query = $matches[1] . $top . ' ' . $matches[2];
1267 7322
1268
        if (stristr($query, 'ORDER BY')) {
1269
            // Inner order by is not valid in SQL Server for our purposes
1270 7444
            // unless it's in a TOP N subquery.
1271 7440
            $query = $this->scrubInnerOrderBy($query);
1272 7440
        }
1273
1274 4926
        // Build a new limited query around the original, using a CTE
1275
        return sprintf(
1276
            'WITH dctrn_cte AS (%s) '
1277 7444
            . 'SELECT * FROM ('
1278 4926
            . 'SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte'
1279
            . ') AS doctrine_tbl '
1280
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1281
            $query,
1282
            implode(' AND ', $where)
1283
        );
1284
    }
1285 7440
1286
    /**
1287
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
1288
     * Caveat: will leave ORDER BY in TOP N subqueries.
1289 7440
     *
1290
     * @param string $query
1291 7440
     *
1292
     * @return string
1293
     */
1294 7366
    private function scrubInnerOrderBy($query)
1295
    {
1296
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1297
        $offset = 0;
1298 7440
1299
        while ($count-- > 0) {
1300
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1301
            if ($orderByPos === false) {
1302
                break;
1303 80
            }
1304 7440
1305 7440
            $qLen            = strlen($query);
1306
            $parenCount      = 0;
1307
            $currentPosition = $orderByPos;
1308
1309
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1310
                if ($query[$currentPosition] === '(') {
1311
                    $parenCount++;
1312
                } elseif ($query[$currentPosition] === ')') {
1313
                    $parenCount--;
1314
                }
1315
1316
                $currentPosition++;
1317 7366
            }
1318
1319 7366
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1320 7366
                // If the order by clause is in a TOP N subquery, do not remove
1321
                // it and continue iteration from the current position.
1322 7366
                $offset = $currentPosition;
1323 7366
                continue;
1324 7366
            }
1325 2672
1326
            if ($currentPosition >= $qLen - 1) {
1327
                continue;
1328 7366
            }
1329 7366
1330 7366
            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1331
            $offset = $orderByPos;
1332 7366
        }
1333 7366
1334 7042
        return $query;
1335 7366
    }
1336 7204
1337
    /**
1338
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
1339 7366
     *
1340
     * @param string $query           The query
1341
     * @param int    $currentPosition Start position of ORDER BY clause
1342 7366
     *
1343
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
1344
     */
1345 7360
    private function isOrderByInTopNSubquery($query, $currentPosition)
1346 7360
    {
1347
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1348
        $subQueryBuffer = '';
1349 7196
        $parenCount     = 0;
1350
1351
        // If $parenCount goes negative, we've exited the subquery we're examining.
1352
        // If $currentPosition goes negative, we've reached the beginning of the query.
1353 7196
        while ($parenCount >= 0 && $currentPosition >= 0) {
1354 7196
            if ($query[$currentPosition] === '(') {
1355
                $parenCount--;
1356
            } elseif ($query[$currentPosition] === ')') {
1357 7366
                $parenCount++;
1358
            }
1359
1360
            // Only yank query text on the same nesting level as the ORDER BY clause.
1361
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1362
1363
            $currentPosition--;
1364
        }
1365
1366
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1367
    }
1368 7366
1369
    /**
1370
     * {@inheritDoc}
1371 7366
     */
1372 7366
    public function supportsLimitOffset()
1373
    {
1374
        return false;
1375
    }
1376 7366
1377 7366
    /**
1378 7210
     * {@inheritDoc}
1379 7366
     */
1380 7135
    public function convertBooleans($item)
1381
    {
1382
        if (is_array($item)) {
1383
            foreach ($item as $key => $value) {
1384 7366
                if (! is_bool($value) && ! is_numeric($value)) {
1385
                    continue;
1386 7366
                }
1387
1388
                $item[$key] = $value ? 1 : 0;
1389 7366
            }
1390
        } elseif (is_bool($item) || is_numeric($item)) {
1391
            $item = $item ? 1 : 0;
1392
        }
1393
1394
        return $item;
1395 2438
    }
1396
1397 2438
    /**
1398
     * {@inheritDoc}
1399
     */
1400
    public function getCreateTemporaryTableSnippetSQL()
1401
    {
1402
        return 'CREATE TABLE';
1403 5958
    }
1404
1405 5958
    /**
1406
     * {@inheritDoc}
1407
     */
1408
    public function getTemporaryTableName($tableName)
1409
    {
1410
        return '#' . $tableName;
1411
    }
1412
1413 5958
    /**
1414 5958
     * {@inheritDoc}
1415
     */
1416
    public function getDateTimeFormatString()
1417 5958
    {
1418
        return 'Y-m-d H:i:s.000';
1419
    }
1420
1421
    /**
1422
     * {@inheritDoc}
1423 726
     */
1424
    public function getDateFormatString()
1425 726
    {
1426
        return 'Y-m-d H:i:s.000';
1427
    }
1428
1429
    /**
1430
     * {@inheritDoc}
1431 726
     */
1432
    public function getTimeFormatString()
1433 726
    {
1434
        return 'Y-m-d H:i:s.000';
1435
    }
1436
1437
    /**
1438
     * {@inheritDoc}
1439
     */
1440
    public function getDateTimeTzFormatString()
1441
    {
1442
        return $this->getDateTimeFormatString();
1443
    }
1444
1445
    /**
1446
     * {@inheritDoc}
1447
     */
1448
    public function getName()
1449
    {
1450
        return 'mssql';
1451
    }
1452
1453
    /**
1454
     * {@inheritDoc}
1455
     */
1456
    protected function initializeDoctrineTypeMappings()
1457
    {
1458
        $this->doctrineTypeMapping = [
1459
            'bigint' => 'bigint',
1460
            'numeric' => 'decimal',
1461
            'bit' => 'boolean',
1462
            'smallint' => 'smallint',
1463
            'decimal' => 'decimal',
1464
            'smallmoney' => 'integer',
1465
            'int' => 'integer',
1466
            'tinyint' => 'smallint',
1467
            'money' => 'integer',
1468
            'float' => 'float',
1469
            'real' => 'float',
1470
            'double' => 'float',
1471 3227
            'double precision' => 'float',
1472
            'smalldatetime' => 'datetime',
1473 3227
            'datetime' => 'datetime',
1474
            'char' => 'string',
1475
            'varchar' => 'string',
1476
            'text' => 'text',
1477
            'nchar' => 'string',
1478
            'nvarchar' => 'string',
1479 6971
            'ntext' => 'text',
1480
            'binary' => 'binary',
1481 6971
            'varbinary' => 'binary',
1482
            'image' => 'blob',
1483
            'uniqueidentifier' => 'guid',
1484
        ];
1485
    }
1486
1487
    /**
1488
     * {@inheritDoc}
1489
     */
1490
    public function createSavePoint($savepoint)
1491
    {
1492
        return 'SAVE TRANSACTION ' . $savepoint;
1493
    }
1494
1495
    /**
1496
     * {@inheritDoc}
1497
     */
1498
    public function releaseSavePoint($savepoint)
1499
    {
1500
        return '';
1501
    }
1502
1503
    /**
1504
     * {@inheritDoc}
1505
     */
1506
    public function rollbackSavePoint($savepoint)
1507
    {
1508 6971
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1509
    }
1510
1511
    /**
1512
     * {@inheritdoc}
1513 999
     */
1514
    public function getForeignKeyReferentialActionSQL($action)
1515 999
    {
1516
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1517
        if (strtoupper($action) === 'RESTRICT') {
1518
            return 'NO ACTION';
1519
        }
1520
1521
        return parent::getForeignKeyReferentialActionSQL($action);
1522
    }
1523
1524
    /**
1525
     * {@inheritDoc}
1526
     */
1527
    public function appendLockHint($fromClause, $lockMode)
1528
    {
1529 999
        switch (true) {
1530
            case $lockMode === LockMode::NONE:
1531 999
                return $fromClause . ' WITH (NOLOCK)';
1532
1533
            case $lockMode === LockMode::PESSIMISTIC_READ:
1534
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1535
1536
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1537 6346
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1538
1539
            default:
1540 6346
                return $fromClause;
1541 5917
        }
1542
    }
1543
1544 6340
    /**
1545
     * {@inheritDoc}
1546
     */
1547
    public function getForUpdateSQL()
1548
    {
1549
        return ' ';
1550 3285
    }
1551
1552 3285
    /**
1553
     * {@inheritDoc}
1554 2762
     */
1555
    protected function getReservedKeywordsClass()
1556 3283
    {
1557 2716
        return Keywords\SQLServerKeywords::class;
1558
    }
1559 3281
1560 3189
    /**
1561
     * {@inheritDoc}
1562
     */
1563 2791
    public function quoteSingleIdentifier($str)
1564
    {
1565
        return '[' . str_replace(']', '][', $str) . ']';
1566
    }
1567
1568
    /**
1569
     * {@inheritDoc}
1570 730
     */
1571
    public function getTruncateTableSQL($tableName, $cascade = false)
1572 730
    {
1573
        $tableIdentifier = new Identifier($tableName);
1574
1575
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1576
    }
1577
1578 2217
    /**
1579
     * {@inheritDoc}
1580 2217
     */
1581
    public function getBlobTypeDeclarationSQL(array $field)
1582
    {
1583
        return 'VARBINARY(MAX)';
1584
    }
1585
1586 7494
    /**
1587
     * {@inheritdoc}
1588 7494
     *
1589
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1590
     */
1591
    public function getColumnDeclarationSQL($name, array $field)
1592
    {
1593
        if (isset($field['columnDefinition'])) {
1594 5768
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1595
        } else {
1596 5768
            $collation = isset($field['collation']) && $field['collation'] ?
1597
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1598 5768
1599
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1600
1601
            $unique = isset($field['unique']) && $field['unique'] ?
1602
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1603
1604 7025
            $check = isset($field['check']) && $field['check'] ?
1605
                ' ' . $field['check'] : '';
1606 7025
1607
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1608
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1609
        }
1610
1611
        return $name . ' ' . $columnDef;
1612
    }
1613
1614 8068
    /**
1615
     * Returns a unique default constraint name for a table and column.
1616 8068
     *
1617 5526
     * @param string $table  Name of the table to generate the unique default constraint name for.
1618
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1619 8062
     *
1620 8062
     * @return string
1621
     */
1622 8062
    private function generateDefaultConstraintName($table, $column)
1623
    {
1624 8062
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1625 8062
    }
1626
1627 8062
    /**
1628 8062
     * Returns a hash value for a given identifier.
1629
     *
1630 8062
     * @param string $identifier Identifier to generate a hash value for.
1631 8062
     *
1632
     * @return string
1633
     */
1634 8068
    private function generateIdentifierName($identifier)
1635
    {
1636
        // Always generate name for unquoted identifiers to ensure consistency.
1637
        $identifier = new Identifier($identifier);
1638
1639
        return strtoupper(dechex(crc32($identifier->getName())));
1640
    }
1641
1642
    protected function getCommentOnTableSQL(string $tableName, ?string $comment) : string
1643
    {
1644
        return sprintf(
1645 7007
            <<<'SQL'
1646
EXEC sys.sp_addextendedproperty @name=N'MS_Description', 
1647 7007
  @value=N%s, @level0type=N'SCHEMA', @level0name=N'dbo', 
1648
  @level1type=N'TABLE', @level1name=N%s
1649
SQL
1650
            ,
1651
            $this->quoteStringLiteral((string) $comment),
1652
            $this->quoteStringLiteral($tableName)
1653
        );
1654
    }
1655
1656
    public function getListTableMetadataSQL(string $table) : string
1657 7013
    {
1658
        return sprintf(
1659
            <<<'SQL'
1660 7013
SELECT
1661
  p.value AS [table_comment]
1662 7013
FROM
1663
  sys.tables AS tbl
1664
  INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1
1665 772
WHERE
1666
  (tbl.name=N%s and SCHEMA_NAME(tbl.schema_id)=N'dbo' and p.name=N'MS_Description')
1667 772
SQL
1668
            ,
1669
            $this->quoteStringLiteral($table)
1670
        );
1671
    }
1672
}
1673