diff options
Diffstat (limited to 'vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php')
-rw-r--r-- | vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php | 1223 |
1 files changed, 1223 insertions, 0 deletions
diff --git a/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php b/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php new file mode 100644 index 0000000..7a10a32 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php | |||
@@ -0,0 +1,1223 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\DBAL\Platforms; | ||
6 | |||
7 | use Doctrine\DBAL\Connection; | ||
8 | use Doctrine\DBAL\Exception\InvalidColumnType\ColumnLengthRequired; | ||
9 | use Doctrine\DBAL\LockMode; | ||
10 | use Doctrine\DBAL\Platforms\Keywords\KeywordList; | ||
11 | use Doctrine\DBAL\Platforms\Keywords\SQLServerKeywords; | ||
12 | use Doctrine\DBAL\Platforms\SQLServer\SQL\Builder\SQLServerSelectSQLBuilder; | ||
13 | use Doctrine\DBAL\Schema\Column; | ||
14 | use Doctrine\DBAL\Schema\ColumnDiff; | ||
15 | use Doctrine\DBAL\Schema\Identifier; | ||
16 | use Doctrine\DBAL\Schema\Index; | ||
17 | use Doctrine\DBAL\Schema\Sequence; | ||
18 | use Doctrine\DBAL\Schema\SQLServerSchemaManager; | ||
19 | use Doctrine\DBAL\Schema\TableDiff; | ||
20 | use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder; | ||
21 | use Doctrine\DBAL\TransactionIsolationLevel; | ||
22 | use Doctrine\DBAL\Types\Types; | ||
23 | use InvalidArgumentException; | ||
24 | |||
25 | use function array_merge; | ||
26 | use function array_unique; | ||
27 | use function array_values; | ||
28 | use function explode; | ||
29 | use function implode; | ||
30 | use function is_array; | ||
31 | use function is_bool; | ||
32 | use function is_numeric; | ||
33 | use function preg_match; | ||
34 | use function preg_match_all; | ||
35 | use function sprintf; | ||
36 | use function str_contains; | ||
37 | use function str_ends_with; | ||
38 | use function str_replace; | ||
39 | use function str_starts_with; | ||
40 | use function strtoupper; | ||
41 | use function substr; | ||
42 | use function substr_count; | ||
43 | |||
44 | use const PREG_OFFSET_CAPTURE; | ||
45 | |||
46 | /** | ||
47 | * Provides the behavior, features and SQL dialect of the Microsoft SQL Server database platform | ||
48 | * of the oldest supported version. | ||
49 | */ | ||
50 | class SQLServerPlatform extends AbstractPlatform | ||
51 | { | ||
52 | /** @internal Should be used only from within the {@see AbstractSchemaManager} class hierarchy. */ | ||
53 | public const OPTION_DEFAULT_CONSTRAINT_NAME = 'default_constraint_name'; | ||
54 | |||
55 | public function createSelectSQLBuilder(): SelectSQLBuilder | ||
56 | { | ||
57 | return new SQLServerSelectSQLBuilder($this); | ||
58 | } | ||
59 | |||
60 | public function getCurrentDateSQL(): string | ||
61 | { | ||
62 | return $this->getConvertExpression('date', 'GETDATE()'); | ||
63 | } | ||
64 | |||
65 | public function getCurrentTimeSQL(): string | ||
66 | { | ||
67 | return $this->getConvertExpression('time', 'GETDATE()'); | ||
68 | } | ||
69 | |||
70 | /** | ||
71 | * Returns an expression that converts an expression of one data type to another. | ||
72 | * | ||
73 | * @param string $dataType The target native data type. Alias data types cannot be used. | ||
74 | * @param string $expression The SQL expression to convert. | ||
75 | */ | ||
76 | private function getConvertExpression(string $dataType, string $expression): string | ||
77 | { | ||
78 | return sprintf('CONVERT(%s, %s)', $dataType, $expression); | ||
79 | } | ||
80 | |||
81 | protected function getDateArithmeticIntervalExpression( | ||
82 | string $date, | ||
83 | string $operator, | ||
84 | string $interval, | ||
85 | DateIntervalUnit $unit, | ||
86 | ): string { | ||
87 | $factorClause = ''; | ||
88 | |||
89 | if ($operator === '-') { | ||
90 | $factorClause = '-1 * '; | ||
91 | } | ||
92 | |||
93 | return 'DATEADD(' . $unit->value . ', ' . $factorClause . $interval . ', ' . $date . ')'; | ||
94 | } | ||
95 | |||
96 | public function getDateDiffExpression(string $date1, string $date2): string | ||
97 | { | ||
98 | return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')'; | ||
99 | } | ||
100 | |||
101 | /** | ||
102 | * {@inheritDoc} | ||
103 | * | ||
104 | * Microsoft SQL Server supports this through AUTO_INCREMENT columns. | ||
105 | */ | ||
106 | public function supportsIdentityColumns(): bool | ||
107 | { | ||
108 | return true; | ||
109 | } | ||
110 | |||
111 | public function supportsReleaseSavepoints(): bool | ||
112 | { | ||
113 | return false; | ||
114 | } | ||
115 | |||
116 | public function supportsSchemas(): bool | ||
117 | { | ||
118 | return true; | ||
119 | } | ||
120 | |||
121 | /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ | ||
122 | public function supportsColumnCollation(): bool | ||
123 | { | ||
124 | return true; | ||
125 | } | ||
126 | |||
127 | public function supportsSequences(): bool | ||
128 | { | ||
129 | return true; | ||
130 | } | ||
131 | |||
132 | public function getAlterSequenceSQL(Sequence $sequence): string | ||
133 | { | ||
134 | return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . | ||
135 | ' INCREMENT BY ' . $sequence->getAllocationSize(); | ||
136 | } | ||
137 | |||
138 | public function getCreateSequenceSQL(Sequence $sequence): string | ||
139 | { | ||
140 | return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . | ||
141 | ' START WITH ' . $sequence->getInitialValue() . | ||
142 | ' INCREMENT BY ' . $sequence->getAllocationSize() . | ||
143 | ' MINVALUE ' . $sequence->getInitialValue(); | ||
144 | } | ||
145 | |||
146 | /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ | ||
147 | public function getListSequencesSQL(string $database): string | ||
148 | { | ||
149 | return 'SELECT seq.name, | ||
150 | CAST( | ||
151 | seq.increment AS VARCHAR(MAX) | ||
152 | ) AS increment, -- CAST avoids driver error for sql_variant type | ||
153 | CAST( | ||
154 | seq.start_value AS VARCHAR(MAX) | ||
155 | ) AS start_value -- CAST avoids driver error for sql_variant type | ||
156 | FROM sys.sequences AS seq'; | ||
157 | } | ||
158 | |||
159 | public function getSequenceNextValSQL(string $sequence): string | ||
160 | { | ||
161 | return 'SELECT NEXT VALUE FOR ' . $sequence; | ||
162 | } | ||
163 | |||
164 | public function getDropForeignKeySQL(string $foreignKey, string $table): string | ||
165 | { | ||
166 | return $this->getDropConstraintSQL($foreignKey, $table); | ||
167 | } | ||
168 | |||
169 | public function getDropIndexSQL(string $name, string $table): string | ||
170 | { | ||
171 | return 'DROP INDEX ' . $name . ' ON ' . $table; | ||
172 | } | ||
173 | |||
174 | /** | ||
175 | * {@inheritDoc} | ||
176 | */ | ||
177 | protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array | ||
178 | { | ||
179 | $defaultConstraintsSql = []; | ||
180 | $commentsSql = []; | ||
181 | |||
182 | $tableComment = $options['comment'] ?? null; | ||
183 | if ($tableComment !== null) { | ||
184 | $commentsSql[] = $this->getCommentOnTableSQL($name, $tableComment); | ||
185 | } | ||
186 | |||
187 | // @todo does other code breaks because of this? | ||
188 | // force primary keys to be not null | ||
189 | foreach ($columns as &$column) { | ||
190 | if (! empty($column['primary'])) { | ||
191 | $column['notnull'] = true; | ||
192 | } | ||
193 | |||
194 | // Build default constraints SQL statements. | ||
195 | if (isset($column['default'])) { | ||
196 | $defaultConstraintsSql[] = 'ALTER TABLE ' . $name . | ||
197 | ' ADD' . $this->getDefaultConstraintDeclarationSQL($column); | ||
198 | } | ||
199 | |||
200 | if (empty($column['comment']) && ! is_numeric($column['comment'])) { | ||
201 | continue; | ||
202 | } | ||
203 | |||
204 | $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']); | ||
205 | } | ||
206 | |||
207 | $columnListSql = $this->getColumnDeclarationListSQL($columns); | ||
208 | |||
209 | if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { | ||
210 | foreach ($options['uniqueConstraints'] as $definition) { | ||
211 | $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition); | ||
212 | } | ||
213 | } | ||
214 | |||
215 | if (isset($options['primary']) && ! empty($options['primary'])) { | ||
216 | $flags = ''; | ||
217 | if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) { | ||
218 | $flags = ' NONCLUSTERED'; | ||
219 | } | ||
220 | |||
221 | $columnListSql .= ', PRIMARY KEY' . $flags | ||
222 | . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')'; | ||
223 | } | ||
224 | |||
225 | $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql; | ||
226 | |||
227 | $check = $this->getCheckDeclarationSQL($columns); | ||
228 | if (! empty($check)) { | ||
229 | $query .= ', ' . $check; | ||
230 | } | ||
231 | |||
232 | $query .= ')'; | ||
233 | |||
234 | $sql = [$query]; | ||
235 | |||
236 | if (isset($options['indexes']) && ! empty($options['indexes'])) { | ||
237 | foreach ($options['indexes'] as $index) { | ||
238 | $sql[] = $this->getCreateIndexSQL($index, $name); | ||
239 | } | ||
240 | } | ||
241 | |||
242 | if (isset($options['foreignKeys'])) { | ||
243 | foreach ($options['foreignKeys'] as $definition) { | ||
244 | $sql[] = $this->getCreateForeignKeySQL($definition, $name); | ||
245 | } | ||
246 | } | ||
247 | |||
248 | return array_merge($sql, $commentsSql, $defaultConstraintsSql); | ||
249 | } | ||
250 | |||
251 | public function getCreatePrimaryKeySQL(Index $index, string $table): string | ||
252 | { | ||
253 | $sql = 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY'; | ||
254 | |||
255 | if ($index->hasFlag('nonclustered')) { | ||
256 | $sql .= ' NONCLUSTERED'; | ||
257 | } | ||
258 | |||
259 | return $sql . ' (' . implode(', ', $index->getQuotedColumns($this)) . ')'; | ||
260 | } | ||
261 | |||
262 | private function unquoteSingleIdentifier(string $possiblyQuotedName): string | ||
263 | { | ||
264 | return str_starts_with($possiblyQuotedName, '[') && str_ends_with($possiblyQuotedName, ']') | ||
265 | ? substr($possiblyQuotedName, 1, -1) | ||
266 | : $possiblyQuotedName; | ||
267 | } | ||
268 | |||
269 | /** | ||
270 | * Returns the SQL statement for creating a column comment. | ||
271 | * | ||
272 | * SQL Server does not support native column comments, | ||
273 | * therefore the extended properties functionality is used | ||
274 | * as a workaround to store them. | ||
275 | * The property name used to store column comments is "MS_Description" | ||
276 | * which provides compatibility with SQL Server Management Studio, | ||
277 | * as column comments are stored in the same property there when | ||
278 | * specifying a column's "Description" attribute. | ||
279 | * | ||
280 | * @param string $tableName The quoted table name to which the column belongs. | ||
281 | * @param string $columnName The quoted column name to create the comment for. | ||
282 | * @param string $comment The column's comment. | ||
283 | */ | ||
284 | protected function getCreateColumnCommentSQL(string $tableName, string $columnName, string $comment): string | ||
285 | { | ||
286 | if (str_contains($tableName, '.')) { | ||
287 | [$schemaName, $tableName] = explode('.', $tableName); | ||
288 | } else { | ||
289 | $schemaName = 'dbo'; | ||
290 | } | ||
291 | |||
292 | return $this->getAddExtendedPropertySQL( | ||
293 | 'MS_Description', | ||
294 | $comment, | ||
295 | 'SCHEMA', | ||
296 | $this->quoteStringLiteral($this->unquoteSingleIdentifier($schemaName)), | ||
297 | 'TABLE', | ||
298 | $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), | ||
299 | 'COLUMN', | ||
300 | $this->quoteStringLiteral($this->unquoteSingleIdentifier($columnName)), | ||
301 | ); | ||
302 | } | ||
303 | |||
304 | /** | ||
305 | * Returns the SQL snippet for declaring a default constraint. | ||
306 | * | ||
307 | * @param mixed[] $column Column definition. | ||
308 | */ | ||
309 | protected function getDefaultConstraintDeclarationSQL(array $column): string | ||
310 | { | ||
311 | if (! isset($column['default'])) { | ||
312 | throw new InvalidArgumentException('Incomplete column definition. "default" required.'); | ||
313 | } | ||
314 | |||
315 | $columnName = new Identifier($column['name']); | ||
316 | |||
317 | return $this->getDefaultValueDeclarationSQL($column) . ' FOR ' . $columnName->getQuotedName($this); | ||
318 | } | ||
319 | |||
320 | public function getCreateIndexSQL(Index $index, string $table): string | ||
321 | { | ||
322 | $constraint = parent::getCreateIndexSQL($index, $table); | ||
323 | |||
324 | if ($index->isUnique() && ! $index->isPrimary()) { | ||
325 | $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index); | ||
326 | } | ||
327 | |||
328 | return $constraint; | ||
329 | } | ||
330 | |||
331 | protected function getCreateIndexSQLFlags(Index $index): string | ||
332 | { | ||
333 | $type = ''; | ||
334 | if ($index->isUnique()) { | ||
335 | $type .= 'UNIQUE '; | ||
336 | } | ||
337 | |||
338 | if ($index->hasFlag('clustered')) { | ||
339 | $type .= 'CLUSTERED '; | ||
340 | } elseif ($index->hasFlag('nonclustered')) { | ||
341 | $type .= 'NONCLUSTERED '; | ||
342 | } | ||
343 | |||
344 | return $type; | ||
345 | } | ||
346 | |||
347 | /** | ||
348 | * Extend unique key constraint with required filters | ||
349 | */ | ||
350 | private function _appendUniqueConstraintDefinition(string $sql, Index $index): string | ||
351 | { | ||
352 | $fields = []; | ||
353 | |||
354 | foreach ($index->getQuotedColumns($this) as $field) { | ||
355 | $fields[] = $field . ' IS NOT NULL'; | ||
356 | } | ||
357 | |||
358 | return $sql . ' WHERE ' . implode(' AND ', $fields); | ||
359 | } | ||
360 | |||
361 | /** | ||
362 | * {@inheritDoc} | ||
363 | */ | ||
364 | public function getAlterTableSQL(TableDiff $diff): array | ||
365 | { | ||
366 | $queryParts = []; | ||
367 | $sql = []; | ||
368 | $columnSql = []; | ||
369 | $commentsSql = []; | ||
370 | |||
371 | $table = $diff->getOldTable(); | ||
372 | |||
373 | $tableName = $table->getName(); | ||
374 | |||
375 | foreach ($diff->getAddedColumns() as $column) { | ||
376 | $columnProperties = $column->toArray(); | ||
377 | |||
378 | $addColumnSql = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnProperties); | ||
379 | |||
380 | if (isset($columnProperties['default'])) { | ||
381 | $addColumnSql .= $this->getDefaultValueDeclarationSQL($columnProperties); | ||
382 | } | ||
383 | |||
384 | $queryParts[] = $addColumnSql; | ||
385 | |||
386 | $comment = $column->getComment(); | ||
387 | |||
388 | if ($comment === '') { | ||
389 | continue; | ||
390 | } | ||
391 | |||
392 | $commentsSql[] = $this->getCreateColumnCommentSQL( | ||
393 | $tableName, | ||
394 | $column->getQuotedName($this), | ||
395 | $comment, | ||
396 | ); | ||
397 | } | ||
398 | |||
399 | foreach ($diff->getDroppedColumns() as $column) { | ||
400 | if ($column->getDefault() !== null) { | ||
401 | $queryParts[] = $this->getAlterTableDropDefaultConstraintClause($column); | ||
402 | } | ||
403 | |||
404 | $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); | ||
405 | } | ||
406 | |||
407 | foreach ($diff->getModifiedColumns() as $columnDiff) { | ||
408 | $newColumn = $columnDiff->getNewColumn(); | ||
409 | $newComment = $newColumn->getComment(); | ||
410 | $hasNewComment = $newComment !== ''; | ||
411 | |||
412 | $oldColumn = $columnDiff->getOldColumn(); | ||
413 | $oldComment = $oldColumn->getComment(); | ||
414 | $hasOldComment = $oldComment !== ''; | ||
415 | |||
416 | if ($hasOldComment && $hasNewComment && $newComment !== $oldComment) { | ||
417 | $commentsSql[] = $this->getAlterColumnCommentSQL( | ||
418 | $tableName, | ||
419 | $newColumn->getQuotedName($this), | ||
420 | $newComment, | ||
421 | ); | ||
422 | } elseif ($hasOldComment && ! $hasNewComment) { | ||
423 | $commentsSql[] = $this->getDropColumnCommentSQL( | ||
424 | $tableName, | ||
425 | $newColumn->getQuotedName($this), | ||
426 | ); | ||
427 | } elseif (! $hasOldComment && $hasNewComment) { | ||
428 | $commentsSql[] = $this->getCreateColumnCommentSQL( | ||
429 | $tableName, | ||
430 | $newColumn->getQuotedName($this), | ||
431 | $newComment, | ||
432 | ); | ||
433 | } | ||
434 | |||
435 | $columnNameSQL = $newColumn->getQuotedName($this); | ||
436 | |||
437 | $oldDeclarationSQL = $this->getColumnDeclarationSQL($columnNameSQL, $oldColumn->toArray()); | ||
438 | $newDeclarationSQL = $this->getColumnDeclarationSQL($columnNameSQL, $newColumn->toArray()); | ||
439 | |||
440 | $declarationSQLChanged = $newDeclarationSQL !== $oldDeclarationSQL; | ||
441 | $defaultChanged = $columnDiff->hasDefaultChanged(); | ||
442 | |||
443 | if (! $declarationSQLChanged && ! $defaultChanged) { | ||
444 | continue; | ||
445 | } | ||
446 | |||
447 | $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff); | ||
448 | |||
449 | if ($requireDropDefaultConstraint) { | ||
450 | $queryParts[] = $this->getAlterTableDropDefaultConstraintClause($oldColumn); | ||
451 | } | ||
452 | |||
453 | if ($declarationSQLChanged) { | ||
454 | $queryParts[] = 'ALTER COLUMN ' . $newDeclarationSQL; | ||
455 | } | ||
456 | |||
457 | if ( | ||
458 | $newColumn->getDefault() === null | ||
459 | || (! $requireDropDefaultConstraint && ! $defaultChanged) | ||
460 | ) { | ||
461 | continue; | ||
462 | } | ||
463 | |||
464 | $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($tableName, $newColumn); | ||
465 | } | ||
466 | |||
467 | $tableNameSQL = $table->getQuotedName($this); | ||
468 | |||
469 | foreach ($diff->getRenamedColumns() as $oldColumnName => $newColumn) { | ||
470 | $oldColumnName = new Identifier($oldColumnName); | ||
471 | |||
472 | $sql[] = sprintf( | ||
473 | "sp_rename '%s.%s', '%s', 'COLUMN'", | ||
474 | $tableNameSQL, | ||
475 | $oldColumnName->getQuotedName($this), | ||
476 | $newColumn->getQuotedName($this), | ||
477 | ); | ||
478 | } | ||
479 | |||
480 | foreach ($queryParts as $query) { | ||
481 | $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; | ||
482 | } | ||
483 | |||
484 | return array_merge( | ||
485 | $this->getPreAlterTableIndexForeignKeySQL($diff), | ||
486 | $sql, | ||
487 | $commentsSql, | ||
488 | $this->getPostAlterTableIndexForeignKeySQL($diff), | ||
489 | $columnSql, | ||
490 | ); | ||
491 | } | ||
492 | |||
493 | public function getRenameTableSQL(string $oldName, string $newName): string | ||
494 | { | ||
495 | return sprintf( | ||
496 | 'sp_rename %s, %s', | ||
497 | $this->quoteStringLiteral($oldName), | ||
498 | $this->quoteStringLiteral($newName), | ||
499 | ); | ||
500 | } | ||
501 | |||
502 | /** | ||
503 | * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement. | ||
504 | * | ||
505 | * @param string $tableName The name of the table to generate the clause for. | ||
506 | * @param Column $column The column to generate the clause for. | ||
507 | */ | ||
508 | private function getAlterTableAddDefaultConstraintClause(string $tableName, Column $column): string | ||
509 | { | ||
510 | $columnDef = $column->toArray(); | ||
511 | $columnDef['name'] = $column->getQuotedName($this); | ||
512 | |||
513 | return 'ADD' . $this->getDefaultConstraintDeclarationSQL($columnDef); | ||
514 | } | ||
515 | |||
516 | /** | ||
517 | * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement. | ||
518 | */ | ||
519 | private function getAlterTableDropDefaultConstraintClause(Column $column): string | ||
520 | { | ||
521 | if (! $column->hasPlatformOption(self::OPTION_DEFAULT_CONSTRAINT_NAME)) { | ||
522 | throw new InvalidArgumentException( | ||
523 | 'Column ' . $column->getName() . ' was not properly introspected as it has a default value' | ||
524 | . ' but does not have the default constraint name.', | ||
525 | ); | ||
526 | } | ||
527 | |||
528 | return 'DROP CONSTRAINT ' . $this->quoteIdentifier( | ||
529 | $column->getPlatformOption(self::OPTION_DEFAULT_CONSTRAINT_NAME), | ||
530 | ); | ||
531 | } | ||
532 | |||
533 | /** | ||
534 | * Checks whether a column alteration requires dropping its default constraint first. | ||
535 | * | ||
536 | * Different to other database vendors SQL Server implements column default values | ||
537 | * as constraints and therefore changes in a column's default value as well as changes | ||
538 | * in a column's type require dropping the default constraint first before being to | ||
539 | * alter the particular column to the new definition. | ||
540 | */ | ||
541 | private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff): bool | ||
542 | { | ||
543 | // We only need to drop an existing default constraint if we know the | ||
544 | // column was defined with a default value before. | ||
545 | if ($columnDiff->getOldColumn()->getDefault() === null) { | ||
546 | return false; | ||
547 | } | ||
548 | |||
549 | // We need to drop an existing default constraint if the column was | ||
550 | // defined with a default value before and it has changed. | ||
551 | if ($columnDiff->hasDefaultChanged()) { | ||
552 | return true; | ||
553 | } | ||
554 | |||
555 | // We need to drop an existing default constraint if the column was | ||
556 | // defined with a default value before and the native column type has changed. | ||
557 | return $columnDiff->hasTypeChanged() || $columnDiff->hasFixedChanged(); | ||
558 | } | ||
559 | |||
560 | /** | ||
561 | * Returns the SQL statement for altering a column comment. | ||
562 | * | ||
563 | * SQL Server does not support native column comments, | ||
564 | * therefore the extended properties functionality is used | ||
565 | * as a workaround to store them. | ||
566 | * The property name used to store column comments is "MS_Description" | ||
567 | * which provides compatibility with SQL Server Management Studio, | ||
568 | * as column comments are stored in the same property there when | ||
569 | * specifying a column's "Description" attribute. | ||
570 | * | ||
571 | * @param string $tableName The quoted table name to which the column belongs. | ||
572 | * @param string $columnName The quoted column name to alter the comment for. | ||
573 | * @param string $comment The column's comment. | ||
574 | */ | ||
575 | protected function getAlterColumnCommentSQL(string $tableName, string $columnName, string $comment): string | ||
576 | { | ||
577 | if (str_contains($tableName, '.')) { | ||
578 | [$schemaName, $tableName] = explode('.', $tableName); | ||
579 | } else { | ||
580 | $schemaName = 'dbo'; | ||
581 | } | ||
582 | |||
583 | return $this->getUpdateExtendedPropertySQL( | ||
584 | 'MS_Description', | ||
585 | $comment, | ||
586 | 'SCHEMA', | ||
587 | $this->quoteStringLiteral($this->unquoteSingleIdentifier($schemaName)), | ||
588 | 'TABLE', | ||
589 | $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), | ||
590 | 'COLUMN', | ||
591 | $this->quoteStringLiteral($this->unquoteSingleIdentifier($columnName)), | ||
592 | ); | ||
593 | } | ||
594 | |||
595 | /** | ||
596 | * Returns the SQL statement for dropping a column comment. | ||
597 | * | ||
598 | * SQL Server does not support native column comments, | ||
599 | * therefore the extended properties functionality is used | ||
600 | * as a workaround to store them. | ||
601 | * The property name used to store column comments is "MS_Description" | ||
602 | * which provides compatibility with SQL Server Management Studio, | ||
603 | * as column comments are stored in the same property there when | ||
604 | * specifying a column's "Description" attribute. | ||
605 | * | ||
606 | * @param string $tableName The quoted table name to which the column belongs. | ||
607 | * @param string $columnName The quoted column name to drop the comment for. | ||
608 | */ | ||
609 | protected function getDropColumnCommentSQL(string $tableName, string $columnName): string | ||
610 | { | ||
611 | if (str_contains($tableName, '.')) { | ||
612 | [$schemaName, $tableName] = explode('.', $tableName); | ||
613 | } else { | ||
614 | $schemaName = 'dbo'; | ||
615 | } | ||
616 | |||
617 | return $this->getDropExtendedPropertySQL( | ||
618 | 'MS_Description', | ||
619 | 'SCHEMA', | ||
620 | $this->quoteStringLiteral($this->unquoteSingleIdentifier($schemaName)), | ||
621 | 'TABLE', | ||
622 | $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), | ||
623 | 'COLUMN', | ||
624 | $this->quoteStringLiteral($this->unquoteSingleIdentifier($columnName)), | ||
625 | ); | ||
626 | } | ||
627 | |||
628 | /** | ||
629 | * {@inheritDoc} | ||
630 | */ | ||
631 | protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array | ||
632 | { | ||
633 | return [sprintf( | ||
634 | "EXEC sp_rename N'%s.%s', N'%s', N'INDEX'", | ||
635 | $tableName, | ||
636 | $oldIndexName, | ||
637 | $index->getQuotedName($this), | ||
638 | ), | ||
639 | ]; | ||
640 | } | ||
641 | |||
642 | /** | ||
643 | * Returns the SQL statement for adding an extended property to a database object. | ||
644 | * | ||
645 | * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx | ||
646 | * | ||
647 | * @param string $name The name of the property to add. | ||
648 | * @param string|null $value The value of the property to add. | ||
649 | * @param string|null $level0Type The type of the object at level 0 the property belongs to. | ||
650 | * @param string|null $level0Name The name of the object at level 0 the property belongs to. | ||
651 | * @param string|null $level1Type The type of the object at level 1 the property belongs to. | ||
652 | * @param string|null $level1Name The name of the object at level 1 the property belongs to. | ||
653 | * @param string|null $level2Type The type of the object at level 2 the property belongs to. | ||
654 | * @param string|null $level2Name The name of the object at level 2 the property belongs to. | ||
655 | */ | ||
656 | protected function getAddExtendedPropertySQL( | ||
657 | string $name, | ||
658 | ?string $value = null, | ||
659 | ?string $level0Type = null, | ||
660 | ?string $level0Name = null, | ||
661 | ?string $level1Type = null, | ||
662 | ?string $level1Name = null, | ||
663 | ?string $level2Type = null, | ||
664 | ?string $level2Name = null, | ||
665 | ): string { | ||
666 | return 'EXEC sp_addextendedproperty ' . | ||
667 | 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral($value ?? '') . ', ' . | ||
668 | 'N' . $this->quoteStringLiteral($level0Type ?? '') . ', ' . $level0Name . ', ' . | ||
669 | 'N' . $this->quoteStringLiteral($level1Type ?? '') . ', ' . $level1Name . | ||
670 | ($level2Type !== null || $level2Name !== null | ||
671 | ? ', N' . $this->quoteStringLiteral($level2Type ?? '') . ', ' . $level2Name | ||
672 | : '' | ||
673 | ); | ||
674 | } | ||
675 | |||
676 | /** | ||
677 | * Returns the SQL statement for dropping an extended property from a database object. | ||
678 | * | ||
679 | * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx | ||
680 | * | ||
681 | * @param string $name The name of the property to drop. | ||
682 | * @param string|null $level0Type The type of the object at level 0 the property belongs to. | ||
683 | * @param string|null $level0Name The name of the object at level 0 the property belongs to. | ||
684 | * @param string|null $level1Type The type of the object at level 1 the property belongs to. | ||
685 | * @param string|null $level1Name The name of the object at level 1 the property belongs to. | ||
686 | * @param string|null $level2Type The type of the object at level 2 the property belongs to. | ||
687 | * @param string|null $level2Name The name of the object at level 2 the property belongs to. | ||
688 | */ | ||
689 | protected function getDropExtendedPropertySQL( | ||
690 | string $name, | ||
691 | ?string $level0Type = null, | ||
692 | ?string $level0Name = null, | ||
693 | ?string $level1Type = null, | ||
694 | ?string $level1Name = null, | ||
695 | ?string $level2Type = null, | ||
696 | ?string $level2Name = null, | ||
697 | ): string { | ||
698 | return 'EXEC sp_dropextendedproperty ' . | ||
699 | 'N' . $this->quoteStringLiteral($name) . ', ' . | ||
700 | 'N' . $this->quoteStringLiteral($level0Type ?? '') . ', ' . $level0Name . ', ' . | ||
701 | 'N' . $this->quoteStringLiteral($level1Type ?? '') . ', ' . $level1Name . | ||
702 | ($level2Type !== null || $level2Name !== null | ||
703 | ? ', N' . $this->quoteStringLiteral($level2Type ?? '') . ', ' . $level2Name | ||
704 | : '' | ||
705 | ); | ||
706 | } | ||
707 | |||
708 | /** | ||
709 | * Returns the SQL statement for updating an extended property of a database object. | ||
710 | * | ||
711 | * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx | ||
712 | * | ||
713 | * @param string $name The name of the property to update. | ||
714 | * @param string|null $value The value of the property to update. | ||
715 | * @param string|null $level0Type The type of the object at level 0 the property belongs to. | ||
716 | * @param string|null $level0Name The name of the object at level 0 the property belongs to. | ||
717 | * @param string|null $level1Type The type of the object at level 1 the property belongs to. | ||
718 | * @param string|null $level1Name The name of the object at level 1 the property belongs to. | ||
719 | * @param string|null $level2Type The type of the object at level 2 the property belongs to. | ||
720 | * @param string|null $level2Name The name of the object at level 2 the property belongs to. | ||
721 | */ | ||
722 | protected function getUpdateExtendedPropertySQL( | ||
723 | string $name, | ||
724 | ?string $value = null, | ||
725 | ?string $level0Type = null, | ||
726 | ?string $level0Name = null, | ||
727 | ?string $level1Type = null, | ||
728 | ?string $level1Name = null, | ||
729 | ?string $level2Type = null, | ||
730 | ?string $level2Name = null, | ||
731 | ): string { | ||
732 | return 'EXEC sp_updateextendedproperty ' . | ||
733 | 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral($value ?? '') . ', ' . | ||
734 | 'N' . $this->quoteStringLiteral($level0Type ?? '') . ', ' . $level0Name . ', ' . | ||
735 | 'N' . $this->quoteStringLiteral($level1Type ?? '') . ', ' . $level1Name . | ||
736 | ($level2Type !== null || $level2Name !== null | ||
737 | ? ', N' . $this->quoteStringLiteral($level2Type ?? '') . ', ' . $level2Name | ||
738 | : '' | ||
739 | ); | ||
740 | } | ||
741 | |||
742 | public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string | ||
743 | { | ||
744 | return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES'; | ||
745 | } | ||
746 | |||
747 | /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ | ||
748 | public function getListViewsSQL(string $database): string | ||
749 | { | ||
750 | return "SELECT name, definition FROM sysobjects | ||
751 | INNER JOIN sys.sql_modules ON sysobjects.id = sys.sql_modules.object_id | ||
752 | WHERE type = 'V' ORDER BY name"; | ||
753 | } | ||
754 | |||
755 | public function getLocateExpression(string $string, string $substring, ?string $start = null): string | ||
756 | { | ||
757 | if ($start === null) { | ||
758 | return sprintf('CHARINDEX(%s, %s)', $substring, $string); | ||
759 | } | ||
760 | |||
761 | return sprintf('CHARINDEX(%s, %s, %s)', $substring, $string, $start); | ||
762 | } | ||
763 | |||
764 | public function getModExpression(string $dividend, string $divisor): string | ||
765 | { | ||
766 | return $dividend . ' % ' . $divisor; | ||
767 | } | ||
768 | |||
769 | public function getTrimExpression( | ||
770 | string $str, | ||
771 | TrimMode $mode = TrimMode::UNSPECIFIED, | ||
772 | ?string $char = null, | ||
773 | ): string { | ||
774 | if ($char === null) { | ||
775 | return match ($mode) { | ||
776 | TrimMode::LEADING => 'LTRIM(' . $str . ')', | ||
777 | TrimMode::TRAILING => 'RTRIM(' . $str . ')', | ||
778 | default => 'LTRIM(RTRIM(' . $str . '))', | ||
779 | }; | ||
780 | } | ||
781 | |||
782 | $pattern = "'%[^' + " . $char . " + ']%'"; | ||
783 | |||
784 | if ($mode === TrimMode::LEADING) { | ||
785 | return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)'; | ||
786 | } | ||
787 | |||
788 | if ($mode === TrimMode::TRAILING) { | ||
789 | return 'reverse(stuff(reverse(' . $str . '), 1, ' | ||
790 | . 'patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))'; | ||
791 | } | ||
792 | |||
793 | return 'reverse(stuff(reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)), 1, ' | ||
794 | . 'patindex(' . $pattern . ', reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str | ||
795 | . ') - 1, null))) - 1, null))'; | ||
796 | } | ||
797 | |||
798 | public function getConcatExpression(string ...$string): string | ||
799 | { | ||
800 | return sprintf('CONCAT(%s)', implode(', ', $string)); | ||
801 | } | ||
802 | |||
803 | /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ | ||
804 | public function getListDatabasesSQL(): string | ||
805 | { | ||
806 | return 'SELECT * FROM sys.databases'; | ||
807 | } | ||
808 | |||
809 | public function getSubstringExpression(string $string, string $start, ?string $length = null): string | ||
810 | { | ||
811 | if ($length === null) { | ||
812 | return sprintf('SUBSTRING(%s, %s, LEN(%s) - %s + 1)', $string, $start, $string, $start); | ||
813 | } | ||
814 | |||
815 | return sprintf('SUBSTRING(%s, %s, %s)', $string, $start, $length); | ||
816 | } | ||
817 | |||
818 | public function getLengthExpression(string $string): string | ||
819 | { | ||
820 | return 'LEN(' . $string . ')'; | ||
821 | } | ||
822 | |||
823 | public function getCurrentDatabaseExpression(): string | ||
824 | { | ||
825 | return 'DB_NAME()'; | ||
826 | } | ||
827 | |||
828 | public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string | ||
829 | { | ||
830 | return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); | ||
831 | } | ||
832 | |||
833 | /** | ||
834 | * {@inheritDoc} | ||
835 | */ | ||
836 | public function getIntegerTypeDeclarationSQL(array $column): string | ||
837 | { | ||
838 | return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); | ||
839 | } | ||
840 | |||
841 | /** | ||
842 | * {@inheritDoc} | ||
843 | */ | ||
844 | public function getBigIntTypeDeclarationSQL(array $column): string | ||
845 | { | ||
846 | return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); | ||
847 | } | ||
848 | |||
849 | /** | ||
850 | * {@inheritDoc} | ||
851 | */ | ||
852 | public function getSmallIntTypeDeclarationSQL(array $column): string | ||
853 | { | ||
854 | return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); | ||
855 | } | ||
856 | |||
857 | /** | ||
858 | * {@inheritDoc} | ||
859 | */ | ||
860 | public function getGuidTypeDeclarationSQL(array $column): string | ||
861 | { | ||
862 | return 'UNIQUEIDENTIFIER'; | ||
863 | } | ||
864 | |||
865 | /** | ||
866 | * {@inheritDoc} | ||
867 | */ | ||
868 | public function getDateTimeTzTypeDeclarationSQL(array $column): string | ||
869 | { | ||
870 | return 'DATETIMEOFFSET(6)'; | ||
871 | } | ||
872 | |||
873 | protected function getCharTypeDeclarationSQLSnippet(?int $length): string | ||
874 | { | ||
875 | $sql = 'NCHAR'; | ||
876 | |||
877 | if ($length !== null) { | ||
878 | $sql .= sprintf('(%d)', $length); | ||
879 | } | ||
880 | |||
881 | return $sql; | ||
882 | } | ||
883 | |||
884 | protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string | ||
885 | { | ||
886 | if ($length === null) { | ||
887 | throw ColumnLengthRequired::new($this, 'NVARCHAR'); | ||
888 | } | ||
889 | |||
890 | return sprintf('NVARCHAR(%d)', $length); | ||
891 | } | ||
892 | |||
893 | /** | ||
894 | * {@inheritDoc} | ||
895 | */ | ||
896 | public function getAsciiStringTypeDeclarationSQL(array $column): string | ||
897 | { | ||
898 | $length = $column['length'] ?? null; | ||
899 | |||
900 | if (empty($column['fixed'])) { | ||
901 | return parent::getVarcharTypeDeclarationSQLSnippet($length); | ||
902 | } | ||
903 | |||
904 | return parent::getCharTypeDeclarationSQLSnippet($length); | ||
905 | } | ||
906 | |||
907 | /** | ||
908 | * {@inheritDoc} | ||
909 | */ | ||
910 | public function getClobTypeDeclarationSQL(array $column): string | ||
911 | { | ||
912 | return 'VARCHAR(MAX)'; | ||
913 | } | ||
914 | |||
915 | /** | ||
916 | * {@inheritDoc} | ||
917 | */ | ||
918 | protected function _getCommonIntegerTypeDeclarationSQL(array $column): string | ||
919 | { | ||
920 | return ! empty($column['autoincrement']) ? ' IDENTITY' : ''; | ||
921 | } | ||
922 | |||
923 | /** | ||
924 | * {@inheritDoc} | ||
925 | */ | ||
926 | public function getDateTimeTypeDeclarationSQL(array $column): string | ||
927 | { | ||
928 | // 3 - microseconds precision length | ||
929 | // http://msdn.microsoft.com/en-us/library/ms187819.aspx | ||
930 | return 'DATETIME2(6)'; | ||
931 | } | ||
932 | |||
933 | /** | ||
934 | * {@inheritDoc} | ||
935 | */ | ||
936 | public function getDateTypeDeclarationSQL(array $column): string | ||
937 | { | ||
938 | return 'DATE'; | ||
939 | } | ||
940 | |||
941 | /** | ||
942 | * {@inheritDoc} | ||
943 | */ | ||
944 | public function getTimeTypeDeclarationSQL(array $column): string | ||
945 | { | ||
946 | return 'TIME(0)'; | ||
947 | } | ||
948 | |||
949 | /** | ||
950 | * {@inheritDoc} | ||
951 | */ | ||
952 | public function getBooleanTypeDeclarationSQL(array $column): string | ||
953 | { | ||
954 | return 'BIT'; | ||
955 | } | ||
956 | |||
957 | protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string | ||
958 | { | ||
959 | if ($limit === null && $offset <= 0) { | ||
960 | return $query; | ||
961 | } | ||
962 | |||
963 | if ($this->shouldAddOrderBy($query)) { | ||
964 | if (preg_match('/^SELECT\s+DISTINCT/im', $query) > 0) { | ||
965 | // SQL Server won't let us order by a non-selected column in a DISTINCT query, | ||
966 | // so we have to do this madness. This says, order by the first column in the | ||
967 | // result. SQL Server's docs say that a nonordered query's result order is non- | ||
968 | // deterministic anyway, so this won't do anything that a bunch of update and | ||
969 | // deletes to the table wouldn't do anyway. | ||
970 | $query .= ' ORDER BY 1'; | ||
971 | } else { | ||
972 | // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you | ||
973 | // use constant expressions in the order by list. | ||
974 | $query .= ' ORDER BY (SELECT 0)'; | ||
975 | } | ||
976 | } | ||
977 | |||
978 | // This looks somewhat like MYSQL, but limit/offset are in inverse positions | ||
979 | // Supposedly SQL:2008 core standard. | ||
980 | // Per TSQL spec, FETCH NEXT n ROWS ONLY is not valid without OFFSET n ROWS. | ||
981 | $query .= sprintf(' OFFSET %d ROWS', $offset); | ||
982 | |||
983 | if ($limit !== null) { | ||
984 | $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit); | ||
985 | } | ||
986 | |||
987 | return $query; | ||
988 | } | ||
989 | |||
990 | public function convertBooleans(mixed $item): mixed | ||
991 | { | ||
992 | if (is_array($item)) { | ||
993 | foreach ($item as $key => $value) { | ||
994 | if (! is_bool($value) && ! is_numeric($value)) { | ||
995 | continue; | ||
996 | } | ||
997 | |||
998 | $item[$key] = (int) (bool) $value; | ||
999 | } | ||
1000 | } elseif (is_bool($item) || is_numeric($item)) { | ||
1001 | $item = (int) (bool) $item; | ||
1002 | } | ||
1003 | |||
1004 | return $item; | ||
1005 | } | ||
1006 | |||
1007 | public function getCreateTemporaryTableSnippetSQL(): string | ||
1008 | { | ||
1009 | return 'CREATE TABLE'; | ||
1010 | } | ||
1011 | |||
1012 | public function getTemporaryTableName(string $tableName): string | ||
1013 | { | ||
1014 | return '#' . $tableName; | ||
1015 | } | ||
1016 | |||
1017 | public function getDateTimeFormatString(): string | ||
1018 | { | ||
1019 | return 'Y-m-d H:i:s.u'; | ||
1020 | } | ||
1021 | |||
1022 | public function getDateFormatString(): string | ||
1023 | { | ||
1024 | return 'Y-m-d'; | ||
1025 | } | ||
1026 | |||
1027 | public function getTimeFormatString(): string | ||
1028 | { | ||
1029 | return 'H:i:s'; | ||
1030 | } | ||
1031 | |||
1032 | public function getDateTimeTzFormatString(): string | ||
1033 | { | ||
1034 | return 'Y-m-d H:i:s.u P'; | ||
1035 | } | ||
1036 | |||
1037 | protected function initializeDoctrineTypeMappings(): void | ||
1038 | { | ||
1039 | $this->doctrineTypeMapping = [ | ||
1040 | 'bigint' => Types::BIGINT, | ||
1041 | 'binary' => Types::BINARY, | ||
1042 | 'bit' => Types::BOOLEAN, | ||
1043 | 'blob' => Types::BLOB, | ||
1044 | 'char' => Types::STRING, | ||
1045 | 'date' => Types::DATE_MUTABLE, | ||
1046 | 'datetime' => Types::DATETIME_MUTABLE, | ||
1047 | 'datetime2' => Types::DATETIME_MUTABLE, | ||
1048 | 'datetimeoffset' => Types::DATETIMETZ_MUTABLE, | ||
1049 | 'decimal' => Types::DECIMAL, | ||
1050 | 'double' => Types::FLOAT, | ||
1051 | 'double precision' => Types::FLOAT, | ||
1052 | 'float' => Types::FLOAT, | ||
1053 | 'image' => Types::BLOB, | ||
1054 | 'int' => Types::INTEGER, | ||
1055 | 'money' => Types::INTEGER, | ||
1056 | 'nchar' => Types::STRING, | ||
1057 | 'ntext' => Types::TEXT, | ||
1058 | 'numeric' => Types::DECIMAL, | ||
1059 | 'nvarchar' => Types::STRING, | ||
1060 | 'real' => Types::FLOAT, | ||
1061 | 'smalldatetime' => Types::DATETIME_MUTABLE, | ||
1062 | 'smallint' => Types::SMALLINT, | ||
1063 | 'smallmoney' => Types::INTEGER, | ||
1064 | 'text' => Types::TEXT, | ||
1065 | 'time' => Types::TIME_MUTABLE, | ||
1066 | 'tinyint' => Types::SMALLINT, | ||
1067 | 'uniqueidentifier' => Types::GUID, | ||
1068 | 'varbinary' => Types::BINARY, | ||
1069 | 'varchar' => Types::STRING, | ||
1070 | ]; | ||
1071 | } | ||
1072 | |||
1073 | public function createSavePoint(string $savepoint): string | ||
1074 | { | ||
1075 | return 'SAVE TRANSACTION ' . $savepoint; | ||
1076 | } | ||
1077 | |||
1078 | public function releaseSavePoint(string $savepoint): string | ||
1079 | { | ||
1080 | return ''; | ||
1081 | } | ||
1082 | |||
1083 | public function rollbackSavePoint(string $savepoint): string | ||
1084 | { | ||
1085 | return 'ROLLBACK TRANSACTION ' . $savepoint; | ||
1086 | } | ||
1087 | |||
1088 | /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ | ||
1089 | public function getForeignKeyReferentialActionSQL(string $action): string | ||
1090 | { | ||
1091 | // RESTRICT is not supported, therefore falling back to NO ACTION. | ||
1092 | if (strtoupper($action) === 'RESTRICT') { | ||
1093 | return 'NO ACTION'; | ||
1094 | } | ||
1095 | |||
1096 | return parent::getForeignKeyReferentialActionSQL($action); | ||
1097 | } | ||
1098 | |||
1099 | public function appendLockHint(string $fromClause, LockMode $lockMode): string | ||
1100 | { | ||
1101 | return match ($lockMode) { | ||
1102 | LockMode::NONE, | ||
1103 | LockMode::OPTIMISTIC => $fromClause, | ||
1104 | LockMode::PESSIMISTIC_READ => $fromClause . ' WITH (HOLDLOCK, ROWLOCK)', | ||
1105 | LockMode::PESSIMISTIC_WRITE => $fromClause . ' WITH (UPDLOCK, ROWLOCK)', | ||
1106 | }; | ||
1107 | } | ||
1108 | |||
1109 | protected function createReservedKeywordsList(): KeywordList | ||
1110 | { | ||
1111 | return new SQLServerKeywords(); | ||
1112 | } | ||
1113 | |||
1114 | public function quoteSingleIdentifier(string $str): string | ||
1115 | { | ||
1116 | return '[' . str_replace(']', ']]', $str) . ']'; | ||
1117 | } | ||
1118 | |||
1119 | public function getTruncateTableSQL(string $tableName, bool $cascade = false): string | ||
1120 | { | ||
1121 | $tableIdentifier = new Identifier($tableName); | ||
1122 | |||
1123 | return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this); | ||
1124 | } | ||
1125 | |||
1126 | /** | ||
1127 | * {@inheritDoc} | ||
1128 | */ | ||
1129 | public function getBlobTypeDeclarationSQL(array $column): string | ||
1130 | { | ||
1131 | return 'VARBINARY(MAX)'; | ||
1132 | } | ||
1133 | |||
1134 | /** | ||
1135 | * {@inheritDoc} | ||
1136 | * | ||
1137 | * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. | ||
1138 | */ | ||
1139 | public function getColumnDeclarationSQL(string $name, array $column): string | ||
1140 | { | ||
1141 | if (isset($column['columnDefinition'])) { | ||
1142 | $declaration = $column['columnDefinition']; | ||
1143 | } else { | ||
1144 | $collation = ! empty($column['collation']) ? | ||
1145 | ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : ''; | ||
1146 | |||
1147 | $notnull = ! empty($column['notnull']) ? ' NOT NULL' : ''; | ||
1148 | |||
1149 | $typeDecl = $column['type']->getSQLDeclaration($column, $this); | ||
1150 | $declaration = $typeDecl . $collation . $notnull; | ||
1151 | } | ||
1152 | |||
1153 | return $name . ' ' . $declaration; | ||
1154 | } | ||
1155 | |||
1156 | /** | ||
1157 | * SQL Server does not support quoting collation identifiers. | ||
1158 | */ | ||
1159 | public function getColumnCollationDeclarationSQL(string $collation): string | ||
1160 | { | ||
1161 | return 'COLLATE ' . $collation; | ||
1162 | } | ||
1163 | |||
1164 | public function columnsEqual(Column $column1, Column $column2): bool | ||
1165 | { | ||
1166 | if (! parent::columnsEqual($column1, $column2)) { | ||
1167 | return false; | ||
1168 | } | ||
1169 | |||
1170 | return $this->getDefaultValueDeclarationSQL($column1->toArray()) | ||
1171 | === $this->getDefaultValueDeclarationSQL($column2->toArray()); | ||
1172 | } | ||
1173 | |||
1174 | protected function getLikeWildcardCharacters(): string | ||
1175 | { | ||
1176 | return parent::getLikeWildcardCharacters() . '[]^'; | ||
1177 | } | ||
1178 | |||
1179 | protected function getCommentOnTableSQL(string $tableName, string $comment): string | ||
1180 | { | ||
1181 | return $this->getAddExtendedPropertySQL( | ||
1182 | 'MS_Description', | ||
1183 | $comment, | ||
1184 | 'SCHEMA', | ||
1185 | $this->quoteStringLiteral('dbo'), | ||
1186 | 'TABLE', | ||
1187 | $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), | ||
1188 | ); | ||
1189 | } | ||
1190 | |||
1191 | private function shouldAddOrderBy(string $query): bool | ||
1192 | { | ||
1193 | // Find the position of the last instance of ORDER BY and ensure it is not within a parenthetical statement | ||
1194 | // but can be in a newline | ||
1195 | $matches = []; | ||
1196 | $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE); | ||
1197 | if ($matchesCount === 0) { | ||
1198 | return true; | ||
1199 | } | ||
1200 | |||
1201 | // ORDER BY instance may be in a subquery after ORDER BY | ||
1202 | // e.g. SELECT col1 FROM test ORDER BY (SELECT col2 from test ORDER BY col2) | ||
1203 | // if in the searched query ORDER BY clause was found where | ||
1204 | // number of open parentheses after the occurrence of the clause is equal to | ||
1205 | // number of closed brackets after the occurrence of the clause, | ||
1206 | // it means that ORDER BY is included in the query being checked | ||
1207 | while ($matchesCount > 0) { | ||
1208 | $orderByPos = $matches[0][--$matchesCount][1]; | ||
1209 | $openBracketsCount = substr_count($query, '(', $orderByPos); | ||
1210 | $closedBracketsCount = substr_count($query, ')', $orderByPos); | ||
1211 | if ($openBracketsCount === $closedBracketsCount) { | ||
1212 | return false; | ||
1213 | } | ||
1214 | } | ||
1215 | |||
1216 | return true; | ||
1217 | } | ||
1218 | |||
1219 | public function createSchemaManager(Connection $connection): SQLServerSchemaManager | ||
1220 | { | ||
1221 | return new SQLServerSchemaManager($connection, $this); | ||
1222 | } | ||
1223 | } | ||