Completed
Push — develop ( a59880...a5109c )
by Sergei
112:22 queued 47:20
created

SQLServerPlatform::getLocateExpression()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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