Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Doctrine\DBAL\Platforms;
4
 
5
use Doctrine\DBAL\Connection;
6
use Doctrine\DBAL\Exception\InvalidLockMode;
7
use Doctrine\DBAL\LockMode;
8
use Doctrine\DBAL\Schema\Column;
9
use Doctrine\DBAL\Schema\ColumnDiff;
10
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
11
use Doctrine\DBAL\Schema\Identifier;
12
use Doctrine\DBAL\Schema\Index;
13
use Doctrine\DBAL\Schema\Sequence;
14
use Doctrine\DBAL\Schema\SQLServerSchemaManager;
15
use Doctrine\DBAL\Schema\Table;
16
use Doctrine\DBAL\Schema\TableDiff;
17
use Doctrine\Deprecations\Deprecation;
18
use InvalidArgumentException;
19
 
20
use function array_merge;
21
use function array_unique;
22
use function array_values;
23
use function count;
24
use function crc32;
25
use function dechex;
26
use function explode;
27
use function func_get_arg;
28
use function func_get_args;
29
use function func_num_args;
30
use function implode;
31
use function is_array;
32
use function is_bool;
33
use function is_numeric;
34
use function is_string;
35
use function preg_match;
36
use function preg_match_all;
37
use function sprintf;
38
use function str_replace;
39
use function strpos;
40
use function strtoupper;
41
use function substr_count;
42
 
43
use const PREG_OFFSET_CAPTURE;
44
 
45
/**
46
 * Provides the behavior, features and SQL dialect of the Microsoft SQL Server database platform
47
 * of the oldest supported version.
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
    private function getConvertExpression($dataType, $expression): string
74
    {
75
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
76
    }
77
 
78
    /**
79
     * {@inheritdoc}
80
     */
81
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
82
    {
83
        $factorClause = '';
84
 
85
        if ($operator === '-') {
86
            $factorClause = '-1 * ';
87
        }
88
 
89
        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
90
    }
91
 
92
    /**
93
     * {@inheritDoc}
94
     */
95
    public function getDateDiffExpression($date1, $date2)
96
    {
97
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
98
    }
99
 
100
    /**
101
     * {@inheritDoc}
102
     *
103
     * Microsoft SQL Server prefers "autoincrement" identity columns
104
     * since sequences can only be emulated with a table.
105
     *
106
     * @deprecated
107
     */
108
    public function prefersIdentityColumns()
109
    {
110
        Deprecation::trigger(
111
            'doctrine/dbal',
112
            'https://github.com/doctrine/dbal/pull/1519',
113
            'SQLServerPlatform::prefersIdentityColumns() is deprecated.',
114
        );
115
 
116
        return true;
117
    }
118
 
119
    /**
120
     * {@inheritDoc}
121
     *
122
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
123
     */
124
    public function supportsIdentityColumns()
125
    {
126
        return true;
127
    }
128
 
129
    /**
130
     * {@inheritDoc}
131
     */
132
    public function supportsReleaseSavepoints()
133
    {
134
        return false;
135
    }
136
 
137
    /**
138
     * {@inheritdoc}
139
     */
140
    public function supportsSchemas()
141
    {
142
        return true;
143
    }
144
 
145
    /**
146
     * {@inheritdoc}
147
     *
148
     * @deprecated
149
     */
150
    public function getDefaultSchemaName()
151
    {
152
        Deprecation::trigger(
153
            'doctrine/dbal',
154
            'https://github.com/doctrine/dbal/pull/5513',
155
            '%s is deprecated.',
156
            __METHOD__,
157
        );
158
 
159
        return 'dbo';
160
    }
161
 
162
    /**
163
     * {@inheritDoc}
164
     *
165
     * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
166
     */
167
    public function supportsColumnCollation()
168
    {
169
        return true;
170
    }
171
 
172
    public function supportsSequences(): bool
173
    {
174
        return true;
175
    }
176
 
177
    public function getAlterSequenceSQL(Sequence $sequence): string
178
    {
179
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
180
            ' INCREMENT BY ' . $sequence->getAllocationSize();
181
    }
182
 
183
    public function getCreateSequenceSQL(Sequence $sequence): string
184
    {
185
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
186
            ' START WITH ' . $sequence->getInitialValue() .
187
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
188
            ' MINVALUE ' . $sequence->getInitialValue();
189
    }
190
 
191
    /**
192
     * {@inheritdoc}
193
     *
194
     * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy.
195
     */
196
    public function getListSequencesSQL($database)
197
    {
198
        return 'SELECT seq.name,
199
                       CAST(
200
                           seq.increment AS VARCHAR(MAX)
201
                       ) AS increment, -- CAST avoids driver error for sql_variant type
202
                       CAST(
203
                           seq.start_value AS VARCHAR(MAX)
204
                       ) AS start_value -- CAST avoids driver error for sql_variant type
205
                FROM   sys.sequences AS seq';
206
    }
207
 
208
    /**
209
     * {@inheritdoc}
210
     */
211
    public function getSequenceNextValSQL($sequence)
212
    {
213
        return 'SELECT NEXT VALUE FOR ' . $sequence;
214
    }
215
 
216
    /**
217
     * {@inheritDoc}
218
     *
219
     * @deprecated
220
     */
221
    public function hasNativeGuidType()
222
    {
223
        Deprecation::triggerIfCalledFromOutside(
224
            'doctrine/dbal',
225
            'https://github.com/doctrine/dbal/pull/5509',
226
            '%s is deprecated.',
227
            __METHOD__,
228
        );
229
 
230
        return true;
231
    }
232
 
233
    /**
234
     * {@inheritDoc}
235
     */
236
    public function getDropForeignKeySQL($foreignKey, $table)
237
    {
238
        if (! $foreignKey instanceof ForeignKeyConstraint) {
239
            $foreignKey = new Identifier($foreignKey);
240
        }
241
 
242
        if (! $table instanceof Table) {
243
            $table = new Identifier($table);
244
        }
245
 
246
        $foreignKey = $foreignKey->getQuotedName($this);
247
        $table      = $table->getQuotedName($this);
248
 
249
        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
250
    }
251
 
252
    /**
253
     * {@inheritDoc}
254
     */
255
    public function getDropIndexSQL($index, $table = null)
256
    {
257
        if ($index instanceof Index) {
258
            Deprecation::trigger(
259
                'doctrine/dbal',
260
                'https://github.com/doctrine/dbal/issues/4798',
261
                'Passing $index as an Index object to %s is deprecated. Pass it as a quoted name instead.',
262
                __METHOD__,
263
            );
264
 
265
            $index = $index->getQuotedName($this);
266
        } elseif (! is_string($index)) {
267
            throw new InvalidArgumentException(
268
                __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.',
269
            );
270
        }
271
 
272
        if ($table instanceof Table) {
273
            Deprecation::trigger(
274
                'doctrine/dbal',
275
                'https://github.com/doctrine/dbal/issues/4798',
276
                'Passing $table as an Table object to %s is deprecated. Pass it as a quoted name instead.',
277
                __METHOD__,
278
            );
279
 
280
            $table = $table->getQuotedName($this);
281
        } elseif (! is_string($table)) {
282
            throw new InvalidArgumentException(
283
                __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.',
284
            );
285
        }
286
 
287
        return 'DROP INDEX ' . $index . ' ON ' . $table;
288
    }
289
 
290
    /**
291
     * {@inheritDoc}
292
     */
293
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
294
    {
295
        $defaultConstraintsSql = [];
296
        $commentsSql           = [];
297
 
298
        $tableComment = $options['comment'] ?? null;
299
        if ($tableComment !== null) {
300
            $commentsSql[] = $this->getCommentOnTableSQL($name, $tableComment);
301
        }
302
 
303
        // @todo does other code breaks because of this?
304
        // force primary keys to be not null
305
        foreach ($columns as &$column) {
306
            if (! empty($column['primary'])) {
307
                $column['notnull'] = true;
308
            }
309
 
310
            // Build default constraints SQL statements.
311
            if (isset($column['default'])) {
312
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $name .
313
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($name, $column);
314
            }
315
 
316
            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
317
                continue;
318
            }
319
 
320
            $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']);
321
        }
322
 
323
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
324
 
325
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
326
            foreach ($options['uniqueConstraints'] as $constraintName => $definition) {
327
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($constraintName, $definition);
328
            }
329
        }
330
 
331
        if (isset($options['primary']) && ! empty($options['primary'])) {
332
            $flags = '';
333
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
334
                $flags = ' NONCLUSTERED';
335
            }
336
 
337
            $columnListSql .= ', PRIMARY KEY' . $flags
338
                . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
339
        }
340
 
341
        $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql;
342
 
343
        $check = $this->getCheckDeclarationSQL($columns);
344
        if (! empty($check)) {
345
            $query .= ', ' . $check;
346
        }
347
 
348
        $query .= ')';
349
 
350
        $sql = [$query];
351
 
352
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
353
            foreach ($options['indexes'] as $index) {
354
                $sql[] = $this->getCreateIndexSQL($index, $name);
355
            }
356
        }
357
 
358
        if (isset($options['foreignKeys'])) {
359
            foreach ($options['foreignKeys'] as $definition) {
360
                $sql[] = $this->getCreateForeignKeySQL($definition, $name);
361
            }
362
        }
363
 
364
        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
365
    }
366
 
367
    /**
368
     * {@inheritDoc}
369
     */
370
    public function getCreatePrimaryKeySQL(Index $index, $table)
371
    {
372
        if ($table instanceof Table) {
373
            Deprecation::trigger(
374
                'doctrine/dbal',
375
                'https://github.com/doctrine/dbal/issues/4798',
376
                'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.',
377
                __METHOD__,
378
            );
379
 
380
            $identifier = $table->getQuotedName($this);
381
        } else {
382
            $identifier = $table;
383
        }
384
 
385
        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';
386
 
387
        if ($index->hasFlag('nonclustered')) {
388
            $sql .= ' NONCLUSTERED';
389
        }
390
 
391
        return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
392
    }
393
 
394
    /**
395
     * Returns the SQL statement for creating a column comment.
396
     *
397
     * SQL Server does not support native column comments,
398
     * therefore the extended properties functionality is used
399
     * as a workaround to store them.
400
     * The property name used to store column comments is "MS_Description"
401
     * which provides compatibility with SQL Server Management Studio,
402
     * as column comments are stored in the same property there when
403
     * specifying a column's "Description" attribute.
404
     *
405
     * @param string      $tableName  The quoted table name to which the column belongs.
406
     * @param string      $columnName The quoted column name to create the comment for.
407
     * @param string|null $comment    The column's comment.
408
     *
409
     * @return string
410
     */
411
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
412
    {
413
        if (strpos($tableName, '.') !== false) {
414
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
415
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
416
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
417
        } else {
418
            $schemaSQL = "'dbo'";
419
            $tableSQL  = $this->quoteStringLiteral($tableName);
420
        }
421
 
422
        return $this->getAddExtendedPropertySQL(
423
            'MS_Description',
424
            $comment,
425
            'SCHEMA',
426
            $schemaSQL,
427
            'TABLE',
428
            $tableSQL,
429
            'COLUMN',
430
            $columnName,
431
        );
432
    }
433
 
434
    /**
435
     * Returns the SQL snippet for declaring a default constraint.
436
     *
437
     * @internal The method should be only used from within the SQLServerPlatform class hierarchy.
438
     *
439
     * @param string  $table  Name of the table to return the default constraint declaration for.
440
     * @param mixed[] $column Column definition.
441
     *
442
     * @return string
443
     *
444
     * @throws InvalidArgumentException
445
     */
446
    public function getDefaultConstraintDeclarationSQL($table, array $column)
447
    {
448
        if (! isset($column['default'])) {
449
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
450
        }
451
 
452
        $columnName = new Identifier($column['name']);
453
 
454
        return ' CONSTRAINT ' .
455
            $this->generateDefaultConstraintName($table, $column['name']) .
456
            $this->getDefaultValueDeclarationSQL($column) .
457
            ' FOR ' . $columnName->getQuotedName($this);
458
    }
459
 
460
    /**
461
     * {@inheritDoc}
462
     */
463
    public function getCreateIndexSQL(Index $index, $table)
464
    {
465
        $constraint = parent::getCreateIndexSQL($index, $table);
466
 
467
        if ($index->isUnique() && ! $index->isPrimary()) {
468
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
469
        }
470
 
471
        return $constraint;
472
    }
473
 
474
    /**
475
     * {@inheritDoc}
476
     */
477
    protected function getCreateIndexSQLFlags(Index $index)
478
    {
479
        $type = '';
480
        if ($index->isUnique()) {
481
            $type .= 'UNIQUE ';
482
        }
483
 
484
        if ($index->hasFlag('clustered')) {
485
            $type .= 'CLUSTERED ';
486
        } elseif ($index->hasFlag('nonclustered')) {
487
            $type .= 'NONCLUSTERED ';
488
        }
489
 
490
        return $type;
491
    }
492
 
493
    /**
494
     * Extend unique key constraint with required filters
495
     *
496
     * @param string $sql
497
     */
498
    private function _appendUniqueConstraintDefinition($sql, Index $index): string
499
    {
500
        $fields = [];
501
 
502
        foreach ($index->getQuotedColumns($this) as $field) {
503
            $fields[] = $field . ' IS NOT NULL';
504
        }
505
 
506
        return $sql . ' WHERE ' . implode(' AND ', $fields);
507
    }
508
 
509
    /**
510
     * {@inheritDoc}
511
     */
512
    public function getAlterTableSQL(TableDiff $diff)
513
    {
514
        $queryParts  = [];
515
        $sql         = [];
516
        $columnSql   = [];
517
        $commentsSql = [];
518
 
519
        $table = $diff->getOldTable() ?? $diff->getName($this);
520
 
521
        $tableName = $table->getName();
522
 
523
        foreach ($diff->getAddedColumns() as $column) {
524
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
525
                continue;
526
            }
527
 
528
            $columnProperties = $column->toArray();
529
 
530
            $addColumnSql = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnProperties);
531
 
532
            if (isset($columnProperties['default'])) {
533
                $addColumnSql .= ' CONSTRAINT ' . $this->generateDefaultConstraintName(
534
                    $tableName,
535
                    $column->getQuotedName($this),
536
                ) . $this->getDefaultValueDeclarationSQL($columnProperties);
537
            }
538
 
539
            $queryParts[] = $addColumnSql;
540
 
541
            $comment = $this->getColumnComment($column);
542
 
543
            if (empty($comment) && ! is_numeric($comment)) {
544
                continue;
545
            }
546
 
547
            $commentsSql[] = $this->getCreateColumnCommentSQL(
548
                $tableName,
549
                $column->getQuotedName($this),
550
                $comment,
551
            );
552
        }
553
 
554
        foreach ($diff->getDroppedColumns() as $column) {
555
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
556
                continue;
557
            }
558
 
559
            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
560
        }
561
 
562
        foreach ($diff->getModifiedColumns() as $columnDiff) {
563
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
564
                continue;
565
            }
566
 
567
            $newColumn     = $columnDiff->getNewColumn();
568
            $newComment    = $this->getColumnComment($newColumn);
569
            $hasNewComment = ! empty($newComment) || is_numeric($newComment);
570
 
571
            $oldColumn = $columnDiff->getOldColumn();
572
 
573
            if ($oldColumn instanceof Column) {
574
                $oldComment    = $this->getColumnComment($oldColumn);
575
                $hasOldComment = ! empty($oldComment) || is_numeric($oldComment);
576
 
577
                if ($hasOldComment && $hasNewComment && $oldComment !== $newComment) {
578
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
579
                        $tableName,
580
                        $newColumn->getQuotedName($this),
581
                        $newComment,
582
                    );
583
                } elseif ($hasOldComment && ! $hasNewComment) {
584
                    $commentsSql[] = $this->getDropColumnCommentSQL(
585
                        $tableName,
586
                        $newColumn->getQuotedName($this),
587
                    );
588
                } elseif (! $hasOldComment && $hasNewComment) {
589
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
590
                        $tableName,
591
                        $newColumn->getQuotedName($this),
592
                        $newComment,
593
                    );
594
                }
595
            }
596
 
597
            // Do not add query part if only comment has changed.
598
            if ($columnDiff->hasCommentChanged() && count($columnDiff->changedProperties) === 1) {
599
                continue;
600
            }
601
 
602
            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
603
 
604
            if ($requireDropDefaultConstraint) {
605
                $oldColumn = $columnDiff->getOldColumn();
606
 
607
                if ($oldColumn !== null) {
608
                    $oldColumnName = $oldColumn->getName();
609
                } else {
610
                    $oldColumnName = $columnDiff->oldColumnName;
611
                }
612
 
613
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause($tableName, $oldColumnName);
614
            }
615
 
616
            $columnProperties = $newColumn->toArray();
617
 
618
            $queryParts[] = 'ALTER COLUMN ' .
619
                    $this->getColumnDeclarationSQL($newColumn->getQuotedName($this), $columnProperties);
620
 
621
            if (
622
                ! isset($columnProperties['default'])
623
                || (! $requireDropDefaultConstraint && ! $columnDiff->hasDefaultChanged())
624
            ) {
625
                continue;
626
            }
627
 
628
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($tableName, $newColumn);
629
        }
630
 
631
        $tableNameSQL = $table->getQuotedName($this);
632
 
633
        foreach ($diff->getRenamedColumns() as $oldColumnName => $newColumn) {
634
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $newColumn, $diff, $columnSql)) {
635
                continue;
636
            }
637
 
638
            $oldColumnName = new Identifier($oldColumnName);
639
 
640
            $sql[] = "sp_rename '" . $tableNameSQL . '.' . $oldColumnName->getQuotedName($this) .
641
                "', '" . $newColumn->getQuotedName($this) . "', 'COLUMN'";
642
 
643
            // Recreate default constraint with new column name if necessary (for future reference).
644
            if ($newColumn->getDefault() === null) {
645
                continue;
646
            }
647
 
648
            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
649
                $tableName,
650
                $oldColumnName->getQuotedName($this),
651
            );
652
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($tableName, $newColumn);
653
        }
654
 
655
        $tableSql = [];
656
 
657
        if ($this->onSchemaAlterTable($diff, $tableSql)) {
658
            return array_merge($tableSql, $columnSql);
659
        }
660
 
661
        foreach ($queryParts as $query) {
662
            $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query;
663
        }
664
 
665
        $sql = array_merge($sql, $commentsSql);
666
 
667
        $newName = $diff->getNewName();
668
 
669
        if ($newName !== false) {
670
            Deprecation::trigger(
671
                'doctrine/dbal',
672
                'https://github.com/doctrine/dbal/pull/5663',
673
                'Generation of "rename table" SQL using %s is deprecated. Use getRenameTableSQL() instead.',
674
                __METHOD__,
675
            );
676
 
677
            $sql = array_merge($sql, $this->getRenameTableSQL($tableName, $newName->getName()));
678
        }
679
 
680
        $sql = array_merge(
681
            $this->getPreAlterTableIndexForeignKeySQL($diff),
682
            $sql,
683
            $this->getPostAlterTableIndexForeignKeySQL($diff),
684
        );
685
 
686
        return array_merge($sql, $tableSql, $columnSql);
687
    }
688
 
689
    /**
690
     * {@inheritDoc}
691
     */
692
    public function getRenameTableSQL(string $oldName, string $newName): array
693
    {
694
        return [
695
            sprintf('sp_rename %s, %s', $this->quoteStringLiteral($oldName), $this->quoteStringLiteral($newName)),
696
 
697
            /* Rename table's default constraints names
698
             * to match the new table name.
699
             * This is necessary to ensure that the default
700
             * constraints can be referenced in future table
701
             * alterations as the table name is encoded in
702
             * default constraints' names. */
703
            sprintf(
704
                <<<'SQL'
705
                DECLARE @sql NVARCHAR(MAX) = N'';
706
                SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N'''
707
                    + REPLACE(dc.name, '%s', '%s') + ''', ''OBJECT'';'
708
                    FROM sys.default_constraints dc
709
                    JOIN sys.tables tbl
710
                        ON dc.parent_object_id = tbl.object_id
711
                    WHERE tbl.name = %s;
712
                EXEC sp_executesql @sql
713
                SQL,
714
                $this->generateIdentifierName($oldName),
715
                $this->generateIdentifierName($newName),
716
                $this->quoteStringLiteral($newName),
717
            ),
718
        ];
719
    }
720
 
721
    /**
722
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
723
     *
724
     * @param string $tableName The name of the table to generate the clause for.
725
     * @param Column $column    The column to generate the clause for.
726
     */
727
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column): string
728
    {
729
        $columnDef         = $column->toArray();
730
        $columnDef['name'] = $column->getQuotedName($this);
731
 
732
        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
733
    }
734
 
735
    /**
736
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
737
     *
738
     * @param string $tableName  The name of the table to generate the clause for.
739
     * @param string $columnName The name of the column to generate the clause for.
740
     */
741
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName): string
742
    {
743
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
744
    }
745
 
746
    /**
747
     * Checks whether a column alteration requires dropping its default constraint first.
748
     *
749
     * Different to other database vendors SQL Server implements column default values
750
     * as constraints and therefore changes in a column's default value as well as changes
751
     * in a column's type require dropping the default constraint first before being to
752
     * alter the particular column to the new definition.
753
     */
754
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff): bool
755
    {
756
        $oldColumn = $columnDiff->getOldColumn();
757
 
758
        // We can only decide whether to drop an existing default constraint
759
        // if we know the original default value.
760
        if (! $oldColumn instanceof Column) {
761
            return false;
762
        }
763
 
764
        // We only need to drop an existing default constraint if we know the
765
        // column was defined with a default value before.
766
        if ($oldColumn->getDefault() === null) {
767
            return false;
768
        }
769
 
770
        // We need to drop an existing default constraint if the column was
771
        // defined with a default value before and it has changed.
772
        if ($columnDiff->hasDefaultChanged()) {
773
            return true;
774
        }
775
 
776
        // We need to drop an existing default constraint if the column was
777
        // defined with a default value before and the native column type has changed.
778
        return $columnDiff->hasTypeChanged() || $columnDiff->hasFixedChanged();
779
    }
780
 
781
    /**
782
     * Returns the SQL statement for altering a column comment.
783
     *
784
     * SQL Server does not support native column comments,
785
     * therefore the extended properties functionality is used
786
     * as a workaround to store them.
787
     * The property name used to store column comments is "MS_Description"
788
     * which provides compatibility with SQL Server Management Studio,
789
     * as column comments are stored in the same property there when
790
     * specifying a column's "Description" attribute.
791
     *
792
     * @param string      $tableName  The quoted table name to which the column belongs.
793
     * @param string      $columnName The quoted column name to alter the comment for.
794
     * @param string|null $comment    The column's comment.
795
     *
796
     * @return string
797
     */
798
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
799
    {
800
        if (strpos($tableName, '.') !== false) {
801
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
802
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
803
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
804
        } else {
805
            $schemaSQL = "'dbo'";
806
            $tableSQL  = $this->quoteStringLiteral($tableName);
807
        }
808
 
809
        return $this->getUpdateExtendedPropertySQL(
810
            'MS_Description',
811
            $comment,
812
            'SCHEMA',
813
            $schemaSQL,
814
            'TABLE',
815
            $tableSQL,
816
            'COLUMN',
817
            $columnName,
818
        );
819
    }
820
 
821
    /**
822
     * Returns the SQL statement for dropping a column comment.
823
     *
824
     * SQL Server does not support native column comments,
825
     * therefore the extended properties functionality is used
826
     * as a workaround to store them.
827
     * The property name used to store column comments is "MS_Description"
828
     * which provides compatibility with SQL Server Management Studio,
829
     * as column comments are stored in the same property there when
830
     * specifying a column's "Description" attribute.
831
     *
832
     * @param string $tableName  The quoted table name to which the column belongs.
833
     * @param string $columnName The quoted column name to drop the comment for.
834
     *
835
     * @return string
836
     */
837
    protected function getDropColumnCommentSQL($tableName, $columnName)
838
    {
839
        if (strpos($tableName, '.') !== false) {
840
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
841
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
842
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
843
        } else {
844
            $schemaSQL = "'dbo'";
845
            $tableSQL  = $this->quoteStringLiteral($tableName);
846
        }
847
 
848
        return $this->getDropExtendedPropertySQL(
849
            'MS_Description',
850
            'SCHEMA',
851
            $schemaSQL,
852
            'TABLE',
853
            $tableSQL,
854
            'COLUMN',
855
            $columnName,
856
        );
857
    }
858
 
859
    /**
860
     * {@inheritdoc}
861
     */
862
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
863
    {
864
        return [sprintf(
865
            "EXEC sp_rename N'%s.%s', N'%s', N'INDEX'",
866
            $tableName,
867
            $oldIndexName,
868
            $index->getQuotedName($this),
869
        ),
870
        ];
871
    }
872
 
873
    /**
874
     * Returns the SQL statement for adding an extended property to a database object.
875
     *
876
     * @internal The method should be only used from within the SQLServerPlatform class hierarchy.
877
     *
878
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
879
     *
880
     * @param string      $name       The name of the property to add.
881
     * @param string|null $value      The value of the property to add.
882
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
883
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
884
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
885
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
886
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
887
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
888
     *
889
     * @return string
890
     */
891
    public function getAddExtendedPropertySQL(
892
        $name,
893
        $value = null,
894
        $level0Type = null,
895
        $level0Name = null,
896
        $level1Type = null,
897
        $level1Name = null,
898
        $level2Type = null,
899
        $level2Name = null
900
    ) {
901
        return 'EXEC sp_addextendedproperty ' .
902
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
903
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
904
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
905
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
906
    }
907
 
908
    /**
909
     * Returns the SQL statement for dropping an extended property from a database object.
910
     *
911
     * @internal The method should be only used from within the SQLServerPlatform class hierarchy.
912
     *
913
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
914
     *
915
     * @param string      $name       The name of the property to drop.
916
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
917
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
918
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
919
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
920
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
921
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
922
     *
923
     * @return string
924
     */
925
    public function getDropExtendedPropertySQL(
926
        $name,
927
        $level0Type = null,
928
        $level0Name = null,
929
        $level1Type = null,
930
        $level1Name = null,
931
        $level2Type = null,
932
        $level2Name = null
933
    ) {
934
        return 'EXEC sp_dropextendedproperty ' .
935
            'N' . $this->quoteStringLiteral($name) . ', ' .
936
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
937
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
938
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
939
    }
940
 
941
    /**
942
     * Returns the SQL statement for updating an extended property of a database object.
943
     *
944
     * @internal The method should be only used from within the SQLServerPlatform class hierarchy.
945
     *
946
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
947
     *
948
     * @param string      $name       The name of the property to update.
949
     * @param string|null $value      The value of the property to update.
950
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
951
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
952
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
953
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
954
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
955
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
956
     *
957
     * @return string
958
     */
959
    public function getUpdateExtendedPropertySQL(
960
        $name,
961
        $value = null,
962
        $level0Type = null,
963
        $level0Name = null,
964
        $level1Type = null,
965
        $level1Name = null,
966
        $level2Type = null,
967
        $level2Name = null
968
    ) {
969
        return 'EXEC sp_updateextendedproperty ' .
970
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
971
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
972
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
973
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
974
    }
975
 
976
    /**
977
     * {@inheritDoc}
978
     */
979
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
980
    {
981
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
982
    }
983
 
984
    /**
985
     * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon.
986
     *
987
     * {@inheritDoc}
988
     */
989
    public function getListTablesSQL()
990
    {
991
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
992
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
993
        return 'SELECT name, SCHEMA_NAME (uid) AS schema_name FROM sysobjects'
994
            . " WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
995
    }
996
 
997
    /**
998
     * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon.
999
     *
1000
     * {@inheritDoc}
1001
     */
1002
    public function getListTableColumnsSQL($table, $database = null)
1003
    {
1004
        return "SELECT    col.name,
1005
                          type.name AS type,
1006
                          col.max_length AS length,
1007
                          ~col.is_nullable AS notnull,
1008
                          def.definition AS [default],
1009
                          col.scale,
1010
                          col.precision,
1011
                          col.is_identity AS autoincrement,
1012
                          col.collation_name AS collation,
1013
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
1014
                FROM      sys.columns AS col
1015
                JOIN      sys.types AS type
1016
                ON        col.user_type_id = type.user_type_id
1017
                JOIN      sys.objects AS obj
1018
                ON        col.object_id = obj.object_id
1019
                JOIN      sys.schemas AS scm
1020
                ON        obj.schema_id = scm.schema_id
1021
                LEFT JOIN sys.default_constraints def
1022
                ON        col.default_object_id = def.object_id
1023
                AND       col.object_id = def.parent_object_id
1024
                LEFT JOIN sys.extended_properties AS prop
1025
                ON        obj.object_id = prop.major_id
1026
                AND       col.column_id = prop.minor_id
1027
                AND       prop.name = 'MS_Description'
1028
                WHERE     obj.type = 'U'
1029
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
1030
    }
1031
 
1032
    /**
1033
     * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon.
1034
     *
1035
     * @param string      $table
1036
     * @param string|null $database
1037
     *
1038
     * @return string
1039
     */
1040
    public function getListTableForeignKeysSQL($table, $database = null)
1041
    {
1042
        return 'SELECT f.name AS ForeignKey,
1043
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
1044
                OBJECT_NAME (f.parent_object_id) AS TableName,
1045
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
1046
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
1047
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
1048
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
1049
                f.delete_referential_action_desc,
1050
                f.update_referential_action_desc
1051
                FROM sys.foreign_keys AS f
1052
                INNER JOIN sys.foreign_key_columns AS fc
1053
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
1054
                ON f.OBJECT_ID = fc.constraint_object_id
1055
                WHERE ' .
1056
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)') .
1057
                ' ORDER BY fc.constraint_column_id';
1058
    }
1059
 
1060
    /**
1061
     * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon.
1062
     *
1063
     * {@inheritDoc}
1064
     */
1065
    public function getListTableIndexesSQL($table, $database = null)
1066
    {
1067
        return "SELECT idx.name AS key_name,
1068
                       col.name AS column_name,
1069
                       ~idx.is_unique AS non_unique,
1070
                       idx.is_primary_key AS [primary],
1071
                       CASE idx.type
1072
                           WHEN '1' THEN 'clustered'
1073
                           WHEN '2' THEN 'nonclustered'
1074
                           ELSE NULL
1075
                       END AS flags
1076
                FROM sys.tables AS tbl
1077
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
1078
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
1079
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
1080
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
1081
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
1082
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
1083
    }
1084
 
1085
    /**
1086
     * {@inheritDoc}
1087
     *
1088
     * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy.
1089
     */
1090
    public function getListViewsSQL($database)
1091
    {
1092
        return "SELECT name, definition FROM sysobjects
1093
                    INNER JOIN sys.sql_modules ON sysobjects.id = sys.sql_modules.object_id
1094
                WHERE type = 'V' ORDER BY name";
1095
    }
1096
 
1097
    /**
1098
     * Returns the where clause to filter schema and table name in a query.
1099
     *
1100
     * @param string $table        The full qualified name of the table.
1101
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
1102
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
1103
     */
1104
    private function getTableWhereClause($table, $schemaColumn, $tableColumn): string
1105
    {
1106
        if (strpos($table, '.') !== false) {
1107
            [$schema, $table] = explode('.', $table);
1108
            $schema           = $this->quoteStringLiteral($schema);
1109
            $table            = $this->quoteStringLiteral($table);
1110
        } else {
1111
            $schema = 'SCHEMA_NAME()';
1112
            $table  = $this->quoteStringLiteral($table);
1113
        }
1114
 
1115
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
1116
    }
1117
 
1118
    /**
1119
     * {@inheritDoc}
1120
     */
1121
    public function getLocateExpression($str, $substr, $startPos = false)
1122
    {
1123
        if ($startPos === false) {
1124
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
1125
        }
1126
 
1127
        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
1128
    }
1129
 
1130
    /**
1131
     * {@inheritDoc}
1132
     */
1133
    public function getModExpression($expression1, $expression2)
1134
    {
1135
        return $expression1 . ' % ' . $expression2;
1136
    }
1137
 
1138
    /**
1139
     * {@inheritDoc}
1140
     */
1141
    public function getTrimExpression($str, $mode = TrimMode::UNSPECIFIED, $char = false)
1142
    {
1143
        if ($char === false) {
1144
            switch ($mode) {
1145
                case TrimMode::LEADING:
1146
                    $trimFn = 'LTRIM';
1147
                    break;
1148
 
1149
                case TrimMode::TRAILING:
1150
                    $trimFn = 'RTRIM';
1151
                    break;
1152
 
1153
                default:
1154
                    return 'LTRIM(RTRIM(' . $str . '))';
1155
            }
1156
 
1157
            return $trimFn . '(' . $str . ')';
1158
        }
1159
 
1160
        $pattern = "'%[^' + " . $char . " + ']%'";
1161
 
1162
        if ($mode === TrimMode::LEADING) {
1163
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
1164
        }
1165
 
1166
        if ($mode === TrimMode::TRAILING) {
1167
            return 'reverse(stuff(reverse(' . $str . '), 1, '
1168
                . 'patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
1169
        }
1170
 
1171
        return 'reverse(stuff(reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)), 1, '
1172
            . 'patindex(' . $pattern . ', reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str
1173
            . ') - 1, null))) - 1, null))';
1174
    }
1175
 
1176
    /**
1177
     * {@inheritDoc}
1178
     */
1179
    public function getConcatExpression()
1180
    {
1181
        return sprintf('CONCAT(%s)', implode(', ', func_get_args()));
1182
    }
1183
 
1184
    /**
1185
     * {@inheritDoc}
1186
     *
1187
     * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy.
1188
     */
1189
    public function getListDatabasesSQL()
1190
    {
1191
        return 'SELECT * FROM sys.databases';
1192
    }
1193
 
1194
    /**
1195
     * {@inheritDoc}
1196
     *
1197
     * @deprecated Use {@see SQLServerSchemaManager::listSchemaNames()} instead.
1198
     */
1199
    public function getListNamespacesSQL()
1200
    {
1201
        Deprecation::triggerIfCalledFromOutside(
1202
            'doctrine/dbal',
1203
            'https://github.com/doctrine/dbal/issues/4503',
1204
            'SQLServerPlatform::getListNamespacesSQL() is deprecated,'
1205
                . ' use SQLServerSchemaManager::listSchemaNames() instead.',
1206
        );
1207
 
1208
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
1209
    }
1210
 
1211
    /**
1212
     * {@inheritDoc}
1213
     */
1214
    public function getSubstringExpression($string, $start, $length = null)
1215
    {
1216
        if ($length !== null) {
1217
            return 'SUBSTRING(' . $string . ', ' . $start . ', ' . $length . ')';
1218
        }
1219
 
1220
        return 'SUBSTRING(' . $string . ', ' . $start . ', LEN(' . $string . ') - ' . $start . ' + 1)';
1221
    }
1222
 
1223
    /**
1224
     * {@inheritDoc}
1225
     */
1226
    public function getLengthExpression($column)
1227
    {
1228
        return 'LEN(' . $column . ')';
1229
    }
1230
 
1231
    public function getCurrentDatabaseExpression(): string
1232
    {
1233
        return 'DB_NAME()';
1234
    }
1235
 
1236
    /**
1237
     * {@inheritDoc}
1238
     */
1239
    public function getSetTransactionIsolationSQL($level)
1240
    {
1241
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
1242
    }
1243
 
1244
    /**
1245
     * {@inheritDoc}
1246
     */
1247
    public function getIntegerTypeDeclarationSQL(array $column)
1248
    {
1249
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
1250
    }
1251
 
1252
    /**
1253
     * {@inheritDoc}
1254
     */
1255
    public function getBigIntTypeDeclarationSQL(array $column)
1256
    {
1257
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
1258
    }
1259
 
1260
    /**
1261
     * {@inheritDoc}
1262
     */
1263
    public function getSmallIntTypeDeclarationSQL(array $column)
1264
    {
1265
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
1266
    }
1267
 
1268
    /**
1269
     * {@inheritDoc}
1270
     */
1271
    public function getGuidTypeDeclarationSQL(array $column)
1272
    {
1273
        return 'UNIQUEIDENTIFIER';
1274
    }
1275
 
1276
    /**
1277
     * {@inheritDoc}
1278
     */
1279
    public function getDateTimeTzTypeDeclarationSQL(array $column)
1280
    {
1281
        return 'DATETIMEOFFSET(6)';
1282
    }
1283
 
1284
    /**
1285
     * {@inheritDoc}
1286
     */
1287
    public function getAsciiStringTypeDeclarationSQL(array $column): string
1288
    {
1289
        $length = $column['length'] ?? null;
1290
 
1291
        if (! isset($column['fixed'])) {
1292
            return sprintf('VARCHAR(%d)', $length ?? 255);
1293
        }
1294
 
1295
        return sprintf('CHAR(%d)', $length ?? 255);
1296
    }
1297
 
1298
    /**
1299
     * {@inheritDoc}
1300
     */
1301
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/)
1302
    {
1303
        if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) {
1304
            Deprecation::trigger(
1305
                'doctrine/dbal',
1306
                'https://github.com/doctrine/dbal/issues/3263',
1307
                'Relying on the default string column length on SQL Server is deprecated'
1308
                    . ', specify the length explicitly.',
1309
            );
1310
        }
1311
 
1312
        return $fixed
1313
            ? 'NCHAR(' . ($length > 0 ? $length : 255) . ')'
1314
            : 'NVARCHAR(' . ($length > 0 ? $length : 255) . ')';
1315
    }
1316
 
1317
    /**
1318
     * {@inheritdoc}
1319
     */
1320
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/)
1321
    {
1322
        if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) {
1323
            Deprecation::trigger(
1324
                'doctrine/dbal',
1325
                'https://github.com/doctrine/dbal/issues/3263',
1326
                'Relying on the default binary column length on SQL Server is deprecated'
1327
                    . ', specify the length explicitly.',
1328
            );
1329
        }
1330
 
1331
        return $fixed
1332
            ? 'BINARY(' . ($length > 0 ? $length : 255) . ')'
1333
            : 'VARBINARY(' . ($length > 0 ? $length : 255) . ')';
1334
    }
1335
 
1336
    /**
1337
     * {@inheritdoc}
1338
     *
1339
     * @deprecated
1340
     */
1341
    public function getBinaryMaxLength()
1342
    {
1343
        Deprecation::triggerIfCalledFromOutside(
1344
            'doctrine/dbal',
1345
            'https://github.com/doctrine/dbal/issues/3263',
1346
            'SQLServerPlatform::getBinaryMaxLength() is deprecated.',
1347
        );
1348
 
1349
        return 8000;
1350
    }
1351
 
1352
    /**
1353
     * {@inheritDoc}
1354
     */
1355
    public function getClobTypeDeclarationSQL(array $column)
1356
    {
1357
        return 'VARCHAR(MAX)';
1358
    }
1359
 
1360
    /**
1361
     * {@inheritDoc}
1362
     */
1363
    protected function _getCommonIntegerTypeDeclarationSQL(array $column)
1364
    {
1365
        return ! empty($column['autoincrement']) ? ' IDENTITY' : '';
1366
    }
1367
 
1368
    /**
1369
     * {@inheritDoc}
1370
     */
1371
    public function getDateTimeTypeDeclarationSQL(array $column)
1372
    {
1373
        // 3 - microseconds precision length
1374
        // http://msdn.microsoft.com/en-us/library/ms187819.aspx
1375
        return 'DATETIME2(6)';
1376
    }
1377
 
1378
    /**
1379
     * {@inheritDoc}
1380
     */
1381
    public function getDateTypeDeclarationSQL(array $column)
1382
    {
1383
        return 'DATE';
1384
    }
1385
 
1386
    /**
1387
     * {@inheritDoc}
1388
     */
1389
    public function getTimeTypeDeclarationSQL(array $column)
1390
    {
1391
        return 'TIME(0)';
1392
    }
1393
 
1394
    /**
1395
     * {@inheritDoc}
1396
     */
1397
    public function getBooleanTypeDeclarationSQL(array $column)
1398
    {
1399
        return 'BIT';
1400
    }
1401
 
1402
    /**
1403
     * {@inheritDoc}
1404
     */
1405
    protected function doModifyLimitQuery($query, $limit, $offset)
1406
    {
1407
        if ($limit === null && $offset <= 0) {
1408
            return $query;
1409
        }
1410
 
1411
        if ($this->shouldAddOrderBy($query)) {
1412
            if (preg_match('/^SELECT\s+DISTINCT/im', $query) > 0) {
1413
                // SQL Server won't let us order by a non-selected column in a DISTINCT query,
1414
                // so we have to do this madness. This says, order by the first column in the
1415
                // result. SQL Server's docs say that a nonordered query's result order is non-
1416
                // deterministic anyway, so this won't do anything that a bunch of update and
1417
                // deletes to the table wouldn't do anyway.
1418
                $query .= ' ORDER BY 1';
1419
            } else {
1420
                // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you
1421
                // use constant expressions in the order by list.
1422
                $query .= ' ORDER BY (SELECT 0)';
1423
            }
1424
        }
1425
 
1426
        // This looks somewhat like MYSQL, but limit/offset are in inverse positions
1427
        // Supposedly SQL:2008 core standard.
1428
        // Per TSQL spec, FETCH NEXT n ROWS ONLY is not valid without OFFSET n ROWS.
1429
        $query .= sprintf(' OFFSET %d ROWS', $offset);
1430
 
1431
        if ($limit !== null) {
1432
            $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit);
1433
        }
1434
 
1435
        return $query;
1436
    }
1437
 
1438
    /**
1439
     * {@inheritDoc}
1440
     */
1441
    public function convertBooleans($item)
1442
    {
1443
        if (is_array($item)) {
1444
            foreach ($item as $key => $value) {
1445
                if (! is_bool($value) && ! is_numeric($value)) {
1446
                    continue;
1447
                }
1448
 
1449
                $item[$key] = (int) (bool) $value;
1450
            }
1451
        } elseif (is_bool($item) || is_numeric($item)) {
1452
            $item = (int) (bool) $item;
1453
        }
1454
 
1455
        return $item;
1456
    }
1457
 
1458
    /**
1459
     * {@inheritDoc}
1460
     */
1461
    public function getCreateTemporaryTableSnippetSQL()
1462
    {
1463
        return 'CREATE TABLE';
1464
    }
1465
 
1466
    /**
1467
     * {@inheritDoc}
1468
     */
1469
    public function getTemporaryTableName($tableName)
1470
    {
1471
        return '#' . $tableName;
1472
    }
1473
 
1474
    /**
1475
     * {@inheritDoc}
1476
     */
1477
    public function getDateTimeFormatString()
1478
    {
1479
        return 'Y-m-d H:i:s.u';
1480
    }
1481
 
1482
    /**
1483
     * {@inheritDoc}
1484
     */
1485
    public function getDateFormatString()
1486
    {
1487
        return 'Y-m-d';
1488
    }
1489
 
1490
    /**
1491
     * {@inheritDoc}
1492
     */
1493
    public function getTimeFormatString()
1494
    {
1495
        return 'H:i:s';
1496
    }
1497
 
1498
    /**
1499
     * {@inheritDoc}
1500
     */
1501
    public function getDateTimeTzFormatString()
1502
    {
1503
        return 'Y-m-d H:i:s.u P';
1504
    }
1505
 
1506
    /**
1507
     * {@inheritDoc}
1508
     */
1509
    public function getName()
1510
    {
1511
        return 'mssql';
1512
    }
1513
 
1514
    /**
1515
     * {@inheritDoc}
1516
     */
1517
    protected function initializeDoctrineTypeMappings()
1518
    {
1519
        $this->doctrineTypeMapping = [
1520
            'bigint'           => 'bigint',
1521
            'binary'           => 'binary',
1522
            'bit'              => 'boolean',
1523
            'blob'             => 'blob',
1524
            'char'             => 'string',
1525
            'date'             => 'date',
1526
            'datetime'         => 'datetime',
1527
            'datetime2'        => 'datetime',
1528
            'datetimeoffset'   => 'datetimetz',
1529
            'decimal'          => 'decimal',
1530
            'double'           => 'float',
1531
            'double precision' => 'float',
1532
            'float'            => 'float',
1533
            'image'            => 'blob',
1534
            'int'              => 'integer',
1535
            'money'            => 'integer',
1536
            'nchar'            => 'string',
1537
            'ntext'            => 'text',
1538
            'numeric'          => 'decimal',
1539
            'nvarchar'         => 'string',
1540
            'real'             => 'float',
1541
            'smalldatetime'    => 'datetime',
1542
            'smallint'         => 'smallint',
1543
            'smallmoney'       => 'integer',
1544
            'text'             => 'text',
1545
            'time'             => 'time',
1546
            'tinyint'          => 'smallint',
1547
            'uniqueidentifier' => 'guid',
1548
            'varbinary'        => 'binary',
1549
            'varchar'          => 'string',
1550
        ];
1551
    }
1552
 
1553
    /**
1554
     * {@inheritDoc}
1555
     */
1556
    public function createSavePoint($savepoint)
1557
    {
1558
        return 'SAVE TRANSACTION ' . $savepoint;
1559
    }
1560
 
1561
    /**
1562
     * {@inheritDoc}
1563
     */
1564
    public function releaseSavePoint($savepoint)
1565
    {
1566
        return '';
1567
    }
1568
 
1569
    /**
1570
     * {@inheritDoc}
1571
     */
1572
    public function rollbackSavePoint($savepoint)
1573
    {
1574
        return 'ROLLBACK TRANSACTION ' . $savepoint;
1575
    }
1576
 
1577
    /**
1578
     * {@inheritdoc}
1579
     *
1580
     * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1581
     */
1582
    public function getForeignKeyReferentialActionSQL($action)
1583
    {
1584
        // RESTRICT is not supported, therefore falling back to NO ACTION.
1585
        if (strtoupper($action) === 'RESTRICT') {
1586
            return 'NO ACTION';
1587
        }
1588
 
1589
        return parent::getForeignKeyReferentialActionSQL($action);
1590
    }
1591
 
1592
    public function appendLockHint(string $fromClause, int $lockMode): string
1593
    {
1594
        switch ($lockMode) {
1595
            case LockMode::NONE:
1596
            case LockMode::OPTIMISTIC:
1597
                return $fromClause;
1598
 
1599
            case LockMode::PESSIMISTIC_READ:
1600
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
1601
 
1602
            case LockMode::PESSIMISTIC_WRITE:
1603
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
1604
 
1605
            default:
1606
                throw InvalidLockMode::fromLockMode($lockMode);
1607
        }
1608
    }
1609
 
1610
    /**
1611
     * {@inheritDoc}
1612
     */
1613
    public function getForUpdateSQL()
1614
    {
1615
        return ' ';
1616
    }
1617
 
1618
    /**
1619
     * {@inheritDoc}
1620
     *
1621
     * @deprecated Implement {@see createReservedKeywordsList()} instead.
1622
     */
1623
    protected function getReservedKeywordsClass()
1624
    {
1625
        Deprecation::triggerIfCalledFromOutside(
1626
            'doctrine/dbal',
1627
            'https://github.com/doctrine/dbal/issues/4510',
1628
            'SQLServerPlatform::getReservedKeywordsClass() is deprecated,'
1629
                . ' use SQLServerPlatform::createReservedKeywordsList() instead.',
1630
        );
1631
 
1632
        return Keywords\SQLServer2012Keywords::class;
1633
    }
1634
 
1635
    /**
1636
     * {@inheritDoc}
1637
     */
1638
    public function quoteSingleIdentifier($str)
1639
    {
1640
        return '[' . str_replace(']', ']]', $str) . ']';
1641
    }
1642
 
1643
    /**
1644
     * {@inheritDoc}
1645
     */
1646
    public function getTruncateTableSQL($tableName, $cascade = false)
1647
    {
1648
        $tableIdentifier = new Identifier($tableName);
1649
 
1650
        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
1651
    }
1652
 
1653
    /**
1654
     * {@inheritDoc}
1655
     */
1656
    public function getBlobTypeDeclarationSQL(array $column)
1657
    {
1658
        return 'VARBINARY(MAX)';
1659
    }
1660
 
1661
    /**
1662
     * {@inheritdoc}
1663
     *
1664
     * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1665
     */
1666
    public function getColumnDeclarationSQL($name, array $column)
1667
    {
1668
        if (isset($column['columnDefinition'])) {
1669
            $columnDef = $this->getCustomTypeDeclarationSQL($column);
1670
        } else {
1671
            $collation = ! empty($column['collation']) ?
1672
                ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : '';
1673
 
1674
            $notnull = ! empty($column['notnull']) ? ' NOT NULL' : '';
1675
 
1676
            if (! empty($column['unique'])) {
1677
                Deprecation::trigger(
1678
                    'doctrine/dbal',
1679
                    'https://github.com/doctrine/dbal/pull/5656',
1680
                    'The usage of the "unique" column property is deprecated. Use unique constraints instead.',
1681
                );
1682
 
1683
                $unique = ' ' . $this->getUniqueFieldDeclarationSQL();
1684
            } else {
1685
                $unique = '';
1686
            }
1687
 
1688
            if (! empty($column['check'])) {
1689
                Deprecation::trigger(
1690
                    'doctrine/dbal',
1691
                    'https://github.com/doctrine/dbal/pull/5656',
1692
                    'The usage of the "check" column property is deprecated.',
1693
                );
1694
 
1695
                $check = ' ' . $column['check'];
1696
            } else {
1697
                $check = '';
1698
            }
1699
 
1700
            $typeDecl  = $column['type']->getSQLDeclaration($column, $this);
1701
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
1702
        }
1703
 
1704
        return $name . ' ' . $columnDef;
1705
    }
1706
 
1707
    /**
1708
     * {@inheritDoc}
1709
     *
1710
     * SQL Server does not support quoting collation identifiers.
1711
     */
1712
    public function getColumnCollationDeclarationSQL($collation)
1713
    {
1714
        return 'COLLATE ' . $collation;
1715
    }
1716
 
1717
    public function columnsEqual(Column $column1, Column $column2): bool
1718
    {
1719
        if (! parent::columnsEqual($column1, $column2)) {
1720
            return false;
1721
        }
1722
 
1723
        return $this->getDefaultValueDeclarationSQL($column1->toArray())
1724
            === $this->getDefaultValueDeclarationSQL($column2->toArray());
1725
    }
1726
 
1727
    protected function getLikeWildcardCharacters(): string
1728
    {
1729
        return parent::getLikeWildcardCharacters() . '[]^';
1730
    }
1731
 
1732
    /**
1733
     * Returns a unique default constraint name for a table and column.
1734
     *
1735
     * @param string $table  Name of the table to generate the unique default constraint name for.
1736
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
1737
     */
1738
    private function generateDefaultConstraintName($table, $column): string
1739
    {
1740
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
1741
    }
1742
 
1743
    /**
1744
     * Returns a hash value for a given identifier.
1745
     *
1746
     * @param string $identifier Identifier to generate a hash value for.
1747
     */
1748
    private function generateIdentifierName($identifier): string
1749
    {
1750
        // Always generate name for unquoted identifiers to ensure consistency.
1751
        $identifier = new Identifier($identifier);
1752
 
1753
        return strtoupper(dechex(crc32($identifier->getName())));
1754
    }
1755
 
1756
    protected function getCommentOnTableSQL(string $tableName, ?string $comment): string
1757
    {
1758
        return sprintf(
1759
            <<<'SQL'
1760
                EXEC sys.sp_addextendedproperty @name=N'MS_Description',
1761
                  @value=N%s, @level0type=N'SCHEMA', @level0name=N'dbo',
1762
                  @level1type=N'TABLE', @level1name=N%s
1763
                SQL
1764
            ,
1765
            $this->quoteStringLiteral((string) $comment),
1766
            $this->quoteStringLiteral($tableName),
1767
        );
1768
    }
1769
 
1770
    /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */
1771
    public function getListTableMetadataSQL(string $table): string
1772
    {
1773
        return sprintf(
1774
            <<<'SQL'
1775
                SELECT
1776
                  p.value AS [table_comment]
1777
                FROM
1778
                  sys.tables AS tbl
1779
                  INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1
1780
                WHERE
1781
                  (tbl.name=N%s and SCHEMA_NAME(tbl.schema_id)=N'dbo' and p.name=N'MS_Description')
1782
                SQL
1783
            ,
1784
            $this->quoteStringLiteral($table),
1785
        );
1786
    }
1787
 
1788
    /** @param string $query */
1789
    private function shouldAddOrderBy($query): bool
1790
    {
1791
        // Find the position of the last instance of ORDER BY and ensure it is not within a parenthetical statement
1792
        // but can be in a newline
1793
        $matches      = [];
1794
        $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE);
1795
        if ($matchesCount === 0) {
1796
            return true;
1797
        }
1798
 
1799
        // ORDER BY instance may be in a subquery after ORDER BY
1800
        // e.g. SELECT col1 FROM test ORDER BY (SELECT col2 from test ORDER BY col2)
1801
        // if in the searched query ORDER BY clause was found where
1802
        // number of open parentheses after the occurrence of the clause is equal to
1803
        // number of closed brackets after the occurrence of the clause,
1804
        // it means that ORDER BY is included in the query being checked
1805
        while ($matchesCount > 0) {
1806
            $orderByPos          = $matches[0][--$matchesCount][1];
1807
            $openBracketsCount   = substr_count($query, '(', $orderByPos);
1808
            $closedBracketsCount = substr_count($query, ')', $orderByPos);
1809
            if ($openBracketsCount === $closedBracketsCount) {
1810
                return false;
1811
            }
1812
        }
1813
 
1814
        return true;
1815
    }
1816
 
1817
    public function createSchemaManager(Connection $connection): SQLServerSchemaManager
1818
    {
1819
        return new SQLServerSchemaManager($connection, $this);
1820
    }
1821
}