Failed Conditions
Pull Request — 3.0.x (#3980)
by Guilherme
30:17 queued 13s
created

SQLServer2012Platform   F

Complexity

Total Complexity 211

Size/Duplication

Total Lines 1647
Duplicated Lines 0 %

Test Coverage

Coverage 93.45%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 471
dl 0
loc 1647
ccs 471
cts 504
cp 0.9345
rs 2
c 1
b 0
f 0
wmc 211

101 Methods

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

How to fix   Complexity   

Complex Class

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

991
    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...
992
    {
993
        return 'SELECT f.name AS ForeignKey,
994
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
995
                OBJECT_NAME (f.parent_object_id) AS TableName,
996
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
997
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
998
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
999
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
1000
                f.delete_referential_action_desc,
1001
                f.update_referential_action_desc
1002
                FROM sys.foreign_keys AS f
1003
                INNER JOIN sys.foreign_key_columns AS fc
1004
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
1005
                ON f.OBJECT_ID = fc.constraint_object_id
1006
                WHERE ' .
1007 134
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
1008
    }
1009
1010
    /**
1011
     * {@inheritDoc}
1012
     */
1013 140
    public function getListTableIndexesSQL($table, $currentDatabase = null)
1014
    {
1015
        return "SELECT idx.name AS key_name,
1016
                       col.name AS column_name,
1017
                       ~idx.is_unique AS non_unique,
1018
                       idx.is_primary_key AS [primary],
1019
                       CASE idx.type
1020
                           WHEN '1' THEN 'clustered'
1021
                           WHEN '2' THEN 'nonclustered'
1022
                           ELSE NULL
1023
                       END AS flags
1024
                FROM sys.tables AS tbl
1025
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
1026
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
1027
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
1028
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
1029 140
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
1030
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
1031
    }
1032
1033
    /**
1034
     * {@inheritDoc}
1035
     */
1036 2
    public function getCreateViewSQL($name, $sql)
1037
    {
1038 2
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
1039
    }
1040
1041
    /**
1042
     * {@inheritDoc}
1043
     */
1044 2
    public function getListViewsSQL($database)
1045
    {
1046 2
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
1047
    }
1048
1049
    /**
1050
     * Returns the where clause to filter schema and table name in a query.
1051
     *
1052
     * @param string $table        The full qualified name of the table.
1053
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
1054
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
1055
     *
1056
     * @return string
1057
     */
1058 258
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
1059
    {
1060 258
        if (strpos($table, '.') !== false) {
1061 68
            [$schema, $table] = explode('.', $table);
1062 68
            $schema           = $this->quoteStringLiteral($schema);
1063 68
            $table            = $this->quoteStringLiteral($table);
1064
        } else {
1065 192
            $schema = 'SCHEMA_NAME()';
1066 192
            $table  = $this->quoteStringLiteral($table);
1067
        }
1068
1069 258
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1070
    }
1071
1072
    /**
1073
     * {@inheritDoc}
1074
     */
1075 2
    public function getDropViewSQL($name)
1076
    {
1077 2
        return 'DROP VIEW ' . $name;
1078
    }
1079
1080
    /**
1081
     * {@inheritDoc}
1082
     *
1083
     * @deprecated Use application-generated UUIDs instead
1084
     */
1085
    public function getGuidExpression()
1086
    {
1087
        return 'NEWID()';
1088
    }
1089
1090
    /**
1091
     * {@inheritDoc}
1092
     */
1093 2
    public function getLocateExpression($str, $substr, $startPos = false)
1094
    {
1095 2
        if ($startPos === false) {
1096 2
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1097
        }
1098
1099 2
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1100
    }
1101
1102
    /**
1103
     * {@inheritDoc}
1104
     */
1105
    public function getModExpression($expression1, $expression2)
1106
    {
1107
        return $expression1 . ' % ' . $expression2;
1108
    }
1109
1110
    /**
1111
     * {@inheritDoc}
1112
     */
1113 72
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1114
    {
1115 72
        if ($char === false) {
1116 16
            switch ($pos) {
1117
                case TrimMode::LEADING:
1118 4
                    $trimFn = 'LTRIM';
1119 4
                    break;
1120
1121
                case TrimMode::TRAILING:
1122 4
                    $trimFn = 'RTRIM';
1123 4
                    break;
1124
1125
                default:
1126 8
                    return 'LTRIM(RTRIM(' . $str . '))';
1127
            }
1128
1129 8
            return $trimFn . '(' . $str . ')';
1130
        }
1131
1132
        /** Original query used to get those expressions
1133
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1134
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1135
          select @c as string
1136
          , @trim_char as trim_char
1137
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1138
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1139
          , 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;
1140
         */
1141 56
        $pattern = "'%[^' + " . $char . " + ']%'";
1142
1143 56
        if ($pos === TrimMode::LEADING) {
1144 14
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1145
        }
1146
1147 42
        if ($pos === TrimMode::TRAILING) {
1148 14
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1149
        }
1150
1151 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))';
1152
    }
1153
1154
    /**
1155
     * {@inheritDoc}
1156
     */
1157 22
    public function getConcatExpression()
1158
    {
1159 22
        $args = func_get_args();
1160
1161 22
        return '(' . implode(' + ', $args) . ')';
1162
    }
1163
1164
    /**
1165
     * {@inheritDoc}
1166
     */
1167 26
    public function getListDatabasesSQL()
1168
    {
1169 26
        return 'SELECT * FROM sys.databases';
1170
    }
1171
1172
    /**
1173
     * {@inheritDoc}
1174
     */
1175 4
    public function getListNamespacesSQL()
1176
    {
1177 4
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1178
    }
1179
1180
    /**
1181
     * {@inheritDoc}
1182
     */
1183
    public function getSubstringExpression($value, $from, $length = null)
1184
    {
1185
        if ($length !== null) {
1186
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1187
        }
1188
1189
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1190
    }
1191
1192
    /**
1193
     * {@inheritDoc}
1194
     */
1195
    public function getLengthExpression($column)
1196
    {
1197
        return 'LEN(' . $column . ')';
1198
    }
1199
1200
    /**
1201
     * {@inheritDoc}
1202
     */
1203 22
    public function getSetTransactionIsolationSQL($level)
1204
    {
1205 22
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1206
    }
1207
1208
    /**
1209
     * {@inheritDoc}
1210
     */
1211 564
    public function getIntegerTypeDeclarationSQL(array $field)
1212
    {
1213 564
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1214
    }
1215
1216
    /**
1217
     * {@inheritDoc}
1218
     */
1219 30
    public function getBigIntTypeDeclarationSQL(array $field)
1220
    {
1221 30
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1222
    }
1223
1224
    /**
1225
     * {@inheritDoc}
1226
     */
1227 2
    public function getSmallIntTypeDeclarationSQL(array $field)
1228
    {
1229 2
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1230
    }
1231
1232
    /**
1233
     * {@inheritDoc}
1234
     */
1235 22
    public function getGuidTypeDeclarationSQL(array $field)
1236
    {
1237 22
        return 'UNIQUEIDENTIFIER';
1238
    }
1239
1240
    /**
1241
     * {@inheritDoc}
1242
     */
1243 52
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
1244
    {
1245 52
        return 'DATETIMEOFFSET(6)';
1246
    }
1247
1248
    /**
1249
     * {@inheritDoc}
1250
     */
1251 686
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1252
    {
1253 686
        return $fixed
1254 80
            ? ($length > 0 ? 'NCHAR(' . $length . ')' : 'CHAR(255)')
1255 686
            : ($length > 0 ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1256
    }
1257
1258
    /**
1259
     * {@inheritdoc}
1260
     */
1261 26
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1262
    {
1263 26
        return $fixed
1264 26
            ? 'BINARY(' . ($length > 0 ? $length : 255) . ')'
1265 26
            : 'VARBINARY(' . ($length > 0 ? $length : 255) . ')';
1266
    }
1267
1268
    /**
1269
     * {@inheritdoc}
1270
     */
1271 70
    public function getBinaryMaxLength()
1272
    {
1273 70
        return 8000;
1274
    }
1275
1276
    /**
1277
     * {@inheritDoc}
1278
     */
1279 206
    public function getClobTypeDeclarationSQL(array $field)
1280
    {
1281 206
        return 'VARCHAR(MAX)';
1282
    }
1283
1284
    /**
1285
     * {@inheritDoc}
1286
     */
1287 564
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1288
    {
1289 564
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1290
    }
1291
1292
    /**
1293
     * {@inheritDoc}
1294
     */
1295 46
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1296
    {
1297
        // 3 - microseconds precision length
1298
        // http://msdn.microsoft.com/en-us/library/ms187819.aspx
1299 46
        return 'DATETIME2(6)';
1300
    }
1301
1302
    /**
1303
     * {@inheritDoc}
1304
     */
1305 38
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1306
    {
1307 38
        return 'DATE';
1308
    }
1309
1310
    /**
1311
     * {@inheritDoc}
1312
     */
1313 38
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1314
    {
1315 38
        return 'TIME(0)';
1316
    }
1317
1318
    /**
1319
     * {@inheritDoc}
1320
     */
1321 56
    public function getBooleanTypeDeclarationSQL(array $field)
1322
    {
1323 56
        return 'BIT';
1324
    }
1325
1326
    /**
1327
     * {@inheritDoc}
1328
     */
1329 588
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1330
    {
1331 588
        if ($limit === null && $offset <= 0) {
1332 24
            return $query;
1333
        }
1334
1335
        // Queries using OFFSET... FETCH MUST have an ORDER BY clause
1336
        // Find the position of the last instance of ORDER BY and ensure it is not within a parenthetical statement
1337
        // but can be in a newline
1338 564
        $matches      = [];
1339 564
        $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE);
1340 564
        $orderByPos   = false;
1341 564
        if ($matchesCount > 0) {
1342 386
            $orderByPos = $matches[0][($matchesCount - 1)][1];
1343
        }
1344
1345 564
        if ($orderByPos === false
1346 564
            || substr_count($query, '(', $orderByPos) !== substr_count($query, ')', $orderByPos)
1347
        ) {
1348 200
            if (preg_match('/^SELECT\s+DISTINCT/im', $query) > 0) {
1349
                // SQL Server won't let us order by a non-selected column in a DISTINCT query,
1350
                // so we have to do this madness. This says, order by the first column in the
1351
                // result. SQL Server's docs say that a nonordered query's result order is non-
1352
                // deterministic anyway, so this won't do anything that a bunch of update and
1353
                // deletes to the table wouldn't do anyway.
1354 44
                $query .= ' ORDER BY 1';
1355
            } else {
1356
                // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you
1357
                // use constant expressions in the order by list.
1358 156
                $query .= ' ORDER BY (SELECT 0)';
1359
            }
1360
        }
1361
1362
        // This looks somewhat like MYSQL, but limit/offset are in inverse positions
1363
        // Supposedly SQL:2008 core standard.
1364
        // Per TSQL spec, FETCH NEXT n ROWS ONLY is not valid without OFFSET n ROWS.
1365 564
        $query .= sprintf(' OFFSET %d ROWS', $offset);
1366
1367 564
        if ($limit !== null) {
1368 564
            $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit);
1369
        }
1370
1371 564
        return $query;
1372
    }
1373
1374
    /**
1375
     * {@inheritDoc}
1376
     */
1377 102
    public function supportsLimitOffset()
1378
    {
1379 102
        return true;
1380
    }
1381
1382
    /**
1383
     * {@inheritDoc}
1384
     */
1385 30
    public function convertBooleans($item)
1386
    {
1387 30
        if (is_array($item)) {
1388
            foreach ($item as $key => $value) {
1389
                if (! is_bool($value) && ! is_numeric($value)) {
1390
                    continue;
1391
                }
1392
1393
                $item[$key] = (int) (bool) $value;
1394
            }
1395 30
        } elseif (is_bool($item) || is_numeric($item)) {
1396 30
            $item = (int) (bool) $item;
1397
        }
1398
1399 30
        return $item;
1400
    }
1401
1402
    /**
1403
     * {@inheritDoc}
1404
     */
1405 4
    public function getCreateTemporaryTableSnippetSQL()
1406
    {
1407 4
        return 'CREATE TABLE';
1408
    }
1409
1410
    /**
1411
     * {@inheritDoc}
1412
     */
1413 4
    public function getTemporaryTableName($tableName)
1414
    {
1415 4
        return '#' . $tableName;
1416
    }
1417
1418
    /**
1419
     * {@inheritDoc}
1420
     */
1421 22
    public function getDateTimeFormatString()
1422
    {
1423 22
        return 'Y-m-d H:i:s.u';
1424
    }
1425
1426
    /**
1427
     * {@inheritDoc}
1428
     */
1429 2
    public function getDateFormatString()
1430
    {
1431 2
        return 'Y-m-d';
1432
    }
1433
1434
    /**
1435
     * {@inheritDoc}
1436
     */
1437 2
    public function getTimeFormatString()
1438
    {
1439 2
        return 'H:i:s';
1440
    }
1441
1442
    /**
1443
     * {@inheritDoc}
1444
     */
1445 2
    public function getDateTimeTzFormatString()
1446
    {
1447 2
        return 'Y-m-d H:i:s.u P';
1448
    }
1449
1450
    /**
1451
     * {@inheritDoc}
1452
     */
1453 144
    public function getName()
1454
    {
1455 144
        return 'mssql';
1456
    }
1457
1458
    /**
1459
     * {@inheritDoc}
1460
     */
1461 112
    protected function initializeDoctrineTypeMappings()
1462
    {
1463 112
        $this->doctrineTypeMapping = [
1464
            'bigint'           => 'bigint',
1465
            'binary'           => 'binary',
1466
            'bit'              => 'boolean',
1467
            'char'             => 'string',
1468
            'date'             => 'date',
1469
            'datetime'         => 'datetime',
1470
            'datetime2'        => 'datetime',
1471
            'datetimeoffset'   => 'datetimetz',
1472
            'decimal'          => 'decimal',
1473
            'double'           => 'float',
1474
            'double precision' => 'float',
1475
            'float'            => 'float',
1476
            'image'            => 'blob',
1477
            'int'              => 'integer',
1478
            'money'            => 'integer',
1479
            'nchar'            => 'string',
1480
            'ntext'            => 'text',
1481
            'numeric'          => 'decimal',
1482
            'nvarchar'         => 'string',
1483
            'real'             => 'float',
1484
            'smalldatetime'    => 'datetime',
1485
            'smallint'         => 'smallint',
1486
            'smallmoney'       => 'integer',
1487
            'text'             => 'text',
1488
            'time'             => 'time',
1489
            'tinyint'          => 'smallint',
1490
            'uniqueidentifier' => 'guid',
1491
            'varbinary'        => 'binary',
1492
            'varchar'          => 'string',
1493
        ];
1494 112
    }
1495
1496
    /**
1497
     * {@inheritDoc}
1498
     */
1499 2
    public function createSavePoint($savepoint)
1500
    {
1501 2
        return 'SAVE TRANSACTION ' . $savepoint;
1502
    }
1503
1504
    /**
1505
     * {@inheritDoc}
1506
     */
1507
    public function releaseSavePoint($savepoint)
1508
    {
1509
        return '';
1510
    }
1511
1512
    /**
1513
     * {@inheritDoc}
1514
     */
1515 2
    public function rollbackSavePoint($savepoint)
1516
    {
1517 2
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1518
    }
1519
1520
    /**
1521
     * {@inheritdoc}
1522
     */
1523 156
    public function getForeignKeyReferentialActionSQL($action)
1524
    {
1525
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1526 156
        if (strtoupper($action) === 'RESTRICT') {
1527 22
            return 'NO ACTION';
1528
        }
1529
1530 134
        return parent::getForeignKeyReferentialActionSQL($action);
1531
    }
1532
1533
    /**
1534
     * {@inheritDoc}
1535
     */
1536 158
    public function appendLockHint($fromClause, $lockMode)
1537
    {
1538 158
        switch (true) {
1539
            case $lockMode === LockMode::NONE:
1540 22
                return $fromClause . ' WITH (NOLOCK)';
1541
1542 136
            case $lockMode === LockMode::PESSIMISTIC_READ:
1543 22
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1544
1545 114
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1546 26
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1547
1548
            default:
1549 88
                return $fromClause;
1550
        }
1551
    }
1552
1553
    /**
1554
     * {@inheritDoc}
1555
     */
1556 4
    public function getForUpdateSQL()
1557
    {
1558 4
        return ' ';
1559
    }
1560
1561
    /**
1562
     * {@inheritDoc}
1563
     */
1564 1404
    protected function getReservedKeywordsClass()
1565
    {
1566 1404
        return Keywords\SQLServer2012Keywords::class;
1567
    }
1568
1569
    /**
1570
     * {@inheritDoc}
1571
     */
1572 690
    public function quoteSingleIdentifier($str)
1573
    {
1574 690
        return '[' . str_replace(']', '][', $str) . ']';
1575
    }
1576
1577
    /**
1578
     * {@inheritDoc}
1579
     */
1580 38
    public function getTruncateTableSQL($tableName, $cascade = false)
1581
    {
1582 38
        $tableIdentifier = new Identifier($tableName);
1583
1584 38
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1585
    }
1586
1587
    /**
1588
     * {@inheritDoc}
1589
     */
1590 38
    public function getBlobTypeDeclarationSQL(array $field)
1591
    {
1592 38
        return 'VARBINARY(MAX)';
1593
    }
1594
1595
    /**
1596
     * {@inheritdoc}
1597
     *
1598
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1599
     */
1600 962
    public function getColumnDeclarationSQL($name, array $field)
1601
    {
1602 962
        if (isset($field['columnDefinition'])) {
1603 22
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1604
        } else {
1605 940
            $collation = ! empty($field['collation']) ?
1606 940
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1607
1608 940
            $notnull = ! empty($field['notnull']) ? ' NOT NULL' : '';
1609
1610 940
            $unique = ! empty($field['unique']) ?
1611 940
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1612
1613 940
            $check = ! empty($field['check']) ?
1614 940
                ' ' . $field['check'] : '';
1615
1616 940
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1617 940
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1618
        }
1619
1620 962
        return $name . ' ' . $columnDef;
1621
    }
1622
1623
    /**
1624
     * {@inheritdoc}
1625
     */
1626 24
    protected function getLikeWildcardCharacters() : string
1627
    {
1628 24
        return parent::getLikeWildcardCharacters() . '[]^';
1629
    }
1630
1631
    /**
1632
     * Returns a unique default constraint name for a table and column.
1633
     *
1634
     * @param string $table  Name of the table to generate the unique default constraint name for.
1635
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1636
     *
1637
     * @return string
1638
     */
1639 360
    private function generateDefaultConstraintName($table, $column)
1640
    {
1641 360
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1642
    }
1643
1644
    /**
1645
     * Returns a hash value for a given identifier.
1646
     *
1647
     * @param string $identifier Identifier to generate a hash value for.
1648
     *
1649
     * @return string
1650
     */
1651 382
    private function generateIdentifierName($identifier)
1652
    {
1653
        // Always generate name for unquoted identifiers to ensure consistency.
1654 382
        $identifier = new Identifier($identifier);
1655
1656 382
        return strtoupper(dechex(crc32($identifier->getName())));
1657
    }
1658
1659 2
    protected function getCommentOnTableSQL(string $tableName, ?string $comment) : string
1660
    {
1661 2
        return sprintf(
1662
            <<<'SQL'
1663
EXEC sys.sp_addextendedproperty @name=N'MS_Description', 
1664
  @value=N%s, @level0type=N'SCHEMA', @level0name=N'dbo', 
1665
  @level1type=N'TABLE', @level1name=N%s
1666
SQL
1667
            ,
1668 2
            $this->quoteStringLiteral((string) $comment),
1669 2
            $this->quoteStringLiteral($tableName)
1670
        );
1671
    }
1672
1673 84
    public function getListTableMetadataSQL(string $table) : string
1674
    {
1675 84
        return sprintf(
1676
            <<<'SQL'
1677
SELECT
1678
  p.value AS [table_comment]
1679
FROM
1680
  sys.tables AS tbl
1681
  INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1
1682
WHERE
1683
  (tbl.name=N%s and SCHEMA_NAME(tbl.schema_id)=N'dbo' and p.name=N'MS_Description')
1684
SQL
1685
            ,
1686 84
            $this->quoteStringLiteral($table)
1687
        );
1688
    }
1689
}
1690