Failed Conditions
Push — master ( 6e3afd...ba421d )
by Marco
18:55 queued 08:20
created

lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
31
/**
32
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
33
 * Microsoft SQL Server database platform.
34
 *
35
 * @since 2.0
36
 * @author Roman Borschel <[email protected]>
37
 * @author Jonathan H. Wage <[email protected]>
38
 * @author Benjamin Eberlei <[email protected]>
39
 * @author Steve Müller <[email protected]>
40
 */
41
class SQLServerPlatform extends AbstractPlatform
42
{
43
    /**
44
     * {@inheritdoc}
45
     */
46 6
    public function getCurrentDateSQL()
47
    {
48 6
        return $this->getConvertExpression('date', 'GETDATE()');
49
    }
50
51
    /**
52
     * {@inheritdoc}
53
     */
54 3
    public function getCurrentTimeSQL()
55
    {
56 3
        return $this->getConvertExpression('time', 'GETDATE()');
57
    }
58
59
    /**
60
     * Returns an expression that converts an expression of one data type to another.
61
     *
62
     * @param string $dataType   The target native data type. Alias data types cannot be used.
63
     * @param string $expression The SQL expression to convert.
64
     *
65
     * @return string
66
     */
67 6
    private function getConvertExpression($dataType, $expression)
68
    {
69 6
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75 View Code Duplication
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
76
    {
77
        $factorClause = '';
78
79
        if ('-' === $operator) {
80
            $factorClause = '-1 * ';
81
        }
82
83
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
84
    }
85
86
    /**
87
     * {@inheritDoc}
88
     */
89
    public function getDateDiffExpression($date1, $date2)
90
    {
91
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
92
    }
93
94
    /**
95
     * {@inheritDoc}
96
     *
97
     * Microsoft SQL Server prefers "autoincrement" identity columns
98
     * since sequences can only be emulated with a table.
99
     */
100 3
    public function prefersIdentityColumns()
101
    {
102 3
        return true;
103
    }
104
105
    /**
106
     * {@inheritDoc}
107
     *
108
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
109
     */
110 3
    public function supportsIdentityColumns()
111
    {
112 3
        return true;
113
    }
114
115
    /**
116
     * {@inheritDoc}
117
     */
118
    public function supportsReleaseSavepoints()
119
    {
120
        return false;
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126 3
    public function supportsSchemas()
127
    {
128 3
        return true;
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134
    public function getDefaultSchemaName()
135
    {
136
        return 'dbo';
137
    }
138
139
    /**
140
     * {@inheritDoc}
141
     */
142
    public function supportsColumnCollation()
143
    {
144
        return true;
145
    }
146
147
    /**
148
     * {@inheritDoc}
149
     */
150 160
    public function hasNativeGuidType()
151
    {
152 160
        return true;
153
    }
154
155
    /**
156
     * {@inheritDoc}
157
     */
158 3
    public function getCreateDatabaseSQL($name)
159
    {
160 3
        return 'CREATE DATABASE ' . $name;
161
    }
162
163
    /**
164
     * {@inheritDoc}
165
     */
166 3
    public function getDropDatabaseSQL($name)
167
    {
168 3
        return 'DROP DATABASE ' . $name;
169
    }
170
171
    /**
172
     * {@inheritDoc}
173
     */
174 3
    public function supportsCreateDropDatabase()
175
    {
176 3
        return true;
177
    }
178
179
    /**
180
     * {@inheritDoc}
181
     */
182 3
    public function getCreateSchemaSQL($schemaName)
183
    {
184 3
        return 'CREATE SCHEMA ' . $schemaName;
185
    }
186
187
    /**
188
     * {@inheritDoc}
189
     */
190 6 View Code Duplication
    public function getDropForeignKeySQL($foreignKey, $table)
191
    {
192 6
        if (! $foreignKey instanceof ForeignKeyConstraint) {
193 3
            $foreignKey = new Identifier($foreignKey);
194
        }
195
196 6
        if (! $table instanceof Table) {
197 6
            $table = new Identifier($table);
198
        }
199
200 6
        $foreignKey = $foreignKey->getQuotedName($this);
201 6
        $table = $table->getQuotedName($this);
202
203 6
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
204
    }
205
206
    /**
207
     * {@inheritDoc}
208
     */
209
    public function getDropIndexSQL($index, $table = null)
210
    {
211
        if ($index instanceof Index) {
212
            $index = $index->getQuotedName($this);
213
        } elseif (!is_string($index)) {
214
            throw new \InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
215
        }
216
217
        if (!isset($table)) {
218
            return 'DROP INDEX ' . $index;
219
        }
220
221
        if ($table instanceof Table) {
222
            $table = $table->getQuotedName($this);
223
        }
224
225
        return "IF EXISTS (SELECT * FROM sysobjects WHERE name = '$index')
226
                    ALTER TABLE " . $table . " DROP CONSTRAINT " . $index . "
227
                ELSE
228
                    DROP INDEX " . $index . " ON " . $table;
229
    }
230
231
    /**
232
     * {@inheritDoc}
233
     */
234 46
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
235
    {
236 46
        $defaultConstraintsSql = [];
237 46
        $commentsSql           = [];
238
239
        // @todo does other code breaks because of this?
240
        // force primary keys to be not null
241 46
        foreach ($columns as &$column) {
242 46
            if (isset($column['primary']) && $column['primary']) {
243 15
                $column['notnull'] = true;
244
            }
245
246
            // Build default constraints SQL statements.
247 46
            if (isset($column['default'])) {
248 12
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
249 12
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
250
            }
251
252 46
            if ( ! empty($column['comment']) || is_numeric($column['comment'])) {
253 46
                $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
254
            }
255
        }
256
257 46
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
258
259 46
        if (isset($options['uniqueConstraints']) && !empty($options['uniqueConstraints'])) {
260
            foreach ($options['uniqueConstraints'] as $name => $definition) {
261
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
262
            }
263
        }
264
265 46 View Code Duplication
        if (isset($options['primary']) && !empty($options['primary'])) {
266 18
            $flags = '';
267 18
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
268 3
                $flags = ' NONCLUSTERED';
269
            }
270 18
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
271
        }
272
273 46
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
274
275 46
        $check = $this->getCheckDeclarationSQL($columns);
276 46
        if (!empty($check)) {
277
            $query .= ', ' . $check;
278
        }
279 46
        $query .= ')';
280
281 46
        $sql[] = $query;
282
283 46 View Code Duplication
        if (isset($options['indexes']) && !empty($options['indexes'])) {
284 9
            foreach ($options['indexes'] as $index) {
285 9
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
286
            }
287
        }
288
289 46 View Code Duplication
        if (isset($options['foreignKeys'])) {
290 3
            foreach ((array) $options['foreignKeys'] as $definition) {
291 3
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
292
            }
293
        }
294
295 46
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
296
    }
297
298
    /**
299
     * {@inheritDoc}
300
     */
301 6
    public function getCreatePrimaryKeySQL(Index $index, $table)
302
    {
303 6
        $flags = '';
304 6
        if ($index->hasFlag('nonclustered')) {
305 3
            $flags = ' NONCLUSTERED';
306
        }
307
308 6
        return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY' . $flags . ' (' . $this->getIndexFieldDeclarationListSQL($index->getQuotedColumns($this)) . ')';
309
    }
310
311
    /**
312
     * Returns the SQL statement for creating a column comment.
313
     *
314
     * SQL Server does not support native column comments,
315
     * therefore the extended properties functionality is used
316
     * as a workaround to store them.
317
     * The property name used to store column comments is "MS_Description"
318
     * which provides compatibility with SQL Server Management Studio,
319
     * as column comments are stored in the same property there when
320
     * specifying a column's "Description" attribute.
321
     *
322
     * @param string $tableName  The quoted table name to which the column belongs.
323
     * @param string $columnName The quoted column name to create the comment for.
324
     * @param string $comment    The column's comment.
325
     *
326
     * @return string
327
     */
328 15
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
329
    {
330 15
        return $this->getAddExtendedPropertySQL(
331 15
            'MS_Description',
332 15
            $comment,
333 15
            'SCHEMA',
334 15
            'dbo',
335 15
            'TABLE',
336 15
            $tableName,
337 15
            'COLUMN',
338 15
            $columnName
339
        );
340
    }
341
342
    /**
343
     * Returns the SQL snippet for declaring a default constraint.
344
     *
345
     * @param string $table  Name of the table to return the default constraint declaration for.
346
     * @param array  $column Column definition.
347
     *
348
     * @return string
349
     *
350
     * @throws \InvalidArgumentException
351
     */
352 45
    public function getDefaultConstraintDeclarationSQL($table, array $column)
353
    {
354 45
        if ( ! isset($column['default'])) {
355
            throw new \InvalidArgumentException("Incomplete column definition. 'default' required.");
356
        }
357
358 45
        $columnName = new Identifier($column['name']);
359
360
        return
361
            ' CONSTRAINT ' .
362 45
            $this->generateDefaultConstraintName($table, $column['name']) .
363 45
            $this->getDefaultValueDeclarationSQL($column) .
364 45
            ' FOR ' . $columnName->getQuotedName($this);
365
    }
366
367
    /**
368
     * {@inheritDoc}
369
     */
370 6
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
371
    {
372 6
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
373
374 6
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
375
376 6
        return $constraint;
377
    }
378
379
    /**
380
     * {@inheritDoc}
381
     */
382 24
    public function getCreateIndexSQL(Index $index, $table)
383
    {
384 24
        $constraint = parent::getCreateIndexSQL($index, $table);
385
386 24
        if ($index->isUnique() && !$index->isPrimary()) {
387 6
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
388
        }
389
390 24
        return $constraint;
391
    }
392
393
    /**
394
     * {@inheritDoc}
395
     */
396 24 View Code Duplication
    protected function getCreateIndexSQLFlags(Index $index)
397
    {
398 24
        $type = '';
399 24
        if ($index->isUnique()) {
400 6
            $type .= 'UNIQUE ';
401
        }
402
403 24
        if ($index->hasFlag('clustered')) {
404 3
            $type .= 'CLUSTERED ';
405 21
        } elseif ($index->hasFlag('nonclustered')) {
406
            $type .= 'NONCLUSTERED ';
407
        }
408
409 24
        return $type;
410
    }
411
412
    /**
413
     * Extend unique key constraint with required filters
414
     *
415
     * @param string                      $sql
416
     * @param \Doctrine\DBAL\Schema\Index $index
417
     *
418
     * @return string
419
     */
420 12
    private function _appendUniqueConstraintDefinition($sql, Index $index)
421
    {
422 12
        $fields = [];
423
424 12
        foreach ($index->getQuotedColumns($this) as $field) {
425 12
            $fields[] = $field . ' IS NOT NULL';
426
        }
427
428 12
        return $sql . ' WHERE ' . implode(' AND ', $fields);
429
    }
430
431
    /**
432
     * {@inheritDoc}
433
     */
434 57
    public function getAlterTableSQL(TableDiff $diff)
435
    {
436 57
        $queryParts  = [];
437 57
        $sql         = [];
438 57
        $columnSql   = [];
439 57
        $commentsSql = [];
440
441
        /** @var \Doctrine\DBAL\Schema\Column $column */
442 57
        foreach ($diff->addedColumns as $column) {
443 27
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
444
                continue;
445
            }
446
447 27
            $columnDef = $column->toArray();
448 27
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
449
450 27
            if (isset($columnDef['default'])) {
451 12
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
452
            }
453
454 27
            $comment = $this->getColumnComment($column);
455
456 27
            if ( ! empty($comment) || is_numeric($comment)) {
457 6
                $commentsSql[] = $this->getCreateColumnCommentSQL(
458 6
                    $diff->name,
459 6
                    $column->getQuotedName($this),
460 27
                    $comment
461
                );
462
            }
463
        }
464
465 57 View Code Duplication
        foreach ($diff->removedColumns as $column) {
466 24
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
467
                continue;
468
            }
469
470 24
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
471
        }
472
473
        /* @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
474 57
        foreach ($diff->changedColumns as $columnDiff) {
475 36
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
476
                continue;
477
            }
478
479 36
            $column     = $columnDiff->column;
480 36
            $comment    = $this->getColumnComment($column);
481 36
            $hasComment = ! empty ($comment) || is_numeric($comment);
482
483 36
            if ($columnDiff->fromColumn instanceof Column) {
484 21
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
485 21
                $hasFromComment = ! empty ($fromComment) || is_numeric($fromComment);
486
487 21
                if ($hasFromComment && $hasComment && $fromComment != $comment) {
488 3
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
489 3
                        $diff->name,
490 3
                        $column->getQuotedName($this),
491 3
                        $comment
492
                    );
493 21
                } elseif ($hasFromComment && ! $hasComment) {
494 3
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
495 21
                } elseif ($hasComment) {
496 3
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
497 3
                        $diff->name,
498 3
                        $column->getQuotedName($this),
499 21
                        $comment
500
                    );
501
                }
502
            } else {
503
                // todo: Original comment cannot be determined. What to do? Add, update, drop or skip?
504
            }
505
506
            // Do not add query part if only comment has changed.
507 36
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
508 6
                continue;
509
            }
510
511 33
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
512
513 33
            if ($requireDropDefaultConstraint) {
514 15
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
515 15
                    $diff->name,
516 15
                    $columnDiff->oldColumnName
517
                );
518
            }
519
520 33
            $columnDef = $column->toArray();
521
522 33
            $queryParts[] = 'ALTER COLUMN ' .
523 33
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
524
525 33
            if (isset($columnDef['default']) && ($requireDropDefaultConstraint || $columnDiff->hasChanged('default'))) {
526 33
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
527
            }
528
        }
529
530 57
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
531 15
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
532
                continue;
533
            }
534
535 15
            $oldColumnName = new Identifier($oldColumnName);
536
537 15
            $sql[] = "sp_RENAME '" .
538 15
                $diff->getName($this)->getQuotedName($this) . "." . $oldColumnName->getQuotedName($this) .
539 15
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
540
541
            // Recreate default constraint with new column name if necessary (for future reference).
542 15
            if ($column->getDefault() !== null) {
543 3
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
544 3
                    $diff->name,
545 3
                    $oldColumnName->getQuotedName($this)
546
                );
547 15
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
548
            }
549
        }
550
551 57
        $tableSql = [];
552
553 57
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
554
            return array_merge($tableSql, $columnSql);
555
        }
556
557 57
        foreach ($queryParts as $query) {
558 39
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
559
        }
560
561 57
        $sql = array_merge($sql, $commentsSql);
562
563 57
        if ($diff->newName !== false) {
564 6
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $diff->getNewName()->getName() . "'";
565
566
            /**
567
             * Rename table's default constraints names
568
             * to match the new table name.
569
             * This is necessary to ensure that the default
570
             * constraints can be referenced in future table
571
             * alterations as the table name is encoded in
572
             * default constraints' names.
573
             */
574 6
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
575
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
576 6
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
577 6
                "'" . $this->generateIdentifierName($diff->newName) . "') + ''', ''OBJECT'';' " .
578 6
                "FROM sys.default_constraints dc " .
579 6
                "JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id " .
580 6
                "WHERE tbl.name = '" . $diff->getNewName()->getName() . "';" .
581 6
                "EXEC sp_executesql @sql";
582
        }
583
584 57
        $sql = array_merge(
585 57
            $this->getPreAlterTableIndexForeignKeySQL($diff),
586 57
            $sql,
587 57
            $this->getPostAlterTableIndexForeignKeySQL($diff)
588
        );
589
590 57
        return array_merge($sql, $tableSql, $columnSql);
591
    }
592
593
    /**
594
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
595
     *
596
     * @param string $tableName The name of the table to generate the clause for.
597
     * @param Column $column    The column to generate the clause for.
598
     *
599
     * @return string
600
     */
601 21
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
602
    {
603 21
        $columnDef = $column->toArray();
604 21
        $columnDef['name'] = $column->getQuotedName($this);
605
606 21
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
607
    }
608
609
    /**
610
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
611
     *
612
     * @param string $tableName  The name of the table to generate the clause for.
613
     * @param string $columnName The name of the column to generate the clause for.
614
     *
615
     * @return string
616
     */
617 18
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
618
    {
619 18
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
620
    }
621
622
    /**
623
     * Checks whether a column alteration requires dropping its default constraint first.
624
     *
625
     * Different to other database vendors SQL Server implements column default values
626
     * as constraints and therefore changes in a column's default value as well as changes
627
     * in a column's type require dropping the default constraint first before being to
628
     * alter the particular column to the new definition.
629
     *
630
     * @param ColumnDiff $columnDiff The column diff to evaluate.
631
     *
632
     * @return boolean True if the column alteration requires dropping its default constraint first, false otherwise.
633
     */
634 33
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
635
    {
636
        // We can only decide whether to drop an existing default constraint
637
        // if we know the original default value.
638 33
        if ( ! $columnDiff->fromColumn instanceof Column) {
639 12
            return false;
640
        }
641
642
        // We only need to drop an existing default constraint if we know the
643
        // column was defined with a default value before.
644 21
        if ($columnDiff->fromColumn->getDefault() === null) {
645 6
            return false;
646
        }
647
648
        // We need to drop an existing default constraint if the column was
649
        // defined with a default value before and it has changed.
650 15
        if ($columnDiff->hasChanged('default')) {
651 12
            return true;
652
        }
653
654
        // We need to drop an existing default constraint if the column was
655
        // defined with a default value before and the native column type has changed.
656 3
        if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed')) {
657 3
            return true;
658
        }
659
660
        return false;
661
    }
662
663
    /**
664
     * Returns the SQL statement for altering a column comment.
665
     *
666
     * SQL Server does not support native column comments,
667
     * therefore the extended properties functionality is used
668
     * as a workaround to store them.
669
     * The property name used to store column comments is "MS_Description"
670
     * which provides compatibility with SQL Server Management Studio,
671
     * as column comments are stored in the same property there when
672
     * specifying a column's "Description" attribute.
673
     *
674
     * @param string $tableName  The quoted table name to which the column belongs.
675
     * @param string $columnName The quoted column name to alter the comment for.
676
     * @param string $comment    The column's comment.
677
     *
678
     * @return string
679
     */
680 3
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
681
    {
682 3
        return $this->getUpdateExtendedPropertySQL(
683 3
            'MS_Description',
684 3
            $comment,
685 3
            'SCHEMA',
686 3
            'dbo',
687 3
            'TABLE',
688 3
            $tableName,
689 3
            'COLUMN',
690 3
            $columnName
691
        );
692
    }
693
694
    /**
695
     * Returns the SQL statement for dropping a column comment.
696
     *
697
     * SQL Server does not support native column comments,
698
     * therefore the extended properties functionality is used
699
     * as a workaround to store them.
700
     * The property name used to store column comments is "MS_Description"
701
     * which provides compatibility with SQL Server Management Studio,
702
     * as column comments are stored in the same property there when
703
     * specifying a column's "Description" attribute.
704
     *
705
     * @param string $tableName  The quoted table name to which the column belongs.
706
     * @param string $columnName The quoted column name to drop the comment for.
707
     *
708
     * @return string
709
     */
710 3
    protected function getDropColumnCommentSQL($tableName, $columnName)
711
    {
712 3
        return $this->getDropExtendedPropertySQL(
713 3
            'MS_Description',
714 3
            'SCHEMA',
715 3
            'dbo',
716 3
            'TABLE',
717 3
            $tableName,
718 3
            'COLUMN',
719 3
            $columnName
720
        );
721
    }
722
723
    /**
724
     * {@inheritdoc}
725
     */
726 15
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
727
    {
728
        return [
729 15
            sprintf(
730 15
                "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
731 15
                $tableName,
732 15
                $oldIndexName,
733 15
                $index->getQuotedName($this)
734
            )
735
        ];
736
    }
737
738
    /**
739
     * Returns the SQL statement for adding an extended property to a database object.
740
     *
741
     * @param string      $name       The name of the property to add.
742
     * @param string|null $value      The value of the property to add.
743
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
744
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
745
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
746
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
747
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
748
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
749
     *
750
     * @return string
751
     *
752
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
753
     */
754 15 View Code Duplication
    public function getAddExtendedPropertySQL(
755
        $name,
756
        $value = null,
757
        $level0Type = null,
758
        $level0Name = null,
759
        $level1Type = null,
760
        $level1Name = null,
761
        $level2Type = null,
762
        $level2Name = null
763
    ) {
764
        return "EXEC sp_addextendedproperty " .
765 15
            "N" . $this->quoteStringLiteral($name) . ", N" . $this->quoteStringLiteral($value) . ", " .
766 15
            "N" . $this->quoteStringLiteral($level0Type) . ", " . $level0Name . ', ' .
767 15
            "N" . $this->quoteStringLiteral($level1Type) . ", " . $level1Name . ', ' .
768 15
            "N" . $this->quoteStringLiteral($level2Type) . ", " . $level2Name;
769
    }
770
771
    /**
772
     * Returns the SQL statement for dropping an extended property from a database object.
773
     *
774
     * @param string      $name       The name of the property to drop.
775
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
776
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
777
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
778
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
779
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
780
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
781
     *
782
     * @return string
783
     *
784
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
785
     */
786 3
    public function getDropExtendedPropertySQL(
787
        $name,
788
        $level0Type = null,
789
        $level0Name = null,
790
        $level1Type = null,
791
        $level1Name = null,
792
        $level2Type = null,
793
        $level2Name = null
794
    ) {
795
        return "EXEC sp_dropextendedproperty " .
796 3
            "N" . $this->quoteStringLiteral($name) . ", " .
797 3
            "N" . $this->quoteStringLiteral($level0Type) . ", " . $level0Name . ', ' .
798 3
            "N" . $this->quoteStringLiteral($level1Type) . ", " . $level1Name . ', ' .
799 3
            "N" . $this->quoteStringLiteral($level2Type) . ", " . $level2Name;
800
    }
801
802
    /**
803
     * Returns the SQL statement for updating an extended property of a database object.
804
     *
805
     * @param string      $name       The name of the property to update.
806
     * @param string|null $value      The value of the property to update.
807
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
808
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
809
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
810
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
811
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
812
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
813
     *
814
     * @return string
815
     *
816
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
817
     */
818 3 View Code Duplication
    public function getUpdateExtendedPropertySQL(
819
        $name,
820
        $value = null,
821
        $level0Type = null,
822
        $level0Name = null,
823
        $level1Type = null,
824
        $level1Name = null,
825
        $level2Type = null,
826
        $level2Name = null
827
    ) {
828
        return "EXEC sp_updateextendedproperty " .
829 3
        "N" . $this->quoteStringLiteral($name) . ", N" . $this->quoteStringLiteral($value) . ", " .
830 3
        "N" . $this->quoteStringLiteral($level0Type) . ", " . $level0Name . ', ' .
831 3
        "N" . $this->quoteStringLiteral($level1Type) . ", " . $level1Name . ', ' .
832 3
        "N" . $this->quoteStringLiteral($level2Type) . ", " . $level2Name;
833
    }
834
835
    /**
836
     * {@inheritDoc}
837
     */
838
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
839
    {
840
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
841
    }
842
843
    /**
844
     * {@inheritDoc}
845
     */
846
    public function getListTablesSQL()
847
    {
848
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
849
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
850
        return "SELECT name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
851
    }
852
853
    /**
854
     * {@inheritDoc}
855
     */
856 6
    public function getListTableColumnsSQL($table, $database = null)
857
    {
858
        return "SELECT    col.name,
859
                          type.name AS type,
860
                          col.max_length AS length,
861
                          ~col.is_nullable AS notnull,
862
                          def.definition AS [default],
863
                          col.scale,
864
                          col.precision,
865
                          col.is_identity AS autoincrement,
866
                          col.collation_name AS collation,
867
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
868
                FROM      sys.columns AS col
869
                JOIN      sys.types AS type
870
                ON        col.user_type_id = type.user_type_id
871
                JOIN      sys.objects AS obj
872
                ON        col.object_id = obj.object_id
873
                JOIN      sys.schemas AS scm
874
                ON        obj.schema_id = scm.schema_id
875
                LEFT JOIN sys.default_constraints def
876
                ON        col.default_object_id = def.object_id
877
                AND       col.object_id = def.parent_object_id
878
                LEFT JOIN sys.extended_properties AS prop
879
                ON        obj.object_id = prop.major_id
880
                AND       col.column_id = prop.minor_id
881
                AND       prop.name = 'MS_Description'
882
                WHERE     obj.type = 'U'
883 6
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
884
    }
885
886
    /**
887
     * {@inheritDoc}
888
     */
889 6
    public function getListTableForeignKeysSQL($table, $database = null)
890
    {
891
        return "SELECT f.name AS ForeignKey,
892
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
893
                OBJECT_NAME (f.parent_object_id) AS TableName,
894
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
895
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
896
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
897
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
898
                f.delete_referential_action_desc,
899
                f.update_referential_action_desc
900
                FROM sys.foreign_keys AS f
901
                INNER JOIN sys.foreign_key_columns AS fc
902
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
903
                ON f.OBJECT_ID = fc.constraint_object_id
904
                WHERE " .
905 6
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
906
    }
907
908
    /**
909
     * {@inheritDoc}
910
     */
911 6
    public function getListTableIndexesSQL($table, $currentDatabase = null)
912
    {
913
        return "SELECT idx.name AS key_name,
914
                       col.name AS column_name,
915
                       ~idx.is_unique AS non_unique,
916
                       idx.is_primary_key AS [primary],
917
                       CASE idx.type
918
                           WHEN '1' THEN 'clustered'
919
                           WHEN '2' THEN 'nonclustered'
920
                           ELSE NULL
921
                       END AS flags
922
                FROM sys.tables AS tbl
923
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
924
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
925
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
926
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
927 6
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . "
928
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC";
929
    }
930
931
    /**
932
     * {@inheritDoc}
933
     */
934
    public function getCreateViewSQL($name, $sql)
935
    {
936
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
937
    }
938
939
    /**
940
     * {@inheritDoc}
941
     */
942
    public function getListViewsSQL($database)
943
    {
944
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
945
    }
946
947
    /**
948
     * Returns the where clause to filter schema and table name in a query.
949
     *
950
     * @param string $table        The full qualified name of the table.
951
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
952
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
953
     *
954
     * @return string
955
     */
956 18 View Code Duplication
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
957
    {
958 18
        if (strpos($table, ".") !== false) {
959 9
            list($schema, $table) = explode(".", $table);
960 9
            $schema = $this->quoteStringLiteral($schema);
961 9
            $table = $this->quoteStringLiteral($table);
962
        } else {
963 9
            $schema = "SCHEMA_NAME()";
964 9
            $table = $this->quoteStringLiteral($table);
965
        }
966
967 18
        return "({$tableColumn} = {$table} AND {$schemaColumn} = {$schema})";
968
    }
969
970
    /**
971
     * {@inheritDoc}
972
     */
973
    public function getDropViewSQL($name)
974
    {
975
        return 'DROP VIEW ' . $name;
976
    }
977
978
    /**
979
     * {@inheritDoc}
980
     */
981
    public function getGuidExpression()
982
    {
983
        return 'NEWID()';
984
    }
985
986
    /**
987
     * {@inheritDoc}
988
     */
989 View Code Duplication
    public function getLocateExpression($str, $substr, $startPos = false)
990
    {
991
        if ($startPos == false) {
992
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
993
        }
994
995
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
996
    }
997
998
    /**
999
     * {@inheritDoc}
1000
     */
1001
    public function getModExpression($expression1, $expression2)
1002
    {
1003
        return $expression1 . ' % ' . $expression2;
1004
    }
1005
1006
    /**
1007
     * {@inheritDoc}
1008
     */
1009
    public function getTrimExpression($str, $pos = self::TRIM_UNSPECIFIED, $char = false)
1010
    {
1011
        if ( ! $char) {
1012 View Code Duplication
            switch ($pos) {
1013
                case self::TRIM_LEADING:
1014
                    $trimFn = 'LTRIM';
1015
                    break;
1016
1017
                case self::TRIM_TRAILING:
1018
                    $trimFn = 'RTRIM';
1019
                    break;
1020
1021
                default:
1022
                    return 'LTRIM(RTRIM(' . $str . '))';
1023
            }
1024
1025
            return $trimFn . '(' . $str . ')';
1026
        }
1027
1028
        /** Original query used to get those expressions
1029
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1030
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1031
          select @c as string
1032
          , @trim_char as trim_char
1033
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1034
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1035
          , 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;
1036
         */
1037
        $pattern = "'%[^' + $char + ']%'";
1038
1039 View Code Duplication
        if ($pos == self::TRIM_LEADING) {
1040
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1041
        }
1042
1043 View Code Duplication
        if ($pos == self::TRIM_TRAILING) {
1044
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1045
        }
1046
1047
        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))';
1048
    }
1049
1050
    /**
1051
     * {@inheritDoc}
1052
     */
1053 3
    public function getConcatExpression()
1054
    {
1055 3
        $args = func_get_args();
1056
1057 3
        return '(' . implode(' + ', $args) . ')';
1058
    }
1059
1060
    /**
1061
     * {@inheritDoc}
1062
     */
1063 3
    public function getListDatabasesSQL()
1064
    {
1065 3
        return 'SELECT * FROM sys.databases';
1066
    }
1067
1068
    /**
1069
     * {@inheritDoc}
1070
     */
1071
    public function getListNamespacesSQL()
1072
    {
1073
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1074
    }
1075
1076
    /**
1077
     * {@inheritDoc}
1078
     */
1079
    public function getSubstringExpression($value, $from, $length = null)
1080
    {
1081
        if (!is_null($length)) {
1082
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1083
        }
1084
1085
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1086
    }
1087
1088
    /**
1089
     * {@inheritDoc}
1090
     */
1091
    public function getLengthExpression($column)
1092
    {
1093
        return 'LEN(' . $column . ')';
1094
    }
1095
1096
    /**
1097
     * {@inheritDoc}
1098
     */
1099 3
    public function getSetTransactionIsolationSQL($level)
1100
    {
1101 3
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1102
    }
1103
1104
    /**
1105
     * {@inheritDoc}
1106
     */
1107 37
    public function getIntegerTypeDeclarationSQL(array $field)
1108
    {
1109 37
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1110
    }
1111
1112
    /**
1113
     * {@inheritDoc}
1114
     */
1115
    public function getBigIntTypeDeclarationSQL(array $field)
1116
    {
1117
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1118
    }
1119
1120
    /**
1121
     * {@inheritDoc}
1122
     */
1123
    public function getSmallIntTypeDeclarationSQL(array $field)
1124
    {
1125
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1126
    }
1127
1128
    /**
1129
     * {@inheritDoc}
1130
     */
1131 3
    public function getGuidTypeDeclarationSQL(array $field)
1132
    {
1133 3
        return 'UNIQUEIDENTIFIER';
1134
    }
1135
1136
    /**
1137
     * {@inheritDoc}
1138
     */
1139 69
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1140
    {
1141 69
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1142
    }
1143
1144
    /**
1145
     * {@inheritdoc}
1146
     */
1147 3
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1148
    {
1149 3
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1150
    }
1151
1152
    /**
1153
     * {@inheritdoc}
1154
     */
1155 6
    public function getBinaryMaxLength()
1156
    {
1157 6
        return 8000;
1158
    }
1159
1160
    /**
1161
     * {@inheritDoc}
1162
     */
1163 5
    public function getClobTypeDeclarationSQL(array $field)
1164
    {
1165 5
        return 'VARCHAR(MAX)';
1166
    }
1167
1168
    /**
1169
     * {@inheritDoc}
1170
     */
1171 37
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1172
    {
1173 37
        return (!empty($columnDef['autoincrement'])) ? ' IDENTITY' : '';
1174
    }
1175
1176
    /**
1177
     * {@inheritDoc}
1178
     */
1179
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1180
    {
1181
        return 'DATETIME';
1182
    }
1183
1184
    /**
1185
     * {@inheritDoc}
1186
     */
1187
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1188
    {
1189
        return 'DATETIME';
1190
    }
1191
1192
    /**
1193
     * {@inheritDoc}
1194
     */
1195
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1196
    {
1197
        return 'DATETIME';
1198
    }
1199
1200
    /**
1201
     * {@inheritDoc}
1202
     */
1203 3
    public function getBooleanTypeDeclarationSQL(array $field)
1204
    {
1205 3
        return 'BIT';
1206
    }
1207
1208
    /**
1209
     * {@inheritDoc}
1210
     */
1211 40
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1212
    {
1213 40
        if ($limit === null) {
1214
            return $query;
1215
        }
1216
1217 40
        $start   = $offset + 1;
1218 40
        $end     = $offset + $limit;
1219
1220
        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
1221
        // Even if the TOP n is very large, the use of a CTE will
1222
        // allow the SQL Server query planner to optimize it so it doesn't
1223
        // actually scan the entire range covered by the TOP clause.
1224 40
        $selectPattern = '/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/im';
1225 40
        $replacePattern = sprintf('$1%s $2', "TOP $end");
1226 40
        $query = preg_replace($selectPattern, $replacePattern, $query);
1227
1228 40
        if (stristr($query, "ORDER BY")) {
1229
            // Inner order by is not valid in SQL Server for our purposes
1230
            // unless it's in a TOP N subquery.
1231 26
            $query = $this->scrubInnerOrderBy($query);
1232
        }
1233
1234
        // Build a new limited query around the original, using a CTE
1235 40
        return sprintf(
1236
            "WITH dctrn_cte AS (%s) "
1237
            . "SELECT * FROM ("
1238
            . "SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte"
1239
            . ") AS doctrine_tbl "
1240 40
            . "WHERE doctrine_rownum BETWEEN %d AND %d ORDER BY doctrine_rownum ASC",
1241 40
            $query,
1242 40
            $start,
1243 40
            $end
1244
        );
1245
    }
1246
1247
    /**
1248
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
1249
     * Caveat: will leave ORDER BY in TOP N subqueries.
1250
     *
1251
     * @param $query
1252
     * @return string
1253
     */
1254 26
    private function scrubInnerOrderBy($query)
1255
    {
1256 26
        $count = substr_count(strtoupper($query), 'ORDER BY');
1257 26
        $offset = 0;
1258
1259 26
        while ($count-- > 0) {
1260 26
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1261 26
            if ($orderByPos === false) {
1262 2
                break;
1263
            }
1264
1265 26
            $qLen = strlen($query);
1266 26
            $parenCount = 0;
1267 26
            $currentPosition = $orderByPos;
1268
1269 26
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1270 26 View Code Duplication
                if ($query[$currentPosition] === '(') {
1271 2
                    $parenCount++;
1272 26
                } elseif ($query[$currentPosition] === ')') {
1273 14
                    $parenCount--;
1274
                }
1275
1276 26
                $currentPosition++;
1277
            }
1278
1279 26
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1280
                // If the order by clause is in a TOP N subquery, do not remove
1281
                // it and continue iteration from the current position.
1282 23
                $offset = $currentPosition;
1283 23
                continue;
1284
            }
1285
1286 10
            if ($currentPosition < $qLen - 1) {
1287 10
                $query = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1288 10
                $offset = $orderByPos;
1289
            }
1290
        }
1291 26
        return $query;
1292
    }
1293
1294
    /**
1295
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
1296
     *
1297
     * @param string $query The query
1298
     * @param int $currentPosition Start position of ORDER BY clause
1299
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
1300
     */
1301 26
    private function isOrderByInTopNSubquery($query, $currentPosition)
1302
    {
1303
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1304 26
        $subQueryBuffer = '';
1305 26
        $parenCount = 0;
1306
1307
        // If $parenCount goes negative, we've exited the subquery we're examining.
1308
        // If $currentPosition goes negative, we've reached the beginning of the query.
1309 26
        while ($parenCount >= 0 && $currentPosition >= 0) {
1310 26 View Code Duplication
            if ($query[$currentPosition] === '(') {
1311 17
                $parenCount--;
1312 26
            } elseif ($query[$currentPosition] === ')') {
1313 14
                $parenCount++;
1314
            }
1315
1316
            // Only yank query text on the same nesting level as the ORDER BY clause.
1317 26
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1318
1319 26
            $currentPosition--;
1320
        }
1321
1322 26
        if (preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer)) {
1323 23
            return true;
1324
        }
1325
1326 10
        return false;
1327
    }
1328
1329
    /**
1330
     * {@inheritDoc}
1331
     */
1332
    public function supportsLimitOffset()
1333
    {
1334
        return false;
1335
    }
1336
1337
    /**
1338
     * {@inheritDoc}
1339
     */
1340 3 View Code Duplication
    public function convertBooleans($item)
1341
    {
1342 3
        if (is_array($item)) {
1343
            foreach ($item as $key => $value) {
1344
                if (is_bool($value) || is_numeric($item)) {
1345
                    $item[$key] = ($value) ? 1 : 0;
1346
                }
1347
            }
1348 3
        } elseif (is_bool($item) || is_numeric($item)) {
1349 3
            $item = ($item) ? 1 : 0;
1350
        }
1351
1352 3
        return $item;
1353
    }
1354
1355
    /**
1356
     * {@inheritDoc}
1357
     */
1358
    public function getCreateTemporaryTableSnippetSQL()
1359
    {
1360
        return "CREATE TABLE";
1361
    }
1362
1363
    /**
1364
     * {@inheritDoc}
1365
     */
1366
    public function getTemporaryTableName($tableName)
1367
    {
1368
        return '#' . $tableName;
1369
    }
1370
1371
    /**
1372
     * {@inheritDoc}
1373
     */
1374
    public function getDateTimeFormatString()
1375
    {
1376
        return 'Y-m-d H:i:s.000';
1377
    }
1378
1379
    /**
1380
     * {@inheritDoc}
1381
     */
1382
    public function getDateFormatString()
1383
    {
1384
        return 'Y-m-d H:i:s.000';
1385
    }
1386
1387
    /**
1388
     * {@inheritDoc}
1389
     */
1390
    public function getTimeFormatString()
1391
    {
1392
        return 'Y-m-d H:i:s.000';
1393
    }
1394
1395
    /**
1396
     * {@inheritDoc}
1397
     */
1398
    public function getDateTimeTzFormatString()
1399
    {
1400
        return $this->getDateTimeFormatString();
1401
    }
1402
1403
    /**
1404
     * {@inheritDoc}
1405
     */
1406 1
    public function getName()
1407
    {
1408 1
        return 'mssql';
1409
    }
1410
1411
    /**
1412
     * {@inheritDoc}
1413
     */
1414 15
    protected function initializeDoctrineTypeMappings()
1415
    {
1416 15
        $this->doctrineTypeMapping = [
1417
            'bigint' => 'bigint',
1418
            'numeric' => 'decimal',
1419
            'bit' => 'boolean',
1420
            'smallint' => 'smallint',
1421
            'decimal' => 'decimal',
1422
            'smallmoney' => 'integer',
1423
            'int' => 'integer',
1424
            'tinyint' => 'smallint',
1425
            'money' => 'integer',
1426
            'float' => 'float',
1427
            'real' => 'float',
1428
            'double' => 'float',
1429
            'double precision' => 'float',
1430
            'smalldatetime' => 'datetime',
1431
            'datetime' => 'datetime',
1432
            'char' => 'string',
1433
            'varchar' => 'string',
1434
            'text' => 'text',
1435
            'nchar' => 'string',
1436
            'nvarchar' => 'string',
1437
            'ntext' => 'text',
1438
            'binary' => 'binary',
1439
            'varbinary' => 'binary',
1440
            'image' => 'blob',
1441
            'uniqueidentifier' => 'guid',
1442
        ];
1443 15
    }
1444
1445
    /**
1446
     * {@inheritDoc}
1447
     */
1448
    public function createSavePoint($savepoint)
1449
    {
1450
        return 'SAVE TRANSACTION ' . $savepoint;
1451
    }
1452
1453
    /**
1454
     * {@inheritDoc}
1455
     */
1456
    public function releaseSavePoint($savepoint)
1457
    {
1458
        return '';
1459
    }
1460
1461
    /**
1462
     * {@inheritDoc}
1463
     */
1464
    public function rollbackSavePoint($savepoint)
1465
    {
1466
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1467
    }
1468
1469
    /**
1470
     * {@inheritdoc}
1471
     */
1472 21
    public function getForeignKeyReferentialActionSQL($action)
1473
    {
1474
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1475 21
        if (strtoupper($action) === 'RESTRICT') {
1476 3
            return 'NO ACTION';
1477
        }
1478
1479 18
        return parent::getForeignKeyReferentialActionSQL($action);
1480
    }
1481
1482
    /**
1483
     * {@inheritDoc}
1484
     */
1485 7 View Code Duplication
    public function appendLockHint($fromClause, $lockMode)
1486
    {
1487
        switch (true) {
1488 7
            case LockMode::NONE === $lockMode:
1489 1
                return $fromClause . ' WITH (NOLOCK)';
1490
1491 6
            case LockMode::PESSIMISTIC_READ === $lockMode:
1492 1
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1493
1494 5
            case LockMode::PESSIMISTIC_WRITE === $lockMode:
1495 1
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1496
1497
            default:
1498 4
                return $fromClause;
1499
        }
1500
    }
1501
1502
    /**
1503
     * {@inheritDoc}
1504
     */
1505
    public function getForUpdateSQL()
1506
    {
1507
        return ' ';
1508
    }
1509
1510
    /**
1511
     * {@inheritDoc}
1512
     */
1513 57
    protected function getReservedKeywordsClass()
1514
    {
1515 57
        return Keywords\SQLServerKeywords::class;
1516
    }
1517
1518
    /**
1519
     * {@inheritDoc}
1520
     */
1521 91
    public function quoteSingleIdentifier($str)
1522
    {
1523 91
        return "[" . str_replace("]", "][", $str) . "]";
1524
    }
1525
1526
    /**
1527
     * {@inheritDoc}
1528
     */
1529 3
    public function getTruncateTableSQL($tableName, $cascade = false)
1530
    {
1531 3
        $tableIdentifier = new Identifier($tableName);
1532
1533 3
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1534
    }
1535
1536
    /**
1537
     * {@inheritDoc}
1538
     */
1539 3
    public function getBlobTypeDeclarationSQL(array $field)
1540
    {
1541 3
        return 'VARBINARY(MAX)';
1542
    }
1543
1544
    /**
1545
     * {@inheritDoc}
1546
     */
1547 57
    public function getDefaultValueDeclarationSQL($field)
1548
    {
1549 57
        if ( ! isset($field['default'])) {
1550
            return empty($field['notnull']) ? ' NULL' : '';
1551
        }
1552
1553 57
        if ( ! isset($field['type'])) {
1554 12
            return " DEFAULT '" . $field['default'] . "'";
1555
        }
1556
1557 45
        if (in_array((string) $field['type'], ['Integer', 'BigInt', 'SmallInt'])) {
1558 9
            return " DEFAULT " . $field['default'];
1559
        }
1560
1561 39 View Code Duplication
        if (in_array((string) $field['type'], ['DateTime', 'DateTimeTz']) && $field['default'] == $this->getCurrentTimestampSQL()) {
1562 3
            return " DEFAULT " . $this->getCurrentTimestampSQL();
1563
        }
1564
1565 36 View Code Duplication
        if ((string) $field['type'] == 'Boolean') {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1566 3
            return " DEFAULT '" . $this->convertBooleans($field['default']) . "'";
1567
        }
1568
1569 36
        return " DEFAULT '" . $field['default'] . "'";
1570
    }
1571
1572
    /**
1573
     * {@inheritdoc}
1574
     *
1575
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1576
     */
1577 85
    public function getColumnDeclarationSQL($name, array $field)
1578
    {
1579 85
        if (isset($field['columnDefinition'])) {
1580 3
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1581
        } else {
1582 82
            $collation = (isset($field['collation']) && $field['collation']) ?
1583 82
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1584
1585 82
            $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : '';
1586
1587 82
            $unique = (isset($field['unique']) && $field['unique']) ?
1588 82
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1589
1590 82
            $check = (isset($field['check']) && $field['check']) ?
1591 82
                ' ' . $field['check'] : '';
1592
1593 82
            $typeDecl = $field['type']->getSQLDeclaration($field, $this);
1594 82
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1595
        }
1596
1597 85
        return $name . ' ' . $columnDef;
1598
    }
1599
1600
    /**
1601
     * Returns a unique default constraint name for a table and column.
1602
     *
1603
     * @param string $table  Name of the table to generate the unique default constraint name for.
1604
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1605
     *
1606
     * @return string
1607
     */
1608 45
    private function generateDefaultConstraintName($table, $column)
1609
    {
1610 45
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1611
    }
1612
1613
    /**
1614
     * Returns a hash value for a given identifier.
1615
     *
1616
     * @param string $identifier Identifier to generate a hash value for.
1617
     *
1618
     * @return string
1619
     */
1620 48
    private function generateIdentifierName($identifier)
1621
    {
1622
        // Always generate name for unquoted identifiers to ensure consistency.
1623 48
        $identifier = new Identifier($identifier);
1624
1625 48
        return strtoupper(dechex(crc32($identifier->getName())));
1626
    }
1627
}
1628