summaryrefslogtreecommitdiff
path: root/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php')
-rw-r--r--vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php2219
1 files changed, 2219 insertions, 0 deletions
diff --git a/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php b/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php
new file mode 100644
index 0000000..0beb37a
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php
@@ -0,0 +1,2219 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Platforms;
6
7use Doctrine\DBAL\Connection;
8use Doctrine\DBAL\Exception;
9use Doctrine\DBAL\Exception\InvalidArgumentException;
10use Doctrine\DBAL\Exception\InvalidColumnDeclaration;
11use Doctrine\DBAL\Exception\InvalidColumnType;
12use Doctrine\DBAL\Exception\InvalidColumnType\ColumnLengthRequired;
13use Doctrine\DBAL\Exception\InvalidColumnType\ColumnPrecisionRequired;
14use Doctrine\DBAL\Exception\InvalidColumnType\ColumnScaleRequired;
15use Doctrine\DBAL\LockMode;
16use Doctrine\DBAL\Platforms\Exception\NoColumnsSpecifiedForTable;
17use Doctrine\DBAL\Platforms\Exception\NotSupported;
18use Doctrine\DBAL\Platforms\Keywords\KeywordList;
19use Doctrine\DBAL\Schema\AbstractSchemaManager;
20use Doctrine\DBAL\Schema\Column;
21use Doctrine\DBAL\Schema\ForeignKeyConstraint;
22use Doctrine\DBAL\Schema\Identifier;
23use Doctrine\DBAL\Schema\Index;
24use Doctrine\DBAL\Schema\SchemaDiff;
25use Doctrine\DBAL\Schema\Sequence;
26use Doctrine\DBAL\Schema\Table;
27use Doctrine\DBAL\Schema\TableDiff;
28use Doctrine\DBAL\Schema\UniqueConstraint;
29use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
30use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
31use Doctrine\DBAL\SQL\Parser;
32use Doctrine\DBAL\TransactionIsolationLevel;
33use Doctrine\DBAL\Types;
34use Doctrine\DBAL\Types\Exception\TypeNotFound;
35use Doctrine\DBAL\Types\Type;
36
37use function addcslashes;
38use function array_map;
39use function array_merge;
40use function array_unique;
41use function array_values;
42use function assert;
43use function count;
44use function explode;
45use function implode;
46use function in_array;
47use function is_array;
48use function is_bool;
49use function is_float;
50use function is_int;
51use function is_string;
52use function preg_quote;
53use function preg_replace;
54use function sprintf;
55use function str_contains;
56use function str_replace;
57use function strlen;
58use function strtolower;
59use function strtoupper;
60
61/**
62 * Base class for all DatabasePlatforms. The DatabasePlatforms are the central
63 * point of abstraction of platform-specific behaviors, features and SQL dialects.
64 * They are a passive source of information.
65 *
66 * @todo Remove any unnecessary methods.
67 */
68abstract class AbstractPlatform
69{
70 /** @deprecated */
71 public const CREATE_INDEXES = 1;
72
73 /** @deprecated */
74 public const CREATE_FOREIGNKEYS = 2;
75
76 /** @var string[]|null */
77 protected ?array $doctrineTypeMapping = null;
78
79 /**
80 * Holds the KeywordList instance for the current platform.
81 */
82 protected ?KeywordList $_keywords = null;
83
84 /**
85 * Returns the SQL snippet that declares a boolean column.
86 *
87 * @param mixed[] $column
88 */
89 abstract public function getBooleanTypeDeclarationSQL(array $column): string;
90
91 /**
92 * Returns the SQL snippet that declares a 4 byte integer column.
93 *
94 * @param mixed[] $column
95 */
96 abstract public function getIntegerTypeDeclarationSQL(array $column): string;
97
98 /**
99 * Returns the SQL snippet that declares an 8 byte integer column.
100 *
101 * @param mixed[] $column
102 */
103 abstract public function getBigIntTypeDeclarationSQL(array $column): string;
104
105 /**
106 * Returns the SQL snippet that declares a 2 byte integer column.
107 *
108 * @param mixed[] $column
109 */
110 abstract public function getSmallIntTypeDeclarationSQL(array $column): string;
111
112 /**
113 * Returns the SQL snippet that declares common properties of an integer column.
114 *
115 * @param mixed[] $column
116 */
117 abstract protected function _getCommonIntegerTypeDeclarationSQL(array $column): string;
118
119 /**
120 * Lazy load Doctrine Type Mappings.
121 */
122 abstract protected function initializeDoctrineTypeMappings(): void;
123
124 /**
125 * Initializes Doctrine Type Mappings with the platform defaults
126 * and with all additional type mappings.
127 */
128 private function initializeAllDoctrineTypeMappings(): void
129 {
130 $this->initializeDoctrineTypeMappings();
131
132 foreach (Type::getTypesMap() as $typeName => $className) {
133 foreach (Type::getType($typeName)->getMappedDatabaseTypes($this) as $dbType) {
134 $dbType = strtolower($dbType);
135 $this->doctrineTypeMapping[$dbType] = $typeName;
136 }
137 }
138 }
139
140 /**
141 * Returns the SQL snippet used to declare a column that can
142 * store characters in the ASCII character set
143 *
144 * @param array<string, mixed> $column The column definition.
145 */
146 public function getAsciiStringTypeDeclarationSQL(array $column): string
147 {
148 return $this->getStringTypeDeclarationSQL($column);
149 }
150
151 /**
152 * Returns the SQL snippet used to declare a string column type.
153 *
154 * @param array<string, mixed> $column The column definition.
155 */
156 public function getStringTypeDeclarationSQL(array $column): string
157 {
158 $length = $column['length'] ?? null;
159
160 if (empty($column['fixed'])) {
161 try {
162 return $this->getVarcharTypeDeclarationSQLSnippet($length);
163 } catch (InvalidColumnType $e) {
164 throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], $e);
165 }
166 }
167
168 return $this->getCharTypeDeclarationSQLSnippet($length);
169 }
170
171 /**
172 * Returns the SQL snippet used to declare a binary string column type.
173 *
174 * @param array<string, mixed> $column The column definition.
175 */
176 public function getBinaryTypeDeclarationSQL(array $column): string
177 {
178 $length = $column['length'] ?? null;
179
180 try {
181 if (empty($column['fixed'])) {
182 return $this->getVarbinaryTypeDeclarationSQLSnippet($length);
183 }
184
185 return $this->getBinaryTypeDeclarationSQLSnippet($length);
186 } catch (InvalidColumnType $e) {
187 throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], $e);
188 }
189 }
190
191 /**
192 * Returns the SQL snippet to declare a GUID/UUID column.
193 *
194 * By default this maps directly to a CHAR(36) and only maps to more
195 * special datatypes when the underlying databases support this datatype.
196 *
197 * @param array<string, mixed> $column The column definition.
198 */
199 public function getGuidTypeDeclarationSQL(array $column): string
200 {
201 $column['length'] = 36;
202 $column['fixed'] = true;
203
204 return $this->getStringTypeDeclarationSQL($column);
205 }
206
207 /**
208 * Returns the SQL snippet to declare a JSON column.
209 *
210 * By default this maps directly to a CLOB and only maps to more
211 * special datatypes when the underlying databases support this datatype.
212 *
213 * @param mixed[] $column
214 */
215 public function getJsonTypeDeclarationSQL(array $column): string
216 {
217 return $this->getClobTypeDeclarationSQL($column);
218 }
219
220 /**
221 * @param int|null $length The length of the column in characters
222 * or NULL if the length should be omitted.
223 */
224 protected function getCharTypeDeclarationSQLSnippet(?int $length): string
225 {
226 $sql = 'CHAR';
227
228 if ($length !== null) {
229 $sql .= sprintf('(%d)', $length);
230 }
231
232 return $sql;
233 }
234
235 /**
236 * @param int|null $length The length of the column in characters
237 * or NULL if the length should be omitted.
238 */
239 protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string
240 {
241 if ($length === null) {
242 throw ColumnLengthRequired::new($this, 'VARCHAR');
243 }
244
245 return sprintf('VARCHAR(%d)', $length);
246 }
247
248 /**
249 * Returns the SQL snippet used to declare a fixed length binary column type.
250 *
251 * @param int|null $length The length of the column in bytes
252 * or NULL if the length should be omitted.
253 */
254 protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string
255 {
256 $sql = 'BINARY';
257
258 if ($length !== null) {
259 $sql .= sprintf('(%d)', $length);
260 }
261
262 return $sql;
263 }
264
265 /**
266 * Returns the SQL snippet used to declare a variable length binary column type.
267 *
268 * @param int|null $length The length of the column in bytes
269 * or NULL if the length should be omitted.
270 */
271 protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string
272 {
273 if ($length === null) {
274 throw ColumnLengthRequired::new($this, 'VARBINARY');
275 }
276
277 return sprintf('VARBINARY(%d)', $length);
278 }
279
280 /**
281 * Returns the SQL snippet used to declare a CLOB column type.
282 *
283 * @param mixed[] $column
284 */
285 abstract public function getClobTypeDeclarationSQL(array $column): string;
286
287 /**
288 * Returns the SQL Snippet used to declare a BLOB column type.
289 *
290 * @param mixed[] $column
291 */
292 abstract public function getBlobTypeDeclarationSQL(array $column): string;
293
294 /**
295 * Registers a doctrine type to be used in conjunction with a column type of this platform.
296 *
297 * @throws Exception If the type is not found.
298 */
299 public function registerDoctrineTypeMapping(string $dbType, string $doctrineType): void
300 {
301 if ($this->doctrineTypeMapping === null) {
302 $this->initializeAllDoctrineTypeMappings();
303 }
304
305 if (! Types\Type::hasType($doctrineType)) {
306 throw TypeNotFound::new($doctrineType);
307 }
308
309 $dbType = strtolower($dbType);
310 $this->doctrineTypeMapping[$dbType] = $doctrineType;
311 }
312
313 /**
314 * Gets the Doctrine type that is mapped for the given database column type.
315 */
316 public function getDoctrineTypeMapping(string $dbType): string
317 {
318 if ($this->doctrineTypeMapping === null) {
319 $this->initializeAllDoctrineTypeMappings();
320 }
321
322 $dbType = strtolower($dbType);
323
324 if (! isset($this->doctrineTypeMapping[$dbType])) {
325 throw new InvalidArgumentException(sprintf(
326 'Unknown database type "%s" requested, %s may not support it.',
327 $dbType,
328 static::class,
329 ));
330 }
331
332 return $this->doctrineTypeMapping[$dbType];
333 }
334
335 /**
336 * Checks if a database type is currently supported by this platform.
337 */
338 public function hasDoctrineTypeMappingFor(string $dbType): bool
339 {
340 if ($this->doctrineTypeMapping === null) {
341 $this->initializeAllDoctrineTypeMappings();
342 }
343
344 $dbType = strtolower($dbType);
345
346 return isset($this->doctrineTypeMapping[$dbType]);
347 }
348
349 /**
350 * Returns the regular expression operator.
351 */
352 public function getRegexpExpression(): string
353 {
354 throw NotSupported::new(__METHOD__);
355 }
356
357 /**
358 * Returns the SQL snippet to get the length of a text column in characters.
359 *
360 * @param string $string SQL expression producing the string.
361 */
362 public function getLengthExpression(string $string): string
363 {
364 return 'LENGTH(' . $string . ')';
365 }
366
367 /**
368 * Returns the SQL snippet to get the remainder of the operation of division of dividend by divisor.
369 *
370 * @param string $dividend SQL expression producing the dividend.
371 * @param string $divisor SQL expression producing the divisor.
372 */
373 public function getModExpression(string $dividend, string $divisor): string
374 {
375 return 'MOD(' . $dividend . ', ' . $divisor . ')';
376 }
377
378 /**
379 * Returns the SQL snippet to trim a string.
380 *
381 * @param string $str The expression to apply the trim to.
382 * @param TrimMode $mode The position of the trim.
383 * @param string|null $char The char to trim, has to be quoted already. Defaults to space.
384 */
385 public function getTrimExpression(
386 string $str,
387 TrimMode $mode = TrimMode::UNSPECIFIED,
388 ?string $char = null,
389 ): string {
390 $tokens = [];
391
392 switch ($mode) {
393 case TrimMode::UNSPECIFIED:
394 break;
395
396 case TrimMode::LEADING:
397 $tokens[] = 'LEADING';
398 break;
399
400 case TrimMode::TRAILING:
401 $tokens[] = 'TRAILING';
402 break;
403
404 case TrimMode::BOTH:
405 $tokens[] = 'BOTH';
406 break;
407 }
408
409 if ($char !== null) {
410 $tokens[] = $char;
411 }
412
413 if (count($tokens) > 0) {
414 $tokens[] = 'FROM';
415 }
416
417 $tokens[] = $str;
418
419 return sprintf('TRIM(%s)', implode(' ', $tokens));
420 }
421
422 /**
423 * Returns the SQL snippet to get the position of the first occurrence of the substring in the string.
424 *
425 * @param string $string SQL expression producing the string to locate the substring in.
426 * @param string $substring SQL expression producing the substring to locate.
427 * @param string|null $start SQL expression producing the position to start at.
428 * Defaults to the beginning of the string.
429 */
430 abstract public function getLocateExpression(string $string, string $substring, ?string $start = null): string;
431
432 /**
433 * Returns an SQL snippet to get a substring inside the string.
434 *
435 * Note: Not SQL92, but common functionality.
436 *
437 * @param string $string SQL expression producing the string from which a substring should be extracted.
438 * @param string $start SQL expression producing the position to start at,
439 * @param string|null $length SQL expression producing the length of the substring portion to be returned.
440 * By default, the entire substring is returned.
441 */
442 public function getSubstringExpression(string $string, string $start, ?string $length = null): string
443 {
444 if ($length === null) {
445 return sprintf('SUBSTRING(%s FROM %s)', $string, $start);
446 }
447
448 return sprintf('SUBSTRING(%s FROM %s FOR %s)', $string, $start, $length);
449 }
450
451 /**
452 * Returns a SQL snippet to concatenate the given strings.
453 */
454 public function getConcatExpression(string ...$string): string
455 {
456 return implode(' || ', $string);
457 }
458
459 /**
460 * Returns the SQL to calculate the difference in days between the two passed dates.
461 *
462 * Computes diff = date1 - date2.
463 */
464 abstract public function getDateDiffExpression(string $date1, string $date2): string;
465
466 /**
467 * Returns the SQL to add the number of given seconds to a date.
468 *
469 * @param string $date SQL expression producing the date.
470 * @param string $seconds SQL expression producing the number of seconds.
471 */
472 public function getDateAddSecondsExpression(string $date, string $seconds): string
473 {
474 return $this->getDateArithmeticIntervalExpression($date, '+', $seconds, DateIntervalUnit::SECOND);
475 }
476
477 /**
478 * Returns the SQL to subtract the number of given seconds from a date.
479 *
480 * @param string $date SQL expression producing the date.
481 * @param string $seconds SQL expression producing the number of seconds.
482 */
483 public function getDateSubSecondsExpression(string $date, string $seconds): string
484 {
485 return $this->getDateArithmeticIntervalExpression($date, '-', $seconds, DateIntervalUnit::SECOND);
486 }
487
488 /**
489 * Returns the SQL to add the number of given minutes to a date.
490 *
491 * @param string $date SQL expression producing the date.
492 * @param string $minutes SQL expression producing the number of minutes.
493 */
494 public function getDateAddMinutesExpression(string $date, string $minutes): string
495 {
496 return $this->getDateArithmeticIntervalExpression($date, '+', $minutes, DateIntervalUnit::MINUTE);
497 }
498
499 /**
500 * Returns the SQL to subtract the number of given minutes from a date.
501 *
502 * @param string $date SQL expression producing the date.
503 * @param string $minutes SQL expression producing the number of minutes.
504 */
505 public function getDateSubMinutesExpression(string $date, string $minutes): string
506 {
507 return $this->getDateArithmeticIntervalExpression($date, '-', $minutes, DateIntervalUnit::MINUTE);
508 }
509
510 /**
511 * Returns the SQL to add the number of given hours to a date.
512 *
513 * @param string $date SQL expression producing the date.
514 * @param string $hours SQL expression producing the number of hours.
515 */
516 public function getDateAddHourExpression(string $date, string $hours): string
517 {
518 return $this->getDateArithmeticIntervalExpression($date, '+', $hours, DateIntervalUnit::HOUR);
519 }
520
521 /**
522 * Returns the SQL to subtract the number of given hours to a date.
523 *
524 * @param string $date SQL expression producing the date.
525 * @param string $hours SQL expression producing the number of hours.
526 */
527 public function getDateSubHourExpression(string $date, string $hours): string
528 {
529 return $this->getDateArithmeticIntervalExpression($date, '-', $hours, DateIntervalUnit::HOUR);
530 }
531
532 /**
533 * Returns the SQL to add the number of given days to a date.
534 *
535 * @param string $date SQL expression producing the date.
536 * @param string $days SQL expression producing the number of days.
537 */
538 public function getDateAddDaysExpression(string $date, string $days): string
539 {
540 return $this->getDateArithmeticIntervalExpression($date, '+', $days, DateIntervalUnit::DAY);
541 }
542
543 /**
544 * Returns the SQL to subtract the number of given days to a date.
545 *
546 * @param string $date SQL expression producing the date.
547 * @param string $days SQL expression producing the number of days.
548 */
549 public function getDateSubDaysExpression(string $date, string $days): string
550 {
551 return $this->getDateArithmeticIntervalExpression($date, '-', $days, DateIntervalUnit::DAY);
552 }
553
554 /**
555 * Returns the SQL to add the number of given weeks to a date.
556 *
557 * @param string $date SQL expression producing the date.
558 * @param string $weeks SQL expression producing the number of weeks.
559 */
560 public function getDateAddWeeksExpression(string $date, string $weeks): string
561 {
562 return $this->getDateArithmeticIntervalExpression($date, '+', $weeks, DateIntervalUnit::WEEK);
563 }
564
565 /**
566 * Returns the SQL to subtract the number of given weeks from a date.
567 *
568 * @param string $date SQL expression producing the date.
569 * @param string $weeks SQL expression producing the number of weeks.
570 */
571 public function getDateSubWeeksExpression(string $date, string $weeks): string
572 {
573 return $this->getDateArithmeticIntervalExpression($date, '-', $weeks, DateIntervalUnit::WEEK);
574 }
575
576 /**
577 * Returns the SQL to add the number of given months to a date.
578 *
579 * @param string $date SQL expression producing the date.
580 * @param string $months SQL expression producing the number of months.
581 */
582 public function getDateAddMonthExpression(string $date, string $months): string
583 {
584 return $this->getDateArithmeticIntervalExpression($date, '+', $months, DateIntervalUnit::MONTH);
585 }
586
587 /**
588 * Returns the SQL to subtract the number of given months to a date.
589 *
590 * @param string $date SQL expression producing the date.
591 * @param string $months SQL expression producing the number of months.
592 */
593 public function getDateSubMonthExpression(string $date, string $months): string
594 {
595 return $this->getDateArithmeticIntervalExpression($date, '-', $months, DateIntervalUnit::MONTH);
596 }
597
598 /**
599 * Returns the SQL to add the number of given quarters to a date.
600 *
601 * @param string $date SQL expression producing the date.
602 * @param string $quarters SQL expression producing the number of quarters.
603 */
604 public function getDateAddQuartersExpression(string $date, string $quarters): string
605 {
606 return $this->getDateArithmeticIntervalExpression($date, '+', $quarters, DateIntervalUnit::QUARTER);
607 }
608
609 /**
610 * Returns the SQL to subtract the number of given quarters from a date.
611 *
612 * @param string $date SQL expression producing the date.
613 * @param string $quarters SQL expression producing the number of quarters.
614 */
615 public function getDateSubQuartersExpression(string $date, string $quarters): string
616 {
617 return $this->getDateArithmeticIntervalExpression($date, '-', $quarters, DateIntervalUnit::QUARTER);
618 }
619
620 /**
621 * Returns the SQL to add the number of given years to a date.
622 *
623 * @param string $date SQL expression producing the date.
624 * @param string $years SQL expression producing the number of years.
625 */
626 public function getDateAddYearsExpression(string $date, string $years): string
627 {
628 return $this->getDateArithmeticIntervalExpression($date, '+', $years, DateIntervalUnit::YEAR);
629 }
630
631 /**
632 * Returns the SQL to subtract the number of given years from a date.
633 *
634 * @param string $date SQL expression producing the date.
635 * @param string $years SQL expression producing the number of years.
636 */
637 public function getDateSubYearsExpression(string $date, string $years): string
638 {
639 return $this->getDateArithmeticIntervalExpression($date, '-', $years, DateIntervalUnit::YEAR);
640 }
641
642 /**
643 * Returns the SQL for a date arithmetic expression.
644 *
645 * @param string $date SQL expression representing a date to perform the arithmetic operation on.
646 * @param string $operator The arithmetic operator (+ or -).
647 * @param string $interval SQL expression representing the value of the interval that shall be calculated
648 * into the date.
649 * @param DateIntervalUnit $unit The unit of the interval that shall be calculated into the date.
650 */
651 abstract protected function getDateArithmeticIntervalExpression(
652 string $date,
653 string $operator,
654 string $interval,
655 DateIntervalUnit $unit,
656 ): string;
657
658 /**
659 * Generates the SQL expression which represents the given date interval multiplied by a number
660 *
661 * @param string $interval SQL expression describing the interval value
662 * @param int $multiplier Interval multiplier
663 */
664 protected function multiplyInterval(string $interval, int $multiplier): string
665 {
666 return sprintf('(%s * %d)', $interval, $multiplier);
667 }
668
669 /**
670 * Returns the SQL bit AND comparison expression.
671 *
672 * @param string $value1 SQL expression producing the first value.
673 * @param string $value2 SQL expression producing the second value.
674 */
675 public function getBitAndComparisonExpression(string $value1, string $value2): string
676 {
677 return '(' . $value1 . ' & ' . $value2 . ')';
678 }
679
680 /**
681 * Returns the SQL bit OR comparison expression.
682 *
683 * @param string $value1 SQL expression producing the first value.
684 * @param string $value2 SQL expression producing the second value.
685 */
686 public function getBitOrComparisonExpression(string $value1, string $value2): string
687 {
688 return '(' . $value1 . ' | ' . $value2 . ')';
689 }
690
691 /**
692 * Returns the SQL expression which represents the currently selected database.
693 */
694 abstract public function getCurrentDatabaseExpression(): string;
695
696 /**
697 * Honors that some SQL vendors such as MsSql use table hints for locking instead of the
698 * ANSI SQL FOR UPDATE specification.
699 *
700 * @param string $fromClause The FROM clause to append the hint for the given lock mode to
701 */
702 public function appendLockHint(string $fromClause, LockMode $lockMode): string
703 {
704 return $fromClause;
705 }
706
707 /**
708 * Returns the SQL snippet to drop an existing table.
709 */
710 public function getDropTableSQL(string $table): string
711 {
712 return 'DROP TABLE ' . $table;
713 }
714
715 /**
716 * Returns the SQL to safely drop a temporary table WITHOUT implicitly committing an open transaction.
717 */
718 public function getDropTemporaryTableSQL(string $table): string
719 {
720 return $this->getDropTableSQL($table);
721 }
722
723 /**
724 * Returns the SQL to drop an index from a table.
725 */
726 public function getDropIndexSQL(string $name, string $table): string
727 {
728 return 'DROP INDEX ' . $name;
729 }
730
731 /**
732 * Returns the SQL to drop a constraint.
733 *
734 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
735 */
736 protected function getDropConstraintSQL(string $name, string $table): string
737 {
738 return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $name;
739 }
740
741 /**
742 * Returns the SQL to drop a foreign key.
743 */
744 public function getDropForeignKeySQL(string $foreignKey, string $table): string
745 {
746 return 'ALTER TABLE ' . $table . ' DROP FOREIGN KEY ' . $foreignKey;
747 }
748
749 /**
750 * Returns the SQL to drop a unique constraint.
751 */
752 public function getDropUniqueConstraintSQL(string $name, string $tableName): string
753 {
754 return $this->getDropConstraintSQL($name, $tableName);
755 }
756
757 /**
758 * Returns the SQL statement(s) to create a table with the specified name, columns and constraints
759 * on this platform.
760 *
761 * @return list<string> The list of SQL statements.
762 */
763 public function getCreateTableSQL(Table $table): array
764 {
765 return $this->buildCreateTableSQL($table, true);
766 }
767
768 public function createSelectSQLBuilder(): SelectSQLBuilder
769 {
770 return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', 'SKIP LOCKED');
771 }
772
773 /**
774 * @internal
775 *
776 * @return list<string>
777 */
778 final protected function getCreateTableWithoutForeignKeysSQL(Table $table): array
779 {
780 return $this->buildCreateTableSQL($table, false);
781 }
782
783 /** @return list<string> */
784 private function buildCreateTableSQL(Table $table, bool $createForeignKeys): array
785 {
786 if (count($table->getColumns()) === 0) {
787 throw NoColumnsSpecifiedForTable::new($table->getName());
788 }
789
790 $tableName = $table->getQuotedName($this);
791 $options = $table->getOptions();
792 $options['uniqueConstraints'] = [];
793 $options['indexes'] = [];
794 $options['primary'] = [];
795
796 foreach ($table->getIndexes() as $index) {
797 if (! $index->isPrimary()) {
798 $options['indexes'][$index->getQuotedName($this)] = $index;
799
800 continue;
801 }
802
803 $options['primary'] = $index->getQuotedColumns($this);
804 $options['primary_index'] = $index;
805 }
806
807 foreach ($table->getUniqueConstraints() as $uniqueConstraint) {
808 $options['uniqueConstraints'][$uniqueConstraint->getQuotedName($this)] = $uniqueConstraint;
809 }
810
811 if ($createForeignKeys) {
812 $options['foreignKeys'] = [];
813
814 foreach ($table->getForeignKeys() as $fkConstraint) {
815 $options['foreignKeys'][] = $fkConstraint;
816 }
817 }
818
819 $columnSql = [];
820 $columns = [];
821
822 foreach ($table->getColumns() as $column) {
823 $columnData = $this->columnToArray($column);
824
825 if (in_array($column->getName(), $options['primary'], true)) {
826 $columnData['primary'] = true;
827 }
828
829 $columns[] = $columnData;
830 }
831
832 $sql = $this->_getCreateTableSQL($tableName, $columns, $options);
833
834 if ($this->supportsCommentOnStatement()) {
835 if ($table->hasOption('comment')) {
836 $sql[] = $this->getCommentOnTableSQL($tableName, $table->getOption('comment'));
837 }
838
839 foreach ($table->getColumns() as $column) {
840 $comment = $column->getComment();
841
842 if ($comment === '') {
843 continue;
844 }
845
846 $sql[] = $this->getCommentOnColumnSQL($tableName, $column->getQuotedName($this), $comment);
847 }
848 }
849
850 return array_merge($sql, $columnSql);
851 }
852
853 /**
854 * @param array<Table> $tables
855 *
856 * @return list<string>
857 */
858 public function getCreateTablesSQL(array $tables): array
859 {
860 $sql = [];
861
862 foreach ($tables as $table) {
863 $sql = array_merge($sql, $this->getCreateTableWithoutForeignKeysSQL($table));
864 }
865
866 foreach ($tables as $table) {
867 foreach ($table->getForeignKeys() as $foreignKey) {
868 $sql[] = $this->getCreateForeignKeySQL(
869 $foreignKey,
870 $table->getQuotedName($this),
871 );
872 }
873 }
874
875 return $sql;
876 }
877
878 /**
879 * @param array<Table> $tables
880 *
881 * @return list<string>
882 */
883 public function getDropTablesSQL(array $tables): array
884 {
885 $sql = [];
886
887 foreach ($tables as $table) {
888 foreach ($table->getForeignKeys() as $foreignKey) {
889 $sql[] = $this->getDropForeignKeySQL(
890 $foreignKey->getQuotedName($this),
891 $table->getQuotedName($this),
892 );
893 }
894 }
895
896 foreach ($tables as $table) {
897 $sql[] = $this->getDropTableSQL($table->getQuotedName($this));
898 }
899
900 return $sql;
901 }
902
903 protected function getCommentOnTableSQL(string $tableName, string $comment): string
904 {
905 $tableName = new Identifier($tableName);
906
907 return sprintf(
908 'COMMENT ON TABLE %s IS %s',
909 $tableName->getQuotedName($this),
910 $this->quoteStringLiteral($comment),
911 );
912 }
913
914 /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */
915 public function getCommentOnColumnSQL(string $tableName, string $columnName, string $comment): string
916 {
917 $tableName = new Identifier($tableName);
918 $columnName = new Identifier($columnName);
919
920 return sprintf(
921 'COMMENT ON COLUMN %s.%s IS %s',
922 $tableName->getQuotedName($this),
923 $columnName->getQuotedName($this),
924 $this->quoteStringLiteral($comment),
925 );
926 }
927
928 /**
929 * Returns the SQL to create inline comment on a column.
930 *
931 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
932 */
933 public function getInlineColumnCommentSQL(string $comment): string
934 {
935 if (! $this->supportsInlineColumnComments()) {
936 throw NotSupported::new(__METHOD__);
937 }
938
939 return 'COMMENT ' . $this->quoteStringLiteral($comment);
940 }
941
942 /**
943 * Returns the SQL used to create a table.
944 *
945 * @param mixed[][] $columns
946 * @param mixed[] $options
947 *
948 * @return array<int, string>
949 */
950 protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array
951 {
952 $columnListSql = $this->getColumnDeclarationListSQL($columns);
953
954 if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
955 foreach ($options['uniqueConstraints'] as $definition) {
956 $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition);
957 }
958 }
959
960 if (isset($options['primary']) && ! empty($options['primary'])) {
961 $columnListSql .= ', PRIMARY KEY(' . implode(', ', array_unique(array_values($options['primary']))) . ')';
962 }
963
964 if (isset($options['indexes']) && ! empty($options['indexes'])) {
965 foreach ($options['indexes'] as $index => $definition) {
966 $columnListSql .= ', ' . $this->getIndexDeclarationSQL($definition);
967 }
968 }
969
970 $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql;
971 $check = $this->getCheckDeclarationSQL($columns);
972
973 if (! empty($check)) {
974 $query .= ', ' . $check;
975 }
976
977 $query .= ')';
978
979 $sql = [$query];
980
981 if (isset($options['foreignKeys'])) {
982 foreach ($options['foreignKeys'] as $definition) {
983 $sql[] = $this->getCreateForeignKeySQL($definition, $name);
984 }
985 }
986
987 return $sql;
988 }
989
990 public function getCreateTemporaryTableSnippetSQL(): string
991 {
992 return 'CREATE TEMPORARY TABLE';
993 }
994
995 /**
996 * Generates SQL statements that can be used to apply the diff.
997 *
998 * @return list<string>
999 */
1000 public function getAlterSchemaSQL(SchemaDiff $diff): array
1001 {
1002 $sql = [];
1003
1004 if ($this->supportsSchemas()) {
1005 foreach ($diff->getCreatedSchemas() as $schema) {
1006 $sql[] = $this->getCreateSchemaSQL($schema);
1007 }
1008 }
1009
1010 if ($this->supportsSequences()) {
1011 foreach ($diff->getAlteredSequences() as $sequence) {
1012 $sql[] = $this->getAlterSequenceSQL($sequence);
1013 }
1014
1015 foreach ($diff->getDroppedSequences() as $sequence) {
1016 $sql[] = $this->getDropSequenceSQL($sequence->getQuotedName($this));
1017 }
1018
1019 foreach ($diff->getCreatedSequences() as $sequence) {
1020 $sql[] = $this->getCreateSequenceSQL($sequence);
1021 }
1022 }
1023
1024 $sql = array_merge(
1025 $sql,
1026 $this->getCreateTablesSQL(
1027 $diff->getCreatedTables(),
1028 ),
1029 $this->getDropTablesSQL(
1030 $diff->getDroppedTables(),
1031 ),
1032 );
1033
1034 foreach ($diff->getAlteredTables() as $tableDiff) {
1035 $sql = array_merge($sql, $this->getAlterTableSQL($tableDiff));
1036 }
1037
1038 return $sql;
1039 }
1040
1041 /**
1042 * Returns the SQL to create a sequence on this platform.
1043 */
1044 public function getCreateSequenceSQL(Sequence $sequence): string
1045 {
1046 throw NotSupported::new(__METHOD__);
1047 }
1048
1049 /**
1050 * Returns the SQL to change a sequence on this platform.
1051 */
1052 public function getAlterSequenceSQL(Sequence $sequence): string
1053 {
1054 throw NotSupported::new(__METHOD__);
1055 }
1056
1057 /**
1058 * Returns the SQL snippet to drop an existing sequence.
1059 */
1060 public function getDropSequenceSQL(string $name): string
1061 {
1062 if (! $this->supportsSequences()) {
1063 throw NotSupported::new(__METHOD__);
1064 }
1065
1066 return 'DROP SEQUENCE ' . $name;
1067 }
1068
1069 /**
1070 * Returns the SQL to create an index on a table on this platform.
1071 */
1072 public function getCreateIndexSQL(Index $index, string $table): string
1073 {
1074 $name = $index->getQuotedName($this);
1075 $columns = $index->getColumns();
1076
1077 if (count($columns) === 0) {
1078 throw new InvalidArgumentException(sprintf(
1079 'Incomplete or invalid index definition %s on table %s',
1080 $name,
1081 $table,
1082 ));
1083 }
1084
1085 if ($index->isPrimary()) {
1086 return $this->getCreatePrimaryKeySQL($index, $table);
1087 }
1088
1089 $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table;
1090 $query .= ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index);
1091
1092 return $query;
1093 }
1094
1095 /**
1096 * Adds condition for partial index.
1097 */
1098 protected function getPartialIndexSQL(Index $index): string
1099 {
1100 if ($this->supportsPartialIndexes() && $index->hasOption('where')) {
1101 return ' WHERE ' . $index->getOption('where');
1102 }
1103
1104 return '';
1105 }
1106
1107 /**
1108 * Adds additional flags for index generation.
1109 */
1110 protected function getCreateIndexSQLFlags(Index $index): string
1111 {
1112 return $index->isUnique() ? 'UNIQUE ' : '';
1113 }
1114
1115 /**
1116 * Returns the SQL to create an unnamed primary key constraint.
1117 */
1118 public function getCreatePrimaryKeySQL(Index $index, string $table): string
1119 {
1120 return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY (' . implode(', ', $index->getQuotedColumns($this)) . ')';
1121 }
1122
1123 /**
1124 * Returns the SQL to create a named schema.
1125 */
1126 public function getCreateSchemaSQL(string $schemaName): string
1127 {
1128 if (! $this->supportsSchemas()) {
1129 throw NotSupported::new(__METHOD__);
1130 }
1131
1132 return 'CREATE SCHEMA ' . $schemaName;
1133 }
1134
1135 /**
1136 * Returns the SQL to create a unique constraint on a table on this platform.
1137 */
1138 public function getCreateUniqueConstraintSQL(UniqueConstraint $constraint, string $tableName): string
1139 {
1140 return 'ALTER TABLE ' . $tableName . ' ADD CONSTRAINT ' . $constraint->getQuotedName($this) . ' UNIQUE'
1141 . ' (' . implode(', ', $constraint->getQuotedColumns($this)) . ')';
1142 }
1143
1144 /**
1145 * Returns the SQL snippet to drop a schema.
1146 */
1147 public function getDropSchemaSQL(string $schemaName): string
1148 {
1149 if (! $this->supportsSchemas()) {
1150 throw NotSupported::new(__METHOD__);
1151 }
1152
1153 return 'DROP SCHEMA ' . $schemaName;
1154 }
1155
1156 /**
1157 * Quotes a string so that it can be safely used as a table or column name,
1158 * even if it is a reserved word of the platform. This also detects identifier
1159 * chains separated by dot and quotes them independently.
1160 *
1161 * NOTE: Just because you CAN use quoted identifiers doesn't mean
1162 * you SHOULD use them. In general, they end up causing way more
1163 * problems than they solve.
1164 *
1165 * @param string $identifier The identifier name to be quoted.
1166 *
1167 * @return string The quoted identifier string.
1168 */
1169 public function quoteIdentifier(string $identifier): string
1170 {
1171 if (str_contains($identifier, '.')) {
1172 $parts = array_map($this->quoteSingleIdentifier(...), explode('.', $identifier));
1173
1174 return implode('.', $parts);
1175 }
1176
1177 return $this->quoteSingleIdentifier($identifier);
1178 }
1179
1180 /**
1181 * Quotes a single identifier (no dot chain separation).
1182 *
1183 * @param string $str The identifier name to be quoted.
1184 *
1185 * @return string The quoted identifier string.
1186 */
1187 public function quoteSingleIdentifier(string $str): string
1188 {
1189 return '"' . str_replace('"', '""', $str) . '"';
1190 }
1191
1192 /**
1193 * Returns the SQL to create a new foreign key.
1194 *
1195 * @param ForeignKeyConstraint $foreignKey The foreign key constraint.
1196 * @param string $table The name of the table on which the foreign key is to be created.
1197 */
1198 public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, string $table): string
1199 {
1200 return 'ALTER TABLE ' . $table . ' ADD ' . $this->getForeignKeyDeclarationSQL($foreignKey);
1201 }
1202
1203 /**
1204 * Gets the SQL statements for altering an existing table.
1205 *
1206 * This method returns an array of SQL statements, since some platforms need several statements.
1207 *
1208 * @return list<string>
1209 */
1210 abstract public function getAlterTableSQL(TableDiff $diff): array;
1211
1212 public function getRenameTableSQL(string $oldName, string $newName): string
1213 {
1214 return sprintf('ALTER TABLE %s RENAME TO %s', $oldName, $newName);
1215 }
1216
1217 /** @return list<string> */
1218 protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff): array
1219 {
1220 $tableNameSQL = $diff->getOldTable()->getQuotedName($this);
1221
1222 $sql = [];
1223
1224 foreach ($diff->getDroppedForeignKeys() as $foreignKey) {
1225 $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableNameSQL);
1226 }
1227
1228 foreach ($diff->getModifiedForeignKeys() as $foreignKey) {
1229 $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableNameSQL);
1230 }
1231
1232 foreach ($diff->getDroppedIndexes() as $index) {
1233 $sql[] = $this->getDropIndexSQL($index->getQuotedName($this), $tableNameSQL);
1234 }
1235
1236 foreach ($diff->getModifiedIndexes() as $index) {
1237 $sql[] = $this->getDropIndexSQL($index->getQuotedName($this), $tableNameSQL);
1238 }
1239
1240 return $sql;
1241 }
1242
1243 /** @return list<string> */
1244 protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff): array
1245 {
1246 $sql = [];
1247
1248 $tableNameSQL = $diff->getOldTable()->getQuotedName($this);
1249
1250 foreach ($diff->getAddedForeignKeys() as $foreignKey) {
1251 $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableNameSQL);
1252 }
1253
1254 foreach ($diff->getModifiedForeignKeys() as $foreignKey) {
1255 $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableNameSQL);
1256 }
1257
1258 foreach ($diff->getAddedIndexes() as $index) {
1259 $sql[] = $this->getCreateIndexSQL($index, $tableNameSQL);
1260 }
1261
1262 foreach ($diff->getModifiedIndexes() as $index) {
1263 $sql[] = $this->getCreateIndexSQL($index, $tableNameSQL);
1264 }
1265
1266 foreach ($diff->getRenamedIndexes() as $oldIndexName => $index) {
1267 $oldIndexName = new Identifier($oldIndexName);
1268 $sql = array_merge(
1269 $sql,
1270 $this->getRenameIndexSQL($oldIndexName->getQuotedName($this), $index, $tableNameSQL),
1271 );
1272 }
1273
1274 return $sql;
1275 }
1276
1277 /**
1278 * Returns the SQL for renaming an index on a table.
1279 *
1280 * @param string $oldIndexName The name of the index to rename from.
1281 * @param Index $index The definition of the index to rename to.
1282 * @param string $tableName The table to rename the given index on.
1283 *
1284 * @return list<string> The sequence of SQL statements for renaming the given index.
1285 */
1286 protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array
1287 {
1288 return [
1289 $this->getDropIndexSQL($oldIndexName, $tableName),
1290 $this->getCreateIndexSQL($index, $tableName),
1291 ];
1292 }
1293
1294 /**
1295 * Gets declaration of a number of columns in bulk.
1296 *
1297 * @param mixed[][] $columns A multidimensional array.
1298 * The first dimension determines the ordinal position of the column,
1299 * while the second dimension is keyed with the name of the properties
1300 * of the column being declared as array indexes. Currently, the types
1301 * of supported column properties are as follows:
1302 *
1303 * length
1304 * Integer value that determines the maximum length of the text
1305 * column. If this argument is missing the column should be
1306 * declared to have the longest length allowed by the DBMS.
1307 * default
1308 * Text value to be used as default for this column.
1309 * notnull
1310 * Boolean flag that indicates whether this column is constrained
1311 * to not be set to null.
1312 * charset
1313 * Text value with the default CHARACTER SET for this column.
1314 * collation
1315 * Text value with the default COLLATION for this column.
1316 */
1317 public function getColumnDeclarationListSQL(array $columns): string
1318 {
1319 $declarations = [];
1320
1321 foreach ($columns as $column) {
1322 $declarations[] = $this->getColumnDeclarationSQL($column['name'], $column);
1323 }
1324
1325 return implode(', ', $declarations);
1326 }
1327
1328 /**
1329 * Obtains DBMS specific SQL code portion needed to declare a generic type
1330 * column to be used in statements like CREATE TABLE.
1331 *
1332 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1333 *
1334 * @param string $name The name the column to be declared.
1335 * @param mixed[] $column An associative array with the name of the properties
1336 * of the column being declared as array indexes. Currently, the types
1337 * of supported column properties are as follows:
1338 *
1339 * length
1340 * Integer value that determines the maximum length of the text
1341 * column. If this argument is missing the column should be
1342 * declared to have the longest length allowed by the DBMS.
1343 * default
1344 * Text value to be used as default for this column.
1345 * notnull
1346 * Boolean flag that indicates whether this column is constrained
1347 * to not be set to null.
1348 * charset
1349 * Text value with the default CHARACTER SET for this column.
1350 * collation
1351 * Text value with the default COLLATION for this column.
1352 * columnDefinition
1353 * a string that defines the complete column
1354 *
1355 * @return string DBMS specific SQL code portion that should be used to declare the column.
1356 */
1357 public function getColumnDeclarationSQL(string $name, array $column): string
1358 {
1359 if (isset($column['columnDefinition'])) {
1360 $declaration = $column['columnDefinition'];
1361 } else {
1362 $default = $this->getDefaultValueDeclarationSQL($column);
1363
1364 $charset = ! empty($column['charset']) ?
1365 ' ' . $this->getColumnCharsetDeclarationSQL($column['charset']) : '';
1366
1367 $collation = ! empty($column['collation']) ?
1368 ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : '';
1369
1370 $notnull = ! empty($column['notnull']) ? ' NOT NULL' : '';
1371
1372 $typeDecl = $column['type']->getSQLDeclaration($column, $this);
1373 $declaration = $typeDecl . $charset . $default . $notnull . $collation;
1374
1375 if ($this->supportsInlineColumnComments() && isset($column['comment']) && $column['comment'] !== '') {
1376 $declaration .= ' ' . $this->getInlineColumnCommentSQL($column['comment']);
1377 }
1378 }
1379
1380 return $name . ' ' . $declaration;
1381 }
1382
1383 /**
1384 * Returns the SQL snippet that declares a floating point column of arbitrary precision.
1385 *
1386 * @param mixed[] $column
1387 */
1388 public function getDecimalTypeDeclarationSQL(array $column): string
1389 {
1390 if (! isset($column['precision'])) {
1391 $e = ColumnPrecisionRequired::new();
1392 } elseif (! isset($column['scale'])) {
1393 $e = ColumnScaleRequired::new();
1394 } else {
1395 $e = null;
1396 }
1397
1398 if ($e !== null) {
1399 throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], $e);
1400 }
1401
1402 return 'NUMERIC(' . $column['precision'] . ', ' . $column['scale'] . ')';
1403 }
1404
1405 /**
1406 * Obtains DBMS specific SQL code portion needed to set a default value
1407 * declaration to be used in statements like CREATE TABLE.
1408 *
1409 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1410 *
1411 * @param mixed[] $column The column definition array.
1412 *
1413 * @return string DBMS specific SQL code portion needed to set a default value.
1414 */
1415 public function getDefaultValueDeclarationSQL(array $column): string
1416 {
1417 if (! isset($column['default'])) {
1418 return empty($column['notnull']) ? ' DEFAULT NULL' : '';
1419 }
1420
1421 $default = $column['default'];
1422
1423 if (! isset($column['type'])) {
1424 return " DEFAULT '" . $default . "'";
1425 }
1426
1427 $type = $column['type'];
1428
1429 if ($type instanceof Types\PhpIntegerMappingType) {
1430 return ' DEFAULT ' . $default;
1431 }
1432
1433 if ($type instanceof Types\PhpDateTimeMappingType && $default === $this->getCurrentTimestampSQL()) {
1434 return ' DEFAULT ' . $this->getCurrentTimestampSQL();
1435 }
1436
1437 if ($type instanceof Types\PhpTimeMappingType && $default === $this->getCurrentTimeSQL()) {
1438 return ' DEFAULT ' . $this->getCurrentTimeSQL();
1439 }
1440
1441 if ($type instanceof Types\PhpDateMappingType && $default === $this->getCurrentDateSQL()) {
1442 return ' DEFAULT ' . $this->getCurrentDateSQL();
1443 }
1444
1445 if ($type instanceof Types\BooleanType) {
1446 return ' DEFAULT ' . $this->convertBooleans($default);
1447 }
1448
1449 if (is_int($default) || is_float($default)) {
1450 return ' DEFAULT ' . $default;
1451 }
1452
1453 return ' DEFAULT ' . $this->quoteStringLiteral($default);
1454 }
1455
1456 /**
1457 * Obtains DBMS specific SQL code portion needed to set a CHECK constraint
1458 * declaration to be used in statements like CREATE TABLE.
1459 *
1460 * @param string[]|mixed[][] $definition The check definition.
1461 *
1462 * @return string DBMS specific SQL code portion needed to set a CHECK constraint.
1463 */
1464 public function getCheckDeclarationSQL(array $definition): string
1465 {
1466 $constraints = [];
1467 foreach ($definition as $def) {
1468 if (is_string($def)) {
1469 $constraints[] = 'CHECK (' . $def . ')';
1470 } else {
1471 if (isset($def['min'])) {
1472 $constraints[] = 'CHECK (' . $def['name'] . ' >= ' . $def['min'] . ')';
1473 }
1474
1475 if (! isset($def['max'])) {
1476 continue;
1477 }
1478
1479 $constraints[] = 'CHECK (' . $def['name'] . ' <= ' . $def['max'] . ')';
1480 }
1481 }
1482
1483 return implode(', ', $constraints);
1484 }
1485
1486 /**
1487 * Obtains DBMS specific SQL code portion needed to set a unique
1488 * constraint declaration to be used in statements like CREATE TABLE.
1489 *
1490 * @param UniqueConstraint $constraint The unique constraint definition.
1491 *
1492 * @return string DBMS specific SQL code portion needed to set a constraint.
1493 */
1494 public function getUniqueConstraintDeclarationSQL(UniqueConstraint $constraint): string
1495 {
1496 $columns = $constraint->getColumns();
1497
1498 if (count($columns) === 0) {
1499 throw new InvalidArgumentException('Incomplete definition. "columns" required.');
1500 }
1501
1502 $chunks = ['CONSTRAINT'];
1503
1504 if ($constraint->getName() !== '') {
1505 $chunks[] = $constraint->getQuotedName($this);
1506 }
1507
1508 $chunks[] = 'UNIQUE';
1509
1510 if ($constraint->hasFlag('clustered')) {
1511 $chunks[] = 'CLUSTERED';
1512 }
1513
1514 $chunks[] = sprintf('(%s)', implode(', ', $columns));
1515
1516 return implode(' ', $chunks);
1517 }
1518
1519 /**
1520 * Obtains DBMS specific SQL code portion needed to set an index
1521 * declaration to be used in statements like CREATE TABLE.
1522 *
1523 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1524 *
1525 * @param Index $index The index definition.
1526 *
1527 * @return string DBMS specific SQL code portion needed to set an index.
1528 */
1529 public function getIndexDeclarationSQL(Index $index): string
1530 {
1531 $columns = $index->getColumns();
1532
1533 if (count($columns) === 0) {
1534 throw new InvalidArgumentException('Incomplete definition. "columns" required.');
1535 }
1536
1537 return $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $index->getQuotedName($this)
1538 . ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index);
1539 }
1540
1541 /**
1542 * Some vendors require temporary table names to be qualified specially.
1543 */
1544 public function getTemporaryTableName(string $tableName): string
1545 {
1546 return $tableName;
1547 }
1548
1549 /**
1550 * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint
1551 * of a column declaration to be used in statements like CREATE TABLE.
1552 *
1553 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1554 *
1555 * @return string DBMS specific SQL code portion needed to set the FOREIGN KEY constraint
1556 * of a column declaration.
1557 */
1558 public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey): string
1559 {
1560 $sql = $this->getForeignKeyBaseDeclarationSQL($foreignKey);
1561 $sql .= $this->getAdvancedForeignKeyOptionsSQL($foreignKey);
1562
1563 return $sql;
1564 }
1565
1566 /**
1567 * Returns the FOREIGN KEY query section dealing with non-standard options
1568 * as MATCH, INITIALLY DEFERRED, ON UPDATE, ...
1569 *
1570 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1571 *
1572 * @param ForeignKeyConstraint $foreignKey The foreign key definition.
1573 */
1574 public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string
1575 {
1576 $query = '';
1577 if ($foreignKey->hasOption('onUpdate')) {
1578 $query .= ' ON UPDATE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onUpdate'));
1579 }
1580
1581 if ($foreignKey->hasOption('onDelete')) {
1582 $query .= ' ON DELETE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete'));
1583 }
1584
1585 return $query;
1586 }
1587
1588 /**
1589 * Returns the given referential action in uppercase if valid, otherwise throws an exception.
1590 *
1591 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1592 *
1593 * @param string $action The foreign key referential action.
1594 */
1595 public function getForeignKeyReferentialActionSQL(string $action): string
1596 {
1597 $upper = strtoupper($action);
1598
1599 return match ($upper) {
1600 'CASCADE',
1601 'SET NULL',
1602 'NO ACTION',
1603 'RESTRICT',
1604 'SET DEFAULT' => $upper,
1605 default => throw new InvalidArgumentException(sprintf('Invalid foreign key action "%s".', $upper)),
1606 };
1607 }
1608
1609 /**
1610 * Obtains DBMS specific SQL code portion needed to set the FOREIGN KEY constraint
1611 * of a column declaration to be used in statements like CREATE TABLE.
1612 */
1613 public function getForeignKeyBaseDeclarationSQL(ForeignKeyConstraint $foreignKey): string
1614 {
1615 $sql = '';
1616 if ($foreignKey->getName() !== '') {
1617 $sql .= 'CONSTRAINT ' . $foreignKey->getQuotedName($this) . ' ';
1618 }
1619
1620 $sql .= 'FOREIGN KEY (';
1621
1622 if (count($foreignKey->getLocalColumns()) === 0) {
1623 throw new InvalidArgumentException('Incomplete definition. "local" required.');
1624 }
1625
1626 if (count($foreignKey->getForeignColumns()) === 0) {
1627 throw new InvalidArgumentException('Incomplete definition. "foreign" required.');
1628 }
1629
1630 if (strlen($foreignKey->getForeignTableName()) === 0) {
1631 throw new InvalidArgumentException('Incomplete definition. "foreignTable" required.');
1632 }
1633
1634 return $sql . implode(', ', $foreignKey->getQuotedLocalColumns($this))
1635 . ') REFERENCES '
1636 . $foreignKey->getQuotedForeignTableName($this) . ' ('
1637 . implode(', ', $foreignKey->getQuotedForeignColumns($this)) . ')';
1638 }
1639
1640 /**
1641 * Obtains DBMS specific SQL code portion needed to set the CHARACTER SET
1642 * of a column declaration to be used in statements like CREATE TABLE.
1643 *
1644 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1645 *
1646 * @param string $charset The name of the charset.
1647 *
1648 * @return string DBMS specific SQL code portion needed to set the CHARACTER SET
1649 * of a column declaration.
1650 */
1651 public function getColumnCharsetDeclarationSQL(string $charset): string
1652 {
1653 return '';
1654 }
1655
1656 /**
1657 * Obtains DBMS specific SQL code portion needed to set the COLLATION
1658 * of a column declaration to be used in statements like CREATE TABLE.
1659 *
1660 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1661 *
1662 * @param string $collation The name of the collation.
1663 *
1664 * @return string DBMS specific SQL code portion needed to set the COLLATION
1665 * of a column declaration.
1666 */
1667 public function getColumnCollationDeclarationSQL(string $collation): string
1668 {
1669 return $this->supportsColumnCollation() ? 'COLLATE ' . $this->quoteSingleIdentifier($collation) : '';
1670 }
1671
1672 /**
1673 * Some platforms need the boolean values to be converted.
1674 *
1675 * The default conversion in this implementation converts to integers (false => 0, true => 1).
1676 *
1677 * Note: if the input is not a boolean the original input might be returned.
1678 *
1679 * There are two contexts when converting booleans: Literals and Prepared Statements.
1680 * This method should handle the literal case
1681 *
1682 * @param mixed $item A boolean or an array of them.
1683 *
1684 * @return mixed A boolean database value or an array of them.
1685 */
1686 public function convertBooleans(mixed $item): mixed
1687 {
1688 if (is_array($item)) {
1689 foreach ($item as $k => $value) {
1690 if (! is_bool($value)) {
1691 continue;
1692 }
1693
1694 $item[$k] = (int) $value;
1695 }
1696 } elseif (is_bool($item)) {
1697 $item = (int) $item;
1698 }
1699
1700 return $item;
1701 }
1702
1703 /**
1704 * Some platforms have boolean literals that needs to be correctly converted
1705 *
1706 * The default conversion tries to convert value into bool "(bool)$item"
1707 *
1708 * @param T $item
1709 *
1710 * @return (T is null ? null : bool)
1711 *
1712 * @template T
1713 */
1714 public function convertFromBoolean(mixed $item): ?bool
1715 {
1716 if ($item === null) {
1717 return null;
1718 }
1719
1720 return (bool) $item;
1721 }
1722
1723 /**
1724 * This method should handle the prepared statements case. When there is no
1725 * distinction, it's OK to use the same method.
1726 *
1727 * Note: if the input is not a boolean the original input might be returned.
1728 *
1729 * @param mixed $item A boolean or an array of them.
1730 *
1731 * @return mixed A boolean database value or an array of them.
1732 */
1733 public function convertBooleansToDatabaseValue(mixed $item): mixed
1734 {
1735 return $this->convertBooleans($item);
1736 }
1737
1738 /**
1739 * Returns the SQL specific for the platform to get the current date.
1740 */
1741 public function getCurrentDateSQL(): string
1742 {
1743 return 'CURRENT_DATE';
1744 }
1745
1746 /**
1747 * Returns the SQL specific for the platform to get the current time.
1748 */
1749 public function getCurrentTimeSQL(): string
1750 {
1751 return 'CURRENT_TIME';
1752 }
1753
1754 /**
1755 * Returns the SQL specific for the platform to get the current timestamp
1756 */
1757 public function getCurrentTimestampSQL(): string
1758 {
1759 return 'CURRENT_TIMESTAMP';
1760 }
1761
1762 /**
1763 * Returns the SQL for a given transaction isolation level Connection constant.
1764 */
1765 protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string
1766 {
1767 return match ($level) {
1768 TransactionIsolationLevel::READ_UNCOMMITTED => 'READ UNCOMMITTED',
1769 TransactionIsolationLevel::READ_COMMITTED => 'READ COMMITTED',
1770 TransactionIsolationLevel::REPEATABLE_READ => 'REPEATABLE READ',
1771 TransactionIsolationLevel::SERIALIZABLE => 'SERIALIZABLE',
1772 };
1773 }
1774
1775 /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */
1776 public function getListDatabasesSQL(): string
1777 {
1778 throw NotSupported::new(__METHOD__);
1779 }
1780
1781 /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */
1782 public function getListSequencesSQL(string $database): string
1783 {
1784 throw NotSupported::new(__METHOD__);
1785 }
1786
1787 /**
1788 * Returns the SQL to list all views of a database or user.
1789 *
1790 * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy.
1791 */
1792 abstract public function getListViewsSQL(string $database): string;
1793
1794 public function getCreateViewSQL(string $name, string $sql): string
1795 {
1796 return 'CREATE VIEW ' . $name . ' AS ' . $sql;
1797 }
1798
1799 public function getDropViewSQL(string $name): string
1800 {
1801 return 'DROP VIEW ' . $name;
1802 }
1803
1804 public function getSequenceNextValSQL(string $sequence): string
1805 {
1806 throw NotSupported::new(__METHOD__);
1807 }
1808
1809 /**
1810 * Returns the SQL to create a new database.
1811 *
1812 * @param string $name The name of the database that should be created.
1813 */
1814 public function getCreateDatabaseSQL(string $name): string
1815 {
1816 return 'CREATE DATABASE ' . $name;
1817 }
1818
1819 /**
1820 * Returns the SQL snippet to drop an existing database.
1821 *
1822 * @param string $name The name of the database that should be dropped.
1823 */
1824 public function getDropDatabaseSQL(string $name): string
1825 {
1826 return 'DROP DATABASE ' . $name;
1827 }
1828
1829 /**
1830 * Returns the SQL to set the transaction isolation level.
1831 */
1832 abstract public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string;
1833
1834 /**
1835 * Obtains DBMS specific SQL to be used to create datetime columns in
1836 * statements like CREATE TABLE.
1837 *
1838 * @param mixed[] $column
1839 */
1840 abstract public function getDateTimeTypeDeclarationSQL(array $column): string;
1841
1842 /**
1843 * Obtains DBMS specific SQL to be used to create datetime with timezone offset columns.
1844 *
1845 * @param mixed[] $column
1846 */
1847 public function getDateTimeTzTypeDeclarationSQL(array $column): string
1848 {
1849 return $this->getDateTimeTypeDeclarationSQL($column);
1850 }
1851
1852 /**
1853 * Obtains DBMS specific SQL to be used to create date columns in statements
1854 * like CREATE TABLE.
1855 *
1856 * @param mixed[] $column
1857 */
1858 abstract public function getDateTypeDeclarationSQL(array $column): string;
1859
1860 /**
1861 * Obtains DBMS specific SQL to be used to create time columns in statements
1862 * like CREATE TABLE.
1863 *
1864 * @param mixed[] $column
1865 */
1866 abstract public function getTimeTypeDeclarationSQL(array $column): string;
1867
1868 /** @param mixed[] $column */
1869 public function getFloatDeclarationSQL(array $column): string
1870 {
1871 return 'DOUBLE PRECISION';
1872 }
1873
1874 /**
1875 * Gets the default transaction isolation level of the platform.
1876 *
1877 * @return TransactionIsolationLevel The default isolation level.
1878 */
1879 public function getDefaultTransactionIsolationLevel(): TransactionIsolationLevel
1880 {
1881 return TransactionIsolationLevel::READ_COMMITTED;
1882 }
1883
1884 /* supports*() methods */
1885
1886 /**
1887 * Whether the platform supports sequences.
1888 */
1889 public function supportsSequences(): bool
1890 {
1891 return false;
1892 }
1893
1894 /**
1895 * Whether the platform supports identity columns.
1896 *
1897 * Identity columns are columns that receive an auto-generated value from the
1898 * database on insert of a row.
1899 */
1900 public function supportsIdentityColumns(): bool
1901 {
1902 return false;
1903 }
1904
1905 /**
1906 * Whether the platform supports partial indexes.
1907 *
1908 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1909 */
1910 public function supportsPartialIndexes(): bool
1911 {
1912 return false;
1913 }
1914
1915 /**
1916 * Whether the platform supports indexes with column length definitions.
1917 */
1918 public function supportsColumnLengthIndexes(): bool
1919 {
1920 return false;
1921 }
1922
1923 /**
1924 * Whether the platform supports savepoints.
1925 */
1926 public function supportsSavepoints(): bool
1927 {
1928 return true;
1929 }
1930
1931 /**
1932 * Whether the platform supports releasing savepoints.
1933 */
1934 public function supportsReleaseSavepoints(): bool
1935 {
1936 return $this->supportsSavepoints();
1937 }
1938
1939 /**
1940 * Whether the platform supports database schemas.
1941 */
1942 public function supportsSchemas(): bool
1943 {
1944 return false;
1945 }
1946
1947 /**
1948 * Whether this platform support to add inline column comments as postfix.
1949 *
1950 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1951 */
1952 public function supportsInlineColumnComments(): bool
1953 {
1954 return false;
1955 }
1956
1957 /**
1958 * Whether this platform support the proprietary syntax "COMMENT ON asset".
1959 *
1960 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1961 */
1962 public function supportsCommentOnStatement(): bool
1963 {
1964 return false;
1965 }
1966
1967 /**
1968 * Does this platform support column collation?
1969 *
1970 * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1971 */
1972 public function supportsColumnCollation(): bool
1973 {
1974 return false;
1975 }
1976
1977 /**
1978 * Gets the format string, as accepted by the date() function, that describes
1979 * the format of a stored datetime value of this platform.
1980 *
1981 * @return string The format string.
1982 */
1983 public function getDateTimeFormatString(): string
1984 {
1985 return 'Y-m-d H:i:s';
1986 }
1987
1988 /**
1989 * Gets the format string, as accepted by the date() function, that describes
1990 * the format of a stored datetime with timezone value of this platform.
1991 *
1992 * @return string The format string.
1993 */
1994 public function getDateTimeTzFormatString(): string
1995 {
1996 return 'Y-m-d H:i:s';
1997 }
1998
1999 /**
2000 * Gets the format string, as accepted by the date() function, that describes
2001 * the format of a stored date value of this platform.
2002 *
2003 * @return string The format string.
2004 */
2005 public function getDateFormatString(): string
2006 {
2007 return 'Y-m-d';
2008 }
2009
2010 /**
2011 * Gets the format string, as accepted by the date() function, that describes
2012 * the format of a stored time value of this platform.
2013 *
2014 * @return string The format string.
2015 */
2016 public function getTimeFormatString(): string
2017 {
2018 return 'H:i:s';
2019 }
2020
2021 /**
2022 * Adds an driver-specific LIMIT clause to the query.
2023 */
2024 final public function modifyLimitQuery(string $query, ?int $limit, int $offset = 0): string
2025 {
2026 if ($offset < 0) {
2027 throw new InvalidArgumentException(sprintf(
2028 'Offset must be a positive integer or zero, %d given.',
2029 $offset,
2030 ));
2031 }
2032
2033 return $this->doModifyLimitQuery($query, $limit, $offset);
2034 }
2035
2036 /**
2037 * Adds an platform-specific LIMIT clause to the query.
2038 */
2039 protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string
2040 {
2041 if ($limit !== null) {
2042 $query .= sprintf(' LIMIT %d', $limit);
2043 }
2044
2045 if ($offset > 0) {
2046 $query .= sprintf(' OFFSET %d', $offset);
2047 }
2048
2049 return $query;
2050 }
2051
2052 /**
2053 * Maximum length of any given database identifier, like tables or column names.
2054 */
2055 public function getMaxIdentifierLength(): int
2056 {
2057 return 63;
2058 }
2059
2060 /**
2061 * Returns the insert SQL for an empty insert statement.
2062 */
2063 public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string
2064 {
2065 return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (null)';
2066 }
2067
2068 /**
2069 * Generates a Truncate Table SQL statement for a given table.
2070 *
2071 * Cascade is not supported on many platforms but would optionally cascade the truncate by
2072 * following the foreign keys.
2073 */
2074 public function getTruncateTableSQL(string $tableName, bool $cascade = false): string
2075 {
2076 $tableIdentifier = new Identifier($tableName);
2077
2078 return 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
2079 }
2080
2081 /**
2082 * This is for test reasons, many vendors have special requirements for dummy statements.
2083 */
2084 public function getDummySelectSQL(string $expression = '1'): string
2085 {
2086 return sprintf('SELECT %s', $expression);
2087 }
2088
2089 /**
2090 * Returns the SQL to create a new savepoint.
2091 */
2092 public function createSavePoint(string $savepoint): string
2093 {
2094 return 'SAVEPOINT ' . $savepoint;
2095 }
2096
2097 /**
2098 * Returns the SQL to release a savepoint.
2099 */
2100 public function releaseSavePoint(string $savepoint): string
2101 {
2102 return 'RELEASE SAVEPOINT ' . $savepoint;
2103 }
2104
2105 /**
2106 * Returns the SQL to rollback a savepoint.
2107 */
2108 public function rollbackSavePoint(string $savepoint): string
2109 {
2110 return 'ROLLBACK TO SAVEPOINT ' . $savepoint;
2111 }
2112
2113 /**
2114 * Returns the keyword list instance of this platform.
2115 */
2116 final public function getReservedKeywordsList(): KeywordList
2117 {
2118 // Store the instance so it doesn't need to be generated on every request.
2119 return $this->_keywords ??= $this->createReservedKeywordsList();
2120 }
2121
2122 /**
2123 * Creates an instance of the reserved keyword list of this platform.
2124 */
2125 abstract protected function createReservedKeywordsList(): KeywordList;
2126
2127 /**
2128 * Quotes a literal string.
2129 * This method is NOT meant to fix SQL injections!
2130 * It is only meant to escape this platform's string literal
2131 * quote character inside the given literal string.
2132 *
2133 * @param string $str The literal string to be quoted.
2134 *
2135 * @return string The quoted literal string.
2136 */
2137 public function quoteStringLiteral(string $str): string
2138 {
2139 return "'" . str_replace("'", "''", $str) . "'";
2140 }
2141
2142 /**
2143 * Escapes metacharacters in a string intended to be used with a LIKE
2144 * operator.
2145 *
2146 * @param string $inputString a literal, unquoted string
2147 * @param string $escapeChar should be reused by the caller in the LIKE
2148 * expression.
2149 */
2150 final public function escapeStringForLike(string $inputString, string $escapeChar): string
2151 {
2152 $sql = preg_replace(
2153 '~([' . preg_quote($this->getLikeWildcardCharacters() . $escapeChar, '~') . '])~u',
2154 addcslashes($escapeChar, '\\') . '$1',
2155 $inputString,
2156 );
2157
2158 assert(is_string($sql));
2159
2160 return $sql;
2161 }
2162
2163 /**
2164 * @return array<string,mixed> An associative array with the name of the properties
2165 * of the column being declared as array indexes.
2166 */
2167 private function columnToArray(Column $column): array
2168 {
2169 return array_merge($column->toArray(), [
2170 'name' => $column->getQuotedName($this),
2171 'version' => $column->hasPlatformOption('version') ? $column->getPlatformOption('version') : false,
2172 'comment' => $column->getComment(),
2173 ]);
2174 }
2175
2176 /** @internal */
2177 public function createSQLParser(): Parser
2178 {
2179 return new Parser(false);
2180 }
2181
2182 protected function getLikeWildcardCharacters(): string
2183 {
2184 return '%_';
2185 }
2186
2187 /**
2188 * Compares the definitions of the given columns in the context of this platform.
2189 */
2190 public function columnsEqual(Column $column1, Column $column2): bool
2191 {
2192 $column1Array = $this->columnToArray($column1);
2193 $column2Array = $this->columnToArray($column2);
2194
2195 // ignore explicit columnDefinition since it's not set on the Column generated by the SchemaManager
2196 unset($column1Array['columnDefinition']);
2197 unset($column2Array['columnDefinition']);
2198
2199 if (
2200 $this->getColumnDeclarationSQL('', $column1Array)
2201 !== $this->getColumnDeclarationSQL('', $column2Array)
2202 ) {
2203 return false;
2204 }
2205
2206 // If the platform supports inline comments, all comparison is already done above
2207 if ($this->supportsInlineColumnComments()) {
2208 return true;
2209 }
2210
2211 return $column1->getComment() === $column2->getComment();
2212 }
2213
2214 /**
2215 * Creates the schema manager that can be used to inspect and change the underlying
2216 * database schema according to the dialect of the platform.
2217 */
2218 abstract public function createSchemaManager(Connection $connection): AbstractSchemaManager;
2219}