Failed Conditions
Push — develop ( 152bc9...e39bc0 )
by Sergei
102:42 queued 37:39
created

SQLServerPlatform::getLikeWildcardCharacters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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