Completed
Push — 2.10 ( 39b76b...400f60 )
by Sergei
30s queued 15s
created

SQLServerPlatform::_getCreateTableSQL()   F

Complexity

Conditions 21
Paths 864

Size

Total Lines 69
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 21.2352

Importance

Changes 0
Metric Value
eloc 36
dl 0
loc 69
ccs 34
cts 37
cp 0.9189
rs 0.1888
c 0
b 0
f 0
cc 21
nc 864
nop 3
crap 21.2352

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace Doctrine\DBAL\Platforms;
4
5
use Doctrine\DBAL\LockMode;
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\ColumnDiff;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Doctrine\DBAL\Schema\Identifier;
10
use Doctrine\DBAL\Schema\Index;
11
use Doctrine\DBAL\Schema\Table;
12
use Doctrine\DBAL\Schema\TableDiff;
13
use InvalidArgumentException;
14
use function array_merge;
15
use function array_unique;
16
use function array_values;
17
use function count;
18
use function crc32;
19
use function dechex;
20
use function explode;
21
use function func_get_args;
22
use function implode;
23
use function is_array;
24
use function is_bool;
25
use function is_numeric;
26
use function is_string;
27
use function preg_match;
28
use function sprintf;
29
use function str_replace;
30
use function stripos;
31
use function stristr;
32
use function strlen;
33
use function strpos;
34
use function strtoupper;
35
use function substr;
36
use function substr_count;
37
38
/**
39
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
40
 * Microsoft SQL Server database platform.
41
 */
42
class SQLServerPlatform extends AbstractPlatform
43
{
44
    /**
45
     * {@inheritdoc}
46
     */
47 8440
    public function getCurrentDateSQL()
48
    {
49 8440
        return $this->getConvertExpression('date', 'GETDATE()');
50
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55 8432
    public function getCurrentTimeSQL()
56
    {
57 8432
        return $this->getConvertExpression('time', 'GETDATE()');
58
    }
59
60
    /**
61
     * Returns an expression that converts an expression of one data type to another.
62
     *
63
     * @param string $dataType   The target native data type. Alias data types cannot be used.
64
     * @param string $expression The SQL expression to convert.
65
     *
66
     * @return string
67
     */
68 8440
    private function getConvertExpression($dataType, $expression)
69
    {
70 8440
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76 955
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
77
    {
78 955
        $factorClause = '';
79
80 955
        if ($operator === '-') {
81 955
            $factorClause = '-1 * ';
82
        }
83
84 955
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
85
    }
86
87
    /**
88
     * {@inheritDoc}
89
     */
90 934
    public function getDateDiffExpression($date1, $date2)
91
    {
92 934
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
93
    }
94
95
    /**
96
     * {@inheritDoc}
97
     *
98
     * Microsoft SQL Server prefers "autoincrement" identity columns
99
     * since sequences can only be emulated with a table.
100
     */
101 8059
    public function prefersIdentityColumns()
102
    {
103 8059
        return true;
104
    }
105
106
    /**
107
     * {@inheritDoc}
108
     *
109
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
110
     */
111 839
    public function supportsIdentityColumns()
112
    {
113 839
        return true;
114
    }
115
116
    /**
117
     * {@inheritDoc}
118
     */
119 997
    public function supportsReleaseSavepoints()
120
    {
121 997
        return false;
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 869
    public function supportsSchemas()
128
    {
129 869
        return true;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135 809
    public function getDefaultSchemaName()
136
    {
137 809
        return 'dbo';
138
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143 6946
    public function supportsColumnCollation()
144
    {
145 6946
        return true;
146
    }
147
148
    /**
149
     * {@inheritDoc}
150
     */
151 8882
    public function hasNativeGuidType()
152
    {
153 8882
        return true;
154
    }
155
156
    /**
157
     * {@inheritDoc}
158
     */
159 8469
    public function getCreateDatabaseSQL($name)
160
    {
161 8469
        return 'CREATE DATABASE ' . $name;
162
    }
163
164
    /**
165
     * {@inheritDoc}
166
     */
167 8469
    public function getDropDatabaseSQL($name)
168
    {
169 8469
        return 'DROP DATABASE ' . $name;
170
    }
171
172
    /**
173
     * {@inheritDoc}
174
     */
175 1017
    public function supportsCreateDropDatabase()
176
    {
177 1017
        return true;
178
    }
179
180
    /**
181
     * {@inheritDoc}
182
     */
183 7654
    public function getCreateSchemaSQL($schemaName)
184
    {
185 7654
        return 'CREATE SCHEMA ' . $schemaName;
186
    }
187
188
    /**
189
     * {@inheritDoc}
190
     */
191 5924
    public function getDropForeignKeySQL($foreignKey, $table)
192
    {
193 5924
        if (! $foreignKey instanceof ForeignKeyConstraint) {
194 5531
            $foreignKey = new Identifier($foreignKey);
195
        }
196
197 5924
        if (! $table instanceof Table) {
198 5924
            $table = new Identifier($table);
199
        }
200
201 5924
        $foreignKey = $foreignKey->getQuotedName($this);
202 5924
        $table      = $table->getQuotedName($this);
203
204 5924
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
205
    }
206
207
    /**
208
     * {@inheritDoc}
209
     */
210 847
    public function getDropIndexSQL($index, $table = null)
211
    {
212 847
        if ($index instanceof Index) {
213 839
            $index = $index->getQuotedName($this);
214 847
        } elseif (! is_string($index)) {
215
            throw new InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
216
        }
217
218 847
        if (! isset($table)) {
219
            return 'DROP INDEX ' . $index;
220
        }
221
222 847
        if ($table instanceof Table) {
223 847
            $table = $table->getQuotedName($this);
224
        }
225
226 847
        return sprintf(
227
            <<<SQL
228
IF EXISTS (SELECT * FROM sysobjects WHERE name = '%s')
229
    ALTER TABLE %s DROP CONSTRAINT %s
230
ELSE
231
    DROP INDEX %s ON %s
232
SQL
233
            ,
234 847
            $index,
235 847
            $table,
236 847
            $index,
237 847
            $index,
238 847
            $table
239
        );
240
    }
241
242
    /**
243
     * {@inheritDoc}
244
     */
245 8636
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
246
    {
247 8636
        $defaultConstraintsSql = [];
248 8636
        $commentsSql           = [];
249
250 8636
        $tableComment = $options['comment'] ?? null;
251 8636
        if ($tableComment !== null) {
252 771
            $commentsSql[] = $this->getCommentOnTableSQL($tableName, $tableComment);
253
        }
254
255
        // @todo does other code breaks because of this?
256
        // force primary keys to be not null
257 8636
        foreach ($columns as &$column) {
258 8636
            if (isset($column['primary']) && $column['primary']) {
259 7901
                $column['notnull'] = true;
260
            }
261
262
            // Build default constraints SQL statements.
263 8636
            if (isset($column['default'])) {
264 7394
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
265 7394
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
266
            }
267
268 8636
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
269 8624
                continue;
270
            }
271
272 7661
            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
273
        }
274
275 8636
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
276
277 8636
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
278
            foreach ($options['uniqueConstraints'] as $name => $definition) {
279
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
280
            }
281
        }
282
283 8636
        if (isset($options['primary']) && ! empty($options['primary'])) {
284 7907
            $flags = '';
285 7907
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
286 7456
                $flags = ' NONCLUSTERED';
287
            }
288 7907
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
289
        }
290
291 8636
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
292
293 8636
        $check = $this->getCheckDeclarationSQL($columns);
294 8636
        if (! empty($check)) {
295
            $query .= ', ' . $check;
296
        }
297 8636
        $query .= ')';
298
299 8636
        $sql = [$query];
300
301 8636
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
302 6552
            foreach ($options['indexes'] as $index) {
303 6552
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
304
            }
305
        }
306
307 8636
        if (isset($options['foreignKeys'])) {
308 6330
            foreach ((array) $options['foreignKeys'] as $definition) {
309 6164
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
310
            }
311
        }
312
313 8636
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
314
    }
315
316
    /**
317
     * {@inheritDoc}
318
     */
319 7437
    public function getCreatePrimaryKeySQL(Index $index, $table)
320
    {
321 7437
        if ($table instanceof Table) {
322
            $identifier = $table->getQuotedName($this);
323
        } else {
324 7437
            $identifier = $table;
325
        }
326
327 7437
        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';
328
329 7437
        if ($index->hasFlag('nonclustered')) {
330 7431
            $sql .= ' NONCLUSTERED';
331
        }
332
333 7437
        return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
334
    }
335
336
    /**
337
     * Returns the SQL statement for creating a column comment.
338
     *
339
     * SQL Server does not support native column comments,
340
     * therefore the extended properties functionality is used
341
     * as a workaround to store them.
342
     * The property name used to store column comments is "MS_Description"
343
     * which provides compatibility with SQL Server Management Studio,
344
     * as column comments are stored in the same property there when
345
     * specifying a column's "Description" attribute.
346
     *
347
     * @param string      $tableName  The quoted table name to which the column belongs.
348
     * @param string      $columnName The quoted column name to create the comment for.
349
     * @param string|null $comment    The column's comment.
350
     *
351
     * @return string
352
     */
353 7679
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
354
    {
355 7679
        if (strpos($tableName, '.') !== false) {
356 7362
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
357 7362
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
358 7362
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
359
        } else {
360 7575
            $schemaSQL = "'dbo'";
361 7575
            $tableSQL  = $this->quoteStringLiteral($tableName);
362
        }
363
364 7679
        return $this->getAddExtendedPropertySQL(
365 7679
            'MS_Description',
366
            $comment,
367 7679
            'SCHEMA',
368
            $schemaSQL,
369 7679
            'TABLE',
370
            $tableSQL,
371 7679
            'COLUMN',
372
            $columnName
373
        );
374
    }
375
376
    /**
377
     * Returns the SQL snippet for declaring a default constraint.
378
     *
379
     * @param string  $table  Name of the table to return the default constraint declaration for.
380
     * @param mixed[] $column Column definition.
381
     *
382
     * @return string
383
     *
384
     * @throws InvalidArgumentException
385
     */
386 7575
    public function getDefaultConstraintDeclarationSQL($table, array $column)
387
    {
388 7575
        if (! isset($column['default'])) {
389
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
390
        }
391
392 7575
        $columnName = new Identifier($column['name']);
393
394
        return ' CONSTRAINT ' .
395 7575
            $this->generateDefaultConstraintName($table, $column['name']) .
396 7575
            $this->getDefaultValueDeclarationSQL($column) .
397 7575
            ' FOR ' . $columnName->getQuotedName($this);
398
    }
399
400
    /**
401
     * {@inheritDoc}
402
     */
403 6112
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
404
    {
405 6112
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
406
407 6112
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
408
409 6112
        return $constraint;
410
    }
411
412
    /**
413
     * {@inheritDoc}
414
     */
415 7778
    public function getCreateIndexSQL(Index $index, $table)
416
    {
417 7778
        $constraint = parent::getCreateIndexSQL($index, $table);
418
419 7778
        if ($index->isUnique() && ! $index->isPrimary()) {
420 6546
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
421
        }
422
423 7778
        return $constraint;
424
    }
425
426
    /**
427
     * {@inheritDoc}
428
     */
429 7778
    protected function getCreateIndexSQLFlags(Index $index)
430
    {
431 7778
        $type = '';
432 7778
        if ($index->isUnique()) {
433 6546
            $type .= 'UNIQUE ';
434
        }
435
436 7778
        if ($index->hasFlag('clustered')) {
437 7481
            $type .= 'CLUSTERED ';
438 6576
        } elseif ($index->hasFlag('nonclustered')) {
439
            $type .= 'NONCLUSTERED ';
440
        }
441
442 7778
        return $type;
443
    }
444
445
    /**
446
     * Extend unique key constraint with required filters
447
     *
448
     * @param string $sql
449
     *
450
     * @return string
451
     */
452 6558
    private function _appendUniqueConstraintDefinition($sql, Index $index)
453
    {
454 6558
        $fields = [];
455
456 6558
        foreach ($index->getQuotedColumns($this) as $field) {
457 6558
            $fields[] = $field . ' IS NOT NULL';
458
        }
459
460 6558
        return $sql . ' WHERE ' . implode(' AND ', $fields);
461
    }
462
463
    /**
464
     * {@inheritDoc}
465
     */
466 7752
    public function getAlterTableSQL(TableDiff $diff)
467
    {
468 7752
        $queryParts  = [];
469 7752
        $sql         = [];
470 7752
        $columnSql   = [];
471 7752
        $commentsSql = [];
472
473 7752
        foreach ($diff->addedColumns as $column) {
474 7674
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
475
                continue;
476
            }
477
478 7674
            $columnDef    = $column->toArray();
479 7674
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
480
481 7674
            if (isset($columnDef['default'])) {
482 6924
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
483
            }
484
485 7674
            $comment = $this->getColumnComment($column);
486
487 7674
            if (empty($comment) && ! is_numeric($comment)) {
488 7570
                continue;
489
            }
490
491 7632
            $commentsSql[] = $this->getCreateColumnCommentSQL(
492 7632
                $diff->name,
493 7632
                $column->getQuotedName($this),
494
                $comment
495
            );
496
        }
497
498 7752
        foreach ($diff->removedColumns as $column) {
499 7576
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
500
                continue;
501
            }
502
503 7576
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
504
        }
505
506 7752
        foreach ($diff->changedColumns as $columnDiff) {
507 7677
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
508
                continue;
509
            }
510
511 7677
            $column     = $columnDiff->column;
512 7677
            $comment    = $this->getColumnComment($column);
513 7677
            $hasComment = ! empty($comment) || is_numeric($comment);
514
515 7677
            if ($columnDiff->fromColumn instanceof Column) {
516 7647
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
517 7647
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
518
519 7647
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
520 7580
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
521 7580
                        $diff->name,
522 7580
                        $column->getQuotedName($this),
523
                        $comment
524
                    );
525 7641
                } elseif ($hasFromComment && ! $hasComment) {
526 7603
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
527 7566
                } elseif ($hasComment) {
528 7528
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
529 7528
                        $diff->name,
530 7528
                        $column->getQuotedName($this),
531
                        $comment
532
                    );
533
                }
534
            }
535
536
            // Do not add query part if only comment has changed.
537 7677
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
538 7615
                continue;
539
            }
540
541 7590
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
542
543 7590
            if ($requireDropDefaultConstraint) {
544 7462
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
545 7462
                    $diff->name,
546 7462
                    $columnDiff->oldColumnName
547
                );
548
            }
549
550 7590
            $columnDef = $column->toArray();
551
552 7590
            $queryParts[] = 'ALTER COLUMN ' .
553 7590
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
554
555 7590
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
556 7554
                continue;
557
            }
558
559 7468
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
560
        }
561
562 7752
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
563 7554
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
564
                continue;
565
            }
566
567 7554
            $oldColumnName = new Identifier($oldColumnName);
568
569 7554
            $sql[] = "sp_RENAME '" .
570 7554
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
571 7554
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
572
573
            // Recreate default constraint with new column name if necessary (for future reference).
574 7554
            if ($column->getDefault() === null) {
575 7546
                continue;
576
            }
577
578 5897
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
579 5897
                $diff->name,
580 5897
                $oldColumnName->getQuotedName($this)
581
            );
582 5897
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
583
        }
584
585 7752
        $tableSql = [];
586
587 7752
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
588
            return array_merge($tableSql, $columnSql);
589
        }
590
591 7752
        foreach ($queryParts as $query) {
592 7704
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
593
        }
594
595 7752
        $sql = array_merge($sql, $commentsSql);
596
597 7752
        $newName = $diff->getNewName();
598
599 7752
        if ($newName !== false) {
600 6432
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";
601
602
            /**
603
             * Rename table's default constraints names
604
             * to match the new table name.
605
             * This is necessary to ensure that the default
606
             * constraints can be referenced in future table
607
             * alterations as the table name is encoded in
608
             * default constraints' names.
609
             */
610 6432
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
611
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
612 6432
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
613 6432
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
614 6432
                'FROM sys.default_constraints dc ' .
615 6432
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
616 6432
                "WHERE tbl.name = '" . $newName->getName() . "';" .
617 6432
                'EXEC sp_executesql @sql';
618
        }
619
620 7752
        $sql = array_merge(
621 7752
            $this->getPreAlterTableIndexForeignKeySQL($diff),
622 7752
            $sql,
623 7752
            $this->getPostAlterTableIndexForeignKeySQL($diff)
624
        );
625
626 7752
        return array_merge($sql, $tableSql, $columnSql);
627
    }
628
629
    /**
630
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
631
     *
632
     * @param string $tableName The name of the table to generate the clause for.
633
     * @param Column $column    The column to generate the clause for.
634
     *
635
     * @return string
636
     */
637 7474
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
638
    {
639 7474
        $columnDef         = $column->toArray();
640 7474
        $columnDef['name'] = $column->getQuotedName($this);
641
642 7474
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
643
    }
644
645
    /**
646
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
647
     *
648
     * @param string $tableName  The name of the table to generate the clause for.
649
     * @param string $columnName The name of the column to generate the clause for.
650
     *
651
     * @return string
652
     */
653 7468
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
654
    {
655 7468
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
656
    }
657
658
    /**
659
     * Checks whether a column alteration requires dropping its default constraint first.
660
     *
661
     * Different to other database vendors SQL Server implements column default values
662
     * as constraints and therefore changes in a column's default value as well as changes
663
     * in a column's type require dropping the default constraint first before being to
664
     * alter the particular column to the new definition.
665
     *
666
     * @param ColumnDiff $columnDiff The column diff to evaluate.
667
     *
668
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
669
     */
670 7590
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
671
    {
672
        // We can only decide whether to drop an existing default constraint
673
        // if we know the original default value.
674 7590
        if (! $columnDiff->fromColumn instanceof Column) {
675 6049
            return false;
676
        }
677
678
        // We only need to drop an existing default constraint if we know the
679
        // column was defined with a default value before.
680 7566
        if ($columnDiff->fromColumn->getDefault() === null) {
681 7534
            return false;
682
        }
683
684
        // We need to drop an existing default constraint if the column was
685
        // defined with a default value before and it has changed.
686 7462
        if ($columnDiff->hasChanged('default')) {
687 7249
            return true;
688
        }
689
690
        // We need to drop an existing default constraint if the column was
691
        // defined with a default value before and the native column type has changed.
692 7438
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
693
    }
694
695
    /**
696
     * Returns the SQL statement for altering a column comment.
697
     *
698
     * SQL Server does not support native column comments,
699
     * therefore the extended properties functionality is used
700
     * as a workaround to store them.
701
     * The property name used to store column comments is "MS_Description"
702
     * which provides compatibility with SQL Server Management Studio,
703
     * as column comments are stored in the same property there when
704
     * specifying a column's "Description" attribute.
705
     *
706
     * @param string      $tableName  The quoted table name to which the column belongs.
707
     * @param string      $columnName The quoted column name to alter the comment for.
708
     * @param string|null $comment    The column's comment.
709
     *
710
     * @return string
711
     */
712 7580
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
713
    {
714 7580
        if (strpos($tableName, '.') !== false) {
715 7281
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
716 7281
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
717 7281
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
718
        } else {
719 7528
            $schemaSQL = "'dbo'";
720 7528
            $tableSQL  = $this->quoteStringLiteral($tableName);
721
        }
722
723 7580
        return $this->getUpdateExtendedPropertySQL(
724 7580
            'MS_Description',
725
            $comment,
726 7580
            'SCHEMA',
727
            $schemaSQL,
728 7580
            'TABLE',
729
            $tableSQL,
730 7580
            'COLUMN',
731
            $columnName
732
        );
733
    }
734
735
    /**
736
     * Returns the SQL statement for dropping a column comment.
737
     *
738
     * SQL Server does not support native column comments,
739
     * therefore the extended properties functionality is used
740
     * as a workaround to store them.
741
     * The property name used to store column comments is "MS_Description"
742
     * which provides compatibility with SQL Server Management Studio,
743
     * as column comments are stored in the same property there when
744
     * specifying a column's "Description" attribute.
745
     *
746
     * @param string $tableName  The quoted table name to which the column belongs.
747
     * @param string $columnName The quoted column name to drop the comment for.
748
     *
749
     * @return string
750
     */
751 7603
    protected function getDropColumnCommentSQL($tableName, $columnName)
752
    {
753 7603
        if (strpos($tableName, '.') !== false) {
754 7306
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
755 7306
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
756 7306
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
757
        } else {
758 7528
            $schemaSQL = "'dbo'";
759 7528
            $tableSQL  = $this->quoteStringLiteral($tableName);
760
        }
761
762 7603
        return $this->getDropExtendedPropertySQL(
763 7603
            'MS_Description',
764 7603
            'SCHEMA',
765
            $schemaSQL,
766 7603
            'TABLE',
767
            $tableSQL,
768 7603
            'COLUMN',
769
            $columnName
770
        );
771
    }
772
773
    /**
774
     * {@inheritdoc}
775
     */
776 6067
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
777
    {
778 6067
        return [sprintf(
779 30
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
780 6067
            $tableName,
781 6067
            $oldIndexName,
782 6067
            $index->getQuotedName($this)
783
        ),
784
        ];
785
    }
786
787
    /**
788
     * Returns the SQL statement for adding an extended property to a database object.
789
     *
790
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
791
     *
792
     * @param string      $name       The name of the property to add.
793
     * @param string|null $value      The value of the property to add.
794
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
795
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
796
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
797
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
798
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
799
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
800
     *
801
     * @return string
802
     */
803 7679
    public function getAddExtendedPropertySQL(
804
        $name,
805
        $value = null,
806
        $level0Type = null,
807
        $level0Name = null,
808
        $level1Type = null,
809
        $level1Name = null,
810
        $level2Type = null,
811
        $level2Name = null
812
    ) {
813
        return 'EXEC sp_addextendedproperty ' .
814 7679
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
815 7679
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
816 7679
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
817 7679
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
818
    }
819
820
    /**
821
     * Returns the SQL statement for dropping an extended property from a database object.
822
     *
823
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
824
     *
825
     * @param string      $name       The name of the property to drop.
826
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
827
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
828
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
829
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
830
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
831
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
832
     *
833
     * @return string
834
     */
835 7603
    public function getDropExtendedPropertySQL(
836
        $name,
837
        $level0Type = null,
838
        $level0Name = null,
839
        $level1Type = null,
840
        $level1Name = null,
841
        $level2Type = null,
842
        $level2Name = null
843
    ) {
844
        return 'EXEC sp_dropextendedproperty ' .
845 7603
            'N' . $this->quoteStringLiteral($name) . ', ' .
846 7603
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
847 7603
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
848 7603
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
849
    }
850
851
    /**
852
     * Returns the SQL statement for updating an extended property of a database object.
853
     *
854
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
855
     *
856
     * @param string      $name       The name of the property to update.
857
     * @param string|null $value      The value of the property to update.
858
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
859
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
860
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
861
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
862
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
863
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
864
     *
865
     * @return string
866
     */
867 7580
    public function getUpdateExtendedPropertySQL(
868
        $name,
869
        $value = null,
870
        $level0Type = null,
871
        $level0Name = null,
872
        $level1Type = null,
873
        $level1Name = null,
874
        $level2Type = null,
875
        $level2Name = null
876
    ) {
877
        return 'EXEC sp_updateextendedproperty ' .
878 7580
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
879 7580
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
880 7580
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
881 7580
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
882
    }
883
884
    /**
885
     * {@inheritDoc}
886
     */
887 660
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
888
    {
889 660
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
890
    }
891
892
    /**
893
     * {@inheritDoc}
894
     */
895
    public function getListTablesSQL()
896
    {
897
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
898
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
899
        return "SELECT name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
900
    }
901
902
    /**
903
     * {@inheritDoc}
904
     */
905 7113
    public function getListTableColumnsSQL($table, $database = null)
906
    {
907
        return "SELECT    col.name,
908
                          type.name AS type,
909
                          col.max_length AS length,
910
                          ~col.is_nullable AS notnull,
911
                          def.definition AS [default],
912
                          col.scale,
913
                          col.precision,
914
                          col.is_identity AS autoincrement,
915
                          col.collation_name AS collation,
916
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
917
                FROM      sys.columns AS col
918
                JOIN      sys.types AS type
919
                ON        col.user_type_id = type.user_type_id
920
                JOIN      sys.objects AS obj
921
                ON        col.object_id = obj.object_id
922
                JOIN      sys.schemas AS scm
923
                ON        obj.schema_id = scm.schema_id
924
                LEFT JOIN sys.default_constraints def
925
                ON        col.default_object_id = def.object_id
926
                AND       col.object_id = def.parent_object_id
927
                LEFT JOIN sys.extended_properties AS prop
928
                ON        obj.object_id = prop.major_id
929
                AND       col.column_id = prop.minor_id
930
                AND       prop.name = 'MS_Description'
931
                WHERE     obj.type = 'U'
932 7113
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
933
    }
934
935
    /**
936
     * {@inheritDoc}
937
     */
938 7067
    public function getListTableForeignKeysSQL($table, $database = null)
0 ignored issues
show
Unused Code introduced by
The parameter $database is not used and could be removed. ( Ignorable by Annotation )

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

938
    public function getListTableForeignKeysSQL($table, /** @scrutinizer ignore-unused */ $database = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

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