SQLServerPlatform::getListTableForeignKeysSQL()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
612
                "FROM sys.default_constraints dc " .
613
                "JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id " .
614
                "WHERE tbl.name = '" . $diff->getNewName()->getName() . "';" .
615
                "EXEC sp_executesql @sql";
616
        }
617
618
        $sql = array_merge(
619
            $this->getPreAlterTableIndexForeignKeySQL($diff),
620
            $sql,
621
            $this->getPostAlterTableIndexForeignKeySQL($diff)
622
        );
623
624
        return array_merge($sql, $tableSql, $columnSql);
625
    }
626
627
    /**
628
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
629
     *
630
     * @param string $tableName The name of the table to generate the clause for.
631
     * @param Column $column    The column to generate the clause for.
632
     *
633
     * @return string
634
     */
635
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
636
    {
637
        $columnDef = $column->toArray();
638
        $columnDef['name'] = $column->getQuotedName($this);
639
640
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
641
    }
642
643
    /**
644
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
645
     *
646
     * @param string $tableName  The name of the table to generate the clause for.
647
     * @param string $columnName The name of the column to generate the clause for.
648
     *
649
     * @return string
650
     */
651
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
652
    {
653
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
654
    }
655
656
    /**
657
     * Checks whether a column alteration requires dropping its default constraint first.
658
     *
659
     * Different to other database vendors SQL Server implements column default values
660
     * as constraints and therefore changes in a column's default value as well as changes
661
     * in a column's type require dropping the default constraint first before being to
662
     * alter the particular column to the new definition.
663
     *
664
     * @param ColumnDiff $columnDiff The column diff to evaluate.
665
     *
666
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
667
     */
668
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
669
    {
670
        // We can only decide whether to drop an existing default constraint
671
        // if we know the original default value.
672
        if ( ! $columnDiff->fromColumn instanceof Column) {
673
            return false;
674
        }
675
676
        // We only need to drop an existing default constraint if we know the
677
        // column was defined with a default value before.
678
        if ($columnDiff->fromColumn->getDefault() === null) {
679
            return false;
680
        }
681
682
        // We need to drop an existing default constraint if the column was
683
        // defined with a default value before and it has changed.
684
        if ($columnDiff->hasChanged('default')) {
685
            return true;
686
        }
687
688
        // We need to drop an existing default constraint if the column was
689
        // defined with a default value before and the native column type has changed.
690
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
691
    }
692
693
    /**
694
     * Returns the SQL statement for altering a column comment.
695
     *
696
     * SQL Server does not support native column comments,
697
     * therefore the extended properties functionality is used
698
     * as a workaround to store them.
699
     * The property name used to store column comments is "MS_Description"
700
     * which provides compatibility with SQL Server Management Studio,
701
     * as column comments are stored in the same property there when
702
     * specifying a column's "Description" attribute.
703
     *
704
     * @param string $tableName  The quoted table name to which the column belongs.
705
     * @param string $columnName The quoted column name to alter the comment for.
706
     * @param string $comment    The column's comment.
707
     *
708
     * @return string
709
     */
710
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
711
    {
712
        if (strpos($tableName, '.') !== false) {
713
            [$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...
714
            $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...
715
            $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...
716
        } else {
717
            $schemaSQL = "'dbo'";
718
            $tableSQL  = $this->quoteStringLiteral($tableName);
719
        }
720
721
        return $this->getUpdateExtendedPropertySQL(
722
            'MS_Description',
723
            $comment,
724
            'SCHEMA',
725
            $schemaSQL,
726
            'TABLE',
727
            $tableSQL,
728
            'COLUMN',
729
            $columnName
730
        );
731
    }
732
733
    /**
734
     * Returns the SQL statement for dropping a column comment.
735
     *
736
     * SQL Server does not support native column comments,
737
     * therefore the extended properties functionality is used
738
     * as a workaround to store them.
739
     * The property name used to store column comments is "MS_Description"
740
     * which provides compatibility with SQL Server Management Studio,
741
     * as column comments are stored in the same property there when
742
     * specifying a column's "Description" attribute.
743
     *
744
     * @param string $tableName  The quoted table name to which the column belongs.
745
     * @param string $columnName The quoted column name to drop the comment for.
746
     *
747
     * @return string
748
     */
749
    protected function getDropColumnCommentSQL($tableName, $columnName)
750
    {
751
        if (strpos($tableName, '.') !== false) {
752
            [$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...
753
            $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...
754
            $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...
755
        } else {
756
            $schemaSQL = "'dbo'";
757
            $tableSQL  = $this->quoteStringLiteral($tableName);
758
        }
759
760
        return $this->getDropExtendedPropertySQL(
761
            'MS_Description',
762
            'SCHEMA',
763
            $schemaSQL,
764
            'TABLE',
765
            $tableSQL,
766
            'COLUMN',
767
            $columnName
768
        );
769
    }
770
771
    /**
772
     * {@inheritdoc}
773
     */
774
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
775
    {
776
        return [
777
            sprintf(
778
                "EXEC sp_RENAME N'%s.%s', N'%s', N'INDEX'",
779
                $tableName,
780
                $oldIndexName,
781
                $index->getQuotedName($this)
782
            )
783
        ];
784
    }
785
786
    /**
787
     * Returns the SQL statement for adding an extended property to a database object.
788
     *
789
     * @param string      $name       The name of the property to add.
790
     * @param string|null $value      The value of the property to add.
791
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
792
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
793
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
794
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
795
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
796
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
797
     *
798
     * @return string
799
     *
800
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
801
     */
802
    public function getAddExtendedPropertySQL(
803
        $name,
804
        $value = null,
805
        $level0Type = null,
806
        $level0Name = null,
807
        $level1Type = null,
808
        $level1Name = null,
809
        $level2Type = null,
810
        $level2Name = null
811
    ) {
812
        return "EXEC sp_addextendedproperty " .
813
            "N" . $this->quoteStringLiteral($name) . ", N" . $this->quoteStringLiteral($value) . ", " .
814
            "N" . $this->quoteStringLiteral($level0Type) . ", " . $level0Name . ', ' .
815
            "N" . $this->quoteStringLiteral($level1Type) . ", " . $level1Name . ', ' .
816
            "N" . $this->quoteStringLiteral($level2Type) . ", " . $level2Name;
817
    }
818
819
    /**
820
     * Returns the SQL statement for dropping an extended property from a database object.
821
     *
822
     * @param string      $name       The name of the property to drop.
823
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
824
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
825
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
826
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
827
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
828
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
829
     *
830
     * @return string
831
     *
832
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
833
     */
834
    public function getDropExtendedPropertySQL(
835
        $name,
836
        $level0Type = null,
837
        $level0Name = null,
838
        $level1Type = null,
839
        $level1Name = null,
840
        $level2Type = null,
841
        $level2Name = null
842
    ) {
843
        return "EXEC sp_dropextendedproperty " .
844
            "N" . $this->quoteStringLiteral($name) . ", " .
845
            "N" . $this->quoteStringLiteral($level0Type) . ", " . $level0Name . ', ' .
846
            "N" . $this->quoteStringLiteral($level1Type) . ", " . $level1Name . ', ' .
847
            "N" . $this->quoteStringLiteral($level2Type) . ", " . $level2Name;
848
    }
849
850
    /**
851
     * Returns the SQL statement for updating an extended property of a database object.
852
     *
853
     * @param string      $name       The name of the property to update.
854
     * @param string|null $value      The value of the property to update.
855
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
856
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
857
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
858
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
859
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
860
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
861
     *
862
     * @return string
863
     *
864
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
865
     */
866
    public function getUpdateExtendedPropertySQL(
867
        $name,
868
        $value = null,
869
        $level0Type = null,
870
        $level0Name = null,
871
        $level1Type = null,
872
        $level1Name = null,
873
        $level2Type = null,
874
        $level2Name = null
875
    ) {
876
        return "EXEC sp_updateextendedproperty " .
877
        "N" . $this->quoteStringLiteral($name) . ", N" . $this->quoteStringLiteral($value) . ", " .
878
        "N" . $this->quoteStringLiteral($level0Type) . ", " . $level0Name . ', ' .
879
        "N" . $this->quoteStringLiteral($level1Type) . ", " . $level1Name . ', ' .
880
        "N" . $this->quoteStringLiteral($level2Type) . ", " . $level2Name;
881
    }
882
883
    /**
884
     * {@inheritDoc}
885
     */
886
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
887
    {
888
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
889
    }
890
891
    /**
892
     * {@inheritDoc}
893
     */
894
    public function getListTablesSQL()
895
    {
896
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
897
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
898
        return "SELECT name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
899
    }
900
901
    /**
902
     * {@inheritDoc}
903
     */
904
    public function getListTableColumnsSQL($table, $database = null)
905
    {
906
        return "SELECT    col.name,
907
                          type.name AS type,
908
                          col.max_length AS length,
909
                          ~col.is_nullable AS notnull,
910
                          def.definition AS [default],
911
                          col.scale,
912
                          col.precision,
913
                          col.is_identity AS autoincrement,
914
                          col.collation_name AS collation,
915
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
916
                FROM      sys.columns AS col
917
                JOIN      sys.types AS type
918
                ON        col.user_type_id = type.user_type_id
919
                JOIN      sys.objects AS obj
920
                ON        col.object_id = obj.object_id
921
                JOIN      sys.schemas AS scm
922
                ON        obj.schema_id = scm.schema_id
923
                LEFT JOIN sys.default_constraints def
924
                ON        col.default_object_id = def.object_id
925
                AND       col.object_id = def.parent_object_id
926
                LEFT JOIN sys.extended_properties AS prop
927
                ON        obj.object_id = prop.major_id
928
                AND       col.column_id = prop.minor_id
929
                AND       prop.name = 'MS_Description'
930
                WHERE     obj.type = 'U'
931
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
932
    }
933
934
    /**
935
     * {@inheritDoc}
936
     */
937
    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...
938
    {
939
        return "SELECT f.name AS ForeignKey,
940
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
941
                OBJECT_NAME (f.parent_object_id) AS TableName,
942
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
943
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
944
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
945
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
946
                f.delete_referential_action_desc,
947
                f.update_referential_action_desc
948
                FROM sys.foreign_keys AS f
949
                INNER JOIN sys.foreign_key_columns AS fc
950
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
951
                ON f.OBJECT_ID = fc.constraint_object_id
952
                WHERE " .
953
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
954
    }
955
956
    /**
957
     * {@inheritDoc}
958
     */
959
    public function getListTableIndexesSQL($table, $currentDatabase = null)
960
    {
961
        return "SELECT idx.name AS key_name,
962
                       col.name AS column_name,
963
                       ~idx.is_unique AS non_unique,
964
                       idx.is_primary_key AS [primary],
965
                       CASE idx.type
966
                           WHEN '1' THEN 'clustered'
967
                           WHEN '2' THEN 'nonclustered'
968
                           ELSE NULL
969
                       END AS flags
970
                FROM sys.tables AS tbl
971
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
972
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
973
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
974
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
975
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . "
976
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC";
977
    }
978
979
    /**
980
     * {@inheritDoc}
981
     */
982
    public function getCreateViewSQL($name, $sql)
983
    {
984
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
985
    }
986
987
    /**
988
     * {@inheritDoc}
989
     */
990
    public function getListViewsSQL($database)
991
    {
992
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
993
    }
994
995
    /**
996
     * Returns the where clause to filter schema and table name in a query.
997
     *
998
     * @param string $table        The full qualified name of the table.
999
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
1000
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
1001
     *
1002
     * @return string
1003
     */
1004
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
1005
    {
1006
        if (strpos($table, ".") !== false) {
1007
            list($schema, $table) = explode(".", $table);
1008
            $schema = $this->quoteStringLiteral($schema);
1009
            $table = $this->quoteStringLiteral($table);
1010
        } else {
1011
            $schema = "SCHEMA_NAME()";
1012
            $table = $this->quoteStringLiteral($table);
1013
        }
1014
1015
        return "({$tableColumn} = {$table} AND {$schemaColumn} = {$schema})";
1016
    }
1017
1018
    /**
1019
     * {@inheritDoc}
1020
     */
1021
    public function getDropViewSQL($name)
1022
    {
1023
        return 'DROP VIEW ' . $name;
1024
    }
1025
1026
    /**
1027
     * {@inheritDoc}
1028
     *
1029
     * @deprecated Use application-generated UUIDs instead
1030
     */
1031
    public function getGuidExpression()
1032
    {
1033
        return 'NEWID()';
1034
    }
1035
1036
    /**
1037
     * {@inheritDoc}
1038
     */
1039
    public function getLocateExpression($str, $substr, $startPos = false)
1040
    {
1041
        if ($startPos == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1042
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1043
        }
1044
1045
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1046
    }
1047
1048
    /**
1049
     * {@inheritDoc}
1050
     */
1051
    public function getModExpression($expression1, $expression2)
1052
    {
1053
        return $expression1 . ' % ' . $expression2;
1054
    }
1055
1056
    /**
1057
     * {@inheritDoc}
1058
     */
1059
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1060
    {
1061
        if ( ! $char) {
1062
            switch ($pos) {
1063
                case TrimMode::LEADING:
1064
                    $trimFn = 'LTRIM';
1065
                    break;
1066
1067
                case TrimMode::TRAILING:
1068
                    $trimFn = 'RTRIM';
1069
                    break;
1070
1071
                default:
1072
                    return 'LTRIM(RTRIM(' . $str . '))';
1073
            }
1074
1075
            return $trimFn . '(' . $str . ')';
1076
        }
1077
1078
        /** Original query used to get those expressions
1079
          declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x';
1080
          declare @pat varchar(10) = '%[^' + @trim_char + ']%';
1081
          select @c as string
1082
          , @trim_char as trim_char
1083
          , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading
1084
          , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing
1085
          , 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;
1086
         */
1087
        $pattern = "'%[^' + $char + ']%'";
1088
1089
        if ($pos === TrimMode::LEADING) {
1090
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1091
        }
1092
1093
        if ($pos === TrimMode::TRAILING) {
1094
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1095
        }
1096
1097
        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))';
1098
    }
1099
1100
    /**
1101
     * {@inheritDoc}
1102
     */
1103
    public function getConcatExpression()
1104
    {
1105
        $args = func_get_args();
1106
1107
        return '(' . implode(' + ', $args) . ')';
1108
    }
1109
1110
    /**
1111
     * {@inheritDoc}
1112
     */
1113
    public function getListDatabasesSQL()
1114
    {
1115
        return 'SELECT * FROM sys.databases';
1116
    }
1117
1118
    /**
1119
     * {@inheritDoc}
1120
     */
1121
    public function getListNamespacesSQL()
1122
    {
1123
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1124
    }
1125
1126
    /**
1127
     * {@inheritDoc}
1128
     */
1129
    public function getSubstringExpression($value, $from, $length = null)
1130
    {
1131
        if ($length !== null) {
1132
            return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')';
1133
        }
1134
1135
        return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)';
1136
    }
1137
1138
    /**
1139
     * {@inheritDoc}
1140
     */
1141
    public function getLengthExpression($column)
1142
    {
1143
        return 'LEN(' . $column . ')';
1144
    }
1145
1146
    /**
1147
     * {@inheritDoc}
1148
     */
1149
    public function getSetTransactionIsolationSQL($level)
1150
    {
1151
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1152
    }
1153
1154
    /**
1155
     * {@inheritDoc}
1156
     */
1157
    public function getIntegerTypeDeclarationSQL(array $field)
1158
    {
1159
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1160
    }
1161
1162
    /**
1163
     * {@inheritDoc}
1164
     */
1165
    public function getBigIntTypeDeclarationSQL(array $field)
1166
    {
1167
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1168
    }
1169
1170
    /**
1171
     * {@inheritDoc}
1172
     */
1173
    public function getSmallIntTypeDeclarationSQL(array $field)
1174
    {
1175
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1176
    }
1177
1178
    /**
1179
     * {@inheritDoc}
1180
     */
1181
    public function getGuidTypeDeclarationSQL(array $field)
1182
    {
1183
        return 'UNIQUEIDENTIFIER';
1184
    }
1185
1186
    /**
1187
     * {@inheritDoc}
1188
     */
1189
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1190
    {
1191
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1192
    }
1193
1194
    /**
1195
     * {@inheritdoc}
1196
     */
1197
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1198
    {
1199
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1200
    }
1201
1202
    /**
1203
     * {@inheritdoc}
1204
     */
1205
    public function getBinaryMaxLength()
1206
    {
1207
        return 8000;
1208
    }
1209
1210
    /**
1211
     * {@inheritDoc}
1212
     */
1213
    public function getClobTypeDeclarationSQL(array $field)
1214
    {
1215
        return 'VARCHAR(MAX)';
1216
    }
1217
1218
    /**
1219
     * {@inheritDoc}
1220
     */
1221
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1222
    {
1223
        return (!empty($columnDef['autoincrement'])) ? ' IDENTITY' : '';
1224
    }
1225
1226
    /**
1227
     * {@inheritDoc}
1228
     */
1229
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
1230
    {
1231
        return 'DATETIME';
1232
    }
1233
1234
    /**
1235
     * {@inheritDoc}
1236
     */
1237
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1238
    {
1239
        return 'DATETIME';
1240
    }
1241
1242
    /**
1243
     * {@inheritDoc}
1244
     */
1245
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1246
    {
1247
        return 'DATETIME';
1248
    }
1249
1250
    /**
1251
     * {@inheritDoc}
1252
     */
1253
    public function getBooleanTypeDeclarationSQL(array $field)
1254
    {
1255
        return 'BIT';
1256
    }
1257
1258
    /**
1259
     * {@inheritDoc}
1260
     */
1261
    protected function doModifyLimitQuery($query, $limit, $offset = null)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
1262
    {
1263
        $where = [];
1264
1265
        if ($offset > 0) {
1266
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1267
        }
1268
1269
        if ($limit !== null) {
1270
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1271
            $top     = sprintf('TOP %d', $offset + $limit);
1272
        } else {
1273
            $top = 'TOP 9223372036854775807';
1274
        }
1275
1276
        if (empty($where)) {
1277
            return $query;
1278
        }
1279
1280
        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
1281
        // Even if the TOP n is very large, the use of a CTE will
1282
        // allow the SQL Server query planner to optimize it so it doesn't
1283
        // actually scan the entire range covered by the TOP clause.
1284
        $selectPattern = '/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/im';
1285
        $replacePattern = sprintf('$1%s $2', $top);
1286
        $query = preg_replace($selectPattern, $replacePattern, $query);
1287
1288
        if (stristr($query, "ORDER BY")) {
1289
            // Inner order by is not valid in SQL Server for our purposes
1290
            // unless it's in a TOP N subquery.
1291
            $query = $this->scrubInnerOrderBy($query);
1292
        }
1293
1294
        // Build a new limited query around the original, using a CTE
1295
        return sprintf(
1296
            "WITH dctrn_cte AS (%s) "
1297
            . "SELECT * FROM ("
1298
            . "SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte"
1299
            . ") AS doctrine_tbl "
1300
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1301
            $query,
1302
            implode(' AND ', $where)
1303
        );
1304
    }
1305
1306
    /**
1307
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
1308
     * Caveat: will leave ORDER BY in TOP N subqueries.
1309
     *
1310
     * @param string $query
1311
     * @return string
1312
     */
1313
    private function scrubInnerOrderBy($query)
1314
    {
1315
        $count = substr_count(strtoupper($query), 'ORDER BY');
1316
        $offset = 0;
1317
1318
        while ($count-- > 0) {
1319
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1320
            if ($orderByPos === false) {
1321
                break;
1322
            }
1323
1324
            $qLen = strlen($query);
1325
            $parenCount = 0;
1326
            $currentPosition = $orderByPos;
1327
1328
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1329
                if ($query[$currentPosition] === '(') {
1330
                    $parenCount++;
1331
                } elseif ($query[$currentPosition] === ')') {
1332
                    $parenCount--;
1333
                }
1334
1335
                $currentPosition++;
1336
            }
1337
1338
            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
1339
                // If the order by clause is in a TOP N subquery, do not remove
1340
                // it and continue iteration from the current position.
1341
                $offset = $currentPosition;
1342
                continue;
1343
            }
1344
1345
            if ($currentPosition < $qLen - 1) {
1346
                $query = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1347
                $offset = $orderByPos;
1348
            }
1349
        }
1350
        return $query;
1351
    }
1352
1353
    /**
1354
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
1355
     *
1356
     * @param string $query           The query
1357
     * @param int    $currentPosition Start position of ORDER BY clause
1358
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
1359
     */
1360
    private function isOrderByInTopNSubquery($query, $currentPosition)
1361
    {
1362
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1363
        $subQueryBuffer = '';
1364
        $parenCount = 0;
1365
1366
        // If $parenCount goes negative, we've exited the subquery we're examining.
1367
        // If $currentPosition goes negative, we've reached the beginning of the query.
1368
        while ($parenCount >= 0 && $currentPosition >= 0) {
1369
            if ($query[$currentPosition] === '(') {
1370
                $parenCount--;
1371
            } elseif ($query[$currentPosition] === ')') {
1372
                $parenCount++;
1373
            }
1374
1375
            // Only yank query text on the same nesting level as the ORDER BY clause.
1376
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1377
1378
            $currentPosition--;
1379
        }
1380
1381
        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
1382
    }
1383
1384
    /**
1385
     * {@inheritDoc}
1386
     */
1387
    public function supportsLimitOffset()
1388
    {
1389
        return false;
1390
    }
1391
1392
    /**
1393
     * {@inheritDoc}
1394
     */
1395
    public function convertBooleans($item)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
1396
    {
1397
        if (is_array($item)) {
1398
            foreach ($item as $key => $value) {
1399
                if (is_bool($value) || is_numeric($item)) {
1400
                    $item[$key] = ($value) ? 1 : 0;
1401
                }
1402
            }
1403
        } elseif (is_bool($item) || is_numeric($item)) {
1404
            $item = ($item) ? 1 : 0;
1405
        }
1406
1407
        return $item;
1408
    }
1409
1410
    /**
1411
     * {@inheritDoc}
1412
     */
1413
    public function getCreateTemporaryTableSnippetSQL()
1414
    {
1415
        return "CREATE TABLE";
1416
    }
1417
1418
    /**
1419
     * {@inheritDoc}
1420
     */
1421
    public function getTemporaryTableName($tableName)
1422
    {
1423
        return '#' . $tableName;
1424
    }
1425
1426
    /**
1427
     * {@inheritDoc}
1428
     */
1429
    public function getDateTimeFormatString()
1430
    {
1431
        return 'Y-m-d H:i:s.000';
1432
    }
1433
1434
    /**
1435
     * {@inheritDoc}
1436
     */
1437
    public function getDateFormatString()
1438
    {
1439
        return 'Y-m-d H:i:s.000';
1440
    }
1441
1442
    /**
1443
     * {@inheritDoc}
1444
     */
1445
    public function getTimeFormatString()
1446
    {
1447
        return 'Y-m-d H:i:s.000';
1448
    }
1449
1450
    /**
1451
     * {@inheritDoc}
1452
     */
1453
    public function getDateTimeTzFormatString()
1454
    {
1455
        return $this->getDateTimeFormatString();
1456
    }
1457
1458
    /**
1459
     * {@inheritDoc}
1460
     */
1461
    public function getName()
1462
    {
1463
        return 'mssql';
1464
    }
1465
1466
    /**
1467
     * {@inheritDoc}
1468
     */
1469
    protected function initializeDoctrineTypeMappings()
1470
    {
1471
        $this->doctrineTypeMapping = [
1472
            'bigint' => 'bigint',
1473
            'numeric' => 'decimal',
1474
            'bit' => 'boolean',
1475
            'smallint' => 'smallint',
1476
            'decimal' => 'decimal',
1477
            'smallmoney' => 'integer',
1478
            'int' => 'integer',
1479
            'tinyint' => 'smallint',
1480
            'money' => 'integer',
1481
            'float' => 'float',
1482
            'real' => 'float',
1483
            'double' => 'float',
1484
            'double precision' => 'float',
1485
            'smalldatetime' => 'datetime',
1486
            'datetime' => 'datetime',
1487
            'char' => 'string',
1488
            'varchar' => 'string',
1489
            'text' => 'text',
1490
            'nchar' => 'string',
1491
            'nvarchar' => 'string',
1492
            'ntext' => 'text',
1493
            'binary' => 'binary',
1494
            'varbinary' => 'binary',
1495
            'image' => 'blob',
1496
            'uniqueidentifier' => 'guid',
1497
        ];
1498
    }
1499
1500
    /**
1501
     * {@inheritDoc}
1502
     */
1503
    public function createSavePoint($savepoint)
1504
    {
1505
        return 'SAVE TRANSACTION ' . $savepoint;
1506
    }
1507
1508
    /**
1509
     * {@inheritDoc}
1510
     */
1511
    public function releaseSavePoint($savepoint)
1512
    {
1513
        return '';
1514
    }
1515
1516
    /**
1517
     * {@inheritDoc}
1518
     */
1519
    public function rollbackSavePoint($savepoint)
1520
    {
1521
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1522
    }
1523
1524
    /**
1525
     * {@inheritdoc}
1526
     */
1527
    public function getForeignKeyReferentialActionSQL($action)
1528
    {
1529
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1530
        if (strtoupper($action) === 'RESTRICT') {
1531
            return 'NO ACTION';
1532
        }
1533
1534
        return parent::getForeignKeyReferentialActionSQL($action);
1535
    }
1536
1537
    /**
1538
     * {@inheritDoc}
1539
     */
1540
    public function appendLockHint($fromClause, $lockMode)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
1541
    {
1542
        switch (true) {
1543
            case LockMode::NONE === $lockMode:
1544
                return $fromClause . ' WITH (NOLOCK)';
1545
1546
            case LockMode::PESSIMISTIC_READ === $lockMode:
1547
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1548
1549
            case LockMode::PESSIMISTIC_WRITE === $lockMode:
1550
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1551
1552
            default:
1553
                return $fromClause;
1554
        }
1555
    }
1556
1557
    /**
1558
     * {@inheritDoc}
1559
     */
1560
    public function getForUpdateSQL()
1561
    {
1562
        return ' ';
1563
    }
1564
1565
    /**
1566
     * {@inheritDoc}
1567
     */
1568
    protected function getReservedKeywordsClass()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
1569
    {
1570
        return Keywords\SQLServerKeywords::class;
1571
    }
1572
1573
    /**
1574
     * {@inheritDoc}
1575
     */
1576
    public function quoteSingleIdentifier($str)
1577
    {
1578
        return "[" . str_replace("]", "][", $str) . "]";
1579
    }
1580
1581
    /**
1582
     * {@inheritDoc}
1583
     */
1584
    public function getTruncateTableSQL($tableName, $cascade = false)
1585
    {
1586
        $tableIdentifier = new Identifier($tableName);
1587
1588
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1589
    }
1590
1591
    /**
1592
     * {@inheritDoc}
1593
     */
1594
    public function getBlobTypeDeclarationSQL(array $field)
1595
    {
1596
        return 'VARBINARY(MAX)';
1597
    }
1598
1599
    /**
1600
     * {@inheritDoc}
1601
     */
1602
    public function getDefaultValueDeclarationSQL($field)
1603
    {
1604
        if ( ! isset($field['default'])) {
1605
            return empty($field['notnull']) ? ' NULL' : '';
1606
        }
1607
1608
        if ( ! isset($field['type'])) {
1609
            return " DEFAULT '" . $field['default'] . "'";
1610
        }
1611
1612
        $type = $field['type'];
1613
1614
        if ($type instanceof Types\PhpIntegerMappingType) {
1615
            return " DEFAULT " . $field['default'];
1616
        }
1617
1618
        if ($type instanceof Types\PhpDateTimeMappingType && $field['default'] === $this->getCurrentTimestampSQL()) {
1619
            return " DEFAULT " . $this->getCurrentTimestampSQL();
1620
        }
1621
1622
        if ($type instanceof Types\BooleanType) {
1623
            return " DEFAULT '" . $this->convertBooleans($field['default']) . "'";
1624
        }
1625
1626
        return " DEFAULT '" . $field['default'] . "'";
1627
    }
1628
1629
    /**
1630
     * {@inheritdoc}
1631
     *
1632
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1633
     */
1634
    public function getColumnDeclarationSQL($name, array $field)
1635
    {
1636
        if (isset($field['columnDefinition'])) {
1637
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1638
        } else {
1639
            $collation = (isset($field['collation']) && $field['collation']) ?
1640
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1641
1642
            $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : '';
1643
1644
            $unique = (isset($field['unique']) && $field['unique']) ?
1645
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1646
1647
            $check = (isset($field['check']) && $field['check']) ?
1648
                ' ' . $field['check'] : '';
1649
1650
            $typeDecl = $field['type']->getSQLDeclaration($field, $this);
1651
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1652
        }
1653
1654
        return $name . ' ' . $columnDef;
1655
    }
1656
1657
    /**
1658
     * Returns a unique default constraint name for a table and column.
1659
     *
1660
     * @param string $table  Name of the table to generate the unique default constraint name for.
1661
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1662
     *
1663
     * @return string
1664
     */
1665
    private function generateDefaultConstraintName($table, $column)
1666
    {
1667
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1668
    }
1669
1670
    /**
1671
     * Returns a hash value for a given identifier.
1672
     *
1673
     * @param string $identifier Identifier to generate a hash value for.
1674
     *
1675
     * @return string
1676
     */
1677
    private function generateIdentifierName($identifier)
1678
    {
1679
        // Always generate name for unquoted identifiers to ensure consistency.
1680
        $identifier = new Identifier($identifier);
1681
1682
        return strtoupper(dechex(crc32($identifier->getName())));
1683
    }
1684
}
1685