Passed
Pull Request — master (#3225)
by Šimon
12:57
created

SQLServerPlatform::isOrderByInTopNSubquery()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 22
ccs 11
cts 11
cp 1
rs 9.2222
c 0
b 0
f 0
cc 6
nc 7
nop 2
crap 6
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 array_merge;
32
use function array_unique;
33
use function array_values;
34
use function count;
35
use function crc32;
36
use function dechex;
37
use function explode;
38
use function func_get_args;
39
use function implode;
40
use function is_array;
41
use function is_bool;
42
use function is_numeric;
43
use function is_string;
44
use function preg_match;
45
use function preg_replace;
46
use function sprintf;
47
use function str_replace;
48
use function stripos;
49
use function stristr;
50
use function strlen;
51
use function strpos;
52
use function strtoupper;
53
use function substr;
54
use function substr_count;
55
56
/**
57
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
58
 * Microsoft SQL Server database platform.
59
 *
60
 * @since 2.0
61
 * @author Roman Borschel <[email protected]>
62
 * @author Jonathan H. Wage <[email protected]>
63
 * @author Benjamin Eberlei <[email protected]>
64
 * @author Steve Müller <[email protected]>
65
 */
66
class SQLServerPlatform extends AbstractPlatform
67
{
68
    /**
69
     * {@inheritdoc}
70
     */
71 116
    public function getCurrentDateSQL()
72
    {
73 116
        return $this->getConvertExpression('date', 'GETDATE()');
74
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79 59
    public function getCurrentTimeSQL()
80
    {
81 59
        return $this->getConvertExpression('time', 'GETDATE()');
82
    }
83
84
    /**
85
     * Returns an expression that converts an expression of one data type to another.
86
     *
87
     * @param string $dataType   The target native data type. Alias data types cannot be used.
88
     * @param string $expression The SQL expression to convert.
89
     *
90
     * @return string
91
     */
92 116
    private function getConvertExpression($dataType, $expression)
93
    {
94 116
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
95
    }
96
97
    /**
98
     * {@inheritdoc}
99
     */
100 2
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
101
    {
102 2
        $factorClause = '';
103
104 2
        if ('-' === $operator) {
105 2
            $factorClause = '-1 * ';
106
        }
107
108 2
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
109
    }
110
111
    /**
112
     * {@inheritDoc}
113
     */
114 6
    public function getDateDiffExpression($date1, $date2)
115
    {
116 6
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
117
    }
118
119
    /**
120
     * {@inheritDoc}
121
     *
122
     * Microsoft SQL Server prefers "autoincrement" identity columns
123
     * since sequences can only be emulated with a table.
124
     */
125 59
    public function prefersIdentityColumns()
126
    {
127 59
        return true;
128
    }
129
130
    /**
131
     * {@inheritDoc}
132
     *
133
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
134
     */
135 63
    public function supportsIdentityColumns()
136
    {
137 63
        return true;
138
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143 2
    public function supportsReleaseSavepoints()
144
    {
145 2
        return false;
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 65
    public function supportsSchemas()
152
    {
153 65
        return true;
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159 2
    public function getDefaultSchemaName()
160
    {
161 2
        return 'dbo';
162
    }
163
164
    /**
165
     * {@inheritDoc}
166
     */
167 173
    public function supportsColumnCollation()
168
    {
169 173
        return true;
170
    }
171
172
    /**
173
     * {@inheritDoc}
174
     */
175 3340
    public function hasNativeGuidType()
176
    {
177 3340
        return true;
178
    }
179
180
    /**
181
     * {@inheritDoc}
182
     */
183 61
    public function getCreateDatabaseSQL($name)
184
    {
185 61
        return 'CREATE DATABASE ' . $name;
186
    }
187
188
    /**
189
     * {@inheritDoc}
190
     */
191 61
    public function getDropDatabaseSQL($name)
192
    {
193 61
        return 'DROP DATABASE ' . $name;
194
    }
195
196
    /**
197
     * {@inheritDoc}
198
     */
199 61
    public function supportsCreateDropDatabase()
200
    {
201 61
        return true;
202
    }
203
204
    /**
205
     * {@inheritDoc}
206
     */
207 61
    public function getCreateSchemaSQL($schemaName)
208
    {
209 61
        return 'CREATE SCHEMA ' . $schemaName;
210
    }
211
212
    /**
213
     * {@inheritDoc}
214
     */
215 116
    public function getDropForeignKeySQL($foreignKey, $table)
216
    {
217 116
        if (! $foreignKey instanceof ForeignKeyConstraint) {
218 57
            $foreignKey = new Identifier($foreignKey);
219
        }
220
221 116
        if (! $table instanceof Table) {
222 116
            $table = new Identifier($table);
223
        }
224
225 116
        $foreignKey = $foreignKey->getQuotedName($this);
226 116
        $table = $table->getQuotedName($this);
227
228 116
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
229
    }
230
231
    /**
232
     * {@inheritDoc}
233
     */
234 6
    public function getDropIndexSQL($index, $table = null)
235
    {
236 6
        if ($index instanceof Index) {
237 6
            $index = $index->getQuotedName($this);
238
        } elseif (!is_string($index)) {
239
            throw new \InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
240
        }
241
242 6
        if (!isset($table)) {
243
            return 'DROP INDEX ' . $index;
244
        }
245
246 6
        if ($table instanceof Table) {
247 2
            $table = $table->getQuotedName($this);
248
        }
249
250 6
        return "IF EXISTS (SELECT * FROM sysobjects WHERE name = '$index')
251 6
                    ALTER TABLE " . $table . " DROP CONSTRAINT " . $index . "
252
                ELSE
253 6
                    DROP INDEX " . $index . " ON " . $table;
254
    }
255
256
    /**
257
     * {@inheritDoc}
258
     */
259 1242
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
260
    {
261 1242
        $defaultConstraintsSql = [];
262 1242
        $commentsSql           = [];
263
264
        // @todo does other code breaks because of this?
265
        // force primary keys to be not null
266 1242
        foreach ($columns as &$column) {
267 1242
            if (isset($column['primary']) && $column['primary']) {
268 513
                $column['notnull'] = true;
269
            }
270
271
            // Build default constraints SQL statements.
272 1242
            if (isset($column['default'])) {
273 246
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
274 246
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
275
            }
276
277 1242
            if ( ! empty($column['comment']) || is_numeric($column['comment'])) {
278 1242
                $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
279
            }
280
        }
281
282 1242
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
283
284 1242
        if (isset($options['uniqueConstraints']) && !empty($options['uniqueConstraints'])) {
285
            foreach ($options['uniqueConstraints'] as $name => $definition) {
286
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
287
            }
288
        }
289
290 1242
        if (isset($options['primary']) && !empty($options['primary'])) {
291 570
            $flags = '';
292 570
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
293 57
                $flags = ' NONCLUSTERED';
294
            }
295 570
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
296
        }
297
298 1242
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
299
300 1242
        $check = $this->getCheckDeclarationSQL($columns);
301 1242
        if (!empty($check)) {
302
            $query .= ', ' . $check;
303
        }
304 1242
        $query .= ')';
305
306 1242
        $sql = [$query];
307
308 1242
        if (isset($options['indexes']) && !empty($options['indexes'])) {
309 187
            foreach ($options['indexes'] as $index) {
310 187
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
311
            }
312
        }
313
314 1242
        if (isset($options['foreignKeys'])) {
315 307
            foreach ((array) $options['foreignKeys'] as $definition) {
316 67
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
317
            }
318
        }
319
320 1242
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
321
    }
322
323
    /**
324
     * {@inheritDoc}
325
     */
326 114
    public function getCreatePrimaryKeySQL(Index $index, $table)
327
    {
328 114
        $flags = '';
329 114
        if ($index->hasFlag('nonclustered')) {
330 57
            $flags = ' NONCLUSTERED';
331
        }
332
333 114
        return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY' . $flags . ' (' . $this->getIndexFieldDeclarationListSQL($index->getQuotedColumns($this)) . ')';
334
    }
335
336
    /**
337
     * Returns the SQL statement for creating a column comment.
338
     *
339
     * SQL Server does not support native column comments,
340
     * therefore the extended properties functionality is used
341
     * as a workaround to store them.
342
     * The property name used to store column comments is "MS_Description"
343
     * which provides compatibility with SQL Server Management Studio,
344
     * as column comments are stored in the same property there when
345
     * specifying a column's "Description" attribute.
346
     *
347
     * @param string $tableName  The quoted table name to which the column belongs.
348
     * @param string $columnName The quoted column name to create the comment for.
349
     * @param string $comment    The column's comment.
350
     *
351
     * @return string
352
     */
353 463
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
354
    {
355 463
        if (strpos($tableName, '.') !== false) {
356 114
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
357 114
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
358 114
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
359
        } else {
360 349
            $schemaSQL = "'dbo'";
361 349
            $tableSQL  = $this->quoteStringLiteral($tableName);
362
        }
363
364 463
        return $this->getAddExtendedPropertySQL(
365 463
            'MS_Description',
366 463
            $comment,
367 463
            'SCHEMA',
368 463
            $schemaSQL,
369 463
            'TABLE',
370 463
            $tableSQL,
371 463
            'COLUMN',
372 463
            $columnName
373
        );
374
    }
375
376
    /**
377
     * Returns the SQL snippet for declaring a default constraint.
378
     *
379
     * @param string $table  Name of the table to return the default constraint declaration for.
380
     * @param array  $column Column definition.
381
     *
382
     * @return string
383
     *
384
     * @throws \InvalidArgumentException
385
     */
386 873
    public function getDefaultConstraintDeclarationSQL($table, array $column)
387
    {
388 873
        if ( ! isset($column['default'])) {
389
            throw new \InvalidArgumentException("Incomplete column definition. 'default' required.");
390
        }
391
392 873
        $columnName = new Identifier($column['name']);
393
394
        return
395
            ' CONSTRAINT ' .
396 873
            $this->generateDefaultConstraintName($table, $column['name']) .
397 873
            $this->getDefaultValueDeclarationSQL($column) .
398 873
            ' FOR ' . $columnName->getQuotedName($this);
399
    }
400
401
    /**
402
     * {@inheritDoc}
403
     */
404 114
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
405
    {
406 114
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
407
408 114
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
409
410 114
        return $constraint;
411
    }
412
413
    /**
414
     * {@inheritDoc}
415
     */
416 474
    public function getCreateIndexSQL(Index $index, $table)
417
    {
418 474
        $constraint = parent::getCreateIndexSQL($index, $table);
419
420 474
        if ($index->isUnique() && !$index->isPrimary()) {
421 120
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
422
        }
423
424 474
        return $constraint;
425
    }
426
427
    /**
428
     * {@inheritDoc}
429
     */
430 474
    protected function getCreateIndexSQLFlags(Index $index)
431
    {
432 474
        $type = '';
433 474
        if ($index->isUnique()) {
434 120
            $type .= 'UNIQUE ';
435
        }
436
437 474
        if ($index->hasFlag('clustered')) {
438 57
            $type .= 'CLUSTERED ';
439 417
        } elseif ($index->hasFlag('nonclustered')) {
440
            $type .= 'NONCLUSTERED ';
441
        }
442
443 474
        return $type;
444
    }
445
446
    /**
447
     * Extend unique key constraint with required filters
448
     *
449
     * @param string                      $sql
450
     * @param \Doctrine\DBAL\Schema\Index $index
451
     *
452
     * @return string
453
     */
454 234
    private function _appendUniqueConstraintDefinition($sql, Index $index)
455
    {
456 234
        $fields = [];
457
458 234
        foreach ($index->getQuotedColumns($this) as $field) {
459 234
            $fields[] = $field . ' IS NOT NULL';
460
        }
461
462 234
        return $sql . ' WHERE ' . implode(' AND ', $fields);
463
    }
464
465
    /**
466
     * {@inheritDoc}
467
     */
468 1290
    public function getAlterTableSQL(TableDiff $diff)
469
    {
470 1290
        $queryParts  = [];
471 1290
        $sql         = [];
472 1290
        $columnSql   = [];
473 1290
        $commentsSql = [];
474
475
        /** @var \Doctrine\DBAL\Schema\Column $column */
476 1290
        foreach ($diff->addedColumns as $column) {
477 576
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
478
                continue;
479
            }
480
481 576
            $columnDef = $column->toArray();
482 576
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
483
484 576
            if (isset($columnDef['default'])) {
485 230
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
486
            }
487
488 576
            $comment = $this->getColumnComment($column);
489
490 576
            if ( ! empty($comment) || is_numeric($comment)) {
491 173
                $commentsSql[] = $this->getCreateColumnCommentSQL(
492 173
                    $diff->name,
493 173
                    $column->getQuotedName($this),
494 576
                    $comment
495
                );
496
            }
497
        }
498
499 1290
        foreach ($diff->removedColumns as $column) {
500 464
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
501
                continue;
502
            }
503
504 464
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
505
        }
506
507
        /* @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
508 1290
        foreach ($diff->changedColumns as $columnDiff) {
509 826
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
510
                continue;
511
            }
512
513 826
            $column     = $columnDiff->column;
514 826
            $comment    = $this->getColumnComment($column);
515 826
            $hasComment = ! empty ($comment) || is_numeric($comment);
516
517 826
            if ($columnDiff->fromColumn instanceof Column) {
518 541
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
519 541
                $hasFromComment = ! empty ($fromComment) || is_numeric($fromComment);
520
521 541
                if ($hasFromComment && $hasComment && $fromComment != $comment) {
522 122
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
523 122
                        $diff->name,
524 122
                        $column->getQuotedName($this),
525 122
                        $comment
526
                    );
527 484
                } elseif ($hasFromComment && ! $hasComment) {
528 130
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
529 425
                } elseif ($hasComment) {
530 77
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
531 77
                        $diff->name,
532 77
                        $column->getQuotedName($this),
533 541
                        $comment
534
                    );
535
                }
536
            } 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...
537
                // todo: Original comment cannot be determined. What to do? Add, update, drop or skip?
538
            }
539
540
            // Do not add query part if only comment has changed.
541 826
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
542 250
                continue;
543
            }
544
545 635
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
546
547 635
            if ($requireDropDefaultConstraint) {
548 291
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
549 291
                    $diff->name,
550 291
                    $columnDiff->oldColumnName
551
                );
552
            }
553
554 635
            $columnDef = $column->toArray();
555
556 635
            $queryParts[] = 'ALTER COLUMN ' .
557 635
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
558
559 635
            if (isset($columnDef['default']) && ($requireDropDefaultConstraint || $columnDiff->hasChanged('default'))) {
560 635
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
561
            }
562
        }
563
564 1290
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
565 291
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
566
                continue;
567
            }
568
569 291
            $oldColumnName = new Identifier($oldColumnName);
570
571 291
            $sql[] = "sp_RENAME '" .
572 291
                $diff->getName($this)->getQuotedName($this) . "." . $oldColumnName->getQuotedName($this) .
573 291
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
574
575
            // Recreate default constraint with new column name if necessary (for future reference).
576 291
            if ($column->getDefault() !== null) {
577 59
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
578 59
                    $diff->name,
579 59
                    $oldColumnName->getQuotedName($this)
580
                );
581 291
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
582
            }
583
        }
584
585 1290
        $tableSql = [];
586
587 1290
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
588
            return array_merge($tableSql, $columnSql);
589
        }
590
591 1290
        foreach ($queryParts as $query) {
592 810
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
593
        }
594
595 1290
        $sql = array_merge($sql, $commentsSql);
596
597 1290
        if ($diff->newName !== false) {
598 116
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $diff->getNewName()->getName() . "'";
599
600
            /**
601
             * Rename table's default constraints names
602
             * to match the new table name.
603
             * This is necessary to ensure that the default
604
             * constraints can be referenced in future table
605
             * alterations as the table name is encoded in
606
             * default constraints' names.
607
             */
608 116
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
609
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
610 116
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
611 116
                "'" . $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

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

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