Completed
Pull Request — 2.10.x (#4009)
by Grégoire
08:50
created

SQLServerPlatform::getTrimExpression()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 40
ccs 15
cts 15
cp 1
rs 8.6577
c 0
b 0
f 0
cc 6
nc 6
nop 3
crap 6
1
<?php
2
3
namespace Doctrine\DBAL\Platforms;
4
5
use Doctrine\DBAL\LockMode;
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\ColumnDiff;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Doctrine\DBAL\Schema\Identifier;
10
use Doctrine\DBAL\Schema\Index;
11
use Doctrine\DBAL\Schema\Table;
12
use Doctrine\DBAL\Schema\TableDiff;
13
use InvalidArgumentException;
14
use function array_merge;
15
use function array_unique;
16
use function array_values;
17
use function count;
18
use function crc32;
19
use function dechex;
20
use function explode;
21
use function func_get_args;
22
use function implode;
23
use function is_array;
24
use function is_bool;
25
use function is_numeric;
26
use function is_string;
27
use function preg_match;
28
use function sprintf;
29
use function str_replace;
30
use function stripos;
31
use function stristr;
32
use function strlen;
33
use function strpos;
34
use function strtoupper;
35
use function substr;
36
use function substr_count;
37
38
/**
39
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
40
 * Microsoft SQL Server database platform.
41
 */
42
class SQLServerPlatform extends AbstractPlatform
43
{
44
    /**
45
     * {@inheritdoc}
46
     */
47 7727
    public function getCurrentDateSQL()
48
    {
49 7727
        return $this->getConvertExpression('date', 'GETDATE()');
50
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55 7722
    public function getCurrentTimeSQL()
56
    {
57 7722
        return $this->getConvertExpression('time', 'GETDATE()');
58
    }
59
60
    /**
61
     * Returns an expression that converts an expression of one data type to another.
62
     *
63
     * @param string $dataType   The target native data type. Alias data types cannot be used.
64
     * @param string $expression The SQL expression to convert.
65
     *
66
     * @return string
67
     */
68 7727
    private function getConvertExpression($dataType, $expression)
69
    {
70 7727
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76 963
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
77
    {
78 963
        $factorClause = '';
79
80 963
        if ($operator === '-') {
81 963
            $factorClause = '-1 * ';
82
        }
83
84 963
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
85
    }
86
87
    /**
88
     * {@inheritDoc}
89
     */
90 942
    public function getDateDiffExpression($date1, $date2)
91
    {
92 942
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
93
    }
94
95
    /**
96
     * {@inheritDoc}
97
     *
98
     * Microsoft SQL Server prefers "autoincrement" identity columns
99
     * since sequences can only be emulated with a table.
100
     */
101 7345
    public function prefersIdentityColumns()
102
    {
103 7345
        return true;
104
    }
105
106
    /**
107
     * {@inheritDoc}
108
     *
109
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
110
     */
111 832
    public function supportsIdentityColumns()
112
    {
113 832
        return true;
114
    }
115
116
    /**
117
     * {@inheritDoc}
118
     */
119 1005
    public function supportsReleaseSavepoints()
120
    {
121 1005
        return false;
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 862
    public function supportsSchemas()
128
    {
129 862
        return true;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135 805
    public function getDefaultSchemaName()
136
    {
137 805
        return 'dbo';
138
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143 6344
    public function supportsColumnCollation()
144
    {
145 6344
        return true;
146
    }
147
148
    /**
149
     * {@inheritDoc}
150
     */
151 7998
    public function hasNativeGuidType()
152
    {
153 7998
        return true;
154
    }
155
156
    /**
157
     * {@inheritDoc}
158
     */
159 7763
    public function getCreateDatabaseSQL($name)
160
    {
161 7763
        return 'CREATE DATABASE ' . $name;
162
    }
163
164
    /**
165
     * {@inheritDoc}
166
     */
167 7763
    public function getDropDatabaseSQL($name)
168
    {
169 7763
        return 'DROP DATABASE ' . $name;
170
    }
171
172
    /**
173
     * {@inheritDoc}
174
     */
175 1022
    public function supportsCreateDropDatabase()
176
    {
177 1022
        return true;
178
    }
179
180
    /**
181
     * {@inheritDoc}
182
     */
183 6994
    public function getCreateSchemaSQL($schemaName)
184
    {
185 6994
        return 'CREATE SCHEMA ' . $schemaName;
186
    }
187
188
    /**
189
     * {@inheritDoc}
190
     */
191 5430
    public function getDropForeignKeySQL($foreignKey, $table)
192
    {
193 5430
        if (! $foreignKey instanceof ForeignKeyConstraint) {
194 5040
            $foreignKey = new Identifier($foreignKey);
195
        }
196
197 5430
        if (! $table instanceof Table) {
198 5430
            $table = new Identifier($table);
199
        }
200
201 5430
        $foreignKey = $foreignKey->getQuotedName($this);
202 5430
        $table      = $table->getQuotedName($this);
203
204 5430
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
205
    }
206
207
    /**
208
     * {@inheritDoc}
209
     */
210 843
    public function getDropIndexSQL($index, $table = null)
211
    {
212 843
        if ($index instanceof Index) {
213 835
            $index = $index->getQuotedName($this);
214 843
        } elseif (! is_string($index)) {
215
            throw new InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
216
        }
217
218 843
        if (! isset($table)) {
219
            return 'DROP INDEX ' . $index;
220
        }
221
222 843
        if ($table instanceof Table) {
223 843
            $table = $table->getQuotedName($this);
224
        }
225
226 843
        return sprintf(
227
            <<<SQL
228
IF EXISTS (SELECT * FROM sysobjects WHERE name = '%s')
229
    ALTER TABLE %s DROP CONSTRAINT %s
230
ELSE
231
    DROP INDEX %s ON %s
232
SQL
233
            ,
234 843
            $index,
235 843
            $table,
236 843
            $index,
237 843
            $index,
238 843
            $table
239
        );
240
    }
241
242
    /**
243
     * {@inheritDoc}
244
     */
245 7875
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
246
    {
247 7875
        $defaultConstraintsSql = [];
248 7875
        $commentsSql           = [];
249
250 7875
        $tableComment = $options['comment'] ?? null;
251 7875
        if ($tableComment !== null) {
252 767
            $commentsSql[] = $this->getCommentOnTableSQL($tableName, $tableComment);
253
        }
254
255
        // @todo does other code breaks because of this?
256
        // force primary keys to be not null
257 7875
        foreach ($columns as &$column) {
258 7875
            if (isset($column['primary']) && $column['primary']) {
259 7232
                $column['notnull'] = true;
260
            }
261
262
            // Build default constraints SQL statements.
263 7875
            if (isset($column['default'])) {
264 6767
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
265 6767
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
266
            }
267
268 7875
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
269 7869
                continue;
270
            }
271
272 6994
            $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
273
        }
274
275 7875
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
276
277 7875
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
278
            foreach ($options['uniqueConstraints'] as $name => $definition) {
279
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
280
            }
281
        }
282
283 7875
        if (isset($options['primary']) && ! empty($options['primary'])) {
284 7235
            $flags = '';
285 7235
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
286 6788
                $flags = ' NONCLUSTERED';
287
            }
288
289 7235
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
290
        }
291
292 7875
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
293
294 7875
        $check = $this->getCheckDeclarationSQL($columns);
295 7875
        if (! empty($check)) {
296
            $query .= ', ' . $check;
297
        }
298
299 7875
        $query .= ')';
300
301 7875
        $sql = [$query];
302
303 7875
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
304 6012
            foreach ($options['indexes'] as $index) {
305 6012
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
306
            }
307
        }
308
309 7875
        if (isset($options['foreignKeys'])) {
310 5831
            foreach ((array) $options['foreignKeys'] as $definition) {
311 5653
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
312
            }
313
        }
314
315 7875
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
316
    }
317
318
    /**
319
     * {@inheritDoc}
320
     */
321 6768
    public function getCreatePrimaryKeySQL(Index $index, $table)
322
    {
323 6768
        if ($table instanceof Table) {
324
            $identifier = $table->getQuotedName($this);
325
        } else {
326 6768
            $identifier = $table;
327
        }
328
329 6768
        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';
330
331 6768
        if ($index->hasFlag('nonclustered')) {
332 6765
            $sql .= ' NONCLUSTERED';
333
        }
334
335 6768
        return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
336
    }
337
338
    /**
339
     * Returns the SQL statement for creating a column comment.
340
     *
341
     * SQL Server does not support native column comments,
342
     * therefore the extended properties functionality is used
343
     * as a workaround to store them.
344
     * The property name used to store column comments is "MS_Description"
345
     * which provides compatibility with SQL Server Management Studio,
346
     * as column comments are stored in the same property there when
347
     * specifying a column's "Description" attribute.
348
     *
349
     * @param string      $tableName  The quoted table name to which the column belongs.
350
     * @param string      $columnName The quoted column name to create the comment for.
351
     * @param string|null $comment    The column's comment.
352
     *
353
     * @return string
354
     */
355 7003
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
356
    {
357 7003
        if (strpos($tableName, '.') !== false) {
358 6699
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
0 ignored issues
show
Bug introduced by
The variable $schemaSQL seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $tableSQL seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
359 6699
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
0 ignored issues
show
Bug introduced by
The variable $schemaSQL seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
360 6699
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
0 ignored issues
show
Bug introduced by
The variable $tableSQL seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
361
        } else {
362 6913
            $schemaSQL = "'dbo'";
363 6913
            $tableSQL  = $this->quoteStringLiteral($tableName);
364
        }
365
366 7003
        return $this->getAddExtendedPropertySQL(
367 7003
            'MS_Description',
368
            $comment,
369 7003
            'SCHEMA',
370
            $schemaSQL,
371 7003
            'TABLE',
372
            $tableSQL,
373 7003
            'COLUMN',
374
            $columnName
375
        );
376
    }
377
378
    /**
379
     * Returns the SQL snippet for declaring a default constraint.
380
     *
381
     * @param string  $table  Name of the table to return the default constraint declaration for.
382
     * @param mixed[] $column Column definition.
383
     *
384
     * @return string
385
     *
386
     * @throws InvalidArgumentException
387
     */
388 6905
    public function getDefaultConstraintDeclarationSQL($table, array $column)
389
    {
390 6905
        if (! isset($column['default'])) {
391
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
392
        }
393
394 6905
        $columnName = new Identifier($column['name']);
395
396
        return ' CONSTRAINT ' .
397 6905
            $this->generateDefaultConstraintName($table, $column['name']) .
398 6905
            $this->getDefaultValueDeclarationSQL($column) .
399 6905
            ' FOR ' . $columnName->getQuotedName($this);
400
    }
401
402
    /**
403
     * {@inheritDoc}
404
     */
405 5549
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
406
    {
407 5549
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
408
409 5549
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
410
411 5549
        return $constraint;
412
    }
413
414
    /**
415
     * {@inheritDoc}
416
     */
417 7119
    public function getCreateIndexSQL(Index $index, $table)
418
    {
419 7119
        $constraint = parent::getCreateIndexSQL($index, $table);
420
421 7119
        if ($index->isUnique() && ! $index->isPrimary()) {
422 5979
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
423
        }
424
425 7119
        return $constraint;
426
    }
427
428
    /**
429
     * {@inheritDoc}
430
     */
431 7119
    protected function getCreateIndexSQLFlags(Index $index)
432
    {
433 7119
        $type = '';
434 7119
        if ($index->isUnique()) {
435 5979
            $type .= 'UNIQUE ';
436
        }
437
438 7119
        if ($index->hasFlag('clustered')) {
439 6811
            $type .= 'CLUSTERED ';
440 6024
        } elseif ($index->hasFlag('nonclustered')) {
441
            $type .= 'NONCLUSTERED ';
442
        }
443
444 7119
        return $type;
445
    }
446
447
    /**
448
     * Extend unique key constraint with required filters
449
     *
450
     * @param string $sql
451
     *
452
     * @return string
453
     */
454 5985
    private function _appendUniqueConstraintDefinition($sql, Index $index)
455
    {
456 5985
        $fields = [];
457
458 5985
        foreach ($index->getQuotedColumns($this) as $field) {
459 5985
            $fields[] = $field . ' IS NOT NULL';
460
        }
461
462 5985
        return $sql . ' WHERE ' . implode(' AND ', $fields);
463
    }
464
465
    /**
466
     * {@inheritDoc}
467
     */
468 7033
    public function getAlterTableSQL(TableDiff $diff)
469
    {
470 7033
        $queryParts  = [];
471 7033
        $sql         = [];
472 7033
        $columnSql   = [];
473 7033
        $commentsSql = [];
474
475 7033
        foreach ($diff->addedColumns as $column) {
476 6991
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
477
                continue;
478
            }
479
480 6991
            $columnDef    = $column->toArray();
481 6991
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
482
483 6991
            if (isset($columnDef['default'])) {
484 6291
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
485
            }
486
487 6991
            $comment = $this->getColumnComment($column);
488
489 6991
            if (empty($comment) && ! is_numeric($comment)) {
490 6901
                continue;
491
            }
492
493 6970
            $commentsSql[] = $this->getCreateColumnCommentSQL(
494 6970
                $diff->name,
495 6970
                $column->getQuotedName($this),
496
                $comment
497
            );
498
        }
499
500 7033
        foreach ($diff->removedColumns as $column) {
501 6907
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
502
                continue;
503
            }
504
505 6907
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
506
        }
507
508 7033
        foreach ($diff->changedColumns as $columnDiff) {
509 6984
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
510
                continue;
511
            }
512
513 6984
            $column     = $columnDiff->column;
514 6984
            $comment    = $this->getColumnComment($column);
515 6984
            $hasComment = ! empty($comment) || is_numeric($comment);
516
517 6984
            if ($columnDiff->fromColumn instanceof Column) {
518 6969
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
519 6969
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
520
521 6969
                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
522 6925
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
523 6925
                        $diff->name,
524 6925
                        $column->getQuotedName($this),
525
                        $comment
526
                    );
527 6966
                } elseif ($hasFromComment && ! $hasComment) {
528 6946
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
529 6900
                } elseif ($hasComment) {
530 6880
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
531 6880
                        $diff->name,
532 6880
                        $column->getQuotedName($this),
533
                        $comment
534
                    );
535
                }
536
            }
537
538
            // Do not add query part if only comment has changed.
539 6984
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
540 6952
                continue;
541
            }
542
543 6912
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
544
545 6912
            if ($requireDropDefaultConstraint) {
546 6810
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
547 6810
                    $diff->name,
548 6810
                    $columnDiff->oldColumnName
549
                );
550
            }
551
552 6912
            $columnDef = $column->toArray();
553
554 6912
            $queryParts[] = 'ALTER COLUMN ' .
555 6912
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
556
557 6912
            if (! isset($columnDef['default']) || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))) {
558 6894
                continue;
559
            }
560
561 6813
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
562
        }
563
564 7033
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
565 6894
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
566
                continue;
567
            }
568
569 6894
            $oldColumnName = new Identifier($oldColumnName);
570
571 6894
            $sql[] = "sp_RENAME '" .
572 6894
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
573 6894
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
574
575
            // Recreate default constraint with new column name if necessary (for future reference).
576 6894
            if ($column->getDefault() === null) {
577 6889
                continue;
578
            }
579
580 5412
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
581 5412
                $diff->name,
582 5412
                $oldColumnName->getQuotedName($this)
583
            );
584 5412
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
585
        }
586
587 7033
        $tableSql = [];
588
589 7033
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
590
            return array_merge($tableSql, $columnSql);
591
        }
592
593 7033
        foreach ($queryParts as $query) {
594 7009
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
595
        }
596
597 7033
        $sql = array_merge($sql, $commentsSql);
598
599 7033
        $newName = $diff->getNewName();
600
601 7033
        if ($newName !== false) {
602 5898
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";
603
604
            /**
605
             * Rename table's default constraints names
606
             * to match the new table name.
607
             * This is necessary to ensure that the default
608
             * constraints can be referenced in future table
609
             * alterations as the table name is encoded in
610
             * default constraints' names.
611
             */
612 5898
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
613
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
614 5898
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
615 5898
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
616 5898
                'FROM sys.default_constraints dc ' .
617 5898
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
618 5898
                "WHERE tbl.name = '" . $newName->getName() . "';" .
619 5898
                'EXEC sp_executesql @sql';
620
        }
621
622 7033
        $sql = array_merge(
623 7033
            $this->getPreAlterTableIndexForeignKeySQL($diff),
624 7033
            $sql,
625 7033
            $this->getPostAlterTableIndexForeignKeySQL($diff)
626
        );
627
628 7033
        return array_merge($sql, $tableSql, $columnSql);
629
    }
630
631
    /**
632
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
633
     *
634
     * @param string $tableName The name of the table to generate the clause for.
635
     * @param Column $column    The column to generate the clause for.
636
     *
637
     * @return string
638
     */
639 6816
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
640
    {
641 6816
        $columnDef         = $column->toArray();
642 6816
        $columnDef['name'] = $column->getQuotedName($this);
643
644 6816
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
645
    }
646
647
    /**
648
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
649
     *
650
     * @param string $tableName  The name of the table to generate the clause for.
651
     * @param string $columnName The name of the column to generate the clause for.
652
     *
653
     * @return string
654
     */
655 6813
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
656
    {
657 6813
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
658
    }
659
660
    /**
661
     * Checks whether a column alteration requires dropping its default constraint first.
662
     *
663
     * Different to other database vendors SQL Server implements column default values
664
     * as constraints and therefore changes in a column's default value as well as changes
665
     * in a column's type require dropping the default constraint first before being to
666
     * alter the particular column to the new definition.
667
     *
668
     * @param ColumnDiff $columnDiff The column diff to evaluate.
669
     *
670
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
671
     */
672 6912
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
673
    {
674
        // We can only decide whether to drop an existing default constraint
675
        // if we know the original default value.
676 6912
        if (! $columnDiff->fromColumn instanceof Column) {
677 5509
            return false;
678
        }
679
680
        // We only need to drop an existing default constraint if we know the
681
        // column was defined with a default value before.
682 6900
        if ($columnDiff->fromColumn->getDefault() === null) {
683 6883
            return false;
684
        }
685
686
        // We need to drop an existing default constraint if the column was
687
        // defined with a default value before and it has changed.
688 6810
        if ($columnDiff->hasChanged('default')) {
689 6618
            return true;
690
        }
691
692
        // We need to drop an existing default constraint if the column was
693
        // defined with a default value before and the native column type has changed.
694 6798
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
695
    }
696
697
    /**
698
     * Returns the SQL statement for altering a column comment.
699
     *
700
     * SQL Server does not support native column comments,
701
     * therefore the extended properties functionality is used
702
     * as a workaround to store them.
703
     * The property name used to store column comments is "MS_Description"
704
     * which provides compatibility with SQL Server Management Studio,
705
     * as column comments are stored in the same property there when
706
     * specifying a column's "Description" attribute.
707
     *
708
     * @param string      $tableName  The quoted table name to which the column belongs.
709
     * @param string      $columnName The quoted column name to alter the comment for.
710
     * @param string|null $comment    The column's comment.
711
     *
712
     * @return string
713
     */
714 6925
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
715
    {
716 6925
        if (strpos($tableName, '.') !== false) {
717 6627
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
0 ignored issues
show
Bug introduced by
The variable $schemaSQL seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $tableSQL seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
718 6627
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
0 ignored issues
show
Bug introduced by
The variable $schemaSQL seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
719 6627
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
0 ignored issues
show
Bug introduced by
The variable $tableSQL seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
720
        } else {
721 6880
            $schemaSQL = "'dbo'";
722 6880
            $tableSQL  = $this->quoteStringLiteral($tableName);
723
        }
724
725 6925
        return $this->getUpdateExtendedPropertySQL(
726 6925
            'MS_Description',
727
            $comment,
728 6925
            'SCHEMA',
729
            $schemaSQL,
730 6925
            'TABLE',
731
            $tableSQL,
732 6925
            'COLUMN',
733
            $columnName
734
        );
735
    }
736
737
    /**
738
     * Returns the SQL statement for dropping a column comment.
739
     *
740
     * SQL Server does not support native column comments,
741
     * therefore the extended properties functionality is used
742
     * as a workaround to store them.
743
     * The property name used to store column comments is "MS_Description"
744
     * which provides compatibility with SQL Server Management Studio,
745
     * as column comments are stored in the same property there when
746
     * specifying a column's "Description" attribute.
747
     *
748
     * @param string $tableName  The quoted table name to which the column belongs.
749
     * @param string $columnName The quoted column name to drop the comment for.
750
     *
751
     * @return string
752
     */
753 6946
    protected function getDropColumnCommentSQL($tableName, $columnName)
754
    {
755 6946
        if (strpos($tableName, '.') !== false) {
756 6650
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
0 ignored issues
show
Bug introduced by
The variable $schemaSQL seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $tableSQL seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
757 6650
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
0 ignored issues
show
Bug introduced by
The variable $schemaSQL seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
758 6650
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
0 ignored issues
show
Bug introduced by
The variable $tableSQL seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
759
        } else {
760 6880
            $schemaSQL = "'dbo'";
761 6880
            $tableSQL  = $this->quoteStringLiteral($tableName);
762
        }
763
764 6946
        return $this->getDropExtendedPropertySQL(
765 6946
            'MS_Description',
766 6946
            'SCHEMA',
767
            $schemaSQL,
768 6946
            'TABLE',
769
            $tableSQL,
770 6946
            'COLUMN',
771
            $columnName
772
        );
773
    }
774
775
    /**
776
     * {@inheritdoc}
777
     */
778 5554
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
779
    {
780 5554
        return [sprintf(
781 15
            "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
782 5554
            $tableName,
783 5554
            $oldIndexName,
784 5554
            $index->getQuotedName($this)
785
        ),
786
        ];
787
    }
788
789
    /**
790
     * Returns the SQL statement for adding an extended property to a database object.
791
     *
792
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
793
     *
794
     * @param string      $name       The name of the property to add.
795
     * @param string|null $value      The value of the property to add.
796
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
797
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
798
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
799
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
800
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
801
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
802
     *
803
     * @return string
804
     */
805 7003
    public function getAddExtendedPropertySQL(
806
        $name,
807
        $value = null,
808
        $level0Type = null,
809
        $level0Name = null,
810
        $level1Type = null,
811
        $level1Name = null,
812
        $level2Type = null,
813
        $level2Name = null
814
    ) {
815
        return 'EXEC sp_addextendedproperty ' .
816 7003
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
817 7003
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
818 7003
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
819 7003
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
820
    }
821
822
    /**
823
     * Returns the SQL statement for dropping an extended property from a database object.
824
     *
825
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
826
     *
827
     * @param string      $name       The name of the property to drop.
828
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
829
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
830
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
831
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
832
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
833
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
834
     *
835
     * @return string
836
     */
837 6946
    public function getDropExtendedPropertySQL(
838
        $name,
839
        $level0Type = null,
840
        $level0Name = null,
841
        $level1Type = null,
842
        $level1Name = null,
843
        $level2Type = null,
844
        $level2Name = null
845
    ) {
846
        return 'EXEC sp_dropextendedproperty ' .
847 6946
            'N' . $this->quoteStringLiteral($name) . ', ' .
848 6946
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
849 6946
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
850 6946
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
851
    }
852
853
    /**
854
     * Returns the SQL statement for updating an extended property of a database object.
855
     *
856
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
857
     *
858
     * @param string      $name       The name of the property to update.
859
     * @param string|null $value      The value of the property to update.
860
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
861
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
862
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
863
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
864
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
865
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
866
     *
867
     * @return string
868
     */
869 6925
    public function getUpdateExtendedPropertySQL(
870
        $name,
871
        $value = null,
872
        $level0Type = null,
873
        $level0Name = null,
874
        $level1Type = null,
875
        $level1Name = null,
876
        $level2Type = null,
877
        $level2Name = null
878
    ) {
879
        return 'EXEC sp_updateextendedproperty ' .
880 6925
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
881 6925
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
882 6925
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
883 6925
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
884
    }
885
886
    /**
887
     * {@inheritDoc}
888
     */
889 654
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
890
    {
891 654
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
892
    }
893
894
    /**
895
     * {@inheritDoc}
896
     */
897
    public function getListTablesSQL()
898
    {
899
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
900
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
901
        return "SELECT name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
902
    }
903
904
    /**
905
     * {@inheritDoc}
906
     */
907 6511
    public function getListTableColumnsSQL($table, $database = null)
908
    {
909
        return "SELECT    col.name,
910
                          type.name AS type,
911
                          col.max_length AS length,
912
                          ~col.is_nullable AS notnull,
913
                          def.definition AS [default],
914
                          col.scale,
915
                          col.precision,
916
                          col.is_identity AS autoincrement,
917
                          col.collation_name AS collation,
918
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
919
                FROM      sys.columns AS col
920
                JOIN      sys.types AS type
921
                ON        col.user_type_id = type.user_type_id
922
                JOIN      sys.objects AS obj
923
                ON        col.object_id = obj.object_id
924
                JOIN      sys.schemas AS scm
925
                ON        obj.schema_id = scm.schema_id
926
                LEFT JOIN sys.default_constraints def
927
                ON        col.default_object_id = def.object_id
928
                AND       col.object_id = def.parent_object_id
929
                LEFT JOIN sys.extended_properties AS prop
930
                ON        obj.object_id = prop.major_id
931
                AND       col.column_id = prop.minor_id
932
                AND       prop.name = 'MS_Description'
933
                WHERE     obj.type = 'U'
934 6511
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
935
    }
936
937
    /**
938
     * @param string      $table
939
     * @param string|null $database
940
     *
941
     * @return string
942
     */
943 6469
    public function getListTableForeignKeysSQL($table, $database = null)
0 ignored issues
show
Unused Code introduced by
The parameter $database is not used and could be removed.

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

Loading history...
944
    {
945
        return 'SELECT f.name AS ForeignKey,
946
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
947
                OBJECT_NAME (f.parent_object_id) AS TableName,
948
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
949
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
950
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
951
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
952
                f.delete_referential_action_desc,
953
                f.update_referential_action_desc
954
                FROM sys.foreign_keys AS f
955
                INNER JOIN sys.foreign_key_columns AS fc
956
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
957
                ON f.OBJECT_ID = fc.constraint_object_id
958
                WHERE ' .
959 6469
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
960
    }
961
962
    /**
963
     * {@inheritDoc}
964
     */
965 6427
    public function getListTableIndexesSQL($table, $currentDatabase = null)
966
    {
967
        return "SELECT idx.name AS key_name,
968
                       col.name AS column_name,
969
                       ~idx.is_unique AS non_unique,
970
                       idx.is_primary_key AS [primary],
971
                       CASE idx.type
972
                           WHEN '1' THEN 'clustered'
973
                           WHEN '2' THEN 'nonclustered'
974
                           ELSE NULL
975
                       END AS flags
976
                FROM sys.tables AS tbl
977
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
978
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
979
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
980
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
981 6427
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
982
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
983
    }
984
985
    /**
986
     * {@inheritDoc}
987
     */
988 831
    public function getCreateViewSQL($name, $sql)
989
    {
990 831
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
991
    }
992
993
    /**
994
     * {@inheritDoc}
995
     */
996 831
    public function getListViewsSQL($database)
997
    {
998 831
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
999
    }
1000
1001
    /**
1002
     * Returns the where clause to filter schema and table name in a query.
1003
     *
1004
     * @param string $table        The full qualified name of the table.
1005
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
1006
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
1007
     *
1008
     * @return string
1009
     */
1010 6523
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
1011
    {
1012 6523
        if (strpos($table, '.') !== false) {
1013 6400
            [$schema, $table] = explode('.', $table);
0 ignored issues
show
Bug introduced by
The variable $schema seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
1014 6400
            $schema           = $this->quoteStringLiteral($schema);
0 ignored issues
show
Bug introduced by
The variable $schema seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
1015 6400
            $table            = $this->quoteStringLiteral($table);
1016
        } else {
1017 6514
            $schema = 'SCHEMA_NAME()';
1018 6514
            $table  = $this->quoteStringLiteral($table);
1019
        }
1020
1021 6523
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1022
    }
1023
1024
    /**
1025
     * {@inheritDoc}
1026
     */
1027 831
    public function getDropViewSQL($name)
1028
    {
1029 831
        return 'DROP VIEW ' . $name;
1030
    }
1031
1032
    /**
1033
     * {@inheritDoc}
1034
     *
1035
     * @deprecated Use application-generated UUIDs instead
1036
     */
1037
    public function getGuidExpression()
1038
    {
1039
        return 'NEWID()';
1040
    }
1041
1042
    /**
1043
     * {@inheritDoc}
1044
     */
1045 961
    public function getLocateExpression($str, $substr, $startPos = false)
1046
    {
1047 961
        if ($startPos === false) {
1048 961
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1049
        }
1050
1051 961
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1052
    }
1053
1054
    /**
1055
     * {@inheritDoc}
1056
     */
1057
    public function getModExpression($expression1, $expression2)
1058
    {
1059
        return $expression1 . ' % ' . $expression2;
1060
    }
1061
1062
    /**
1063
     * {@inheritDoc}
1064
     */
1065 995
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1066
    {
1067 995
        if (! $char) {
1068 995
            switch ($pos) {
1069
                case TrimMode::LEADING:
1070 993
                    $trimFn = 'LTRIM';
1071 993
                    break;
1072
1073
                case TrimMode::TRAILING:
1074 991
                    $trimFn = 'RTRIM';
1075 991
                    break;
1076
1077
                default:
1078 995
                    return 'LTRIM(RTRIM(' . $str . '))';
1079
            }
1080
1081 993
            return $trimFn . '(' . $str . ')';
1082
        }
1083
1084
        /** Original query used to get those expressions
1085
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1086
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1087
          select @c as string
1088
          , @trim_char as trim_char
1089
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1090
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1091
          , 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;
1092
         */
1093 987
        $pattern = "'%[^' + " . $char . " + ']%'";
1094
1095 987
        if ($pos === TrimMode::LEADING) {
1096 985
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1097
        }
1098
1099 987
        if ($pos === TrimMode::TRAILING) {
1100 983
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1101
        }
1102
1103 987
        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))';
1104
    }
1105
1106
    /**
1107
     * {@inheritDoc}
1108
     */
1109 7432
    public function getConcatExpression()
1110
    {
1111 7432
        $args = func_get_args();
1112
1113 7432
        return '(' . implode(' + ', $args) . ')';
1114
    }
1115
1116
    /**
1117
     * {@inheritDoc}
1118
     */
1119 7611
    public function getListDatabasesSQL()
1120
    {
1121 7611
        return 'SELECT * FROM sys.databases';
1122
    }
1123
1124
    /**
1125
     * {@inheritDoc}
1126
     */
1127 859
    public function getListNamespacesSQL()
1128
    {
1129 859
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1130
    }
1131
1132
    /**
1133
     * {@inheritDoc}
1134
     */
1135
    public function getSubstringExpression($value, $from, $length = null)
1136
    {
1137
        if ($length !== null) {
1138
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1139
        }
1140
1141
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1142
    }
1143
1144
    /**
1145
     * {@inheritDoc}
1146
     */
1147
    public function getLengthExpression($column)
1148
    {
1149
        return 'LEN(' . $column . ')';
1150
    }
1151
1152
    /**
1153
     * {@inheritDoc}
1154
     */
1155 7409
    public function getSetTransactionIsolationSQL($level)
1156
    {
1157 7409
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1158
    }
1159
1160
    /**
1161
     * {@inheritDoc}
1162
     */
1163 7866
    public function getIntegerTypeDeclarationSQL(array $field)
1164
    {
1165 7866
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1166
    }
1167
1168
    /**
1169
     * {@inheritDoc}
1170
     */
1171 715
    public function getBigIntTypeDeclarationSQL(array $field)
1172
    {
1173 715
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1174
    }
1175
1176
    /**
1177
     * {@inheritDoc}
1178
     */
1179 815
    public function getSmallIntTypeDeclarationSQL(array $field)
1180
    {
1181 815
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1182
    }
1183
1184
    /**
1185
     * {@inheritDoc}
1186
     */
1187 6190
    public function getGuidTypeDeclarationSQL(array $field)
1188
    {
1189 6190
        return 'UNIQUEIDENTIFIER';
1190
    }
1191
1192
    /**
1193
     * {@inheritDoc}
1194
     */
1195 7770
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1196
    {
1197 7770
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1198
    }
1199
1200
    /**
1201
     * {@inheritdoc}
1202
     */
1203 6774
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1204
    {
1205 6774
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1206
    }
1207
1208
    /**
1209
     * {@inheritdoc}
1210
     */
1211 6780
    public function getBinaryMaxLength()
1212
    {
1213 6780
        return 8000;
1214
    }
1215
1216
    /**
1217
     * {@inheritDoc}
1218
     */
1219 2512
    public function getClobTypeDeclarationSQL(array $field)
1220
    {
1221 2512
        return 'VARCHAR(MAX)';
1222
    }
1223
1224
    /**
1225
     * {@inheritDoc}
1226
     */
1227 7866
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1228
    {
1229 7866
        return ! empty($columnDef['autoincrement']) ? ' IDENTITY' : '';
1230
    }
1231
1232
    /**
1233
     * {@inheritDoc}
1234
     */
1235
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1236
    {
1237
        return 'DATETIME';
1238
    }
1239
1240
    /**
1241
     * {@inheritDoc}
1242
     */
1243
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1244
    {
1245
        return 'DATETIME';
1246
    }
1247
1248
    /**
1249
     * {@inheritDoc}
1250
     */
1251
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1252
    {
1253
        return 'DATETIME';
1254
    }
1255
1256
    /**
1257
     * {@inheritDoc}
1258
     */
1259 5918
    public function getBooleanTypeDeclarationSQL(array $field)
1260
    {
1261 5918
        return 'BIT';
1262
    }
1263
1264
    /**
1265
     * {@inheritDoc}
1266
     */
1267 7333
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1268
    {
1269 7333
        $where = [];
1270
1271 7333
        if ($offset > 0) {
1272 7249
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1273
        }
1274
1275 7333
        if ($limit !== null) {
1276 7331
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1277 7331
            $top     = sprintf('TOP %d', $offset + $limit);
1278
        } else {
1279 4878
            $top = 'TOP 9223372036854775807';
1280
        }
1281
1282 7333
        if (empty($where)) {
1283 4878
            return $query;
1284
        }
1285
1286
        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
1287
        // Even if the TOP n is very large, the use of a CTE will
1288
        // allow the SQL Server query planner to optimize it so it doesn't
1289
        // actually scan the entire range covered by the TOP clause.
1290 7331
        if (! preg_match('/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/is', $query, $matches)) {
1291
            return $query;
1292
        }
1293
1294 7331
        $query = $matches[1] . $top . ' ' . $matches[2];
1295
1296 7331
        if (stristr($query, 'ORDER BY')) {
1297
            // Inner order by is not valid in SQL Server for our purposes
1298
            // unless it's in a TOP N subquery.
1299 7271
            $query = $this->scrubInnerOrderBy($query);
1300
        }
1301
1302
        // Build a new limited query around the original, using a CTE
1303 7331
        return sprintf(
1304
            'WITH dctrn_cte AS (%s) '
1305
            . 'SELECT * FROM ('
1306
            . 'SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte'
1307
            . ') AS doctrine_tbl '
1308 40
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1309 7331
            $query,
1310 7331
            implode(' AND ', $where)
1311
        );
1312
    }
1313
1314
    /**
1315
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
1316
     * Caveat: will leave ORDER BY in TOP N subqueries.
1317
     *
1318
     * @param string $query
1319
     *
1320
     * @return string
1321
     */
1322 7271
    private function scrubInnerOrderBy($query)
1323
    {
1324 7271
        $count  = substr_count(strtoupper($query), 'ORDER BY');
1325 7271
        $offset = 0;
1326
1327 7271
        while ($count-- > 0) {
1328 7271
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1329 7271
            if ($orderByPos === false) {
1330 2647
                break;
1331
            }
1332
1333 7271
            $qLen            = strlen($query);
1334 7271
            $parenCount      = 0;
1335 7271
            $currentPosition = $orderByPos;
1336
1337 7271
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1338 7271
                if ($query[$currentPosition] === '(') {
1339 6971
                    $parenCount++;
1340 7271
                } elseif ($query[$currentPosition] === ')') {
1341 7121
                    $parenCount--;
1342
                }
1343
1344 7271
                $currentPosition++;
1345
            }
1346
1347 7271
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1348
                // If the order by clause is in a TOP N subquery, do not remove
1349
                // it and continue iteration from the current position.
1350 7268
                $offset = $currentPosition;
1351 7268
                continue;
1352
            }
1353
1354 7117
            if ($currentPosition >= $qLen - 1) {
1355
                continue;
1356
            }
1357
1358 7117
            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1359 7117
            $offset = $orderByPos;
1360
        }
1361
1362 7271
        return $query;
1363
    }
1364
1365
    /**
1366
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
1367
     *
1368
     * @param string $query           The query
1369
     * @param int    $currentPosition Start position of ORDER BY clause
1370
     *
1371
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
1372
     */
1373 7271
    private function isOrderByInTopNSubquery($query, $currentPosition)
1374
    {
1375
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1376 7271
        $subQueryBuffer = '';
1377 7271
        $parenCount     = 0;
1378
1379
        // If $parenCount goes negative, we've exited the subquery we're examining.
1380
        // If $currentPosition goes negative, we've reached the beginning of the query.
1381 7271
        while ($parenCount >= 0 && $currentPosition >= 0) {
1382 7271
            if ($query[$currentPosition] === '(') {
1383 7124
                $parenCount--;
1384 7271
            } elseif ($query[$currentPosition] === ')') {
1385 7052
                $parenCount++;
1386
            }
1387
1388
            // Only yank query text on the same nesting level as the ORDER BY clause.
1389 7271
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1390
1391 7271
            $currentPosition--;
1392
        }
1393
1394 7271
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1395
    }
1396
1397
    /**
1398
     * {@inheritDoc}
1399
     */
1400 2415
    public function supportsLimitOffset()
1401
    {
1402 2415
        return false;
1403
    }
1404
1405
    /**
1406
     * {@inheritDoc}
1407
     */
1408 5918
    public function convertBooleans($item)
1409
    {
1410 5918
        if (is_array($item)) {
1411
            foreach ($item as $key => $value) {
1412
                if (! is_bool($value) && ! is_numeric($value)) {
1413
                    continue;
1414
                }
1415
1416
                $item[$key] = $value ? 1 : 0;
1417
            }
1418 5918
        } elseif (is_bool($item) || is_numeric($item)) {
1419 5918
            $item = $item ? 1 : 0;
1420
        }
1421
1422 5918
        return $item;
1423
    }
1424
1425
    /**
1426
     * {@inheritDoc}
1427
     */
1428 719
    public function getCreateTemporaryTableSnippetSQL()
1429
    {
1430 719
        return 'CREATE TABLE';
1431
    }
1432
1433
    /**
1434
     * {@inheritDoc}
1435
     */
1436 719
    public function getTemporaryTableName($tableName)
1437
    {
1438 719
        return '#' . $tableName;
1439
    }
1440
1441
    /**
1442
     * {@inheritDoc}
1443
     */
1444
    public function getDateTimeFormatString()
1445
    {
1446
        return 'Y-m-d H:i:s.000';
1447
    }
1448
1449
    /**
1450
     * {@inheritDoc}
1451
     */
1452
    public function getDateFormatString()
1453
    {
1454
        return 'Y-m-d H:i:s.000';
1455
    }
1456
1457
    /**
1458
     * {@inheritDoc}
1459
     */
1460
    public function getTimeFormatString()
1461
    {
1462
        return 'Y-m-d H:i:s.000';
1463
    }
1464
1465
    /**
1466
     * {@inheritDoc}
1467
     */
1468
    public function getDateTimeTzFormatString()
1469
    {
1470
        return $this->getDateTimeFormatString();
1471
    }
1472
1473
    /**
1474
     * {@inheritDoc}
1475
     */
1476 3212
    public function getName()
1477
    {
1478 3212
        return 'mssql';
1479
    }
1480
1481
    /**
1482
     * {@inheritDoc}
1483
     */
1484 6898
    protected function initializeDoctrineTypeMappings()
1485
    {
1486 6898
        $this->doctrineTypeMapping = [
0 ignored issues
show
Documentation Bug introduced by
It seems like array('bigint' => 'bigin...eidentifier' => 'guid') of type array<string,string,{"bi...eidentifier":"string"}> is incompatible with the declared type array<integer,string>|null of property $doctrineTypeMapping.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1487
            'bigint' => 'bigint',
1488
            'numeric' => 'decimal',
1489
            'bit' => 'boolean',
1490
            'smallint' => 'smallint',
1491
            'decimal' => 'decimal',
1492
            'smallmoney' => 'integer',
1493
            'int' => 'integer',
1494
            'tinyint' => 'smallint',
1495
            'money' => 'integer',
1496
            'float' => 'float',
1497
            'real' => 'float',
1498
            'double' => 'float',
1499
            'double precision' => 'float',
1500
            'smalldatetime' => 'datetime',
1501
            'datetime' => 'datetime',
1502
            'char' => 'string',
1503
            'varchar' => 'string',
1504
            'text' => 'text',
1505
            'nchar' => 'string',
1506
            'nvarchar' => 'string',
1507
            'ntext' => 'text',
1508
            'binary' => 'binary',
1509
            'varbinary' => 'binary',
1510
            'image' => 'blob',
1511
            'uniqueidentifier' => 'guid',
1512
        ];
1513 6898
    }
1514
1515
    /**
1516
     * {@inheritDoc}
1517
     */
1518 1005
    public function createSavePoint($savepoint)
1519
    {
1520 1005
        return 'SAVE TRANSACTION ' . $savepoint;
1521
    }
1522
1523
    /**
1524
     * {@inheritDoc}
1525
     */
1526
    public function releaseSavePoint($savepoint)
1527
    {
1528
        return '';
1529
    }
1530
1531
    /**
1532
     * {@inheritDoc}
1533
     */
1534 1005
    public function rollbackSavePoint($savepoint)
1535
    {
1536 1005
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1537
    }
1538
1539
    /**
1540
     * {@inheritdoc}
1541
     */
1542 6257
    public function getForeignKeyReferentialActionSQL($action)
1543
    {
1544
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1545 6257
        if (strtoupper($action) === 'RESTRICT') {
1546 5845
            return 'NO ACTION';
1547
        }
1548
1549 6254
        return parent::getForeignKeyReferentialActionSQL($action);
1550
    }
1551
1552
    /**
1553
     * {@inheritDoc}
1554
     */
1555 3250
    public function appendLockHint($fromClause, $lockMode)
1556
    {
1557 3250
        switch (true) {
1558
            case $lockMode === LockMode::NONE:
1559 2738
                return $fromClause . ' WITH (NOLOCK)';
1560
1561 3249
            case $lockMode === LockMode::PESSIMISTIC_READ:
1562 2692
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1563
1564 3248
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1565 3160
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1566
1567
            default:
1568 2764
                return $fromClause;
1569
        }
1570
    }
1571
1572
    /**
1573
     * {@inheritDoc}
1574
     */
1575 723
    public function getForUpdateSQL()
1576
    {
1577 723
        return ' ';
1578
    }
1579
1580
    /**
1581
     * {@inheritDoc}
1582
     */
1583 2131
    protected function getReservedKeywordsClass()
1584
    {
1585 2131
        return Keywords\SQLServerKeywords::class;
1586
    }
1587
1588
    /**
1589
     * {@inheritDoc}
1590
     */
1591 7343
    public function quoteSingleIdentifier($str)
1592
    {
1593 7343
        return '[' . str_replace(']', '][', $str) . ']';
1594
    }
1595
1596
    /**
1597
     * {@inheritDoc}
1598
     */
1599 5729
    public function getTruncateTableSQL($tableName, $cascade = false)
1600
    {
1601 5729
        $tableIdentifier = new Identifier($tableName);
1602
1603 5729
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1604
    }
1605
1606
    /**
1607
     * {@inheritDoc}
1608
     */
1609 6965
    public function getBlobTypeDeclarationSQL(array $field)
1610
    {
1611 6965
        return 'VARBINARY(MAX)';
1612
    }
1613
1614
    /**
1615
     * {@inheritdoc}
1616
     *
1617
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1618
     */
1619 7917
    public function getColumnDeclarationSQL($name, array $field)
1620
    {
1621 7917
        if (isset($field['columnDefinition'])) {
1622 5477
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1623
        } else {
1624 7914
            $collation = isset($field['collation']) && $field['collation'] ?
1625 7914
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1626
1627 7914
            $notnull = isset($field['notnull']) && $field['notnull'] ? ' NOT NULL' : '';
1628
1629 7914
            $unique = isset($field['unique']) && $field['unique'] ?
1630 7914
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1631
1632 7914
            $check = isset($field['check']) && $field['check'] ?
1633 7914
                ' ' . $field['check'] : '';
1634
1635 7914
            $typeDecl  = $field['type']->getSQLDeclaration($field, $this);
1636 7914
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1637
        }
1638
1639 7917
        return $name . ' ' . $columnDef;
1640
    }
1641
1642
    /**
1643
     * Returns a unique default constraint name for a table and column.
1644
     *
1645
     * @param string $table  Name of the table to generate the unique default constraint name for.
1646
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1647
     *
1648
     * @return string
1649
     */
1650 6905
    private function generateDefaultConstraintName($table, $column)
1651
    {
1652 6905
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1653
    }
1654
1655
    /**
1656
     * Returns a hash value for a given identifier.
1657
     *
1658
     * @param string $identifier Identifier to generate a hash value for.
1659
     *
1660
     * @return string
1661
     */
1662 6908
    private function generateIdentifierName($identifier)
1663
    {
1664
        // Always generate name for unquoted identifiers to ensure consistency.
1665 6908
        $identifier = new Identifier($identifier);
1666
1667 6908
        return strtoupper(dechex(crc32($identifier->getName())));
1668
    }
1669
1670 767
    protected function getCommentOnTableSQL(string $tableName, ?string $comment) : string
1671
    {
1672 767
        return sprintf(
1673
            <<<'SQL'
1674
EXEC sys.sp_addextendedproperty @name=N'MS_Description',
1675
  @value=N%s, @level0type=N'SCHEMA', @level0name=N'dbo',
1676
  @level1type=N'TABLE', @level1name=N%s
1677
SQL
1678
            ,
1679 767
            $this->quoteStringLiteral((string) $comment),
1680 767
            $this->quoteStringLiteral($tableName)
1681
        );
1682
    }
1683
1684 898
    public function getListTableMetadataSQL(string $table) : string
1685
    {
1686 898
        return sprintf(
1687
            <<<'SQL'
1688
SELECT
1689
  p.value AS [table_comment]
1690
FROM
1691
  sys.tables AS tbl
1692
  INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1
1693
WHERE
1694
  (tbl.name=N%s and SCHEMA_NAME(tbl.schema_id)=N'dbo' and p.name=N'MS_Description')
1695
SQL
1696
            ,
1697 898
            $this->quoteStringLiteral($table)
1698
        );
1699
    }
1700
}
1701