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