Completed
Pull Request — 2.8 (#3345)
by
unknown
36:29
created

SQLServerPlatform::getAlterTableSQL()   F

Complexity

Conditions 33
Paths > 20000

Size

Total Lines 162
Code Lines 90

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 83
CRAP Score 33.3334

Importance

Changes 0
Metric Value
eloc 90
dl 0
loc 162
ccs 83
cts 89
cp 0.9326
rs 0
c 0
b 0
f 0
cc 33
nc 39600
nop 1
crap 33.3334

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 4
            $index = $index->getQuotedName($this);
238 2
        } 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 {
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
            // Do not add query part if only unsigned has changed.
546 635
            if ($columnDiff->hasChanged('unsigned') && count($columnDiff->changedProperties) === 1) {
547
                continue;
548
            }
549
550 635
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
551
552 635
            if ($requireDropDefaultConstraint) {
553 291
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
554 291
                    $diff->name,
555 291
                    $columnDiff->oldColumnName
556
                );
557
            }
558
559 635
            $columnDef = $column->toArray();
560
561 635
            $queryParts[] = 'ALTER COLUMN ' .
562 635
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
563
564 635
            if (isset($columnDef['default']) && ($requireDropDefaultConstraint || $columnDiff->hasChanged('default'))) {
565 635
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
566
            }
567
        }
568
569 1290
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
570 291
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
571
                continue;
572
            }
573
574 291
            $oldColumnName = new Identifier($oldColumnName);
575
576 291
            $sql[] = "sp_RENAME '" .
577 291
                $diff->getName($this)->getQuotedName($this) . "." . $oldColumnName->getQuotedName($this) .
578 291
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
579
580
            // Recreate default constraint with new column name if necessary (for future reference).
581 291
            if ($column->getDefault() !== null) {
582 59
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
583 59
                    $diff->name,
584 59
                    $oldColumnName->getQuotedName($this)
585
                );
586 291
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
587
            }
588
        }
589
590 1290
        $tableSql = [];
591
592 1290
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
593
            return array_merge($tableSql, $columnSql);
594
        }
595
596 1290
        foreach ($queryParts as $query) {
597 810
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
598
        }
599
600 1290
        $sql = array_merge($sql, $commentsSql);
601
602 1290
        if ($diff->newName !== false) {
603 116
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $diff->getNewName()->getName() . "'";
604
605
            /**
606
             * Rename table's default constraints names
607
             * to match the new table name.
608
             * This is necessary to ensure that the default
609
             * constraints can be referenced in future table
610
             * alterations as the table name is encoded in
611
             * default constraints' names.
612
             */
613 116
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
614
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
615 116
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
616 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

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

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