Passed
Pull Request — master (#3157)
by Sergei
16:37
created

getAlterTableDropDefaultConstraintClause()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

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