Completed
Pull Request — master (#3143)
by Alessandro
17:39
created

SQLServerPlatform::getCreateDatabaseSQL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
crap 1
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_null;
43
use function is_numeric;
44
use function is_string;
45
use function preg_match;
46
use function preg_replace;
47
use function sprintf;
48
use function str_replace;
49
use function stripos;
50
use function stristr;
51
use function strlen;
52
use function strpos;
53
use function strtoupper;
54
use function substr;
55
use function substr_count;
56
57
/**
58
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
59
 * Microsoft SQL Server database platform.
60
 *
61
 * @since 2.0
62
 * @author Roman Borschel <[email protected]>
63
 * @author Jonathan H. Wage <[email protected]>
64
 * @author Benjamin Eberlei <[email protected]>
65
 * @author Steve Müller <[email protected]>
66
 */
67
class SQLServerPlatform extends AbstractPlatform
68
{
69
    /**
70
     * {@inheritdoc}
71
     */
72 116
    public function getCurrentDateSQL()
73
    {
74 116
        return $this->getConvertExpression('date', 'GETDATE()');
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80 59
    public function getCurrentTimeSQL()
81
    {
82 59
        return $this->getConvertExpression('time', 'GETDATE()');
83
    }
84
85
    /**
86
     * Returns an expression that converts an expression of one data type to another.
87
     *
88
     * @param string $dataType   The target native data type. Alias data types cannot be used.
89
     * @param string $expression The SQL expression to convert.
90
     *
91
     * @return string
92
     */
93 116
    private function getConvertExpression($dataType, $expression)
94
    {
95 116
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101 2
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
102
    {
103 2
        $factorClause = '';
104
105 2
        if ('-' === $operator) {
106 2
            $factorClause = '-1 * ';
107
        }
108
109 2
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
110
    }
111
112
    /**
113
     * {@inheritDoc}
114
     */
115 6
    public function getDateDiffExpression($date1, $date2)
116
    {
117 6
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
118
    }
119
120
    /**
121
     * {@inheritDoc}
122
     *
123
     * Microsoft SQL Server prefers "autoincrement" identity columns
124
     * since sequences can only be emulated with a table.
125
     */
126 59
    public function prefersIdentityColumns()
127
    {
128 59
        return true;
129
    }
130
131
    /**
132
     * {@inheritDoc}
133
     *
134
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
135
     */
136 63
    public function supportsIdentityColumns()
137
    {
138 63
        return true;
139
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144 2
    public function supportsReleaseSavepoints()
145
    {
146 2
        return false;
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152 65
    public function supportsSchemas()
153
    {
154 65
        return true;
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160 2
    public function getDefaultSchemaName()
161
    {
162 2
        return 'dbo';
163
    }
164
165
    /**
166
     * {@inheritDoc}
167
     */
168 173
    public function supportsColumnCollation()
169
    {
170 173
        return true;
171
    }
172
173
    /**
174
     * {@inheritDoc}
175
     */
176 3340
    public function hasNativeGuidType()
177
    {
178 3340
        return true;
179
    }
180
181
    /**
182
     * {@inheritDoc}
183
     */
184 61
    public function getCreateDatabaseSQL($name)
185
    {
186 61
        return 'CREATE DATABASE ' . $name;
187
    }
188
189
    /**
190
     * {@inheritDoc}
191
     */
192 61
    public function getDropDatabaseSQL($name)
193
    {
194 61
        return 'DROP DATABASE ' . $name;
195
    }
196
197
    /**
198
     * {@inheritDoc}
199
     */
200 61
    public function supportsCreateDropDatabase()
201
    {
202 61
        return true;
203
    }
204
205
    /**
206
     * {@inheritDoc}
207
     */
208 61
    public function getCreateSchemaSQL($schemaName)
209
    {
210 61
        return 'CREATE SCHEMA ' . $schemaName;
211
    }
212
213
    /**
214
     * {@inheritDoc}
215
     */
216 116
    public function getDropForeignKeySQL($foreignKey, $table)
217
    {
218 116
        if (! $foreignKey instanceof ForeignKeyConstraint) {
219 57
            $foreignKey = new Identifier($foreignKey);
220
        }
221
222 116
        if (! $table instanceof Table) {
223 116
            $table = new Identifier($table);
224
        }
225
226 116
        $foreignKey = $foreignKey->getQuotedName($this);
227 116
        $table = $table->getQuotedName($this);
228
229 116
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
230
    }
231
232
    /**
233
     * {@inheritDoc}
234
     */
235 6
    public function getDropIndexSQL($index, $table = null)
236
    {
237 6
        if ($index instanceof Index) {
238 4
            $index = $index->getQuotedName($this);
239 2
        } elseif (!is_string($index)) {
240
            throw new \InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
241
        }
242
243 6
        if (!isset($table)) {
244
            return 'DROP INDEX ' . $index;
245
        }
246
247 6
        if ($table instanceof Table) {
248 2
            $table = $table->getQuotedName($this);
249
        }
250
251 6
        return "IF EXISTS (SELECT * FROM sysobjects WHERE name = '$index')
252 6
                    ALTER TABLE " . $table . " DROP CONSTRAINT " . $index . "
253
                ELSE
254 6
                    DROP INDEX " . $index . " ON " . $table;
255
    }
256
257
    /**
258
     * {@inheritDoc}
259
     */
260 1242
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
261
    {
262 1242
        $defaultConstraintsSql = [];
263 1242
        $commentsSql           = [];
264
265
        // @todo does other code breaks because of this?
266
        // force primary keys to be not null
267 1242
        foreach ($columns as &$column) {
268 1242
            if (isset($column['primary']) && $column['primary']) {
269 513
                $column['notnull'] = true;
270
            }
271
272
            // Build default constraints SQL statements.
273 1242
            if (isset($column['default'])) {
274 246
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $tableName .
275 246
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $column);
276
            }
277
278 1242
            if ( ! empty($column['comment']) || is_numeric($column['comment'])) {
279 1242
                $commentsSql[] = $this->getCreateColumnCommentSQL($tableName, $column['name'], $column['comment']);
280
            }
281
        }
282
283 1242
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
284
285 1242
        if (isset($options['uniqueConstraints']) && !empty($options['uniqueConstraints'])) {
286
            foreach ($options['uniqueConstraints'] as $name => $definition) {
287
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
288
            }
289
        }
290
291 1242
        if (isset($options['primary']) && !empty($options['primary'])) {
292 570
            $flags = '';
293 570
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
294 57
                $flags = ' NONCLUSTERED';
295
            }
296 570
            $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
297
        }
298
299 1242
        $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql;
300
301 1242
        $check = $this->getCheckDeclarationSQL($columns);
302 1242
        if (!empty($check)) {
303
            $query .= ', ' . $check;
304
        }
305 1242
        $query .= ')';
306
307 1242
        $sql   = [];
308 1242
        $sql[] = $query;
309
310 1242
        if (isset($options['indexes']) && !empty($options['indexes'])) {
311 187
            foreach ($options['indexes'] as $index) {
312 187
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
313
            }
314
        }
315
316 1242
        if (isset($options['foreignKeys'])) {
317 307
            foreach ((array) $options['foreignKeys'] as $definition) {
318 67
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
319
            }
320
        }
321
322 1242
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
323
    }
324
325
    /**
326
     * {@inheritDoc}
327
     */
328 114
    public function getCreatePrimaryKeySQL(Index $index, $table)
329
    {
330 114
        $flags = '';
331 114
        if ($index->hasFlag('nonclustered')) {
332 57
            $flags = ' NONCLUSTERED';
333
        }
334
335 114
        return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY' . $flags . ' (' . $this->getIndexFieldDeclarationListSQL($index->getQuotedColumns($this)) . ')';
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 $comment    The column's comment.
352
     *
353
     * @return string
354
     */
355 463
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
356
    {
357 463
        if (strpos($tableName, '.') !== false) {
358 114
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
359 114
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
360 114
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
361
        } else {
362 349
            $schemaSQL = "'dbo'";
363 349
            $tableSQL  = $this->quoteStringLiteral($tableName);
364
        }
365
366 463
        return $this->getAddExtendedPropertySQL(
367 463
            'MS_Description',
368 463
            $comment,
369 463
            'SCHEMA',
370 463
            $schemaSQL,
371 463
            'TABLE',
372 463
            $tableSQL,
373 463
            'COLUMN',
374 463
            $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 array  $column Column definition.
383
     *
384
     * @return string
385
     *
386
     * @throws \InvalidArgumentException
387
     */
388 873
    public function getDefaultConstraintDeclarationSQL($table, array $column)
389
    {
390 873
        if ( ! isset($column['default'])) {
391
            throw new \InvalidArgumentException("Incomplete column definition. 'default' required.");
392
        }
393
394 873
        $columnName = new Identifier($column['name']);
395
396
        return
397
            ' CONSTRAINT ' .
398 873
            $this->generateDefaultConstraintName($table, $column['name']) .
399 873
            $this->getDefaultValueDeclarationSQL($column) .
400 873
            ' FOR ' . $columnName->getQuotedName($this);
401
    }
402
403
    /**
404
     * {@inheritDoc}
405
     */
406 114
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
407
    {
408 114
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);
409
410 114
        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
411
412 114
        return $constraint;
413
    }
414
415
    /**
416
     * {@inheritDoc}
417
     */
418 474
    public function getCreateIndexSQL(Index $index, $table)
419
    {
420 474
        $constraint = parent::getCreateIndexSQL($index, $table);
421
422 474
        if ($index->isUnique() && !$index->isPrimary()) {
423 120
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
424
        }
425
426 474
        return $constraint;
427
    }
428
429
    /**
430
     * {@inheritDoc}
431
     */
432 474
    protected function getCreateIndexSQLFlags(Index $index)
433
    {
434 474
        $type = '';
435 474
        if ($index->isUnique()) {
436 120
            $type .= 'UNIQUE ';
437
        }
438
439 474
        if ($index->hasFlag('clustered')) {
440 57
            $type .= 'CLUSTERED ';
441 417
        } elseif ($index->hasFlag('nonclustered')) {
442
            $type .= 'NONCLUSTERED ';
443
        }
444
445 474
        return $type;
446
    }
447
448
    /**
449
     * Extend unique key constraint with required filters
450
     *
451
     * @param string                      $sql
452
     * @param \Doctrine\DBAL\Schema\Index $index
453
     *
454
     * @return string
455
     */
456 234
    private function _appendUniqueConstraintDefinition($sql, Index $index)
457
    {
458 234
        $fields = [];
459
460 234
        foreach ($index->getQuotedColumns($this) as $field) {
461 234
            $fields[] = $field . ' IS NOT NULL';
462
        }
463
464 234
        return $sql . ' WHERE ' . implode(' AND ', $fields);
465
    }
466
467
    /**
468
     * {@inheritDoc}
469
     */
470 1290
    public function getAlterTableSQL(TableDiff $diff)
471
    {
472 1290
        $queryParts  = [];
473 1290
        $sql         = [];
474 1290
        $columnSql   = [];
475 1290
        $commentsSql = [];
476
477
        /** @var \Doctrine\DBAL\Schema\Column $column */
478 1290
        foreach ($diff->addedColumns as $column) {
479 576
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
480
                continue;
481
            }
482
483 576
            $columnDef = $column->toArray();
484 576
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
485
486 576
            if (isset($columnDef['default'])) {
487 230
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
488
            }
489
490 576
            $comment = $this->getColumnComment($column);
491
492 576
            if ( ! empty($comment) || is_numeric($comment)) {
493 173
                $commentsSql[] = $this->getCreateColumnCommentSQL(
494 173
                    $diff->name,
495 173
                    $column->getQuotedName($this),
496 576
                    $comment
497
                );
498
            }
499
        }
500
501 1290
        foreach ($diff->removedColumns as $column) {
502 464
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
503
                continue;
504
            }
505
506 464
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
507
        }
508
509
        /* @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
510 1290
        foreach ($diff->changedColumns as $columnDiff) {
511 826
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
512
                continue;
513
            }
514
515 826
            $column     = $columnDiff->column;
516 826
            $comment    = $this->getColumnComment($column);
517 826
            $hasComment = ! empty ($comment) || is_numeric($comment);
518
519 826
            if ($columnDiff->fromColumn instanceof Column) {
520 541
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
521 541
                $hasFromComment = ! empty ($fromComment) || is_numeric($fromComment);
522
523 541
                if ($hasFromComment && $hasComment && $fromComment != $comment) {
524 122
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
525 122
                        $diff->name,
526 122
                        $column->getQuotedName($this),
527 122
                        $comment
528
                    );
529 484
                } elseif ($hasFromComment && ! $hasComment) {
530 130
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
531 425
                } elseif ($hasComment) {
532 77
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
533 77
                        $diff->name,
534 77
                        $column->getQuotedName($this),
535 541
                        $comment
536
                    );
537
                }
538
            } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
539
                // todo: Original comment cannot be determined. What to do? Add, update, drop or skip?
540
            }
541
542
            // Do not add query part if only comment has changed.
543 826
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
544 250
                continue;
545
            }
546
547 635
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
548
549 635
            if ($requireDropDefaultConstraint) {
550 291
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
551 291
                    $diff->name,
552 291
                    $columnDiff->oldColumnName
553
                );
554
            }
555
556 635
            $columnDef = $column->toArray();
557
558 635
            $queryParts[] = 'ALTER COLUMN ' .
559 635
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
560
561 635
            if (isset($columnDef['default']) && ($requireDropDefaultConstraint || $columnDiff->hasChanged('default'))) {
562 635
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
563
            }
564
        }
565
566 1290
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
567 291
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
568
                continue;
569
            }
570
571 291
            $oldColumnName = new Identifier($oldColumnName);
572
573 291
            $sql[] = "sp_RENAME '" .
574 291
                $diff->getName($this)->getQuotedName($this) . "." . $oldColumnName->getQuotedName($this) .
575 291
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
576
577
            // Recreate default constraint with new column name if necessary (for future reference).
578 291
            if ($column->getDefault() !== null) {
579 59
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
580 59
                    $diff->name,
581 59
                    $oldColumnName->getQuotedName($this)
582
                );
583 291
                $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
584
            }
585
        }
586
587 1290
        $tableSql = [];
588
589 1290
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
590
            return array_merge($tableSql, $columnSql);
591
        }
592
593 1290
        foreach ($queryParts as $query) {
594 810
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
595
        }
596
597 1290
        $sql = array_merge($sql, $commentsSql);
598
599 1290
        if ($diff->newName !== false) {
600 116
            $sql[] = "sp_RENAME '" . $diff->getName($this)->getQuotedName($this) . "', '" . $diff->getNewName()->getName() . "'";
601
602
            /**
603
             * Rename table's default constraints names
604
             * to match the new table name.
605
             * This is necessary to ensure that the default
606
             * constraints can be referenced in future table
607
             * alterations as the table name is encoded in
608
             * default constraints' names.
609
             */
610 116
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
611
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
612 116
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
613 116
                "'" . $this->generateIdentifierName($diff->newName) . "') + ''', ''OBJECT'';' " .
0 ignored issues
show
Bug introduced by
It seems like $diff->newName can also be of type true; however, parameter $identifier of Doctrine\DBAL\Platforms\...enerateIdentifierName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

Loading history...
940
    {
941
        return "SELECT f.name AS ForeignKey,
942
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
943
                OBJECT_NAME (f.parent_object_id) AS TableName,
944
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
945
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
946
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
947
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
948
                f.delete_referential_action_desc,
949
                f.update_referential_action_desc
950
                FROM sys.foreign_keys AS f
951
                INNER JOIN sys.foreign_key_columns AS fc
952
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
953
                ON f.OBJECT_ID = fc.constraint_object_id
954
                WHERE " .
955 164
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
956
    }
957
958
    /**
959
     * {@inheritDoc}
960
     */
961 170
    public function getListTableIndexesSQL($table, $currentDatabase = null)
962
    {
963
        return "SELECT idx.name AS key_name,
964
                       col.name AS column_name,
965
                       ~idx.is_unique AS non_unique,
966
                       idx.is_primary_key AS [primary],
967
                       CASE idx.type
968
                           WHEN '1' THEN 'clustered'
969
                           WHEN '2' THEN 'nonclustered'
970
                           ELSE NULL
971
                       END AS flags
972
                FROM sys.tables AS tbl
973
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
974
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
975
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
976
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
977 170
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . "
978
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC";
979
    }
980
981
    /**
982
     * {@inheritDoc}
983
     */
984 2
    public function getCreateViewSQL($name, $sql)
985
    {
986 2
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
987
    }
988
989
    /**
990
     * {@inheritDoc}
991
     */
992 2
    public function getListViewsSQL($database)
993
    {
994 2
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
995
    }
996
997
    /**
998
     * Returns the where clause to filter schema and table name in a query.
999
     *
1000
     * @param string $table        The full qualified name of the table.
1001
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
1002
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
1003
     *
1004
     * @return string
1005
     */
1006 428
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
1007
    {
1008 428
        if (strpos($table, ".") !== false) {
1009 173
            list($schema, $table) = explode(".", $table);
1010 173
            $schema = $this->quoteStringLiteral($schema);
1011 173
            $table = $this->quoteStringLiteral($table);
1012
        } else {
1013 257
            $schema = "SCHEMA_NAME()";
1014 257
            $table = $this->quoteStringLiteral($table);
1015
        }
1016
1017 428
        return "({$tableColumn} = {$table} AND {$schemaColumn} = {$schema})";
1018
    }
1019
1020
    /**
1021
     * {@inheritDoc}
1022
     */
1023 2
    public function getDropViewSQL($name)
1024
    {
1025 2
        return 'DROP VIEW ' . $name;
1026
    }
1027
1028
    /**
1029
     * {@inheritDoc}
1030
     */
1031
    public function getGuidExpression()
1032
    {
1033
        return 'NEWID()';
1034
    }
1035
1036
    /**
1037
     * {@inheritDoc}
1038
     */
1039 2
    public function getLocateExpression($str, $substr, $startPos = false)
1040
    {
1041 2
        if ($startPos == false) {
1042 2
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1043
        }
1044
1045 2
        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 72
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
1060
    {
1061 72
        if ( ! $char) {
1062
            switch ($pos) {
1063 16
                case TrimMode::LEADING:
1064 4
                    $trimFn = 'LTRIM';
1065 4
                    break;
1066
1067 12
                case TrimMode::TRAILING:
1068 4
                    $trimFn = 'RTRIM';
1069 4
                    break;
1070
1071
                default:
1072 8
                    return 'LTRIM(RTRIM(' . $str . '))';
1073
            }
1074
1075 8
            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 56
        $pattern = "'%[^' + $char + ']%'";
1088
1089 56
        if ($pos === TrimMode::LEADING) {
1090 14
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1091
        }
1092
1093 42
        if ($pos === TrimMode::TRAILING) {
1094 14
            return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1095
        }
1096
1097 28
        return 'reverse(stuff(reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)), 1, patindex(' . $pattern . ', reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null))) - 1, null))';
1098
    }
1099
1100
    /**
1101
     * {@inheritDoc}
1102
     */
1103 57
    public function getConcatExpression()
1104
    {
1105 57
        $args = func_get_args();
1106
1107 57
        return '(' . implode(' + ', $args) . ')';
1108
    }
1109
1110
    /**
1111
     * {@inheritDoc}
1112
     */
1113 61
    public function getListDatabasesSQL()
1114
    {
1115 61
        return 'SELECT * FROM sys.databases';
1116
    }
1117
1118
    /**
1119
     * {@inheritDoc}
1120
     */
1121 4
    public function getListNamespacesSQL()
1122
    {
1123 4
        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 (!is_null($length)) {
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 57
    public function getSetTransactionIsolationSQL($level)
1150
    {
1151 57
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1152
    }
1153
1154
    /**
1155
     * {@inheritDoc}
1156
     */
1157 1055
    public function getIntegerTypeDeclarationSQL(array $field)
1158
    {
1159 1055
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1160
    }
1161
1162
    /**
1163
     * {@inheritDoc}
1164
     */
1165 30
    public function getBigIntTypeDeclarationSQL(array $field)
1166
    {
1167 30
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1168
    }
1169
1170
    /**
1171
     * {@inheritDoc}
1172
     */
1173 2
    public function getSmallIntTypeDeclarationSQL(array $field)
1174
    {
1175 2
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
1176
    }
1177
1178
    /**
1179
     * {@inheritDoc}
1180
     */
1181 57
    public function getGuidTypeDeclarationSQL(array $field)
1182
    {
1183 57
        return 'UNIQUEIDENTIFIER';
1184
    }
1185
1186
    /**
1187
     * {@inheritDoc}
1188
     */
1189 1520
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1190
    {
1191 1520
        return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
1192
    }
1193
1194
    /**
1195
     * {@inheritdoc}
1196
     */
1197 61
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1198
    {
1199 61
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
1200
    }
1201
1202
    /**
1203
     * {@inheritdoc}
1204
     */
1205 118
    public function getBinaryMaxLength()
1206
    {
1207 118
        return 8000;
1208
    }
1209
1210
    /**
1211
     * {@inheritDoc}
1212
     */
1213 95
    public function getClobTypeDeclarationSQL(array $field)
1214
    {
1215 95
        return 'VARCHAR(MAX)';
1216
    }
1217
1218
    /**
1219
     * {@inheritDoc}
1220
     */
1221 1055
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1222
    {
1223 1055
        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 89
    public function getBooleanTypeDeclarationSQL(array $field)
1254
    {
1255 89
        return 'BIT';
1256
    }
1257
1258
    /**
1259
     * {@inheritDoc}
1260
     */
1261 760
    protected function doModifyLimitQuery($query, $limit, $offset = null)
1262
    {
1263 760
        $where = [];
1264
1265 760
        if ($offset > 0) {
1266 76
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
1267
        }
1268
1269 760
        if ($limit !== null) {
1270 760
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
1271 760
            $top     = sprintf('TOP %d', $offset + $limit);
1272
        } else {
1273
            $top = 'TOP 9223372036854775807';
1274
        }
1275
1276 760
        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 760
        $selectPattern = '/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/im';
1285 760
        $replacePattern = sprintf('$1%s $2', $top);
1286 760
        $query = preg_replace($selectPattern, $replacePattern, $query);
1287
1288 760
        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 494
            $query = $this->scrubInnerOrderBy($query);
1292
        }
1293
1294
        // Build a new limited query around the original, using a CTE
1295 760
        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 760
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
1301 760
            $query,
1302 760
            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 $query
1311
     * @return string
1312
     */
1313 494
    private function scrubInnerOrderBy($query)
1314
    {
1315 494
        $count = substr_count(strtoupper($query), 'ORDER BY');
1316 494
        $offset = 0;
1317
1318 494
        while ($count-- > 0) {
1319 494
            $orderByPos = stripos($query, ' ORDER BY', $offset);
1320 494
            if ($orderByPos === false) {
1321 38
                break;
1322
            }
1323
1324 494
            $qLen = strlen($query);
1325 494
            $parenCount = 0;
1326 494
            $currentPosition = $orderByPos;
1327
1328 494
            while ($parenCount >= 0 && $currentPosition < $qLen) {
1329 494
                if ($query[$currentPosition] === '(') {
1330 38
                    $parenCount++;
1331 494
                } elseif ($query[$currentPosition] === ')') {
1332 266
                    $parenCount--;
1333
                }
1334
1335 494
                $currentPosition++;
1336
            }
1337
1338 494
            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 437
                $offset = $currentPosition;
1342 437
                continue;
1343
            }
1344
1345 190
            if ($currentPosition < $qLen - 1) {
1346 190
                $query = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
1347 190
                $offset = $orderByPos;
1348
            }
1349
        }
1350 494
        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 494
    private function isOrderByInTopNSubquery($query, $currentPosition)
1361
    {
1362
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
1363 494
        $subQueryBuffer = '';
1364 494
        $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 494
        while ($parenCount >= 0 && $currentPosition >= 0) {
1369 494
            if ($query[$currentPosition] === '(') {
1370 323
                $parenCount--;
1371 494
            } elseif ($query[$currentPosition] === ')') {
1372 266
                $parenCount++;
1373
            }
1374
1375
            // Only yank query text on the same nesting level as the ORDER BY clause.
1376 494
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;
1377
1378 494
            $currentPosition--;
1379
        }
1380
1381 494
        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 63
    public function convertBooleans($item)
1396
    {
1397 63
        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 63
        } elseif (is_bool($item) || is_numeric($item)) {
1404 63
            $item = ($item) ? 1 : 0;
1405
        }
1406
1407 63
        return $item;
1408
    }
1409
1410
    /**
1411
     * {@inheritDoc}
1412
     */
1413 4
    public function getCreateTemporaryTableSnippetSQL()
1414
    {
1415 4
        return "CREATE TABLE";
1416
    }
1417
1418
    /**
1419
     * {@inheritDoc}
1420
     */
1421 4
    public function getTemporaryTableName($tableName)
1422
    {
1423 4
        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 159
    public function getName()
1462
    {
1463 159
        return 'mssql';
1464
    }
1465
1466
    /**
1467
     * {@inheritDoc}
1468
     */
1469 287
    protected function initializeDoctrineTypeMappings()
1470
    {
1471 287
        $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 287
    }
1499
1500
    /**
1501
     * {@inheritDoc}
1502
     */
1503 2
    public function createSavePoint($savepoint)
1504
    {
1505 2
        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 2
    public function rollbackSavePoint($savepoint)
1520
    {
1521 2
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1522
    }
1523
1524
    /**
1525
     * {@inheritdoc}
1526
     */
1527 401
    public function getForeignKeyReferentialActionSQL($action)
1528
    {
1529
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1530 401
        if (strtoupper($action) === 'RESTRICT') {
1531 57
            return 'NO ACTION';
1532
        }
1533
1534 344
        return parent::getForeignKeyReferentialActionSQL($action);
1535
    }
1536
1537
    /**
1538
     * {@inheritDoc}
1539
     */
1540 137
    public function appendLockHint($fromClause, $lockMode)
1541
    {
1542
        switch (true) {
1543 137
            case LockMode::NONE === $lockMode:
1544 19
                return $fromClause . ' WITH (NOLOCK)';
1545
1546 118
            case LockMode::PESSIMISTIC_READ === $lockMode:
1547 19
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1548
1549 99
            case LockMode::PESSIMISTIC_WRITE === $lockMode:
1550 23
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1551
1552
            default:
1553 76
                return $fromClause;
1554
        }
1555
    }
1556
1557
    /**
1558
     * {@inheritDoc}
1559
     */
1560 4
    public function getForUpdateSQL()
1561
    {
1562 4
        return ' ';
1563
    }
1564
1565
    /**
1566
     * {@inheritDoc}
1567
     */
1568 1178
    protected function getReservedKeywordsClass()
1569
    {
1570 1178
        return Keywords\SQLServerKeywords::class;
1571
    }
1572
1573
    /**
1574
     * {@inheritDoc}
1575
     */
1576 1737
    public function quoteSingleIdentifier($str)
1577
    {
1578 1737
        return "[" . str_replace("]", "][", $str) . "]";
1579
    }
1580
1581
    /**
1582
     * {@inheritDoc}
1583
     */
1584 73
    public function getTruncateTableSQL($tableName, $cascade = false)
1585
    {
1586 73
        $tableIdentifier = new Identifier($tableName);
1587
1588 73
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1589
    }
1590
1591
    /**
1592
     * {@inheritDoc}
1593
     */
1594 64
    public function getBlobTypeDeclarationSQL(array $field)
1595
    {
1596 64
        return 'VARBINARY(MAX)';
1597
    }
1598
1599
    /**
1600
     * {@inheritDoc}
1601
     */
1602 1101
    public function getDefaultValueDeclarationSQL($field)
1603
    {
1604 1101
        if ( ! isset($field['default'])) {
1605
            return empty($field['notnull']) ? ' NULL' : '';
1606
        }
1607
1608 1101
        if ( ! isset($field['type'])) {
1609 228
            return " DEFAULT '" . $field['default'] . "'";
1610
        }
1611
1612 873
        $type = $field['type'];
1613
1614 873
        if ($type instanceof Types\PhpIntegerMappingType) {
1615 181
            return " DEFAULT " . $field['default'];
1616
        }
1617
1618 755
        if ($type instanceof Types\PhpDateTimeMappingType && $field['default'] === $this->getCurrentTimestampSQL()) {
1619 59
            return " DEFAULT " . $this->getCurrentTimestampSQL();
1620
        }
1621
1622 698
        if ($type instanceof Types\BooleanType) {
1623 59
            return " DEFAULT '" . $this->convertBooleans($field['default']) . "'";
1624
        }
1625
1626 698
        return " DEFAULT '" . $field['default'] . "'";
1627
    }
1628
1629
    /**
1630
     * {@inheritdoc}
1631
     *
1632
     * Modifies column declaration order as it differs in Microsoft SQL Server.
1633
     */
1634 2040
    public function getColumnDeclarationSQL($name, array $field)
1635
    {
1636 2040
        if (isset($field['columnDefinition'])) {
1637 57
            $columnDef = $this->getCustomTypeDeclarationSQL($field);
1638
        } else {
1639 1983
            $collation = (isset($field['collation']) && $field['collation']) ?
1640 1983
                ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : '';
1641
1642 1983
            $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : '';
1643
1644 1983
            $unique = (isset($field['unique']) && $field['unique']) ?
1645 1983
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';
1646
1647 1983
            $check = (isset($field['check']) && $field['check']) ?
1648 1983
                ' ' . $field['check'] : '';
1649
1650 1983
            $typeDecl = $field['type']->getSQLDeclaration($field, $this);
1651 1983
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1652
        }
1653
1654 2040
        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 873
    private function generateDefaultConstraintName($table, $column)
1666
    {
1667 873
        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 930
    private function generateIdentifierName($identifier)
1678
    {
1679
        // Always generate name for unquoted identifiers to ensure consistency.
1680 930
        $identifier = new Identifier($identifier);
1681
1682 930
        return strtoupper(dechex(crc32($identifier->getName())));
1683
    }
1684
}
1685