diff options
Diffstat (limited to 'vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php')
-rw-r--r-- | vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php | 994 |
1 files changed, 994 insertions, 0 deletions
diff --git a/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php b/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php new file mode 100644 index 0000000..acec163 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php | |||
@@ -0,0 +1,994 @@ | |||
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; | ||
9 | use Doctrine\DBAL\Platforms\Exception\NotSupported; | ||
10 | use Doctrine\DBAL\Platforms\Keywords\KeywordList; | ||
11 | use Doctrine\DBAL\Platforms\Keywords\SQLiteKeywords; | ||
12 | use Doctrine\DBAL\Schema\Column; | ||
13 | use Doctrine\DBAL\Schema\Exception\ColumnDoesNotExist; | ||
14 | use Doctrine\DBAL\Schema\ForeignKeyConstraint; | ||
15 | use Doctrine\DBAL\Schema\Identifier; | ||
16 | use Doctrine\DBAL\Schema\Index; | ||
17 | use Doctrine\DBAL\Schema\SQLiteSchemaManager; | ||
18 | use Doctrine\DBAL\Schema\Table; | ||
19 | use Doctrine\DBAL\Schema\TableDiff; | ||
20 | use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder; | ||
21 | use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder; | ||
22 | use Doctrine\DBAL\TransactionIsolationLevel; | ||
23 | use Doctrine\DBAL\Types; | ||
24 | use InvalidArgumentException; | ||
25 | |||
26 | use function array_combine; | ||
27 | use function array_keys; | ||
28 | use function array_merge; | ||
29 | use function array_search; | ||
30 | use function array_unique; | ||
31 | use function array_values; | ||
32 | use function count; | ||
33 | use function explode; | ||
34 | use function implode; | ||
35 | use function sprintf; | ||
36 | use function str_replace; | ||
37 | use function strpos; | ||
38 | use function strtolower; | ||
39 | use function substr; | ||
40 | use function trim; | ||
41 | |||
42 | /** | ||
43 | * The SQLitePlatform class describes the specifics and dialects of the SQLite | ||
44 | * database platform. | ||
45 | */ | ||
46 | class SQLitePlatform extends AbstractPlatform | ||
47 | { | ||
48 | public function getCreateDatabaseSQL(string $name): string | ||
49 | { | ||
50 | throw NotSupported::new(__METHOD__); | ||
51 | } | ||
52 | |||
53 | public function getDropDatabaseSQL(string $name): string | ||
54 | { | ||
55 | throw NotSupported::new(__METHOD__); | ||
56 | } | ||
57 | |||
58 | public function getRegexpExpression(): string | ||
59 | { | ||
60 | return 'REGEXP'; | ||
61 | } | ||
62 | |||
63 | public function getModExpression(string $dividend, string $divisor): string | ||
64 | { | ||
65 | return $dividend . ' % ' . $divisor; | ||
66 | } | ||
67 | |||
68 | public function getTrimExpression( | ||
69 | string $str, | ||
70 | TrimMode $mode = TrimMode::UNSPECIFIED, | ||
71 | ?string $char = null, | ||
72 | ): string { | ||
73 | $trimFn = match ($mode) { | ||
74 | TrimMode::UNSPECIFIED, | ||
75 | TrimMode::BOTH => 'TRIM', | ||
76 | TrimMode::LEADING => 'LTRIM', | ||
77 | TrimMode::TRAILING => 'RTRIM', | ||
78 | }; | ||
79 | |||
80 | $arguments = [$str]; | ||
81 | |||
82 | if ($char !== null) { | ||
83 | $arguments[] = $char; | ||
84 | } | ||
85 | |||
86 | return sprintf('%s(%s)', $trimFn, implode(', ', $arguments)); | ||
87 | } | ||
88 | |||
89 | public function getSubstringExpression(string $string, string $start, ?string $length = null): string | ||
90 | { | ||
91 | if ($length === null) { | ||
92 | return sprintf('SUBSTR(%s, %s)', $string, $start); | ||
93 | } | ||
94 | |||
95 | return sprintf('SUBSTR(%s, %s, %s)', $string, $start, $length); | ||
96 | } | ||
97 | |||
98 | public function getLocateExpression(string $string, string $substring, ?string $start = null): string | ||
99 | { | ||
100 | if ($start === null || $start === '1') { | ||
101 | return sprintf('INSTR(%s, %s)', $string, $substring); | ||
102 | } | ||
103 | |||
104 | return sprintf( | ||
105 | 'CASE WHEN INSTR(SUBSTR(%1$s, %3$s), %2$s) > 0 THEN INSTR(SUBSTR(%1$s, %3$s), %2$s) + %3$s - 1 ELSE 0 END', | ||
106 | $string, | ||
107 | $substring, | ||
108 | $start, | ||
109 | ); | ||
110 | } | ||
111 | |||
112 | protected function getDateArithmeticIntervalExpression( | ||
113 | string $date, | ||
114 | string $operator, | ||
115 | string $interval, | ||
116 | DateIntervalUnit $unit, | ||
117 | ): string { | ||
118 | switch ($unit) { | ||
119 | case DateIntervalUnit::WEEK: | ||
120 | $interval = $this->multiplyInterval($interval, 7); | ||
121 | $unit = DateIntervalUnit::DAY; | ||
122 | break; | ||
123 | |||
124 | case DateIntervalUnit::QUARTER: | ||
125 | $interval = $this->multiplyInterval($interval, 3); | ||
126 | $unit = DateIntervalUnit::MONTH; | ||
127 | break; | ||
128 | } | ||
129 | |||
130 | return 'DATETIME(' . $date . ',' . $this->getConcatExpression( | ||
131 | $this->quoteStringLiteral($operator), | ||
132 | $interval, | ||
133 | $this->quoteStringLiteral(' ' . $unit->value), | ||
134 | ) . ')'; | ||
135 | } | ||
136 | |||
137 | public function getDateDiffExpression(string $date1, string $date2): string | ||
138 | { | ||
139 | return sprintf("JULIANDAY(%s, 'start of day') - JULIANDAY(%s, 'start of day')", $date1, $date2); | ||
140 | } | ||
141 | |||
142 | /** | ||
143 | * {@inheritDoc} | ||
144 | * | ||
145 | * The DBAL doesn't support databases on the SQLite platform. The expression here always returns a fixed string | ||
146 | * as an indicator of an implicitly selected database. | ||
147 | * | ||
148 | * @link https://www.sqlite.org/lang_select.html | ||
149 | * @see Connection::getDatabase() | ||
150 | */ | ||
151 | public function getCurrentDatabaseExpression(): string | ||
152 | { | ||
153 | return "'main'"; | ||
154 | } | ||
155 | |||
156 | /** @link https://www2.sqlite.org/cvstrac/wiki?p=UnsupportedSql */ | ||
157 | public function createSelectSQLBuilder(): SelectSQLBuilder | ||
158 | { | ||
159 | return new DefaultSelectSQLBuilder($this, null, null); | ||
160 | } | ||
161 | |||
162 | protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string | ||
163 | { | ||
164 | return match ($level) { | ||
165 | TransactionIsolationLevel::READ_UNCOMMITTED => '0', | ||
166 | TransactionIsolationLevel::READ_COMMITTED, | ||
167 | TransactionIsolationLevel::REPEATABLE_READ, | ||
168 | TransactionIsolationLevel::SERIALIZABLE => '1', | ||
169 | }; | ||
170 | } | ||
171 | |||
172 | public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string | ||
173 | { | ||
174 | return 'PRAGMA read_uncommitted = ' . $this->_getTransactionIsolationLevelSQL($level); | ||
175 | } | ||
176 | |||
177 | /** | ||
178 | * {@inheritDoc} | ||
179 | */ | ||
180 | public function getBooleanTypeDeclarationSQL(array $column): string | ||
181 | { | ||
182 | return 'BOOLEAN'; | ||
183 | } | ||
184 | |||
185 | /** | ||
186 | * {@inheritDoc} | ||
187 | */ | ||
188 | public function getIntegerTypeDeclarationSQL(array $column): string | ||
189 | { | ||
190 | return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($column); | ||
191 | } | ||
192 | |||
193 | /** | ||
194 | * {@inheritDoc} | ||
195 | */ | ||
196 | public function getBigIntTypeDeclarationSQL(array $column): string | ||
197 | { | ||
198 | // SQLite autoincrement is implicit for INTEGER PKs, but not for BIGINT fields. | ||
199 | if (! empty($column['autoincrement'])) { | ||
200 | return $this->getIntegerTypeDeclarationSQL($column); | ||
201 | } | ||
202 | |||
203 | return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); | ||
204 | } | ||
205 | |||
206 | /** | ||
207 | * {@inheritDoc} | ||
208 | */ | ||
209 | public function getSmallIntTypeDeclarationSQL(array $column): string | ||
210 | { | ||
211 | // SQLite autoincrement is implicit for INTEGER PKs, but not for SMALLINT fields. | ||
212 | if (! empty($column['autoincrement'])) { | ||
213 | return $this->getIntegerTypeDeclarationSQL($column); | ||
214 | } | ||
215 | |||
216 | return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); | ||
217 | } | ||
218 | |||
219 | /** | ||
220 | * {@inheritDoc} | ||
221 | */ | ||
222 | public function getDateTimeTypeDeclarationSQL(array $column): string | ||
223 | { | ||
224 | return 'DATETIME'; | ||
225 | } | ||
226 | |||
227 | /** | ||
228 | * {@inheritDoc} | ||
229 | */ | ||
230 | public function getDateTypeDeclarationSQL(array $column): string | ||
231 | { | ||
232 | return 'DATE'; | ||
233 | } | ||
234 | |||
235 | /** | ||
236 | * {@inheritDoc} | ||
237 | */ | ||
238 | public function getTimeTypeDeclarationSQL(array $column): string | ||
239 | { | ||
240 | return 'TIME'; | ||
241 | } | ||
242 | |||
243 | /** | ||
244 | * {@inheritDoc} | ||
245 | */ | ||
246 | protected function _getCommonIntegerTypeDeclarationSQL(array $column): string | ||
247 | { | ||
248 | // sqlite autoincrement is only possible for the primary key | ||
249 | if (! empty($column['autoincrement'])) { | ||
250 | return ' PRIMARY KEY AUTOINCREMENT'; | ||
251 | } | ||
252 | |||
253 | return ! empty($column['unsigned']) ? ' UNSIGNED' : ''; | ||
254 | } | ||
255 | |||
256 | /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ | ||
257 | public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey): string | ||
258 | { | ||
259 | return parent::getForeignKeyDeclarationSQL(new ForeignKeyConstraint( | ||
260 | $foreignKey->getQuotedLocalColumns($this), | ||
261 | $foreignKey->getQuotedForeignTableName($this), | ||
262 | $foreignKey->getQuotedForeignColumns($this), | ||
263 | $foreignKey->getName(), | ||
264 | $foreignKey->getOptions(), | ||
265 | )); | ||
266 | } | ||
267 | |||
268 | /** | ||
269 | * {@inheritDoc} | ||
270 | */ | ||
271 | protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array | ||
272 | { | ||
273 | $queryFields = $this->getColumnDeclarationListSQL($columns); | ||
274 | |||
275 | if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { | ||
276 | foreach ($options['uniqueConstraints'] as $definition) { | ||
277 | $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition); | ||
278 | } | ||
279 | } | ||
280 | |||
281 | $queryFields .= $this->getNonAutoincrementPrimaryKeyDefinition($columns, $options); | ||
282 | |||
283 | if (isset($options['foreignKeys'])) { | ||
284 | foreach ($options['foreignKeys'] as $foreignKey) { | ||
285 | $queryFields .= ', ' . $this->getForeignKeyDeclarationSQL($foreignKey); | ||
286 | } | ||
287 | } | ||
288 | |||
289 | $tableComment = ''; | ||
290 | if (isset($options['comment'])) { | ||
291 | $comment = trim($options['comment'], " '"); | ||
292 | |||
293 | $tableComment = $this->getInlineTableCommentSQL($comment); | ||
294 | } | ||
295 | |||
296 | $query = ['CREATE TABLE ' . $name . ' ' . $tableComment . '(' . $queryFields . ')']; | ||
297 | |||
298 | if (isset($options['alter']) && $options['alter'] === true) { | ||
299 | return $query; | ||
300 | } | ||
301 | |||
302 | if (isset($options['indexes']) && ! empty($options['indexes'])) { | ||
303 | foreach ($options['indexes'] as $indexDef) { | ||
304 | $query[] = $this->getCreateIndexSQL($indexDef, $name); | ||
305 | } | ||
306 | } | ||
307 | |||
308 | if (isset($options['unique']) && ! empty($options['unique'])) { | ||
309 | foreach ($options['unique'] as $indexDef) { | ||
310 | $query[] = $this->getCreateIndexSQL($indexDef, $name); | ||
311 | } | ||
312 | } | ||
313 | |||
314 | return $query; | ||
315 | } | ||
316 | |||
317 | /** | ||
318 | * Generate a PRIMARY KEY definition if no autoincrement value is used | ||
319 | * | ||
320 | * @param mixed[][] $columns | ||
321 | * @param mixed[] $options | ||
322 | */ | ||
323 | private function getNonAutoincrementPrimaryKeyDefinition(array $columns, array $options): string | ||
324 | { | ||
325 | if (empty($options['primary'])) { | ||
326 | return ''; | ||
327 | } | ||
328 | |||
329 | $keyColumns = array_unique(array_values($options['primary'])); | ||
330 | |||
331 | foreach ($keyColumns as $keyColumn) { | ||
332 | foreach ($columns as $column) { | ||
333 | if ($column['name'] === $keyColumn && ! empty($column['autoincrement'])) { | ||
334 | return ''; | ||
335 | } | ||
336 | } | ||
337 | } | ||
338 | |||
339 | return ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; | ||
340 | } | ||
341 | |||
342 | protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string | ||
343 | { | ||
344 | return 'BLOB'; | ||
345 | } | ||
346 | |||
347 | protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string | ||
348 | { | ||
349 | $sql = 'VARCHAR'; | ||
350 | |||
351 | if ($length !== null) { | ||
352 | $sql .= sprintf('(%d)', $length); | ||
353 | } | ||
354 | |||
355 | return $sql; | ||
356 | } | ||
357 | |||
358 | protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string | ||
359 | { | ||
360 | return 'BLOB'; | ||
361 | } | ||
362 | |||
363 | /** | ||
364 | * {@inheritDoc} | ||
365 | */ | ||
366 | public function getClobTypeDeclarationSQL(array $column): string | ||
367 | { | ||
368 | return 'CLOB'; | ||
369 | } | ||
370 | |||
371 | /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ | ||
372 | public function getListViewsSQL(string $database): string | ||
373 | { | ||
374 | return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL"; | ||
375 | } | ||
376 | |||
377 | /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ | ||
378 | public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string | ||
379 | { | ||
380 | $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey); | ||
381 | |||
382 | if (! $foreignKey->hasOption('deferrable') || $foreignKey->getOption('deferrable') === false) { | ||
383 | $query .= ' NOT'; | ||
384 | } | ||
385 | |||
386 | $query .= ' DEFERRABLE'; | ||
387 | $query .= ' INITIALLY'; | ||
388 | |||
389 | if ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) { | ||
390 | $query .= ' DEFERRED'; | ||
391 | } else { | ||
392 | $query .= ' IMMEDIATE'; | ||
393 | } | ||
394 | |||
395 | return $query; | ||
396 | } | ||
397 | |||
398 | public function supportsIdentityColumns(): bool | ||
399 | { | ||
400 | return true; | ||
401 | } | ||
402 | |||
403 | /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ | ||
404 | public function supportsColumnCollation(): bool | ||
405 | { | ||
406 | return true; | ||
407 | } | ||
408 | |||
409 | /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ | ||
410 | public function supportsInlineColumnComments(): bool | ||
411 | { | ||
412 | return true; | ||
413 | } | ||
414 | |||
415 | public function getTruncateTableSQL(string $tableName, bool $cascade = false): string | ||
416 | { | ||
417 | $tableIdentifier = new Identifier($tableName); | ||
418 | |||
419 | return 'DELETE FROM ' . $tableIdentifier->getQuotedName($this); | ||
420 | } | ||
421 | |||
422 | /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ | ||
423 | public function getInlineColumnCommentSQL(string $comment): string | ||
424 | { | ||
425 | if ($comment === '') { | ||
426 | return ''; | ||
427 | } | ||
428 | |||
429 | return '--' . str_replace("\n", "\n--", $comment) . "\n"; | ||
430 | } | ||
431 | |||
432 | private function getInlineTableCommentSQL(string $comment): string | ||
433 | { | ||
434 | return $this->getInlineColumnCommentSQL($comment); | ||
435 | } | ||
436 | |||
437 | protected function initializeDoctrineTypeMappings(): void | ||
438 | { | ||
439 | $this->doctrineTypeMapping = [ | ||
440 | 'bigint' => 'bigint', | ||
441 | 'bigserial' => 'bigint', | ||
442 | 'blob' => 'blob', | ||
443 | 'boolean' => 'boolean', | ||
444 | 'char' => 'string', | ||
445 | 'clob' => 'text', | ||
446 | 'date' => 'date', | ||
447 | 'datetime' => 'datetime', | ||
448 | 'decimal' => 'decimal', | ||
449 | 'double' => 'float', | ||
450 | 'double precision' => 'float', | ||
451 | 'float' => 'float', | ||
452 | 'image' => 'string', | ||
453 | 'int' => 'integer', | ||
454 | 'integer' => 'integer', | ||
455 | 'longtext' => 'text', | ||
456 | 'longvarchar' => 'string', | ||
457 | 'mediumint' => 'integer', | ||
458 | 'mediumtext' => 'text', | ||
459 | 'ntext' => 'string', | ||
460 | 'numeric' => 'decimal', | ||
461 | 'nvarchar' => 'string', | ||
462 | 'real' => 'float', | ||
463 | 'serial' => 'integer', | ||
464 | 'smallint' => 'smallint', | ||
465 | 'string' => 'string', | ||
466 | 'text' => 'text', | ||
467 | 'time' => 'time', | ||
468 | 'timestamp' => 'datetime', | ||
469 | 'tinyint' => 'boolean', | ||
470 | 'tinytext' => 'text', | ||
471 | 'varchar' => 'string', | ||
472 | 'varchar2' => 'string', | ||
473 | ]; | ||
474 | } | ||
475 | |||
476 | protected function createReservedKeywordsList(): KeywordList | ||
477 | { | ||
478 | return new SQLiteKeywords(); | ||
479 | } | ||
480 | |||
481 | /** | ||
482 | * {@inheritDoc} | ||
483 | */ | ||
484 | protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff): array | ||
485 | { | ||
486 | return []; | ||
487 | } | ||
488 | |||
489 | /** | ||
490 | * {@inheritDoc} | ||
491 | */ | ||
492 | protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff): array | ||
493 | { | ||
494 | $table = $diff->getOldTable(); | ||
495 | |||
496 | $sql = []; | ||
497 | |||
498 | foreach ($this->getIndexesInAlteredTable($diff, $table) as $index) { | ||
499 | if ($index->isPrimary()) { | ||
500 | continue; | ||
501 | } | ||
502 | |||
503 | $sql[] = $this->getCreateIndexSQL($index, $table->getQuotedName($this)); | ||
504 | } | ||
505 | |||
506 | return $sql; | ||
507 | } | ||
508 | |||
509 | protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string | ||
510 | { | ||
511 | if ($limit === null && $offset > 0) { | ||
512 | $limit = -1; | ||
513 | } | ||
514 | |||
515 | return parent::doModifyLimitQuery($query, $limit, $offset); | ||
516 | } | ||
517 | |||
518 | /** | ||
519 | * {@inheritDoc} | ||
520 | */ | ||
521 | public function getBlobTypeDeclarationSQL(array $column): string | ||
522 | { | ||
523 | return 'BLOB'; | ||
524 | } | ||
525 | |||
526 | public function getTemporaryTableName(string $tableName): string | ||
527 | { | ||
528 | return $tableName; | ||
529 | } | ||
530 | |||
531 | /** | ||
532 | * {@inheritDoc} | ||
533 | */ | ||
534 | public function getCreateTablesSQL(array $tables): array | ||
535 | { | ||
536 | $sql = []; | ||
537 | |||
538 | foreach ($tables as $table) { | ||
539 | $sql = array_merge($sql, $this->getCreateTableSQL($table)); | ||
540 | } | ||
541 | |||
542 | return $sql; | ||
543 | } | ||
544 | |||
545 | /** {@inheritDoc} */ | ||
546 | public function getCreateIndexSQL(Index $index, string $table): string | ||
547 | { | ||
548 | $name = $index->getQuotedName($this); | ||
549 | $columns = $index->getColumns(); | ||
550 | |||
551 | if (count($columns) === 0) { | ||
552 | throw new InvalidArgumentException(sprintf( | ||
553 | 'Incomplete or invalid index definition %s on table %s', | ||
554 | $name, | ||
555 | $table, | ||
556 | )); | ||
557 | } | ||
558 | |||
559 | if ($index->isPrimary()) { | ||
560 | return $this->getCreatePrimaryKeySQL($index, $table); | ||
561 | } | ||
562 | |||
563 | if (strpos($table, '.') !== false) { | ||
564 | [$schema, $table] = explode('.', $table); | ||
565 | $name = $schema . '.' . $name; | ||
566 | } | ||
567 | |||
568 | $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table; | ||
569 | $query .= ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index); | ||
570 | |||
571 | return $query; | ||
572 | } | ||
573 | |||
574 | /** | ||
575 | * {@inheritDoc} | ||
576 | */ | ||
577 | public function getDropTablesSQL(array $tables): array | ||
578 | { | ||
579 | $sql = []; | ||
580 | |||
581 | foreach ($tables as $table) { | ||
582 | $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); | ||
583 | } | ||
584 | |||
585 | return $sql; | ||
586 | } | ||
587 | |||
588 | public function getCreatePrimaryKeySQL(Index $index, string $table): string | ||
589 | { | ||
590 | throw NotSupported::new(__METHOD__); | ||
591 | } | ||
592 | |||
593 | public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, string $table): string | ||
594 | { | ||
595 | throw NotSupported::new(__METHOD__); | ||
596 | } | ||
597 | |||
598 | public function getDropForeignKeySQL(string $foreignKey, string $table): string | ||
599 | { | ||
600 | throw NotSupported::new(__METHOD__); | ||
601 | } | ||
602 | |||
603 | /** | ||
604 | * {@inheritDoc} | ||
605 | */ | ||
606 | public function getAlterTableSQL(TableDiff $diff): array | ||
607 | { | ||
608 | $sql = $this->getSimpleAlterTableSQL($diff); | ||
609 | if ($sql !== false) { | ||
610 | return $sql; | ||
611 | } | ||
612 | |||
613 | $table = $diff->getOldTable(); | ||
614 | |||
615 | $columns = []; | ||
616 | $oldColumnNames = []; | ||
617 | $newColumnNames = []; | ||
618 | $columnSql = []; | ||
619 | |||
620 | foreach ($table->getColumns() as $column) { | ||
621 | $columnName = strtolower($column->getName()); | ||
622 | $columns[$columnName] = $column; | ||
623 | $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this); | ||
624 | } | ||
625 | |||
626 | foreach ($diff->getDroppedColumns() as $column) { | ||
627 | $columnName = strtolower($column->getName()); | ||
628 | if (! isset($columns[$columnName])) { | ||
629 | continue; | ||
630 | } | ||
631 | |||
632 | unset( | ||
633 | $columns[$columnName], | ||
634 | $oldColumnNames[$columnName], | ||
635 | $newColumnNames[$columnName], | ||
636 | ); | ||
637 | } | ||
638 | |||
639 | foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { | ||
640 | $oldColumnName = strtolower($oldColumnName); | ||
641 | |||
642 | $columns = $this->replaceColumn( | ||
643 | $table->getName(), | ||
644 | $columns, | ||
645 | $oldColumnName, | ||
646 | $column, | ||
647 | ); | ||
648 | |||
649 | if (! isset($newColumnNames[$oldColumnName])) { | ||
650 | continue; | ||
651 | } | ||
652 | |||
653 | $newColumnNames[$oldColumnName] = $column->getQuotedName($this); | ||
654 | } | ||
655 | |||
656 | foreach ($diff->getModifiedColumns() as $columnDiff) { | ||
657 | $oldColumnName = strtolower($columnDiff->getOldColumn()->getName()); | ||
658 | $newColumn = $columnDiff->getNewColumn(); | ||
659 | |||
660 | $columns = $this->replaceColumn( | ||
661 | $table->getName(), | ||
662 | $columns, | ||
663 | $oldColumnName, | ||
664 | $newColumn, | ||
665 | ); | ||
666 | |||
667 | if (! isset($newColumnNames[$oldColumnName])) { | ||
668 | continue; | ||
669 | } | ||
670 | |||
671 | $newColumnNames[$oldColumnName] = $newColumn->getQuotedName($this); | ||
672 | } | ||
673 | |||
674 | foreach ($diff->getAddedColumns() as $column) { | ||
675 | $columns[strtolower($column->getName())] = $column; | ||
676 | } | ||
677 | |||
678 | $tableName = $table->getName(); | ||
679 | $pos = strpos($tableName, '.'); | ||
680 | if ($pos !== false) { | ||
681 | $tableName = substr($tableName, $pos + 1); | ||
682 | } | ||
683 | |||
684 | $dataTable = new Table('__temp__' . $tableName); | ||
685 | |||
686 | $newTable = new Table( | ||
687 | $table->getQuotedName($this), | ||
688 | $columns, | ||
689 | $this->getPrimaryIndexInAlteredTable($diff, $table), | ||
690 | [], | ||
691 | $this->getForeignKeysInAlteredTable($diff, $table), | ||
692 | $table->getOptions(), | ||
693 | ); | ||
694 | |||
695 | $newTable->addOption('alter', true); | ||
696 | |||
697 | $sql = $this->getPreAlterTableIndexForeignKeySQL($diff); | ||
698 | |||
699 | $sql[] = sprintf( | ||
700 | 'CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s', | ||
701 | $dataTable->getQuotedName($this), | ||
702 | implode(', ', $oldColumnNames), | ||
703 | $table->getQuotedName($this), | ||
704 | ); | ||
705 | $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); | ||
706 | |||
707 | $sql = array_merge($sql, $this->getCreateTableSQL($newTable)); | ||
708 | $sql[] = sprintf( | ||
709 | 'INSERT INTO %s (%s) SELECT %s FROM %s', | ||
710 | $newTable->getQuotedName($this), | ||
711 | implode(', ', $newColumnNames), | ||
712 | implode(', ', $oldColumnNames), | ||
713 | $dataTable->getQuotedName($this), | ||
714 | ); | ||
715 | $sql[] = $this->getDropTableSQL($dataTable->getQuotedName($this)); | ||
716 | |||
717 | return array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff), $columnSql); | ||
718 | } | ||
719 | |||
720 | /** | ||
721 | * Replace the column with the given name with the new column. | ||
722 | * | ||
723 | * @param array<string,Column> $columns | ||
724 | * | ||
725 | * @return array<string,Column> | ||
726 | * | ||
727 | * @throws Exception | ||
728 | */ | ||
729 | private function replaceColumn(string $tableName, array $columns, string $columnName, Column $column): array | ||
730 | { | ||
731 | $keys = array_keys($columns); | ||
732 | $index = array_search($columnName, $keys, true); | ||
733 | |||
734 | if ($index === false) { | ||
735 | throw ColumnDoesNotExist::new($columnName, $tableName); | ||
736 | } | ||
737 | |||
738 | $values = array_values($columns); | ||
739 | |||
740 | $keys[$index] = strtolower($column->getName()); | ||
741 | $values[$index] = $column; | ||
742 | |||
743 | return array_combine($keys, $values); | ||
744 | } | ||
745 | |||
746 | /** | ||
747 | * @return list<string>|false | ||
748 | * | ||
749 | * @throws Exception | ||
750 | */ | ||
751 | private function getSimpleAlterTableSQL(TableDiff $diff): array|false | ||
752 | { | ||
753 | if ( | ||
754 | count($diff->getModifiedColumns()) > 0 | ||
755 | || count($diff->getDroppedColumns()) > 0 | ||
756 | || count($diff->getRenamedColumns()) > 0 | ||
757 | || count($diff->getAddedIndexes()) > 0 | ||
758 | || count($diff->getModifiedIndexes()) > 0 | ||
759 | || count($diff->getDroppedIndexes()) > 0 | ||
760 | || count($diff->getRenamedIndexes()) > 0 | ||
761 | || count($diff->getAddedForeignKeys()) > 0 | ||
762 | || count($diff->getModifiedForeignKeys()) > 0 | ||
763 | || count($diff->getDroppedForeignKeys()) > 0 | ||
764 | ) { | ||
765 | return false; | ||
766 | } | ||
767 | |||
768 | $table = $diff->getOldTable(); | ||
769 | |||
770 | $sql = []; | ||
771 | $columnSql = []; | ||
772 | |||
773 | foreach ($diff->getAddedColumns() as $column) { | ||
774 | $definition = array_merge([ | ||
775 | 'unique' => null, | ||
776 | 'autoincrement' => null, | ||
777 | 'default' => null, | ||
778 | ], $column->toArray()); | ||
779 | |||
780 | $type = $definition['type']; | ||
781 | |||
782 | /** @psalm-suppress RiskyTruthyFalsyComparison */ | ||
783 | switch (true) { | ||
784 | case isset($definition['columnDefinition']) || $definition['autoincrement'] || $definition['unique']: | ||
785 | case $type instanceof Types\DateTimeType && $definition['default'] === $this->getCurrentTimestampSQL(): | ||
786 | case $type instanceof Types\DateType && $definition['default'] === $this->getCurrentDateSQL(): | ||
787 | case $type instanceof Types\TimeType && $definition['default'] === $this->getCurrentTimeSQL(): | ||
788 | return false; | ||
789 | } | ||
790 | |||
791 | $definition['name'] = $column->getQuotedName($this); | ||
792 | |||
793 | $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' ADD COLUMN ' | ||
794 | . $this->getColumnDeclarationSQL($definition['name'], $definition); | ||
795 | } | ||
796 | |||
797 | return array_merge($sql, $columnSql); | ||
798 | } | ||
799 | |||
800 | /** @return string[] */ | ||
801 | private function getColumnNamesInAlteredTable(TableDiff $diff, Table $oldTable): array | ||
802 | { | ||
803 | $columns = []; | ||
804 | |||
805 | foreach ($oldTable->getColumns() as $column) { | ||
806 | $columnName = $column->getName(); | ||
807 | $columns[strtolower($columnName)] = $columnName; | ||
808 | } | ||
809 | |||
810 | foreach ($diff->getDroppedColumns() as $column) { | ||
811 | $columnName = strtolower($column->getName()); | ||
812 | if (! isset($columns[$columnName])) { | ||
813 | continue; | ||
814 | } | ||
815 | |||
816 | unset($columns[$columnName]); | ||
817 | } | ||
818 | |||
819 | foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { | ||
820 | $columnName = $column->getName(); | ||
821 | $columns[strtolower($oldColumnName)] = $columnName; | ||
822 | $columns[strtolower($columnName)] = $columnName; | ||
823 | } | ||
824 | |||
825 | foreach ($diff->getModifiedColumns() as $columnDiff) { | ||
826 | $oldColumnName = $columnDiff->getOldColumn()->getName(); | ||
827 | $newColumnName = $columnDiff->getNewColumn()->getName(); | ||
828 | $columns[strtolower($oldColumnName)] = $newColumnName; | ||
829 | $columns[strtolower($newColumnName)] = $newColumnName; | ||
830 | } | ||
831 | |||
832 | foreach ($diff->getAddedColumns() as $column) { | ||
833 | $columnName = $column->getName(); | ||
834 | $columns[strtolower($columnName)] = $columnName; | ||
835 | } | ||
836 | |||
837 | return $columns; | ||
838 | } | ||
839 | |||
840 | /** @return Index[] */ | ||
841 | private function getIndexesInAlteredTable(TableDiff $diff, Table $oldTable): array | ||
842 | { | ||
843 | $indexes = $oldTable->getIndexes(); | ||
844 | $columnNames = $this->getColumnNamesInAlteredTable($diff, $oldTable); | ||
845 | |||
846 | foreach ($indexes as $key => $index) { | ||
847 | foreach ($diff->getRenamedIndexes() as $oldIndexName => $renamedIndex) { | ||
848 | if (strtolower($key) !== strtolower($oldIndexName)) { | ||
849 | continue; | ||
850 | } | ||
851 | |||
852 | unset($indexes[$key]); | ||
853 | } | ||
854 | |||
855 | $changed = false; | ||
856 | $indexColumns = []; | ||
857 | foreach ($index->getColumns() as $columnName) { | ||
858 | $normalizedColumnName = strtolower($columnName); | ||
859 | if (! isset($columnNames[$normalizedColumnName])) { | ||
860 | unset($indexes[$key]); | ||
861 | continue 2; | ||
862 | } | ||
863 | |||
864 | $indexColumns[] = $columnNames[$normalizedColumnName]; | ||
865 | if ($columnName === $columnNames[$normalizedColumnName]) { | ||
866 | continue; | ||
867 | } | ||
868 | |||
869 | $changed = true; | ||
870 | } | ||
871 | |||
872 | if (! $changed) { | ||
873 | continue; | ||
874 | } | ||
875 | |||
876 | $indexes[$key] = new Index( | ||
877 | $index->getName(), | ||
878 | $indexColumns, | ||
879 | $index->isUnique(), | ||
880 | $index->isPrimary(), | ||
881 | $index->getFlags(), | ||
882 | ); | ||
883 | } | ||
884 | |||
885 | foreach ($diff->getDroppedIndexes() as $index) { | ||
886 | $indexName = $index->getName(); | ||
887 | |||
888 | if ($indexName === '') { | ||
889 | continue; | ||
890 | } | ||
891 | |||
892 | unset($indexes[strtolower($indexName)]); | ||
893 | } | ||
894 | |||
895 | foreach ( | ||
896 | array_merge( | ||
897 | $diff->getModifiedIndexes(), | ||
898 | $diff->getAddedIndexes(), | ||
899 | $diff->getRenamedIndexes(), | ||
900 | ) as $index | ||
901 | ) { | ||
902 | $indexName = $index->getName(); | ||
903 | |||
904 | if ($indexName !== '') { | ||
905 | $indexes[strtolower($indexName)] = $index; | ||
906 | } else { | ||
907 | $indexes[] = $index; | ||
908 | } | ||
909 | } | ||
910 | |||
911 | return $indexes; | ||
912 | } | ||
913 | |||
914 | /** @return ForeignKeyConstraint[] */ | ||
915 | private function getForeignKeysInAlteredTable(TableDiff $diff, Table $oldTable): array | ||
916 | { | ||
917 | $foreignKeys = $oldTable->getForeignKeys(); | ||
918 | $columnNames = $this->getColumnNamesInAlteredTable($diff, $oldTable); | ||
919 | |||
920 | foreach ($foreignKeys as $key => $constraint) { | ||
921 | $changed = false; | ||
922 | $localColumns = []; | ||
923 | foreach ($constraint->getLocalColumns() as $columnName) { | ||
924 | $normalizedColumnName = strtolower($columnName); | ||
925 | if (! isset($columnNames[$normalizedColumnName])) { | ||
926 | unset($foreignKeys[$key]); | ||
927 | continue 2; | ||
928 | } | ||
929 | |||
930 | $localColumns[] = $columnNames[$normalizedColumnName]; | ||
931 | if ($columnName === $columnNames[$normalizedColumnName]) { | ||
932 | continue; | ||
933 | } | ||
934 | |||
935 | $changed = true; | ||
936 | } | ||
937 | |||
938 | if (! $changed) { | ||
939 | continue; | ||
940 | } | ||
941 | |||
942 | $foreignKeys[$key] = new ForeignKeyConstraint( | ||
943 | $localColumns, | ||
944 | $constraint->getForeignTableName(), | ||
945 | $constraint->getForeignColumns(), | ||
946 | $constraint->getName(), | ||
947 | $constraint->getOptions(), | ||
948 | ); | ||
949 | } | ||
950 | |||
951 | foreach ($diff->getDroppedForeignKeys() as $constraint) { | ||
952 | $constraintName = $constraint->getName(); | ||
953 | |||
954 | if ($constraintName === '') { | ||
955 | continue; | ||
956 | } | ||
957 | |||
958 | unset($foreignKeys[strtolower($constraintName)]); | ||
959 | } | ||
960 | |||
961 | foreach (array_merge($diff->getModifiedForeignKeys(), $diff->getAddedForeignKeys()) as $constraint) { | ||
962 | $constraintName = $constraint->getName(); | ||
963 | |||
964 | if ($constraintName !== '') { | ||
965 | $foreignKeys[strtolower($constraintName)] = $constraint; | ||
966 | } else { | ||
967 | $foreignKeys[] = $constraint; | ||
968 | } | ||
969 | } | ||
970 | |||
971 | return $foreignKeys; | ||
972 | } | ||
973 | |||
974 | /** @return Index[] */ | ||
975 | private function getPrimaryIndexInAlteredTable(TableDiff $diff, Table $oldTable): array | ||
976 | { | ||
977 | $primaryIndex = []; | ||
978 | |||
979 | foreach ($this->getIndexesInAlteredTable($diff, $oldTable) as $index) { | ||
980 | if (! $index->isPrimary()) { | ||
981 | continue; | ||
982 | } | ||
983 | |||
984 | $primaryIndex = [$index->getName() => $index]; | ||
985 | } | ||
986 | |||
987 | return $primaryIndex; | ||
988 | } | ||
989 | |||
990 | public function createSchemaManager(Connection $connection): SQLiteSchemaManager | ||
991 | { | ||
992 | return new SQLiteSchemaManager($connection, $this); | ||
993 | } | ||
994 | } | ||