Completed
Pull Request — develop (#3211)
by Sergei
64:43
created

SQLServerPlatform::getTimeFormatString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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