Failed Conditions
Pull Request — master (#2929)
by Alexander
62:06
created

SQLServerPlatform::getDropPrimaryKeySQL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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

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