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