Completed
Pull Request — master (#3020)
by Michael
35:56 queued 31:09
created

SQLServerPlatform::getCreateColumnCommentSQL()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 16
cts 16
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 16
nc 2
nop 3
crap 2
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
     * {@inheritdoc}
50
     */
51 6
    public function getCurrentDateSQL()
52
    {
53 6
        return $this->getConvertExpression('date', 'GETDATE()');
54
    }
55
56
    /**
57
     * {@inheritdoc}
58
     */
59 3
    public function getCurrentTimeSQL()
60
    {
61 3
        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
     * @return string
71
     */
72 6
    private function getConvertExpression($dataType, $expression)
73
    {
74 6
        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
     * since sequences can only be emulated with a table.
104
     */
105 3
    public function prefersIdentityColumns()
106
    {
107 3
        return true;
108
    }
109
110
    /**
111
     * {@inheritDoc}
112
     *
113
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
114
     */
115 3
    public function supportsIdentityColumns()
116
    {
117 3
        return true;
118
    }
119
120
    /**
121
     * {@inheritDoc}
122
     */
123
    public function supportsReleaseSavepoints()
124
    {
125
        return false;
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131 3
    public function supportsSchemas()
132
    {
133 3
        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
     * {@inheritDoc}
154
     */
155 172
    public function hasNativeGuidType()
156
    {
157 172
        return true;
158
    }
159
160
    /**
161
     * {@inheritDoc}
162
     */
163 3
    public function getCreateDatabaseSQL($name)
164
    {
165 3
        return 'CREATE DATABASE ' . $name;
166
    }
167
168
    /**
169
     * {@inheritDoc}
170
     */
171 3
    public function getDropDatabaseSQL($name)
172
    {
173 3
        return 'DROP DATABASE ' . $name;
174
    }
175
176
    /**
177
     * {@inheritDoc}
178
     */
179 3
    public function supportsCreateDropDatabase()
180
    {
181 3
        return true;
182
    }
183
184
    /**
185
     * {@inheritDoc}
186
     */
187 3
    public function getCreateSchemaSQL($schemaName)
188
    {
189 3
        return 'CREATE SCHEMA ' . $schemaName;
190
    }
191
192
    /**
193
     * {@inheritDoc}
194
     */
195 6
    public function getDropForeignKeySQL($foreignKey, $table)
196
    {
197 6
        if (! $foreignKey instanceof ForeignKeyConstraint) {
198 3
            $foreignKey = new Identifier($foreignKey);
199
        }
200
201 6
        if (! $table instanceof Table) {
202 6
            $table = new Identifier($table);
203
        }
204
205 6
        $foreignKey = $foreignKey->getQuotedName($this);
206 6
        $table = $table->getQuotedName($this);
207
208 6
        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
     * {@inheritDoc}
238
     */
239 49
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
240
    {
241 49
        $defaultConstraintsSql = [];
242 49
        $commentsSql           = [];
243
244
        // @todo does other code breaks because of this?
245
        // force primary keys to be not null
246 49
        foreach ($columns as &$column) {
247 49
            if (isset($column['primary']) && $column['primary']) {
248 18
                $column['notnull'] = true;
249
            }
250
251
            // Build default constraints SQL statements.
252 49
            if (isset($column['default'])) {
253 12
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
254 12
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
255
            }
256
257 49
            if ( ! empty($column['comment']) || is_numeric($column['comment'])) {
258 49
                $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
259
            }
260
        }
261
262 49
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
263
264 49
        if (isset($options['uniqueConstraints']) && !empty($options['uniqueConstraints'])) {
265
            foreach ($options['uniqueConstraints'] as $name => $definition) {
266
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
267
            }
268
        }
269
270 49
        if (isset($options['primary']) && !empty($options['primary'])) {
271 21
            $flags = '';
272 21
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
273 3
                $flags = ' NONCLUSTERED';
274
            }
275 21
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
276
        }
277
278 49
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
279
280 49
        $check = $this->getCheckDeclarationSQL($columns);
281 49
        if (!empty($check)) {
282
            $query .= ', ' . $check;
283
        }
284 49
        $query .= ')';
285
286 49
        $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
288 49
        if (isset($options['indexes']) && !empty($options['indexes'])) {
289 9
            foreach ($options['indexes'] as $index) {
290 9
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
291
            }
292
        }
293
294 49
        if (isset($options['foreignKeys'])) {
295 3
            foreach ((array) $options['foreignKeys'] as $definition) {
296 3
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
297
            }
298
        }
299
300 49
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
301
    }
302
303
    /**
304
     * {@inheritDoc}
305
     */
306 6
    public function getCreatePrimaryKeySQL(Index $index, $table)
307
    {
308 6
        $flags = '';
309 6
        if ($index->hasFlag('nonclustered')) {
310 3
            $flags = ' NONCLUSTERED';
311
        }
312
313 6
        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
     * @return string
332
     */
333 21
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
334
    {
335 21
        if (strpos($tableName, '.') !== false) {
336 6
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
337 6
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
338 6
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
339
        } else {
340 15
            $schemaSQL = "'dbo'";
341 15
            $tableSQL  = $this->quoteStringLiteral($tableName);
342
        }
343
344 21
        return $this->getAddExtendedPropertySQL(
345 21
            'MS_Description',
346 21
            $comment,
347 21
            'SCHEMA',
348 21
            $schemaSQL,
349 21
            'TABLE',
350 21
            $tableSQL,
351 21
            'COLUMN',
352 21
            $columnName
353
        );
354
    }
355
356
    /**
357
     * 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
     *
362
     * @return string
363
     *
364
     * @throws \InvalidArgumentException
365
     */
366 45
    public function getDefaultConstraintDeclarationSQL($table, array $column)
367
    {
368 45
        if ( ! isset($column['default'])) {
369
            throw new \InvalidArgumentException("Incomplete column definition. 'default' required.");
370
        }
371
372 45
        $columnName = new Identifier($column['name']);
373
374
        return
375
            ' CONSTRAINT ' .
376 45
            $this->generateDefaultConstraintName($table, $column['name']) .
377 45
            $this->getDefaultValueDeclarationSQL($column) .
378 45
            ' FOR ' . $columnName->getQuotedName($this);
379
    }
380
381
    /**
382
     * {@inheritDoc}
383
     */
384 6
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
385
    {
386 6
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
387
388 6
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
389
390 6
        return $constraint;
391
    }
392
393
    /**
394
     * {@inheritDoc}
395
     */
396 24
    public function getCreateIndexSQL(Index $index, $table)
397
    {
398 24
        $constraint = parent::getCreateIndexSQL($index, $table);
399
400 24
        if ($index->isUnique() && !$index->isPrimary()) {
401 6
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
402
        }
403
404 24
        return $constraint;
405
    }
406
407
    /**
408
     * {@inheritDoc}
409
     */
410 24
    protected function getCreateIndexSQLFlags(Index $index)
411
    {
412 24
        $type = '';
413 24
        if ($index->isUnique()) {
414 6
            $type .= 'UNIQUE ';
415
        }
416
417 24
        if ($index->hasFlag('clustered')) {
418 3
            $type .= 'CLUSTERED ';
419 21
        } elseif ($index->hasFlag('nonclustered')) {
420
            $type .= 'NONCLUSTERED ';
421
        }
422
423 24
        return $type;
424
    }
425
426
    /**
427
     * Extend unique key constraint with required filters
428
     *
429
     * @param string                      $sql
430
     * @param \Doctrine\DBAL\Schema\Index $index
431
     *
432
     * @return string
433
     */
434 12
    private function _appendUniqueConstraintDefinition($sql, Index $index)
435
    {
436 12
        $fields = [];
437
438 12
        foreach ($index->getQuotedColumns($this) as $field) {
439 12
            $fields[] = $field . ' IS NOT NULL';
440
        }
441
442 12
        return $sql . ' WHERE ' . implode(' AND ', $fields);
443
    }
444
445
    /**
446
     * {@inheritDoc}
447
     */
448 66
    public function getAlterTableSQL(TableDiff $diff)
449
    {
450 66
        $queryParts  = [];
451 66
        $sql         = [];
452 66
        $columnSql   = [];
453 66
        $commentsSql = [];
454
455
        /** @var \Doctrine\DBAL\Schema\Column $column */
456 66
        foreach ($diff->addedColumns as $column) {
457 30
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
458
                continue;
459
            }
460
461 30
            $columnDef = $column->toArray();
462 30
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
463
464 30
            if (isset($columnDef['default'])) {
465 12
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
466
            }
467
468 30
            $comment = $this->getColumnComment($column);
469
470 30
            if ( ! empty($comment) || is_numeric($comment)) {
471 9
                $commentsSql[] = $this->getCreateColumnCommentSQL(
472 9
                    $diff->name,
473 9
                    $column->getQuotedName($this),
474 30
                    $comment
475
                );
476
            }
477
        }
478
479 66
        foreach ($diff->removedColumns as $column) {
480 24
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
481
                continue;
482
            }
483
484 24
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
485
        }
486
487
        /* @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
488 66
        foreach ($diff->changedColumns as $columnDiff) {
489 42
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
490
                continue;
491
            }
492
493 42
            $column     = $columnDiff->column;
494 42
            $comment    = $this->getColumnComment($column);
495 42
            $hasComment = ! empty ($comment) || is_numeric($comment);
496
497 42
            if ($columnDiff->fromColumn instanceof Column) {
498 27
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
499 27
                $hasFromComment = ! empty ($fromComment) || is_numeric($fromComment);
500
501 27
                if ($hasFromComment && $hasComment && $fromComment != $comment) {
502 6
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
503 6
                        $diff->name,
504 6
                        $column->getQuotedName($this),
505 6
                        $comment
506
                    );
507 24
                } elseif ($hasFromComment && ! $hasComment) {
508 6
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
509 21
                } elseif ($hasComment) {
510 3
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
511 3
                        $diff->name,
512 3
                        $column->getQuotedName($this),
513 27
                        $comment
514
                    );
515
                }
516
            } 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
                // todo: Original comment cannot be determined. What to do? Add, update, drop or skip?
518
            }
519
520
            // Do not add query part if only comment has changed.
521 42
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
522 12
                continue;
523
            }
524
525 33
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
526
527 33
            if ($requireDropDefaultConstraint) {
528 15
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
529 15
                    $diff->name,
530 15
                    $columnDiff->oldColumnName
531
                );
532
            }
533
534 33
            $columnDef = $column->toArray();
535
536 33
            $queryParts[] = 'ALTER COLUMN ' .
537 33
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
538
539 33
            if (isset($columnDef['default']) && ($requireDropDefaultConstraint || $columnDiff->hasChanged('default'))) {
540 33
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
541
            }
542
        }
543
544 66
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
545 15
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
546
                continue;
547
            }
548
549 15
            $oldColumnName = new Identifier($oldColumnName);
550
551 15
            $sql[] = "sp_RENAME '" .
552 15
                $diff->getName($this)->getQuotedName($this) . "." . $oldColumnName->getQuotedName($this) .
553 15
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
554
555
            // Recreate default constraint with new column name if necessary (for future reference).
556 15
            if ($column->getDefault() !== null) {
557 3
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
558 3
                    $diff->name,
559 3
                    $oldColumnName->getQuotedName($this)
560
                );
561 15
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
562
            }
563
        }
564
565 66
        $tableSql = [];
566
567 66
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
568
            return array_merge($tableSql, $columnSql);
569
        }
570
571 66
        foreach ($queryParts as $query) {
572 42
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
573
        }
574
575 66
        $sql = array_merge($sql, $commentsSql);
576
577 66
        if ($diff->newName !== false) {
578 6
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $diff->getNewName()->getName() . "'";
579
580
            /**
581
             * Rename table's default constraints names
582
             * to match the new table name.
583
             * This is necessary to ensure that the default
584
             * constraints can be referenced in future table
585
             * alterations as the table name is encoded in
586
             * default constraints' names.
587
             */
588 6
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
589
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
590 6
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
591 6
                "'" . $this->generateIdentifierName($diff->newName) . "') + ''', ''OBJECT'';' " .
0 ignored issues
show
Bug introduced by
It seems like $diff->newName can also be of type true; however, parameter $identifier of Doctrine\DBAL\Platforms\...enerateIdentifierName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

591
                "'" . $this->generateIdentifierName(/** @scrutinizer ignore-type */ $diff->newName) . "') + ''', ''OBJECT'';' " .
Loading history...
592 6
                "FROM sys.default_constraints dc " .
593 6
                "JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id " .
594 6
                "WHERE tbl.name = '" . $diff->getNewName()->getName() . "';" .
595 6
                "EXEC sp_executesql @sql";
596
        }
597
598 66
        $sql = array_merge(
599 66
            $this->getPreAlterTableIndexForeignKeySQL($diff),
600 66
            $sql,
601 66
            $this->getPostAlterTableIndexForeignKeySQL($diff)
602
        );
603
604 66
        return array_merge($sql, $tableSql, $columnSql);
605
    }
606
607
    /**
608
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
609
     *
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 21
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
616
    {
617 21
        $columnDef = $column->toArray();
618 21
        $columnDef['name'] = $column->getQuotedName($this);
619
620 21
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
621
    }
622
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 18
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
632
    {
633 18
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
634
    }
635
636
    /**
637
     * 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
     * in a column's type require dropping the default constraint first before being to
642
     * 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
     */
648 33
    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 33
        if ( ! $columnDiff->fromColumn instanceof Column) {
0 ignored issues
show
introduced by
$columnDiff->fromColumn is always a sub-type of Doctrine\DBAL\Schema\Column.
Loading history...
653 12
            return false;
654
        }
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 21
        if ($columnDiff->fromColumn->getDefault() === null) {
659 6
            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 15
        if ($columnDiff->hasChanged('default')) {
665 12
            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 3
        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
     * The property name used to store column comments is "MS_Description"
680
     * which provides compatibility with SQL Server Management Studio,
681
     * as column comments are stored in the same property there when
682
     * specifying a column's "Description" attribute.
683
     *
684
     * @param string $tableName  The quoted table name to which the column belongs.
685
     * @param string $columnName The quoted column name to alter the comment for.
686
     * @param string $comment    The column's comment.
687
     *
688
     * @return string
689
     */
690 6
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
691
    {
692 6
        if (strpos($tableName, '.') !== false) {
693 3
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
694 3
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
695 3
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
696
        } else {
697 3
            $schemaSQL = "'dbo'";
698 3
            $tableSQL  = $this->quoteStringLiteral($tableName);
699
        }
700
701 6
        return $this->getUpdateExtendedPropertySQL(
702 6
            'MS_Description',
703 6
            $comment,
704 6
            'SCHEMA',
705 6
            $schemaSQL,
706 6
            'TABLE',
707 6
            $tableSQL,
708 6
            'COLUMN',
709 6
            $columnName
710
        );
711
    }
712
713
    /**
714
     * Returns the SQL statement for dropping a column comment.
715
     *
716
     * SQL Server does not support native column comments,
717
     * therefore the extended properties functionality is used
718
     * 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
     * @param string $columnName The quoted column name to drop the comment for.
726
     *
727
     * @return string
728
     */
729 6
    protected function getDropColumnCommentSQL($tableName, $columnName)
730
    {
731 6
        if (strpos($tableName, '.') !== false) {
732 3
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
733 3
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
734 3
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
735
        } else {
736 3
            $schemaSQL = "'dbo'";
737 3
            $tableSQL  = $this->quoteStringLiteral($tableName);
738
        }
739
740 6
        return $this->getDropExtendedPropertySQL(
741 6
            'MS_Description',
742 6
            'SCHEMA',
743 6
            $schemaSQL,
744 6
            'TABLE',
745 6
            $tableSQL,
746 6
            'COLUMN',
747 6
            $columnName
748
        );
749
    }
750
751
    /**
752
     * {@inheritdoc}
753
     */
754 15
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
755
    {
756
        return [
757 15
            sprintf(
758 15
                "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
759 15
                $tableName,
760 15
                $oldIndexName,
761 15
                $index->getQuotedName($this)
762
            )
763
        ];
764
    }
765
766
    /**
767
     * 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 21
    public function getAddExtendedPropertySQL(
783
        $name,
784
        $value = null,
785
        $level0Type = null,
786
        $level0Name = null,
787
        $level1Type = null,
788
        $level1Name = null,
789
        $level2Type = null,
790
        $level2Name = null
791
    ) {
792
        return "EXEC sp_addextendedproperty " .
793 21
            "N" . $this->quoteStringLiteral($name) . ", N" . $this->quoteStringLiteral($value) . ", " .
794 21
            "N" . $this->quoteStringLiteral($level0Type) . ", " . $level0Name . ', ' .
795 21
            "N" . $this->quoteStringLiteral($level1Type) . ", " . $level1Name . ', ' .
796 21
            "N" . $this->quoteStringLiteral($level2Type) . ", " . $level2Name;
797
    }
798
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 6
    public function getDropExtendedPropertySQL(
815
        $name,
816
        $level0Type = null,
817
        $level0Name = null,
818
        $level1Type = null,
819
        $level1Name = null,
820
        $level2Type = null,
821
        $level2Name = null
822
    ) {
823
        return "EXEC sp_dropextendedproperty " .
824 6
            "N" . $this->quoteStringLiteral($name) . ", " .
825 6
            "N" . $this->quoteStringLiteral($level0Type) . ", " . $level0Name . ', ' .
826 6
            "N" . $this->quoteStringLiteral($level1Type) . ", " . $level1Name . ', ' .
827 6
            "N" . $this->quoteStringLiteral($level2Type) . ", " . $level2Name;
828
    }
829
830
    /**
831
     * 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 6
    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
    ) {
856
        return "EXEC sp_updateextendedproperty " .
857 6
        "N" . $this->quoteStringLiteral($name) . ", N" . $this->quoteStringLiteral($value) . ", " .
858 6
        "N" . $this->quoteStringLiteral($level0Type) . ", " . $level0Name . ', ' .
859 6
        "N" . $this->quoteStringLiteral($level1Type) . ", " . $level1Name . ', ' .
860 6
        "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
     * {@inheritDoc}
883
     */
884 6
    public function getListTableColumnsSQL($table, $database = null)
885
    {
886
        return "SELECT    col.name,
887
                          type.name AS type,
888
                          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
                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
                WHERE     obj.type = 'U'
911 6
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
912
    }
913
914
    /**
915
     * {@inheritDoc}
916
     */
917 6
    public function getListTableForeignKeysSQL($table, $database = null)
0 ignored issues
show
Unused Code introduced by
The parameter $database is not used and could be removed. ( Ignorable by Annotation )

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

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
                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 6
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
934
    }
935
936
    /**
937
     * {@inheritDoc}
938
     */
939 6
    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 6
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . "
956
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC";
957
    }
958
959
    /**
960
     * {@inheritDoc}
961
     */
962
    public function getCreateViewSQL($name, $sql)
963
    {
964
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
965
    }
966
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 18
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
985
    {
986 18
        if (strpos($table, ".") !== false) {
987 9
            list($schema, $table) = explode(".", $table);
988 9
            $schema = $this->quoteStringLiteral($schema);
989 9
            $table = $this->quoteStringLiteral($table);
990
        } else {
991 9
            $schema = "SCHEMA_NAME()";
992 9
            $table = $this->quoteStringLiteral($table);
993
        }
994
995 18
        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
1053
            return $trimFn . '(' . $str . ')';
1054
        }
1055
1056
        /** 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
          , 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
         */
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 3
    public function getConcatExpression()
1082
    {
1083 3
        $args = func_get_args();
1084
1085 3
        return '(' . implode(' + ', $args) . ')';
1086
    }
1087
1088
    /**
1089
     * {@inheritDoc}
1090
     */
1091 3
    public function getListDatabasesSQL()
1092
    {
1093 3
        return 'SELECT * FROM sys.databases';
1094
    }
1095
1096
    /**
1097
     * {@inheritDoc}
1098
     */
1099
    public function getListNamespacesSQL()
1100
    {
1101
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1102
    }
1103
1104
    /**
1105
     * {@inheritDoc}
1106
     */
1107
    public function getSubstringExpression($value, $from, $length = null)
1108
    {
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 3
    public function getSetTransactionIsolationSQL($level)
1128
    {
1129 3
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1130
    }
1131
1132
    /**
1133
     * {@inheritDoc}
1134
     */
1135 43
    public function getIntegerTypeDeclarationSQL(array $field)
1136
    {
1137 43
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1138
    }
1139
1140
    /**
1141
     * {@inheritDoc}
1142
     */
1143
    public function getBigIntTypeDeclarationSQL(array $field)
1144
    {
1145
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1146
    }
1147
1148
    /**
1149
     * {@inheritDoc}
1150
     */
1151
    public function getSmallIntTypeDeclarationSQL(array $field)
1152
    {
1153
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1154
    }
1155
1156
    /**
1157
     * {@inheritDoc}
1158
     */
1159 3
    public function getGuidTypeDeclarationSQL(array $field)
1160
    {
1161 3
        return 'UNIQUEIDENTIFIER';
1162
    }
1163
1164
    /**
1165
     * {@inheritDoc}
1166
     */
1167 69
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1168
    {
1169 69
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1170
    }
1171
1172
    /**
1173
     * {@inheritdoc}
1174
     */
1175 3
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1176
    {
1177 3
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1178
    }
1179
1180
    /**
1181
     * {@inheritdoc}
1182
     */
1183 6
    public function getBinaryMaxLength()
1184
    {
1185 6
        return 8000;
1186
    }
1187
1188
    /**
1189
     * {@inheritDoc}
1190
     */
1191 5
    public function getClobTypeDeclarationSQL(array $field)
1192
    {
1193 5
        return 'VARCHAR(MAX)';
1194
    }
1195
1196
    /**
1197
     * {@inheritDoc}
1198
     */
1199 43
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1200
    {
1201 43
        return (!empty($columnDef['autoincrement'])) ? ' IDENTITY' : '';
1202
    }
1203
1204
    /**
1205
     * {@inheritDoc}
1206
     */
1207
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1208
    {
1209
        return 'DATETIME';
1210
    }
1211
1212
    /**
1213
     * {@inheritDoc}
1214
     */
1215
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1216
    {
1217
        return 'DATETIME';
1218
    }
1219
1220
    /**
1221
     * {@inheritDoc}
1222
     */
1223
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1224
    {
1225
        return 'DATETIME';
1226
    }
1227
1228
    /**
1229
     * {@inheritDoc}
1230
     */
1231 3
    public function getBooleanTypeDeclarationSQL(array $field)
1232
    {
1233 3
        return 'BIT';
1234
    }
1235
1236
    /**
1237
     * {@inheritDoc}
1238
     */
1239 40
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1240
    {
1241 40
        $where = [];
1242
1243 40
        if ($offset > 0) {
1244 4
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1245
        }
1246
1247 40
        if ($limit !== null) {
1248 40
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1249 40
            $top     = sprintf('TOP %d', $offset + $limit);
1250
        } else {
1251
            $top = 'TOP 9223372036854775807';
1252
        }
1253
1254 40
        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 40
        $selectPattern = '/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/im';
1263 40
        $replacePattern = sprintf('$1%s $2', $top);
1264 40
        $query = preg_replace($selectPattern, $replacePattern, $query);
1265
1266 40
        if (stristr($query, "ORDER BY")) {
1267
            // Inner order by is not valid in SQL Server for our purposes
1268
            // unless it's in a TOP N subquery.
1269 26
            $query = $this->scrubInnerOrderBy($query);
1270
        }
1271
1272
        // Build a new limited query around the original, using a CTE
1273 40
        return sprintf(
1274
            "WITH dctrn_cte AS (%s) "
1275
            . "SELECT * FROM ("
1276
            . "SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte"
1277
            . ") AS doctrine_tbl "
1278 40
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1279 40
            $query,
1280 40
            implode(' AND ', $where)
1281
        );
1282
    }
1283
1284
    /**
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
     *
1288
     * @param $query
1289
     * @return string
1290
     */
1291 26
    private function scrubInnerOrderBy($query)
1292
    {
1293 26
        $count = substr_count(strtoupper($query), 'ORDER BY');
1294 26
        $offset = 0;
1295
1296 26
        while ($count-- > 0) {
1297 26
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1298 26
            if ($orderByPos === false) {
1299 2
                break;
1300
            }
1301
1302 26
            $qLen = strlen($query);
1303 26
            $parenCount = 0;
1304 26
            $currentPosition = $orderByPos;
1305
1306 26
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1307 26
                if ($query[$currentPosition] === '(') {
1308 2
                    $parenCount++;
1309 26
                } elseif ($query[$currentPosition] === ')') {
1310 14
                    $parenCount--;
1311
                }
1312
1313 26
                $currentPosition++;
1314
            }
1315
1316 26
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1317
                // If the order by clause is in a TOP N subquery, do not remove
1318
                // it and continue iteration from the current position.
1319 23
                $offset = $currentPosition;
1320 23
                continue;
1321
            }
1322
1323 10
            if ($currentPosition < $qLen - 1) {
1324 10
                $query = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1325 10
                $offset = $orderByPos;
1326
            }
1327
        }
1328 26
        return $query;
1329
    }
1330
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 26
    private function isOrderByInTopNSubquery($query, $currentPosition)
1339
    {
1340
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1341 26
        $subQueryBuffer = '';
1342 26
        $parenCount = 0;
1343
1344
        // 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 26
        while ($parenCount >= 0 && $currentPosition >= 0) {
1347 26
            if ($query[$currentPosition] === '(') {
1348 17
                $parenCount--;
1349 26
            } elseif ($query[$currentPosition] === ')') {
1350 14
                $parenCount++;
1351
            }
1352
1353
            // Only yank query text on the same nesting level as the ORDER BY clause.
1354 26
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1355
1356 26
            $currentPosition--;
1357
        }
1358
1359 26
        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 3
    public function convertBooleans($item)
1374
    {
1375 3
        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 3
        } elseif (is_bool($item) || is_numeric($item)) {
1382 3
            $item = ($item) ? 1 : 0;
1383
        }
1384
1385 3
        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
    }
1411
1412
    /**
1413
     * {@inheritDoc}
1414
     */
1415
    public function getDateFormatString()
1416
    {
1417
        return 'Y-m-d H:i:s.000';
1418
    }
1419
1420
    /**
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 1
    public function getName()
1440
    {
1441 1
        return 'mssql';
1442
    }
1443
1444
    /**
1445
     * {@inheritDoc}
1446
     */
1447 15
    protected function initializeDoctrineTypeMappings()
1448
    {
1449 15
        $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 15
    }
1477
1478
    /**
1479
     * {@inheritDoc}
1480
     */
1481
    public function createSavePoint($savepoint)
1482
    {
1483
        return 'SAVE TRANSACTION ' . $savepoint;
1484
    }
1485
1486
    /**
1487
     * {@inheritDoc}
1488
     */
1489
    public function releaseSavePoint($savepoint)
1490
    {
1491
        return '';
1492
    }
1493
1494
    /**
1495
     * {@inheritDoc}
1496
     */
1497
    public function rollbackSavePoint($savepoint)
1498
    {
1499
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1500
    }
1501
1502
    /**
1503
     * {@inheritdoc}
1504
     */
1505 21
    public function getForeignKeyReferentialActionSQL($action)
1506
    {
1507
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1508 21
        if (strtoupper($action) === 'RESTRICT') {
1509 3
            return 'NO ACTION';
1510
        }
1511
1512 18
        return parent::getForeignKeyReferentialActionSQL($action);
1513
    }
1514
1515
    /**
1516
     * {@inheritDoc}
1517
     */
1518 7
    public function appendLockHint($fromClause, $lockMode)
1519
    {
1520
        switch (true) {
1521 7
            case LockMode::NONE === $lockMode:
1522 1
                return $fromClause . ' WITH (NOLOCK)';
1523
1524 6
            case LockMode::PESSIMISTIC_READ === $lockMode:
1525 1
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1526
1527 5
            case LockMode::PESSIMISTIC_WRITE === $lockMode:
1528 1
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1529
1530
            default:
1531 4
                return $fromClause;
1532
        }
1533
    }
1534
1535
    /**
1536
     * {@inheritDoc}
1537
     */
1538
    public function getForUpdateSQL()
1539
    {
1540
        return ' ';
1541
    }
1542
1543
    /**
1544
     * {@inheritDoc}
1545
     */
1546 61
    protected function getReservedKeywordsClass()
1547
    {
1548 61
        return Keywords\SQLServerKeywords::class;
1549
    }
1550
1551
    /**
1552
     * {@inheritDoc}
1553
     */
1554 91
    public function quoteSingleIdentifier($str)
1555
    {
1556 91
        return "[" . str_replace("]", "][", $str) . "]";
1557
    }
1558
1559
    /**
1560
     * {@inheritDoc}
1561
     */
1562 3
    public function getTruncateTableSQL($tableName, $cascade = false)
1563
    {
1564 3
        $tableIdentifier = new Identifier($tableName);
1565
1566 3
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1567
    }
1568
1569
    /**
1570
     * {@inheritDoc}
1571
     */
1572 3
    public function getBlobTypeDeclarationSQL(array $field)
1573
    {
1574 3
        return 'VARBINARY(MAX)';
1575
    }
1576
1577
    /**
1578
     * {@inheritDoc}
1579
     */
1580 57
    public function getDefaultValueDeclarationSQL($field)
1581
    {
1582 57
        if ( ! isset($field['default'])) {
1583
            return empty($field['notnull']) ? ' NULL' : '';
1584
        }
1585
1586 57
        if ( ! isset($field['type'])) {
1587 12
            return " DEFAULT '" . $field['default'] . "'";
1588
        }
1589
1590 45
        $type = $field['type'];
1591
1592 45
        if ($type instanceof Types\PhpIntegerMappingType) {
1593 9
            return " DEFAULT " . $field['default'];
1594
        }
1595
1596 39
        if ($type instanceof Types\PhpDateTimeMappingType && $field['default'] === $this->getCurrentTimestampSQL()) {
1597 3
            return " DEFAULT " . $this->getCurrentTimestampSQL();
1598
        }
1599
1600 36
        if ($type instanceof Types\BooleanType) {
1601 3
            return " DEFAULT '" . $this->convertBooleans($field['default']) . "'";
1602
        }
1603
1604 36
        return " DEFAULT '" . $field['default'] . "'";
1605
    }
1606
1607
    /**
1608
     * {@inheritdoc}
1609
     *
1610
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1611
     */
1612 91
    public function getColumnDeclarationSQL($name, array $field)
1613
    {
1614 91
        if (isset($field['columnDefinition'])) {
1615 3
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1616
        } else {
1617 88
            $collation = (isset($field['collation']) && $field['collation']) ?
1618 88
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1619
1620 88
            $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : '';
1621
1622 88
            $unique = (isset($field['unique']) && $field['unique']) ?
1623 88
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1624
1625 88
            $check = (isset($field['check']) && $field['check']) ?
1626 88
                ' ' . $field['check'] : '';
1627
1628 88
            $typeDecl = $field['type']->getSQLDeclaration($field, $this);
1629 88
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1630
        }
1631
1632 91
        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 45
    private function generateDefaultConstraintName($table, $column)
1644
    {
1645 45
        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 48
    private function generateIdentifierName($identifier)
1656
    {
1657
        // Always generate name for unquoted identifiers to ensure consistency.
1658 48
        $identifier = new Identifier($identifier);
1659
1660 48
        return strtoupper(dechex(crc32($identifier->getName())));
1661
    }
1662
}
1663