1221e471f3cef412a4fc23b5746bd9d3a3a63da5
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Database / Schema / ConnectionMigrator.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Core\Database\Schema;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Doctrine\DBAL\DBALException;
19 use Doctrine\DBAL\Platforms\MySqlPlatform;
20 use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
21 use Doctrine\DBAL\Schema\Column;
22 use Doctrine\DBAL\Schema\ColumnDiff;
23 use Doctrine\DBAL\Schema\ForeignKeyConstraint;
24 use Doctrine\DBAL\Schema\Index;
25 use Doctrine\DBAL\Schema\Schema;
26 use Doctrine\DBAL\Schema\SchemaConfig;
27 use Doctrine\DBAL\Schema\SchemaDiff;
28 use Doctrine\DBAL\Schema\Table;
29 use TYPO3\CMS\Core\Database\Connection;
30 use TYPO3\CMS\Core\Database\ConnectionPool;
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32
33 /**
34 * Handling schema migrations per connection.
35 *
36 * @internal
37 */
38 class ConnectionMigrator
39 {
40 /**
41 * @var string Prefix of deleted tables
42 */
43 protected $deletedPrefix = 'zzz_deleted_';
44
45 /**
46 * @var array
47 */
48 protected $tableAndFieldMaxNameLengthsPerDbPlatform = [
49 'default' => [
50 'tables' => 30,
51 'columns' => 30
52 ],
53 'mysql' => [
54 'tables' => 64,
55 'columns' => 64
56 ],
57 'drizzle_pdo_mysql' => 'mysql',
58 'mysqli' => 'mysql',
59 'pdo_mysql' => 'mysql',
60 'pdo_sqlite' => 'mysql',
61 'postgresql' => [
62 'tables' => 63,
63 'columns' => 63
64 ],
65 'sqlserver' => [
66 'tables' => 128,
67 'columns' => 128
68 ],
69 'pdo_sqlsrv' => 'sqlserver',
70 'sqlsrv' => 'sqlserver',
71 'ibm' => [
72 'tables' => 30,
73 'columns' => 30
74 ],
75 'ibm_db2' => 'ibm',
76 'pdo_ibm' => 'ibm',
77 'oci8' => [
78 'tables' => 30,
79 'columns' => 30
80 ],
81 'sqlanywhere' => [
82 'tables' => 128,
83 'columns' => 128
84 ]
85 ];
86
87 /**
88 * @var Connection
89 */
90 protected $connection;
91
92 /**
93 * @var string
94 */
95 protected $connectionName;
96
97 /**
98 * @var Table[]
99 */
100 protected $tables;
101
102 /**
103 * @param string $connectionName
104 * @param Table[] $tables
105 */
106 public function __construct(string $connectionName, array $tables)
107 {
108 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
109 $this->connection = $connectionPool->getConnectionByName($connectionName);
110 $this->connectionName = $connectionName;
111 $this->tables = $tables;
112 }
113
114 /**
115 * @param string $connectionName
116 * @param Table[] $tables
117 * @return ConnectionMigrator
118 */
119 public static function create(string $connectionName, array $tables)
120 {
121 return GeneralUtility::makeInstance(
122 static::class,
123 $connectionName,
124 $tables
125 );
126 }
127
128 /**
129 * Return the raw Doctrine SchemaDiff object for the current connection.
130 * This diff contains all changes without any pre-processing.
131 *
132 * @return SchemaDiff
133 */
134 public function getSchemaDiff(): SchemaDiff
135 {
136 return $this->buildSchemaDiff(false);
137 }
138
139 /**
140 * Compare current and expected schema definitions and provide updates
141 * suggestions in the form of SQL statements.
142 *
143 * @param bool $remove
144 * @return array
145 */
146 public function getUpdateSuggestions(bool $remove = false): array
147 {
148 $schemaDiff = $this->buildSchemaDiff();
149
150 if ($remove === false) {
151 return array_merge_recursive(
152 ['add' => [], 'create_table' => [], 'change' => [], 'change_currentValue' => []],
153 $this->getNewFieldUpdateSuggestions($schemaDiff),
154 $this->getNewTableUpdateSuggestions($schemaDiff),
155 $this->getChangedFieldUpdateSuggestions($schemaDiff),
156 $this->getChangedTableOptions($schemaDiff)
157 );
158 } else {
159 return array_merge_recursive(
160 ['change' => [], 'change_table' => [], 'drop' => [], 'drop_table' => [], 'tables_count' => []],
161 $this->getUnusedFieldUpdateSuggestions($schemaDiff),
162 $this->getUnusedTableUpdateSuggestions($schemaDiff),
163 $this->getDropTableUpdateSuggestions($schemaDiff),
164 $this->getDropFieldUpdateSuggestions($schemaDiff)
165 );
166 }
167 }
168
169 /**
170 * Perform add/change/create operations on tables and fields in an
171 * optimized, non-interactive, mode using the original doctrine
172 * SchemaManager ->toSaveSql() method.
173 *
174 * @param bool $createOnly
175 * @return array
176 */
177 public function install(bool $createOnly = false): array
178 {
179 $result = [];
180 $schemaDiff = $this->buildSchemaDiff(false);
181
182 $schemaDiff->removedTables = [];
183 foreach ($schemaDiff->changedTables as $key => $changedTable) {
184 $schemaDiff->changedTables[$key]->removedColumns = [];
185 $schemaDiff->changedTables[$key]->removedIndexes = [];
186
187 // With partial ext_tables.sql files the SchemaManager is detecting
188 // existing columns as false positives for a column rename. In this
189 // context every rename is actually a new column.
190 foreach ($changedTable->renamedColumns as $columnName => $renamedColumn) {
191 $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance(
192 Column::class,
193 $renamedColumn->getName(),
194 $renamedColumn->getType(),
195 array_diff_key($renamedColumn->toArray(), ['name', 'type'])
196 );
197 unset($changedTable->renamedColumns[$columnName]);
198 }
199
200 if ($createOnly) {
201 // Ignore new indexes that work on columns that need changes
202 foreach ($changedTable->addedIndexes as $indexName => $addedIndex) {
203 $indexColumns = array_map(
204 function ($columnName) {
205 // Strip MySQL prefix length information to get real column names
206 $columnName = preg_replace('/\(\d+\)$/', '', $columnName);
207 // Strip mssql '[' and ']' from column names
208 $columnName = ltrim($columnName, '[');
209 return rtrim($columnName, ']');
210 },
211 $addedIndex->getColumns()
212 );
213 $columnChanges = array_intersect($indexColumns, array_keys($changedTable->changedColumns));
214 if (!empty($columnChanges)) {
215 unset($schemaDiff->changedTables[$key]->addedIndexes[$indexName]);
216 }
217 }
218 $schemaDiff->changedTables[$key]->changedColumns = [];
219 $schemaDiff->changedTables[$key]->changedIndexes = [];
220 $schemaDiff->changedTables[$key]->renamedIndexes = [];
221 }
222 }
223
224 $statements = $schemaDiff->toSaveSql(
225 $this->connection->getDatabasePlatform()
226 );
227
228 foreach ($statements as $statement) {
229 try {
230 $this->connection->executeUpdate($statement);
231 $result[$statement] = '';
232 } catch (DBALException $e) {
233 $result[$statement] = $e->getPrevious()->getMessage();
234 }
235 }
236
237 return $result;
238 }
239
240 /**
241 * If the schema is not for the Default connection remove all tables from the schema
242 * that have no mapping in the TYPO3 configuration. This avoids update suggestions
243 * for tables that are in the database but have no direct relation to the TYPO3 instance.
244 *
245 * @param bool $renameUnused
246 * @throws \Doctrine\DBAL\DBALException
247 * @return \Doctrine\DBAL\Schema\SchemaDiff
248 * @throws \Doctrine\DBAL\Schema\SchemaException
249 * @throws \InvalidArgumentException
250 */
251 protected function buildSchemaDiff(bool $renameUnused = true): SchemaDiff
252 {
253 // Build the schema definitions
254 $fromSchema = $this->connection->getSchemaManager()->createSchema();
255 $toSchema = $this->buildExpectedSchemaDefinitions($this->connectionName);
256
257 // Add current table options to the fromSchema
258 $tableOptions = $this->getTableOptions($fromSchema->getTableNames());
259 foreach ($fromSchema->getTables() as $table) {
260 $tableName = $table->getName();
261 if (!array_key_exists($tableName, $tableOptions)) {
262 continue;
263 }
264 foreach ($tableOptions[$tableName] as $optionName => $optionValue) {
265 $table->addOption($optionName, $optionValue);
266 }
267 }
268
269 // Build SchemaDiff and handle renames of tables and colums
270 $comparator = GeneralUtility::makeInstance(Comparator::class, $this->connection->getDatabasePlatform());
271 $schemaDiff = $comparator->compare($fromSchema, $toSchema);
272 $schemaDiff = $this->migrateColumnRenamesToDistinctActions($schemaDiff);
273
274 if ($renameUnused) {
275 $schemaDiff = $this->migrateUnprefixedRemovedTablesToRenames($schemaDiff);
276 $schemaDiff = $this->migrateUnprefixedRemovedFieldsToRenames($schemaDiff);
277 }
278
279 // All tables in the default connection are managed by TYPO3
280 if ($this->connectionName === ConnectionPool::DEFAULT_CONNECTION_NAME) {
281 return $schemaDiff;
282 }
283
284 // If there are no mapped tables return a SchemaDiff without any changes
285 // to avoid update suggestions for tables not related to TYPO3.
286 if (empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
287 || !is_array($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
288 ) {
289 /** @var SchemaDiff $schemaDiff */
290 $schemaDiff = GeneralUtility::makeInstance(SchemaDiff::class, [], [], [], $fromSchema);
291
292 return $schemaDiff;
293 }
294
295 // Collect the table names that have been mapped to this connection.
296 $connectionName = $this->connectionName;
297 $tablesForConnection = array_keys(
298 array_filter(
299 $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'],
300 function ($tableConnectionName) use ($connectionName) {
301 return $tableConnectionName === $connectionName;
302 }
303 )
304 );
305
306 // Remove all tables that are not assigned to this connection from the diff
307 $schemaDiff->newTables = $this->removeUnrelatedTables($schemaDiff->newTables, $tablesForConnection);
308 $schemaDiff->changedTables = $this->removeUnrelatedTables($schemaDiff->changedTables, $tablesForConnection);
309 $schemaDiff->removedTables = $this->removeUnrelatedTables($schemaDiff->removedTables, $tablesForConnection);
310
311 return $schemaDiff;
312 }
313
314 /**
315 * Build the expected schema definitons from raw SQL statements.
316 *
317 * @param string $connectionName
318 * @return \Doctrine\DBAL\Schema\Schema
319 * @throws \Doctrine\DBAL\DBALException
320 * @throws \InvalidArgumentException
321 */
322 protected function buildExpectedSchemaDefinitions(string $connectionName): Schema
323 {
324 /** @var Table[] $tablesForConnection */
325 $tablesForConnection = [];
326 foreach ($this->tables as $table) {
327 $tableName = $table->getName();
328
329 // Skip tables for a different connection
330 if ($connectionName !== $this->getConnectionNameForTable($tableName)) {
331 continue;
332 }
333
334 if (!array_key_exists($tableName, $tablesForConnection)) {
335 $tablesForConnection[$tableName] = $table;
336 continue;
337 }
338
339 // Merge multiple table definitions. Later definitions overrule identical
340 // columns, indexes and foreign_keys. Order of definitions is based on
341 // extension load order.
342 $currentTableDefinition = $tablesForConnection[$tableName];
343 $tablesForConnection[$tableName] = GeneralUtility::makeInstance(
344 Table::class,
345 $tableName,
346 array_merge($currentTableDefinition->getColumns(), $table->getColumns()),
347 array_merge($currentTableDefinition->getIndexes(), $table->getIndexes()),
348 array_merge($currentTableDefinition->getForeignKeys(), $table->getForeignKeys()),
349 0,
350 array_merge($currentTableDefinition->getOptions(), $table->getOptions())
351 );
352 }
353
354 $tablesForConnection = $this->transformTablesForDatabasePlatform($tablesForConnection, $this->connection);
355
356 $schemaConfig = GeneralUtility::makeInstance(SchemaConfig::class);
357 $schemaConfig->setName($this->connection->getDatabase());
358
359 return GeneralUtility::makeInstance(Schema::class, $tablesForConnection, [], $schemaConfig);
360 }
361
362 /**
363 * Extract the update suggestions (SQL statements) for newly added tables
364 * from the complete schema diff.
365 *
366 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
367 * @return array
368 * @throws \InvalidArgumentException
369 */
370 protected function getNewTableUpdateSuggestions(SchemaDiff $schemaDiff): array
371 {
372 // Build a new schema diff that only contains added tables
373 $addTableSchemaDiff = GeneralUtility::makeInstance(
374 SchemaDiff::class,
375 $schemaDiff->newTables,
376 [],
377 [],
378 $schemaDiff->fromSchema
379 );
380
381 $statements = $addTableSchemaDiff->toSql($this->connection->getDatabasePlatform());
382
383 return ['create_table' => $this->calculateUpdateSuggestionsHashes($statements)];
384 }
385
386 /**
387 * Extract the update suggestions (SQL statements) for newly added fields
388 * from the complete schema diff.
389 *
390 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
391 * @return array
392 * @throws \Doctrine\DBAL\Schema\SchemaException
393 * @throws \InvalidArgumentException
394 */
395 protected function getNewFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
396 {
397 $changedTables = [];
398
399 foreach ($schemaDiff->changedTables as $index => $changedTable) {
400 $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
401
402 if (count($changedTable->addedColumns) !== 0) {
403 // Treat each added column with a new diff to get a dedicated suggestions
404 // just for this single column.
405 foreach ($changedTable->addedColumns as $addedColumn) {
406 $changedTables[$index . ':tbl_' . $addedColumn->getName()] = GeneralUtility::makeInstance(
407 TableDiff::class,
408 $changedTable->name,
409 [$addedColumn],
410 [],
411 [],
412 [],
413 [],
414 [],
415 $fromTable
416 );
417 }
418 }
419
420 if (count($changedTable->addedIndexes) !== 0) {
421 // Treat each added index with a new diff to get a dedicated suggestions
422 // just for this index.
423 foreach ($changedTable->addedIndexes as $addedIndex) {
424 $changedTables[$index . ':idx_' . $addedIndex->getName()] = GeneralUtility::makeInstance(
425 TableDiff::class,
426 $changedTable->name,
427 [],
428 [],
429 [],
430 [$this->buildQuotedIndex($addedIndex)],
431 [],
432 [],
433 $fromTable
434 );
435 }
436 }
437
438 if (count($changedTable->addedForeignKeys) !== 0) {
439 // Treat each added foreign key with a new diff to get a dedicated suggestions
440 // just for this foreign key.
441 foreach ($changedTable->addedForeignKeys as $addedForeignKey) {
442 $fkIndex = $index . ':fk_' . $addedForeignKey->getName();
443 $changedTables[$fkIndex] = GeneralUtility::makeInstance(
444 TableDiff::class,
445 $changedTable->name,
446 [],
447 [],
448 [],
449 [],
450 [],
451 [],
452 $fromTable
453 );
454 $changedTables[$fkIndex]->addedForeignKeys = [$this->buildQuotedForeignKey($addedForeignKey)];
455 }
456 }
457 }
458
459 // Build a new schema diff that only contains added fields
460 $addFieldSchemaDiff = GeneralUtility::makeInstance(
461 SchemaDiff::class,
462 [],
463 $changedTables,
464 [],
465 $schemaDiff->fromSchema
466 );
467
468 $statements = $addFieldSchemaDiff->toSql($this->connection->getDatabasePlatform());
469
470 return ['add' => $this->calculateUpdateSuggestionsHashes($statements)];
471 }
472
473 /**
474 * Extract update suggestions (SQL statements) for changed options
475 * (like ENGINE) from the complete schema diff.
476 *
477 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
478 * @return array
479 * @throws \Doctrine\DBAL\Schema\SchemaException
480 * @throws \InvalidArgumentException
481 */
482 protected function getChangedTableOptions(SchemaDiff $schemaDiff): array
483 {
484 $updateSuggestions = [];
485
486 foreach ($schemaDiff->changedTables as $tableDiff) {
487 // Skip processing if this is the base TableDiff class or has no table options set.
488 if (!$tableDiff instanceof TableDiff || count($tableDiff->getTableOptions()) === 0) {
489 continue;
490 }
491
492 $tableOptions = $tableDiff->getTableOptions();
493 $tableOptionsDiff = GeneralUtility::makeInstance(
494 TableDiff::class,
495 $tableDiff->name,
496 [],
497 [],
498 [],
499 [],
500 [],
501 [],
502 $tableDiff->fromTable
503 );
504 $tableOptionsDiff->setTableOptions($tableOptions);
505
506 $tableOptionsSchemaDiff = GeneralUtility::makeInstance(
507 SchemaDiff::class,
508 [],
509 [$tableOptionsDiff],
510 [],
511 $schemaDiff->fromSchema
512 );
513
514 $statements = $tableOptionsSchemaDiff->toSaveSql($this->connection->getDatabasePlatform());
515 foreach ($statements as $statement) {
516 $updateSuggestions['change'][md5($statement)] = $statement;
517 }
518 }
519
520 return $updateSuggestions;
521 }
522
523 /**
524 * Extract update suggestions (SQL statements) for changed fields
525 * from the complete schema diff.
526 *
527 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
528 * @return array
529 * @throws \Doctrine\DBAL\Schema\SchemaException
530 * @throws \InvalidArgumentException
531 */
532 protected function getChangedFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
533 {
534 $databasePlatform = $this->connection->getDatabasePlatform();
535 $updateSuggestions = [];
536
537 foreach ($schemaDiff->changedTables as $index => $changedTable) {
538 if (count($changedTable->changedColumns) !== 0) {
539 // Treat each changed column with a new diff to get a dedicated suggestions
540 // just for this single column.
541 $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
542
543 foreach ($changedTable->changedColumns as $changedColumn) {
544 // Field has been renamed and will be handled separately
545 if ($changedColumn->getOldColumnName()->getName() !== $changedColumn->column->getName()) {
546 continue;
547 }
548
549 $changedColumn->fromColumn = $this->buildQuotedColumn($changedColumn->fromColumn);
550
551 // Get the current SQL declaration for the column
552 $currentColumn = $fromTable->getColumn($changedColumn->getOldColumnName()->getName());
553 $currentDeclaration = $databasePlatform->getColumnDeclarationSQL(
554 $currentColumn->getQuotedName($this->connection->getDatabasePlatform()),
555 $currentColumn->toArray()
556 );
557
558 // Build a dedicated diff just for the current column
559 $tableDiff = GeneralUtility::makeInstance(
560 TableDiff::class,
561 $changedTable->name,
562 [],
563 [$changedColumn],
564 [],
565 [],
566 [],
567 [],
568 $fromTable
569 );
570
571 $temporarySchemaDiff = GeneralUtility::makeInstance(
572 SchemaDiff::class,
573 [],
574 [$tableDiff],
575 [],
576 $schemaDiff->fromSchema
577 );
578
579 $statements = $temporarySchemaDiff->toSql($databasePlatform);
580 foreach ($statements as $statement) {
581 $updateSuggestions['change'][md5($statement)] = $statement;
582 $updateSuggestions['change_currentValue'][md5($statement)] = $currentDeclaration;
583 }
584 }
585 }
586
587 // Treat each changed index with a new diff to get a dedicated suggestions
588 // just for this index.
589 if (count($changedTable->changedIndexes) !== 0) {
590 foreach ($changedTable->renamedIndexes as $key => $changedIndex) {
591 $indexDiff = GeneralUtility::makeInstance(
592 TableDiff::class,
593 $changedTable->name,
594 [],
595 [],
596 [],
597 [],
598 [$changedIndex],
599 [],
600 $schemaDiff->fromSchema->getTable($changedTable->name)
601 );
602
603 $temporarySchemaDiff = GeneralUtility::makeInstance(
604 SchemaDiff::class,
605 [],
606 [$indexDiff],
607 [],
608 $schemaDiff->fromSchema
609 );
610
611 $statements = $temporarySchemaDiff->toSql($databasePlatform);
612 foreach ($statements as $statement) {
613 $updateSuggestions['change'][md5($statement)] = $statement;
614 }
615 }
616 }
617
618 // Treat renamed indexes as a field change as it's a simple rename operation
619 if (count($changedTable->renamedIndexes) !== 0) {
620 // Create a base table diff without any changes, there's no constructor
621 // argument to pass in renamed indexes.
622 $tableDiff = GeneralUtility::makeInstance(
623 TableDiff::class,
624 $changedTable->name,
625 [],
626 [],
627 [],
628 [],
629 [],
630 [],
631 $schemaDiff->fromSchema->getTable($changedTable->name)
632 );
633
634 // Treat each renamed index with a new diff to get a dedicated suggestions
635 // just for this index.
636 foreach ($changedTable->renamedIndexes as $key => $renamedIndex) {
637 $indexDiff = clone $tableDiff;
638 $indexDiff->renamedIndexes = [$key => $renamedIndex];
639
640 $temporarySchemaDiff = GeneralUtility::makeInstance(
641 SchemaDiff::class,
642 [],
643 [$indexDiff],
644 [],
645 $schemaDiff->fromSchema
646 );
647
648 $statements = $temporarySchemaDiff->toSql($databasePlatform);
649 foreach ($statements as $statement) {
650 $updateSuggestions['change'][md5($statement)] = $statement;
651 }
652 }
653 }
654
655 // Treat each changed foreign key with a new diff to get a dedicated suggestions
656 // just for this foreign key.
657 if (count($changedTable->changedForeignKeys) !== 0) {
658 $tableDiff = GeneralUtility::makeInstance(
659 TableDiff::class,
660 $changedTable->name,
661 [],
662 [],
663 [],
664 [],
665 [],
666 [],
667 $schemaDiff->fromSchema->getTable($changedTable->name)
668 );
669
670 foreach ($changedTable->changedForeignKeys as $changedForeignKey) {
671 $foreignKeyDiff = clone $tableDiff;
672 $foreignKeyDiff->changedForeignKeys = [$this->buildQuotedForeignKey($changedForeignKey)];
673
674 $temporarySchemaDiff = GeneralUtility::makeInstance(
675 SchemaDiff::class,
676 [],
677 [$foreignKeyDiff],
678 [],
679 $schemaDiff->fromSchema
680 );
681
682 $statements = $temporarySchemaDiff->toSql($databasePlatform);
683 foreach ($statements as $statement) {
684 $updateSuggestions['change'][md5($statement)] = $statement;
685 }
686 }
687 }
688 }
689
690 return $updateSuggestions;
691 }
692
693 /**
694 * Extract update suggestions (SQL statements) for tables that are
695 * no longer present in the expected schema from the schema diff.
696 * In this case the update suggestions are renames of the tables
697 * with a prefix to mark them for deletion in a second sweep.
698 *
699 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
700 * @return array
701 * @throws \Doctrine\DBAL\Schema\SchemaException
702 * @throws \InvalidArgumentException
703 */
704 protected function getUnusedTableUpdateSuggestions(SchemaDiff $schemaDiff): array
705 {
706 $updateSuggestions = [];
707 foreach ($schemaDiff->changedTables as $tableDiff) {
708 // Skip tables that are not being renamed or where the new name isn't prefixed
709 // with the deletion marker.
710 if ($tableDiff->getNewName() === false
711 || strpos($tableDiff->getNewName()->getName(), $this->deletedPrefix) !== 0
712 ) {
713 continue;
714 }
715 // Build a new schema diff that only contains this table
716 $changedFieldDiff = GeneralUtility::makeInstance(
717 SchemaDiff::class,
718 [],
719 [$tableDiff],
720 [],
721 $schemaDiff->fromSchema
722 );
723
724 $statements = $changedFieldDiff->toSql($this->connection->getDatabasePlatform());
725
726 foreach ($statements as $statement) {
727 $updateSuggestions['change_table'][md5($statement)] = $statement;
728 }
729 $updateSuggestions['tables_count'][md5($statements[0])] = $this->getTableRecordCount((string)$tableDiff->name);
730 }
731
732 return $updateSuggestions;
733 }
734
735 /**
736 * Extract update suggestions (SQL statements) for fields that are
737 * no longer present in the expected schema from the schema diff.
738 * In this case the update suggestions are renames of the fields
739 * with a prefix to mark them for deletion in a second sweep.
740 *
741 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
742 * @return array
743 * @throws \Doctrine\DBAL\Schema\SchemaException
744 * @throws \InvalidArgumentException
745 */
746 protected function getUnusedFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
747 {
748 $changedTables = [];
749
750 foreach ($schemaDiff->changedTables as $index => $changedTable) {
751 if (count($changedTable->changedColumns) === 0) {
752 continue;
753 }
754
755 // Treat each changed column with a new diff to get a dedicated suggestions
756 // just for this single column.
757 foreach ($changedTable->changedColumns as $changedColumn) {
758 // Field has not been renamed
759 if ($changedColumn->getOldColumnName()->getName() === $changedColumn->column->getName()) {
760 continue;
761 }
762
763 $changedTables[$index . ':' . $changedColumn->column->getName()] = GeneralUtility::makeInstance(
764 TableDiff::class,
765 $changedTable->name,
766 [],
767 [$changedColumn],
768 [],
769 [],
770 [],
771 [],
772 $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name))
773 );
774 }
775 }
776
777 // Build a new schema diff that only contains unused fields
778 $changedFieldDiff = GeneralUtility::makeInstance(
779 SchemaDiff::class,
780 [],
781 $changedTables,
782 [],
783 $schemaDiff->fromSchema
784 );
785
786 $statements = $changedFieldDiff->toSql($this->connection->getDatabasePlatform());
787
788 return ['change' => $this->calculateUpdateSuggestionsHashes($statements)];
789 }
790
791 /**
792 * Extract update suggestions (SQL statements) for fields that can
793 * be removed from the complete schema diff.
794 * Fields that can be removed have been prefixed in a previous run
795 * of the schema migration.
796 *
797 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
798 * @return array
799 * @throws \Doctrine\DBAL\Schema\SchemaException
800 * @throws \InvalidArgumentException
801 */
802 protected function getDropFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
803 {
804 $changedTables = [];
805
806 foreach ($schemaDiff->changedTables as $index => $changedTable) {
807 $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
808
809 if (count($changedTable->removedColumns) !== 0) {
810 // Treat each changed column with a new diff to get a dedicated suggestions
811 // just for this single column.
812 foreach ($changedTable->removedColumns as $removedColumn) {
813 $changedTables[$index . ':tbl_' . $removedColumn->getName()] = GeneralUtility::makeInstance(
814 TableDiff::class,
815 $changedTable->name,
816 [],
817 [],
818 [$this->buildQuotedColumn($removedColumn)],
819 [],
820 [],
821 [],
822 $fromTable
823 );
824 }
825 }
826
827 if (count($changedTable->removedIndexes) !== 0) {
828 // Treat each removed index with a new diff to get a dedicated suggestions
829 // just for this index.
830 foreach ($changedTable->removedIndexes as $removedIndex) {
831 $changedTables[$index . ':idx_' . $removedIndex->getName()] = GeneralUtility::makeInstance(
832 TableDiff::class,
833 $changedTable->name,
834 [],
835 [],
836 [],
837 [],
838 [],
839 [$this->buildQuotedIndex($removedIndex)],
840 $fromTable
841 );
842 }
843 }
844
845 if (count($changedTable->removedForeignKeys) !== 0) {
846 // Treat each removed foreign key with a new diff to get a dedicated suggestions
847 // just for this foreign key.
848 foreach ($changedTable->removedForeignKeys as $removedForeignKey) {
849 $fkIndex = $index . ':fk_' . $removedForeignKey->getName();
850 $changedTables[$fkIndex] = GeneralUtility::makeInstance(
851 TableDiff::class,
852 $changedTable->name,
853 [],
854 [],
855 [],
856 [],
857 [],
858 [],
859 $fromTable
860 );
861 $changedTables[$fkIndex]->removedForeignKeys = [$this->buildQuotedForeignKey($removedForeignKey)];
862 }
863 }
864 }
865
866 // Build a new schema diff that only contains removable fields
867 $removedFieldDiff = GeneralUtility::makeInstance(
868 SchemaDiff::class,
869 [],
870 $changedTables,
871 [],
872 $schemaDiff->fromSchema
873 );
874
875 $statements = $removedFieldDiff->toSql($this->connection->getDatabasePlatform());
876
877 return ['drop' => $this->calculateUpdateSuggestionsHashes($statements)];
878 }
879
880 /**
881 * Extract update suggestions (SQL statements) for tables that can
882 * be removed from the complete schema diff.
883 * Tables that can be removed have been prefixed in a previous run
884 * of the schema migration.
885 *
886 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
887 * @return array
888 * @throws \Doctrine\DBAL\Schema\SchemaException
889 * @throws \InvalidArgumentException
890 */
891 protected function getDropTableUpdateSuggestions(SchemaDiff $schemaDiff): array
892 {
893 $updateSuggestions = [];
894 foreach ($schemaDiff->removedTables as $removedTable) {
895 // Build a new schema diff that only contains this table
896 $tableDiff = GeneralUtility::makeInstance(
897 SchemaDiff::class,
898 [],
899 [],
900 [$this->buildQuotedTable($removedTable)],
901 $schemaDiff->fromSchema
902 );
903
904 $statements = $tableDiff->toSql($this->connection->getDatabasePlatform());
905 foreach ($statements as $statement) {
906 $updateSuggestions['drop_table'][md5($statement)] = $statement;
907 }
908
909 // Only store the record count for this table for the first statement,
910 // assuming that this is the actual DROP TABLE statement.
911 $updateSuggestions['tables_count'][md5($statements[0])] = $this->getTableRecordCount(
912 $removedTable->getName()
913 );
914 }
915
916 return $updateSuggestions;
917 }
918
919 /**
920 * Move tables to be removed that are not prefixed with the deleted prefix to the list
921 * of changed tables and set a new prefixed name.
922 * Without this help the Doctrine SchemaDiff has no idea if a table has been renamed and
923 * performs a drop of the old table and creates a new table, which leads to all data in
924 * the old table being lost.
925 *
926 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
927 * @return \Doctrine\DBAL\Schema\SchemaDiff
928 * @throws \InvalidArgumentException
929 */
930 protected function migrateUnprefixedRemovedTablesToRenames(SchemaDiff $schemaDiff): SchemaDiff
931 {
932 foreach ($schemaDiff->removedTables as $index => $removedTable) {
933 if (strpos($removedTable->getName(), $this->deletedPrefix) === 0) {
934 continue;
935 }
936 $tableDiff = GeneralUtility::makeInstance(
937 TableDiff::class,
938 $removedTable->getQuotedName($this->connection->getDatabasePlatform()),
939 $addedColumns = [],
940 $changedColumns = [],
941 $removedColumns = [],
942 $addedIndexes = [],
943 $changedIndexes = [],
944 $removedIndexes = [],
945 $this->buildQuotedTable($removedTable)
946 );
947
948 $tableDiff->newName = $this->connection->getDatabasePlatform()->quoteIdentifier(
949 substr($this->deletedPrefix . $removedTable->getName(), 0, $this->getMaxTableNameLength())
950 );
951 $schemaDiff->changedTables[$index] = $tableDiff;
952 unset($schemaDiff->removedTables[$index]);
953 }
954
955 return $schemaDiff;
956 }
957
958 /**
959 * Scan the list of changed tables for fields that are going to be dropped. If
960 * the name of the field does not start with the deleted prefix mark the column
961 * for a rename instead of a drop operation.
962 *
963 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
964 * @return \Doctrine\DBAL\Schema\SchemaDiff
965 * @throws \InvalidArgumentException
966 */
967 protected function migrateUnprefixedRemovedFieldsToRenames(SchemaDiff $schemaDiff): SchemaDiff
968 {
969 foreach ($schemaDiff->changedTables as $tableIndex => $changedTable) {
970 if (count($changedTable->removedColumns) === 0) {
971 continue;
972 }
973
974 foreach ($changedTable->removedColumns as $columnIndex => $removedColumn) {
975 if (strpos($removedColumn->getName(), $this->deletedPrefix) === 0) {
976 continue;
977 }
978
979 // Build a new column object with the same properties as the removed column
980 $renamedColumnName = substr(
981 $this->deletedPrefix . $removedColumn->getName(),
982 0,
983 $this->getMaxColumnNameLength()
984 );
985 $renamedColumn = new Column(
986 $this->connection->quoteIdentifier($renamedColumnName),
987 $removedColumn->getType(),
988 array_diff_key($removedColumn->toArray(), ['name', 'type'])
989 );
990
991 // Build the diff object for the column to rename
992 $columnDiff = GeneralUtility::makeInstance(
993 ColumnDiff::class,
994 $removedColumn->getQuotedName($this->connection->getDatabasePlatform()),
995 $renamedColumn,
996 $changedProperties = [],
997 $this->buildQuotedColumn($removedColumn)
998 );
999
1000 // Add the column with the required rename information to the changed column list
1001 $schemaDiff->changedTables[$tableIndex]->changedColumns[$columnIndex] = $columnDiff;
1002
1003 // Remove the column from the list of columns to be dropped
1004 unset($schemaDiff->changedTables[$tableIndex]->removedColumns[$columnIndex]);
1005 }
1006 }
1007
1008 return $schemaDiff;
1009 }
1010
1011 /**
1012 * Revert the automatic rename optimization that Doctrine performs when it detects
1013 * a column being added and a column being dropped that only differ by name.
1014 *
1015 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
1016 * @return SchemaDiff
1017 * @throws \Doctrine\DBAL\Schema\SchemaException
1018 * @throws \InvalidArgumentException
1019 */
1020 protected function migrateColumnRenamesToDistinctActions(SchemaDiff $schemaDiff): SchemaDiff
1021 {
1022 foreach ($schemaDiff->changedTables as $index => $changedTable) {
1023 if (count($changedTable->renamedColumns) === 0) {
1024 continue;
1025 }
1026
1027 // Treat each renamed column with a new diff to get a dedicated
1028 // suggestion just for this single column.
1029 foreach ($changedTable->renamedColumns as $originalColumnName => $renamedColumn) {
1030 $columnOptions = array_diff_key($renamedColumn->toArray(), ['name', 'type']);
1031
1032 $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance(
1033 Column::class,
1034 $renamedColumn->getName(),
1035 $renamedColumn->getType(),
1036 $columnOptions
1037 );
1038 $changedTable->removedColumns[$originalColumnName] = GeneralUtility::makeInstance(
1039 Column::class,
1040 $originalColumnName,
1041 $renamedColumn->getType(),
1042 $columnOptions
1043 );
1044
1045 unset($changedTable->renamedColumns[$originalColumnName]);
1046 }
1047 }
1048
1049 return $schemaDiff;
1050 }
1051
1052 /**
1053 * Retrieve the database platform-specific limitations on column and schema name sizes as
1054 * defined in the tableAndFieldMaxNameLengthsPerDbPlatform property.
1055 *
1056 * @param string $databasePlatform
1057 * @return array
1058 */
1059 protected function getTableAndFieldNameMaxLengths(string $databasePlatform = '')
1060 {
1061 if ($databasePlatform === '') {
1062 $databasePlatform = $this->connection->getDatabasePlatform()->getName();
1063 }
1064 $databasePlatform = strtolower($databasePlatform);
1065
1066 if (isset($this->tableAndFieldMaxNameLengthsPerDbPlatform[$databasePlatform])) {
1067 $nameLengthRestrictions = $this->tableAndFieldMaxNameLengthsPerDbPlatform[$databasePlatform];
1068 } else {
1069 $nameLengthRestrictions = $this->tableAndFieldMaxNameLengthsPerDbPlatform['default'];
1070 }
1071
1072 if (is_string($nameLengthRestrictions)) {
1073 return $this->getTableAndFieldNameMaxLengths($nameLengthRestrictions);
1074 } else {
1075 return $nameLengthRestrictions;
1076 }
1077 }
1078
1079 /**
1080 * Get the maximum table name length possible for the given DB platform.
1081 *
1082 * @param string $databasePlatform
1083 * @return string
1084 */
1085 protected function getMaxTableNameLength(string $databasePlatform = '')
1086 {
1087 $nameLengthRestrictions = $this->getTableAndFieldNameMaxLengths($databasePlatform);
1088 return $nameLengthRestrictions['tables'];
1089 }
1090
1091 /**
1092 * Get the maximum column name length possible for the given DB platform.
1093 *
1094 * @param string $databasePlatform
1095 * @return string
1096 */
1097 protected function getMaxColumnNameLength(string $databasePlatform = '')
1098 {
1099 $nameLengthRestrictions = $this->getTableAndFieldNameMaxLengths($databasePlatform);
1100 return $nameLengthRestrictions['columns'];
1101 }
1102
1103 /**
1104 * Return the amount of records in the given table.
1105 *
1106 * @param string $tableName
1107 * @return int
1108 * @throws \InvalidArgumentException
1109 */
1110 protected function getTableRecordCount(string $tableName): int
1111 {
1112 return GeneralUtility::makeInstance(ConnectionPool::class)
1113 ->getConnectionForTable($tableName)
1114 ->count('*', $tableName, []);
1115 }
1116
1117 /**
1118 * Determine the connection name for a table
1119 *
1120 * @param string $tableName
1121 * @return string
1122 * @throws \InvalidArgumentException
1123 */
1124 protected function getConnectionNameForTable(string $tableName): string
1125 {
1126 $connectionNames = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionNames();
1127
1128 if (array_key_exists($tableName, (array)$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])) {
1129 return in_array($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName], $connectionNames, true)
1130 ? $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName]
1131 : ConnectionPool::DEFAULT_CONNECTION_NAME;
1132 }
1133
1134 return ConnectionPool::DEFAULT_CONNECTION_NAME;
1135 }
1136
1137 /**
1138 * Replace the array keys with a md5 sum of the actual SQL statement
1139 *
1140 * @param string[] $statements
1141 * @return string[]
1142 */
1143 protected function calculateUpdateSuggestionsHashes(array $statements): array
1144 {
1145 return array_combine(array_map('md5', $statements), $statements);
1146 }
1147
1148 /**
1149 * Helper for buildSchemaDiff to filter an array of TableDiffs against a list of valid table names.
1150 *
1151 * @param TableDiff[]|Table[] $tableDiffs
1152 * @param string[] $validTableNames
1153 * @return TableDiff[]
1154 * @throws \InvalidArgumentException
1155 */
1156 protected function removeUnrelatedTables(array $tableDiffs, array $validTableNames): array
1157 {
1158 return array_filter(
1159 $tableDiffs,
1160 function ($table) use ($validTableNames) {
1161 if ($table instanceof Table) {
1162 $tableName = $table->getName();
1163 } else {
1164 $tableName = $table->newName ?: $table->name;
1165 }
1166
1167 // If the tablename has a deleted prefix strip it of before comparing
1168 // it against the list of valid table names so that drop operations
1169 // don't get removed.
1170 if (strpos($tableName, $this->deletedPrefix) === 0) {
1171 $tableName = substr($tableName, strlen($this->deletedPrefix));
1172 }
1173 return in_array($tableName, $validTableNames, true)
1174 || in_array($this->deletedPrefix . $tableName, $validTableNames, true);
1175 }
1176 );
1177 }
1178
1179 /**
1180 * Transform the table information to conform to specific
1181 * requirements of different database platforms like removing
1182 * the index substring length for Non-MySQL Platforms.
1183 *
1184 * @param Table[] $tables
1185 * @param \TYPO3\CMS\Core\Database\Connection $connection
1186 * @return Table[]
1187 * @throws \InvalidArgumentException
1188 */
1189 protected function transformTablesForDatabasePlatform(array $tables, Connection $connection): array
1190 {
1191 foreach ($tables as &$table) {
1192 $indexes = [];
1193 foreach ($table->getIndexes() as $key => $index) {
1194 $indexName = $index->getName();
1195 // PostgreSQL requires index names to be unique per database/schema.
1196 if ($connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
1197 $indexName = $indexName . '_' . hash('crc32b', $table->getName() . '_' . $indexName);
1198 }
1199
1200 // Remove the length information from column names for indexes if required.
1201 $cleanedColumnNames = array_map(
1202 function (string $columnName) use ($connection) {
1203 if ($connection->getDatabasePlatform() instanceof MySqlPlatform) {
1204 // Returning the unquoted, unmodified version of the column name since
1205 // it can include the length information for BLOB/TEXT columns which
1206 // may not be quoted.
1207 return $columnName;
1208 }
1209
1210 return $connection->quoteIdentifier(preg_replace('/\(\d+\)$/', '', $columnName));
1211 },
1212 $index->getUnquotedColumns()
1213 );
1214
1215 $indexes[$key] = GeneralUtility::makeInstance(
1216 Index::class,
1217 $connection->quoteIdentifier($indexName),
1218 $cleanedColumnNames,
1219 $index->isUnique(),
1220 $index->isPrimary(),
1221 $index->getFlags(),
1222 $index->getOptions()
1223 );
1224 }
1225
1226 $table = GeneralUtility::makeInstance(
1227 Table::class,
1228 $table->getQuotedName($connection->getDatabasePlatform()),
1229 $table->getColumns(),
1230 $indexes,
1231 $table->getForeignKeys(),
1232 0,
1233 $table->getOptions()
1234 );
1235 }
1236
1237 return $tables;
1238 }
1239
1240 /**
1241 * Get COLLATION, ROW_FORMAT, COMMENT and ENGINE table options on MySQL connections.
1242 *
1243 * @param string[] $tableNames
1244 * @return array[]
1245 * @throws \InvalidArgumentException
1246 */
1247 protected function getTableOptions(array $tableNames): array
1248 {
1249 $tableOptions = [];
1250 if (strpos($this->connection->getServerVersion(), 'MySQL') !== 0) {
1251 foreach ($tableNames as $tableName) {
1252 $tableOptions[$tableName] = [];
1253 }
1254
1255 return $tableOptions;
1256 }
1257
1258 $queryBuilder = $this->connection->createQueryBuilder();
1259 $result = $queryBuilder
1260 ->select(
1261 'TABLE_NAME AS table',
1262 'ENGINE AS engine',
1263 'ROW_FORMAT AS row_format',
1264 'TABLE_COLLATION AS collate',
1265 'TABLE_COMMENT AS comment'
1266 )
1267 ->from('information_schema.TABLES')
1268 ->where(
1269 $queryBuilder->expr()->eq(
1270 'TABLE_TYPE',
1271 $queryBuilder->createNamedParameter('BASE TABLE', \PDO::PARAM_STR)
1272 ),
1273 $queryBuilder->expr()->eq(
1274 'TABLE_SCHEMA',
1275 $queryBuilder->createNamedParameter($this->connection->getDatabase(), \PDO::PARAM_STR)
1276 )
1277 )
1278 ->execute();
1279
1280 while ($row = $result->fetch()) {
1281 $index = $row['table'];
1282 unset($row['table']);
1283 $tableOptions[$index] = $row;
1284 }
1285
1286 return $tableOptions;
1287 }
1288
1289 /**
1290 * Helper function to build a table object that has the _quoted attribute set so that the SchemaManager
1291 * will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine doesn't
1292 * provide a method to set the flag after the object has been instantiated and there's no possibility to
1293 * hook into the createSchema() method early enough to influence the original table object.
1294 *
1295 * @param \Doctrine\DBAL\Schema\Table $table
1296 * @return \Doctrine\DBAL\Schema\Table
1297 */
1298 protected function buildQuotedTable(Table $table): Table
1299 {
1300 $databasePlatform = $this->connection->getDatabasePlatform();
1301
1302 return GeneralUtility::makeInstance(
1303 Table::class,
1304 $databasePlatform->quoteIdentifier($table->getName()),
1305 $table->getColumns(),
1306 $table->getIndexes(),
1307 $table->getForeignKeys(),
1308 0,
1309 $table->getOptions()
1310 );
1311 }
1312
1313 /**
1314 * Helper function to build a column object that has the _quoted attribute set so that the SchemaManager
1315 * will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine doesn't
1316 * provide a method to set the flag after the object has been instantiated and there's no possibility to
1317 * hook into the createSchema() method early enough to influence the original column object.
1318 *
1319 * @param \Doctrine\DBAL\Schema\Column $column
1320 * @return \Doctrine\DBAL\Schema\Column
1321 */
1322 protected function buildQuotedColumn(Column $column): Column
1323 {
1324 $databasePlatform = $this->connection->getDatabasePlatform();
1325
1326 return GeneralUtility::makeInstance(
1327 Column::class,
1328 $databasePlatform->quoteIdentifier($column->getName()),
1329 $column->getType(),
1330 array_diff_key($column->toArray(), ['name', 'type'])
1331 );
1332 }
1333
1334 /**
1335 * Helper function to build an index object that has the _quoted attribute set so that the SchemaManager
1336 * will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine doesn't
1337 * provide a method to set the flag after the object has been instantiated and there's no possibility to
1338 * hook into the createSchema() method early enough to influence the original column object.
1339 *
1340 * @param \Doctrine\DBAL\Schema\Index $index
1341 * @return \Doctrine\DBAL\Schema\Index
1342 */
1343 protected function buildQuotedIndex(Index $index): Index
1344 {
1345 $databasePlatform = $this->connection->getDatabasePlatform();
1346
1347 return GeneralUtility::makeInstance(
1348 Index::class,
1349 $databasePlatform->quoteIdentifier($index->getName()),
1350 $index->getColumns(),
1351 $index->isUnique(),
1352 $index->isPrimary(),
1353 $index->getFlags(),
1354 $index->getOptions()
1355 );
1356 }
1357
1358 /**
1359 * Helper function to build a foreign key constraint object that has the _quoted attribute set so that the
1360 * SchemaManager will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine
1361 * doesn't provide a method to set the flag after the object has been instantiated and there's no possibility to
1362 * hook into the createSchema() method early enough to influence the original column object.
1363 *
1364 * @param \Doctrine\DBAL\Schema\ForeignKeyConstraint $index
1365 * @return \Doctrine\DBAL\Schema\ForeignKeyConstraint
1366 */
1367 protected function buildQuotedForeignKey(ForeignKeyConstraint $index): ForeignKeyConstraint
1368 {
1369 $databasePlatform = $this->connection->getDatabasePlatform();
1370
1371 return GeneralUtility::makeInstance(
1372 ForeignKeyConstraint::class,
1373 $index->getLocalColumns(),
1374 $databasePlatform->quoteIdentifier($index->getForeignTableName()),
1375 $index->getForeignColumns(),
1376 $databasePlatform->quoteIdentifier($index->getName()),
1377 $index->getOptions()
1378 );
1379 }
1380 }