Failed Conditions
Pull Request — master (#3050)
by Michael
16:39
created

SQLServerPlatform::doModifyLimitQuery()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 42
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5.0291

Importance

Changes 0
Metric Value
dl 0
loc 42
rs 8.439
c 0
b 0
f 0
ccs 17
cts 19
cp 0.8947
cc 5
eloc 23
nc 12
nop 3
crap 5.0291
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL\Platforms;
21
22
use Doctrine\DBAL\LockMode;
23
use Doctrine\DBAL\Schema\Column;
24
use Doctrine\DBAL\Schema\ColumnDiff;
25
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
26
use Doctrine\DBAL\Schema\Identifier;
27
use Doctrine\DBAL\Schema\Index;
28
use Doctrine\DBAL\Schema\Table;
29
use Doctrine\DBAL\Schema\TableDiff;
30
use Doctrine\DBAL\Types;
31
use function implode;
32
use function sprintf;
33
34
/**
35
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
36
 * Microsoft SQL Server database platform.
37
 *
38
 * @since 2.0
39
 * @author Roman Borschel <[email protected]>
40
 * @author Jonathan H. Wage <[email protected]>
41
 * @author Benjamin Eberlei <[email protected]>
42
 * @author Steve Müller <[email protected]>
43
 */
44
class SQLServerPlatform extends AbstractPlatform
45
{
46
    /**
47
     * {@inheritdoc}
48
     */
49 6
    public function getCurrentDateSQL()
50
    {
51 6
        return $this->getConvertExpression('date', 'GETDATE()');
52
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57 3
    public function getCurrentTimeSQL()
58
    {
59 3
        return $this->getConvertExpression('time', 'GETDATE()');
60
    }
61
62
    /**
63
     * Returns an expression that converts an expression of one data type to another.
64
     *
65
     * @param string $dataType   The target native data type. Alias data types cannot be used.
66
     * @param string $expression The SQL expression to convert.
67
     *
68
     * @return string
69
     */
70 6
    private function getConvertExpression($dataType, $expression)
71
    {
72 6
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
79
    {
80
        $factorClause = '';
81
82
        if ('-' === $operator) {
83
            $factorClause = '-1 * ';
84
        }
85
86
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
87
    }
88
89
    /**
90
     * {@inheritDoc}
91
     */
92
    public function getDateDiffExpression($date1, $date2)
93
    {
94
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
95
    }
96
97
    /**
98
     * {@inheritDoc}
99
     *
100
     * Microsoft SQL Server prefers "autoincrement" identity columns
101
     * since sequences can only be emulated with a table.
102
     */
103 3
    public function prefersIdentityColumns()
104
    {
105 3
        return true;
106
    }
107
108
    /**
109
     * {@inheritDoc}
110
     *
111
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
112
     */
113 3
    public function supportsIdentityColumns()
114
    {
115 3
        return true;
116
    }
117
118
    /**
119
     * {@inheritDoc}
120
     */
121
    public function supportsReleaseSavepoints()
122
    {
123
        return false;
124
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129 3
    public function supportsSchemas()
130
    {
131 3
        return true;
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137
    public function getDefaultSchemaName()
138
    {
139
        return 'dbo';
140
    }
141
142
    /**
143
     * {@inheritDoc}
144
     */
145
    public function supportsColumnCollation()
146
    {
147
        return true;
148
    }
149
150
    /**
151
     * {@inheritDoc}
152
     */
153 160
    public function hasNativeGuidType()
154
    {
155 160
        return true;
156
    }
157
158
    /**
159
     * {@inheritDoc}
160
     */
161 3
    public function getCreateDatabaseSQL($name)
162
    {
163 3
        return 'CREATE DATABASE ' . $name;
164
    }
165
166
    /**
167
     * {@inheritDoc}
168
     */
169 3
    public function getDropDatabaseSQL($name)
170
    {
171 3
        return 'DROP DATABASE ' . $name;
172
    }
173
174
    /**
175
     * {@inheritDoc}
176
     */
177 3
    public function supportsCreateDropDatabase()
178
    {
179 3
        return true;
180
    }
181
182
    /**
183
     * {@inheritDoc}
184
     */
185 3
    public function getCreateSchemaSQL($schemaName)
186
    {
187 3
        return 'CREATE SCHEMA ' . $schemaName;
188
    }
189
190
    /**
191
     * {@inheritDoc}
192
     */
193 6
    public function getDropForeignKeySQL($foreignKey, $table)
194
    {
195 6
        if (! $foreignKey instanceof ForeignKeyConstraint) {
196 3
            $foreignKey = new Identifier($foreignKey);
197
        }
198
199 6
        if (! $table instanceof Table) {
200 6
            $table = new Identifier($table);
201
        }
202
203 6
        $foreignKey = $foreignKey->getQuotedName($this);
204 6
        $table = $table->getQuotedName($this);
205
206 6
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
207
    }
208
209
    /**
210
     * {@inheritDoc}
211
     */
212
    public function getDropIndexSQL($index, $table = null)
213
    {
214
        if ($index instanceof Index) {
215
            $index = $index->getQuotedName($this);
216
        } elseif (!is_string($index)) {
217
            throw new \InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
218
        }
219
220
        if (!isset($table)) {
221
            return 'DROP INDEX ' . $index;
222
        }
223
224
        if ($table instanceof Table) {
225
            $table = $table->getQuotedName($this);
226
        }
227
228
        return "IF EXISTS (SELECT * FROM sysobjects WHERE name = '$index')
229
                    ALTER TABLE " . $table . " DROP CONSTRAINT " . $index . "
230
                ELSE
231
                    DROP INDEX " . $index . " ON " . $table;
232
    }
233
234
    /**
235
     * {@inheritDoc}
236
     */
237 46
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
238
    {
239 46
        $defaultConstraintsSql = [];
240 46
        $commentsSql           = [];
241
242
        // @todo does other code breaks because of this?
243
        // force primary keys to be not null
244 46
        foreach ($columns as &$column) {
245 46
            if (isset($column['primary']) && $column['primary']) {
246 15
                $column['notnull'] = true;
247
            }
248
249
            // Build default constraints SQL statements.
250 46
            if (isset($column['default'])) {
251 12
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
252 12
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
253
            }
254
255 46
            if ( ! empty($column['comment']) || is_numeric($column['comment'])) {
256 46
                $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
257
            }
258
        }
259
260 46
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
261
262 46
        if (isset($options['uniqueConstraints']) && !empty($options['uniqueConstraints'])) {
263
            foreach ($options['uniqueConstraints'] as $name => $definition) {
264
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
265
            }
266
        }
267
268 46
        if (isset($options['primary']) && !empty($options['primary'])) {
269 18
            $flags = '';
270 18
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
271 3
                $flags = ' NONCLUSTERED';
272
            }
273 18
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
274
        }
275
276 46
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
277
278 46
        $check = $this->getCheckDeclarationSQL($columns);
279 46
        if (!empty($check)) {
280
            $query .= ', ' . $check;
281
        }
282 46
        $query .= ')';
283
284 46
        $sql[] = $query;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sql was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sql = array(); before regardless.
Loading history...
285
286 46
        if (isset($options['indexes']) && !empty($options['indexes'])) {
287 9
            foreach ($options['indexes'] as $index) {
288 9
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
289
            }
290
        }
291
292 46
        if (isset($options['foreignKeys'])) {
293 3
            foreach ((array) $options['foreignKeys'] as $definition) {
294 3
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
295
            }
296
        }
297
298 46
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
299
    }
300
301
    /**
302
     * {@inheritDoc}
303
     */
304 6
    public function getCreatePrimaryKeySQL(Index $index, $table)
305
    {
306 6
        $flags = '';
307 6
        if ($index->hasFlag('nonclustered')) {
308 3
            $flags = ' NONCLUSTERED';
309
        }
310
311 6
        return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY' . $flags . ' (' . $this->getIndexFieldDeclarationListSQL($index->getQuotedColumns($this)) . ')';
312
    }
313
314
    /**
315
     * Returns the SQL statement for creating a column comment.
316
     *
317
     * SQL Server does not support native column comments,
318
     * therefore the extended properties functionality is used
319
     * as a workaround to store them.
320
     * The property name used to store column comments is "MS_Description"
321
     * which provides compatibility with SQL Server Management Studio,
322
     * as column comments are stored in the same property there when
323
     * specifying a column's "Description" attribute.
324
     *
325
     * @param string $tableName  The quoted table name to which the column belongs.
326
     * @param string $columnName The quoted column name to create the comment for.
327
     * @param string $comment    The column's comment.
328
     *
329
     * @return string
330
     */
331 15
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
332
    {
333 15
        return $this->getAddExtendedPropertySQL(
334 15
            'MS_Description',
335 15
            $comment,
336 15
            'SCHEMA',
337 15
            'dbo',
338 15
            'TABLE',
339 15
            $tableName,
340 15
            'COLUMN',
341 15
            $columnName
342
        );
343
    }
344
345
    /**
346
     * Returns the SQL snippet for declaring a default constraint.
347
     *
348
     * @param string $table  Name of the table to return the default constraint declaration for.
349
     * @param array  $column Column definition.
350
     *
351
     * @return string
352
     *
353
     * @throws \InvalidArgumentException
354
     */
355 45
    public function getDefaultConstraintDeclarationSQL($table, array $column)
356
    {
357 45
        if ( ! isset($column['default'])) {
358
            throw new \InvalidArgumentException("Incomplete column definition. 'default' required.");
359
        }
360
361 45
        $columnName = new Identifier($column['name']);
362
363
        return
364
            ' CONSTRAINT ' .
365 45
            $this->generateDefaultConstraintName($table, $column['name']) .
366 45
            $this->getDefaultValueDeclarationSQL($column) .
367 45
            ' FOR ' . $columnName->getQuotedName($this);
368
    }
369
370
    /**
371
     * {@inheritDoc}
372
     */
373 6
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
374
    {
375 6
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
376
377 6
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
378
379 6
        return $constraint;
380
    }
381
382
    /**
383
     * {@inheritDoc}
384
     */
385 24
    public function getCreateIndexSQL(Index $index, $table)
386
    {
387 24
        $constraint = parent::getCreateIndexSQL($index, $table);
388
389 24
        if ($index->isUnique() && !$index->isPrimary()) {
390 6
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
391
        }
392
393 24
        return $constraint;
394
    }
395
396
    /**
397
     * {@inheritDoc}
398
     */
399 24
    protected function getCreateIndexSQLFlags(Index $index)
400
    {
401 24
        $type = '';
402 24
        if ($index->isUnique()) {
403 6
            $type .= 'UNIQUE ';
404
        }
405
406 24
        if ($index->hasFlag('clustered')) {
407 3
            $type .= 'CLUSTERED ';
408 21
        } elseif ($index->hasFlag('nonclustered')) {
409
            $type .= 'NONCLUSTERED ';
410
        }
411
412 24
        return $type;
413
    }
414
415
    /**
416
     * Extend unique key constraint with required filters
417
     *
418
     * @param string                      $sql
419
     * @param \Doctrine\DBAL\Schema\Index $index
420
     *
421
     * @return string
422
     */
423 12
    private function _appendUniqueConstraintDefinition($sql, Index $index)
424
    {
425 12
        $fields = [];
426
427 12
        foreach ($index->getQuotedColumns($this) as $field) {
428 12
            $fields[] = $field . ' IS NOT NULL';
429
        }
430
431 12
        return $sql . ' WHERE ' . implode(' AND ', $fields);
432
    }
433
434
    /**
435
     * {@inheritDoc}
436
     */
437 57
    public function getAlterTableSQL(TableDiff $diff)
438
    {
439 57
        $queryParts  = [];
440 57
        $sql         = [];
441 57
        $columnSql   = [];
442 57
        $commentsSql = [];
443
444
        /** @var \Doctrine\DBAL\Schema\Column $column */
445 57
        foreach ($diff->addedColumns as $column) {
446 27
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
447
                continue;
448
            }
449
450 27
            $columnDef = $column->toArray();
451 27
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
452
453 27
            if (isset($columnDef['default'])) {
454 12
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
455
            }
456
457 27
            $comment = $this->getColumnComment($column);
458
459 27
            if ( ! empty($comment) || is_numeric($comment)) {
460 6
                $commentsSql[] = $this->getCreateColumnCommentSQL(
461 6
                    $diff->name,
462 6
                    $column->getQuotedName($this),
463 27
                    $comment
464
                );
465
            }
466
        }
467
468 57
        foreach ($diff->removedColumns as $column) {
469 24
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
470
                continue;
471
            }
472
473 24
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
474
        }
475
476
        /* @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
477 57
        foreach ($diff->changedColumns as $columnDiff) {
478 36
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
479
                continue;
480
            }
481
482 36
            $column     = $columnDiff->column;
483 36
            $comment    = $this->getColumnComment($column);
484 36
            $hasComment = ! empty ($comment) || is_numeric($comment);
485
486 36
            if ($columnDiff->fromColumn instanceof Column) {
487 21
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
488 21
                $hasFromComment = ! empty ($fromComment) || is_numeric($fromComment);
489
490 21
                if ($hasFromComment && $hasComment && $fromComment != $comment) {
491 3
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
492 3
                        $diff->name,
493 3
                        $column->getQuotedName($this),
494 3
                        $comment
495
                    );
496 21
                } elseif ($hasFromComment && ! $hasComment) {
497 3
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
498 21
                } elseif ($hasComment) {
499 3
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
500 3
                        $diff->name,
501 3
                        $column->getQuotedName($this),
502 21
                        $comment
503
                    );
504
                }
505
            } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
506
                // todo: Original comment cannot be determined. What to do? Add, update, drop or skip?
507
            }
508
509
            // Do not add query part if only comment has changed.
510 36
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
511 6
                continue;
512
            }
513
514 33
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
515
516 33
            if ($requireDropDefaultConstraint) {
517 15
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
518 15
                    $diff->name,
519 15
                    $columnDiff->oldColumnName
520
                );
521
            }
522
523 33
            $columnDef = $column->toArray();
524
525 33
            $queryParts[] = 'ALTER COLUMN ' .
526 33
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
527
528 33
            if (isset($columnDef['default']) && ($requireDropDefaultConstraint || $columnDiff->hasChanged('default'))) {
529 33
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
530
            }
531
        }
532
533 57
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
534 15
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
535
                continue;
536
            }
537
538 15
            $oldColumnName = new Identifier($oldColumnName);
539
540 15
            $sql[] = "sp_RENAME '" .
541 15
                $diff->getName($this)->getQuotedName($this) . "." . $oldColumnName->getQuotedName($this) .
542 15
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
543
544
            // Recreate default constraint with new column name if necessary (for future reference).
545 15
            if ($column->getDefault() !== null) {
546 3
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
547 3
                    $diff->name,
548 3
                    $oldColumnName->getQuotedName($this)
549
                );
550 15
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
551
            }
552
        }
553
554 57
        $tableSql = [];
555
556 57
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
557
            return array_merge($tableSql, $columnSql);
558
        }
559
560 57
        foreach ($queryParts as $query) {
561 39
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
562
        }
563
564 57
        $sql = array_merge($sql, $commentsSql);
565
566 57
        if ($diff->newName !== false) {
567 6
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $diff->getNewName()->getName() . "'";
568
569
            /**
570
             * Rename table's default constraints names
571
             * to match the new table name.
572
             * This is necessary to ensure that the default
573
             * constraints can be referenced in future table
574
             * alterations as the table name is encoded in
575
             * default constraints' names.
576
             */
577 6
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
578
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
579 6
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
580 6
                "'" . $this->generateIdentifierName($diff->newName) . "') + ''', ''OBJECT'';' " .
0 ignored issues
show
Bug introduced by
It seems like $diff->newName can also be of type true; however, parameter $identifier of Doctrine\DBAL\Platforms\...enerateIdentifierName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

580
                "'" . $this->generateIdentifierName(/** @scrutinizer ignore-type */ $diff->newName) . "') + ''', ''OBJECT'';' " .
Loading history...
581 6
                "FROM sys.default_constraints dc " .
582 6
                "JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id " .
583 6
                "WHERE tbl.name = '" . $diff->getNewName()->getName() . "';" .
584 6
                "EXEC sp_executesql @sql";
585
        }
586
587 57
        $sql = array_merge(
588 57
            $this->getPreAlterTableIndexForeignKeySQL($diff),
589 57
            $sql,
590 57
            $this->getPostAlterTableIndexForeignKeySQL($diff)
591
        );
592
593 57
        return array_merge($sql, $tableSql, $columnSql);
594
    }
595
596
    /**
597
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
598
     *
599
     * @param string $tableName The name of the table to generate the clause for.
600
     * @param Column $column    The column to generate the clause for.
601
     *
602
     * @return string
603
     */
604 21
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
605
    {
606 21
        $columnDef = $column->toArray();
607 21
        $columnDef['name'] = $column->getQuotedName($this);
608
609 21
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
610
    }
611
612
    /**
613
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
614
     *
615
     * @param string $tableName  The name of the table to generate the clause for.
616
     * @param string $columnName The name of the column to generate the clause for.
617
     *
618
     * @return string
619
     */
620 18
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
621
    {
622 18
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
623
    }
624
625
    /**
626
     * Checks whether a column alteration requires dropping its default constraint first.
627
     *
628
     * Different to other database vendors SQL Server implements column default values
629
     * as constraints and therefore changes in a column's default value as well as changes
630
     * in a column's type require dropping the default constraint first before being to
631
     * alter the particular column to the new definition.
632
     *
633
     * @param ColumnDiff $columnDiff The column diff to evaluate.
634
     *
635
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
636
     */
637 33
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
638
    {
639
        // We can only decide whether to drop an existing default constraint
640
        // if we know the original default value.
641 33
        if ( ! $columnDiff->fromColumn instanceof Column) {
0 ignored issues
show
introduced by
$columnDiff->fromColumn is always a sub-type of Doctrine\DBAL\Schema\Column.
Loading history...
642 12
            return false;
643
        }
644
645
        // We only need to drop an existing default constraint if we know the
646
        // column was defined with a default value before.
647 21
        if ($columnDiff->fromColumn->getDefault() === null) {
648 6
            return false;
649
        }
650
651
        // We need to drop an existing default constraint if the column was
652
        // defined with a default value before and it has changed.
653 15
        if ($columnDiff->hasChanged('default')) {
654 12
            return true;
655
        }
656
657
        // We need to drop an existing default constraint if the column was
658
        // defined with a default value before and the native column type has changed.
659 3
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
660
    }
661
662
    /**
663
     * Returns the SQL statement for altering a column comment.
664
     *
665
     * SQL Server does not support native column comments,
666
     * therefore the extended properties functionality is used
667
     * as a workaround to store them.
668
     * The property name used to store column comments is "MS_Description"
669
     * which provides compatibility with SQL Server Management Studio,
670
     * as column comments are stored in the same property there when
671
     * specifying a column's "Description" attribute.
672
     *
673
     * @param string $tableName  The quoted table name to which the column belongs.
674
     * @param string $columnName The quoted column name to alter the comment for.
675
     * @param string $comment    The column's comment.
676
     *
677
     * @return string
678
     */
679 3
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
680
    {
681 3
        return $this->getUpdateExtendedPropertySQL(
682 3
            'MS_Description',
683 3
            $comment,
684 3
            'SCHEMA',
685 3
            'dbo',
686 3
            'TABLE',
687 3
            $tableName,
688 3
            'COLUMN',
689 3
            $columnName
690
        );
691
    }
692
693
    /**
694
     * Returns the SQL statement for dropping a column comment.
695
     *
696
     * SQL Server does not support native column comments,
697
     * therefore the extended properties functionality is used
698
     * as a workaround to store them.
699
     * The property name used to store column comments is "MS_Description"
700
     * which provides compatibility with SQL Server Management Studio,
701
     * as column comments are stored in the same property there when
702
     * specifying a column's "Description" attribute.
703
     *
704
     * @param string $tableName  The quoted table name to which the column belongs.
705
     * @param string $columnName The quoted column name to drop the comment for.
706
     *
707
     * @return string
708
     */
709 3
    protected function getDropColumnCommentSQL($tableName, $columnName)
710
    {
711 3
        return $this->getDropExtendedPropertySQL(
712 3
            'MS_Description',
713 3
            'SCHEMA',
714 3
            'dbo',
715 3
            'TABLE',
716 3
            $tableName,
717 3
            'COLUMN',
718 3
            $columnName
719
        );
720
    }
721
722
    /**
723
     * {@inheritdoc}
724
     */
725 15
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
726
    {
727
        return [
728 15
            sprintf(
729 15
                "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
730 15
                $tableName,
731 15
                $oldIndexName,
732 15
                $index->getQuotedName($this)
733
            )
734
        ];
735
    }
736
737
    /**
738
     * Returns the SQL statement for adding an extended property to a database object.
739
     *
740
     * @param string      $name       The name of the property to add.
741
     * @param string|null $value      The value of the property to add.
742
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
743
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
744
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
745
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
746
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
747
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
748
     *
749
     * @return string
750
     *
751
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
752
     */
753 15
    public function getAddExtendedPropertySQL(
754
        $name,
755
        $value = null,
756
        $level0Type = null,
757
        $level0Name = null,
758
        $level1Type = null,
759
        $level1Name = null,
760
        $level2Type = null,
761
        $level2Name = null
762
    ) {
763
        return "EXEC sp_addextendedproperty " .
764 15
            "N" . $this->quoteStringLiteral($name) . ", N" . $this->quoteStringLiteral($value) . ", " .
765 15
            "N" . $this->quoteStringLiteral($level0Type) . ", " . $level0Name . ', ' .
766 15
            "N" . $this->quoteStringLiteral($level1Type) . ", " . $level1Name . ', ' .
767 15
            "N" . $this->quoteStringLiteral($level2Type) . ", " . $level2Name;
768
    }
769
770
    /**
771
     * Returns the SQL statement for dropping an extended property from a database object.
772
     *
773
     * @param string      $name       The name of the property to drop.
774
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
775
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
776
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
777
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
778
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
779
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
780
     *
781
     * @return string
782
     *
783
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
784
     */
785 3
    public function getDropExtendedPropertySQL(
786
        $name,
787
        $level0Type = null,
788
        $level0Name = null,
789
        $level1Type = null,
790
        $level1Name = null,
791
        $level2Type = null,
792
        $level2Name = null
793
    ) {
794
        return "EXEC sp_dropextendedproperty " .
795 3
            "N" . $this->quoteStringLiteral($name) . ", " .
796 3
            "N" . $this->quoteStringLiteral($level0Type) . ", " . $level0Name . ', ' .
797 3
            "N" . $this->quoteStringLiteral($level1Type) . ", " . $level1Name . ', ' .
798 3
            "N" . $this->quoteStringLiteral($level2Type) . ", " . $level2Name;
799
    }
800
801
    /**
802
     * Returns the SQL statement for updating an extended property of a database object.
803
     *
804
     * @param string      $name       The name of the property to update.
805
     * @param string|null $value      The value of the property to update.
806
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
807
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
808
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
809
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
810
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
811
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
812
     *
813
     * @return string
814
     *
815
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
816
     */
817 3
    public function getUpdateExtendedPropertySQL(
818
        $name,
819
        $value = null,
820
        $level0Type = null,
821
        $level0Name = null,
822
        $level1Type = null,
823
        $level1Name = null,
824
        $level2Type = null,
825
        $level2Name = null
826
    ) {
827
        return "EXEC sp_updateextendedproperty " .
828 3
        "N" . $this->quoteStringLiteral($name) . ", N" . $this->quoteStringLiteral($value) . ", " .
829 3
        "N" . $this->quoteStringLiteral($level0Type) . ", " . $level0Name . ', ' .
830 3
        "N" . $this->quoteStringLiteral($level1Type) . ", " . $level1Name . ', ' .
831 3
        "N" . $this->quoteStringLiteral($level2Type) . ", " . $level2Name;
832
    }
833
834
    /**
835
     * {@inheritDoc}
836
     */
837
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
838
    {
839
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
840
    }
841
842
    /**
843
     * {@inheritDoc}
844
     */
845
    public function getListTablesSQL()
846
    {
847
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
848
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
849
        return "SELECT name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
850
    }
851
852
    /**
853
     * {@inheritDoc}
854
     */
855 6
    public function getListTableColumnsSQL($table, $database = null)
856
    {
857
        return "SELECT    col.name,
858
                          type.name AS type,
859
                          col.max_length AS length,
860
                          ~col.is_nullable AS notnull,
861
                          def.definition AS [default],
862
                          col.scale,
863
                          col.precision,
864
                          col.is_identity AS autoincrement,
865
                          col.collation_name AS collation,
866
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
867
                FROM      sys.columns AS col
868
                JOIN      sys.types AS type
869
                ON        col.user_type_id = type.user_type_id
870
                JOIN      sys.objects AS obj
871
                ON        col.object_id = obj.object_id
872
                JOIN      sys.schemas AS scm
873
                ON        obj.schema_id = scm.schema_id
874
                LEFT JOIN sys.default_constraints def
875
                ON        col.default_object_id = def.object_id
876
                AND       col.object_id = def.parent_object_id
877
                LEFT JOIN sys.extended_properties AS prop
878
                ON        obj.object_id = prop.major_id
879
                AND       col.column_id = prop.minor_id
880
                AND       prop.name = 'MS_Description'
881
                WHERE     obj.type = 'U'
882 6
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
883
    }
884
885
    /**
886
     * {@inheritDoc}
887
     */
888 6
    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

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