Failed Conditions
Push — master ( cfe3be...296b0e )
by Sergei
33:23 queued 11s
created

SQLServerPlatform   F

Complexity

Total Complexity 226

Size/Duplication

Total Lines 1636
Duplicated Lines 0 %

Test Coverage

Coverage 90.96%

Importance

Changes 0
Metric Value
wmc 226
eloc 484
dl 0
loc 1636
ccs 483
cts 531
cp 0.9096
rs 2
c 0
b 0
f 0

95 Methods

Rating   Name   Duplication   Size   Complexity  
A getCurrentTimeSQL() 0 3 1
A getDropDatabaseSQL() 0 3 1
A getConvertExpression() 0 3 1
A prefersIdentityColumns() 0 3 1
A getCreateDatabaseSQL() 0 3 1
A getCreateSchemaSQL() 0 3 1
A hasNativeGuidType() 0 3 1
A supportsCreateDropDatabase() 0 3 1
A supportsSchemas() 0 3 1
A supportsIdentityColumns() 0 3 1
A getCurrentDateSQL() 0 3 1
A getBinaryTypeDeclarationSQLSnippet() 0 3 4
A getBooleanTypeDeclarationSQL() 0 3 1
B getDefaultValueDeclarationSQL() 0 25 8
A getTruncateTableSQL() 0 5 1
A alterColumnRequiresDropDefaultConstraint() 0 23 5
A getModExpression() 0 3 1
A getSmallIntTypeDeclarationSQL() 0 3 1
A getListTablesSQL() 0 5 1
C getColumnDeclarationSQL() 0 21 10
A getCreateColumnCommentSQL() 0 20 2
A getLengthExpression() 0 3 1
A initializeDoctrineTypeMappings() 0 28 1
A getCreateIndexSQLFlags() 0 14 4
A getUpdateExtendedPropertySQL() 0 15 1
A generateDefaultConstraintName() 0 3 1
A getDateArithmeticIntervalExpression() 0 9 2
A getDateTimeFormatString() 0 3 1
A getDateTimeTypeDeclarationSQL() 0 3 1
A getBlobTypeDeclarationSQL() 0 3 1
A getGuidTypeDeclarationSQL() 0 3 1
A quoteSingleIdentifier() 0 3 1
A getListTableIndexesSQL() 0 17 1
A getForUpdateSQL() 0 3 1
A getName() 0 3 1
A getTemporaryTableName() 0 3 1
B convertBooleans() 0 15 9
F _getCreateTableSQL() 0 64 20
A getConcatExpression() 0 5 1
A getDateTypeDeclarationSQL() 0 3 1
A isOrderByInTopNSubquery() 0 22 6
A getAlterTableDropDefaultConstraintClause() 0 3 1
A getClobTypeDeclarationSQL() 0 3 1
A supportsLimitOffset() 0 3 1
A getSubstringExpression() 0 7 2
A getCreateTemporaryTableSnippetSQL() 0 3 1
A _getCommonIntegerTypeDeclarationSQL() 0 3 2
A rollbackSavePoint() 0 3 1
B scrubInnerOrderBy() 0 40 9
A _appendUniqueConstraintDefinition() 0 9 2
A getDateTimeTzFormatString() 0 3 1
A getDropExtendedPropertySQL() 0 14 1
A getCreateViewSQL() 0 3 1
A getGuidExpression() 0 3 1
A releaseSavePoint() 0 3 1
A getListViewsSQL() 0 3 1
A getDateFormatString() 0 3 1
A getReservedKeywordsClass() 0 3 1
A getDropViewSQL() 0 3 1
A getVarcharTypeDeclarationSQLSnippet() 0 3 4
A getEmptyIdentityInsertSQL() 0 3 1
A getDropIndexSQL() 0 29 5
A createSavePoint() 0 3 1
A getTimeFormatString() 0 3 1
A getAddExtendedPropertySQL() 0 15 1
A getLocateExpression() 0 7 2
A getTimeTypeDeclarationSQL() 0 3 1
A getAlterTableAddDefaultConstraintClause() 0 6 1
A getAlterColumnCommentSQL() 0 20 2
F getAlterTableSQL() 0 161 31
A getDefaultSchemaName() 0 3 1
A getTableWhereClause() 0 12 2
A getListTableColumnsSQL() 0 28 1
A getCreatePrimaryKeySQL() 0 8 2
A getDefaultConstraintDeclarationSQL() 0 12 2
A generateIdentifierName() 0 6 1
A getListDatabasesSQL() 0 3 1
A getSetTransactionIsolationSQL() 0 3 1
A supportsColumnCollation() 0 3 1
A getDropForeignKeySQL() 0 14 3
A getUniqueConstraintDeclarationSQL() 0 7 1
A appendLockHint() 0 14 4
A getCreateIndexSQL() 0 9 3
A getRenameIndexSQL() 0 7 1
A getForeignKeyReferentialActionSQL() 0 8 2
A doModifyLimitQuery() 0 42 5
A getDateDiffExpression() 0 3 1
A getListNamespacesSQL() 0 3 1
A getBigIntTypeDeclarationSQL() 0 3 1
A getIntegerTypeDeclarationSQL() 0 3 1
A getListTableForeignKeysSQL() 0 17 1
A getDropColumnCommentSQL() 0 19 2
A supportsReleaseSavepoints() 0 3 1
A getBinaryMaxLength() 0 3 1
A getTrimExpression() 0 39 6

How to fix   Complexity   

Complex Class

Complex classes like SQLServerPlatform often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SQLServerPlatform, and based on these observations, apply Extract Interface, too.

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

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

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

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