Completed
Push — master ( 585287...decea2 )
by
unknown
42:21
created

ConnectionMigrator   F

Complexity

Total Complexity 118

Size/Duplication

Total Lines 1289
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 508
dl 0
loc 1289
rs 2
c 0
b 0
f 0
wmc 118

29 Methods

Rating   Name   Duplication   Size   Complexity  
A getSchemaDiff() 0 3 1
A __construct() 0 6 1
A getUpdateSuggestions() 0 19 2
B install() 0 63 8
B buildSchemaDiff() 0 56 7
A create() 0 6 1
B getNewFieldUpdateSuggestions() 0 76 8
A getUnusedTableUpdateSuggestions() 0 29 5
C getChangedFieldUpdateSuggestions() 0 159 15
A getNewTableUpdateSuggestions() 0 14 1
A getChangedTableOptions() 0 39 5
A buildExpectedSchemaDefinitions() 0 41 5
A getTableOptions() 0 50 4
A buildQuotedIndex() 0 12 1
A getDropTableUpdateSuggestions() 0 26 3
A migrateUnprefixedRemovedTablesToRenames() 0 30 3
A migrateColumnRenamesToDistinctActions() 0 30 4
B getUnusedFieldUpdateSuggestions() 0 48 6
A buildQuotedForeignKey() 0 11 1
A getConnectionNameForTable() 0 11 3
A removeUnrelatedTables() 0 19 5
A buildQuotedColumn() 0 9 1
A migrateUnprefixedRemovedFieldsToRenames() 0 42 5
A getTableRecordCount() 0 5 1
C getDropFieldUpdateSuggestions() 0 90 13
A buildQuotedTable() 0 12 1
A calculateUpdateSuggestionsHashes() 0 3 1
B transformTablesForDatabasePlatform() 0 52 6
A tableRunsOnSqlite() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ConnectionMigrator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ConnectionMigrator, and based on these observations, apply Extract Interface, too.

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\Platforms\SqlitePlatform;
22
use Doctrine\DBAL\Schema\Column;
23
use Doctrine\DBAL\Schema\ColumnDiff;
24
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
25
use Doctrine\DBAL\Schema\Index;
26
use Doctrine\DBAL\Schema\Schema;
27
use Doctrine\DBAL\Schema\SchemaConfig;
28
use Doctrine\DBAL\Schema\SchemaDiff;
29
use Doctrine\DBAL\Schema\Table;
30
use TYPO3\CMS\Core\Database\Connection;
31
use TYPO3\CMS\Core\Database\ConnectionPool;
32
use TYPO3\CMS\Core\Database\Platform\PlatformInformation;
33
use TYPO3\CMS\Core\Utility\GeneralUtility;
34
35
/**
36
 * Handling schema migrations per connection.
37
 *
38
 * @internal
39
 */
40
class ConnectionMigrator
41
{
42
    /**
43
     * @var string Prefix of deleted tables
44
     */
45
    protected $deletedPrefix = 'zzz_deleted_';
46
47
    /**
48
     * @var Connection
49
     */
50
    protected $connection;
51
52
    /**
53
     * @var string
54
     */
55
    protected $connectionName;
56
57
    /**
58
     * @var Table[]
59
     */
60
    protected $tables;
61
62
    /**
63
     * @param string $connectionName
64
     * @param Table[] $tables
65
     */
66
    public function __construct(string $connectionName, array $tables)
67
    {
68
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
69
        $this->connection = $connectionPool->getConnectionByName($connectionName);
70
        $this->connectionName = $connectionName;
71
        $this->tables = $tables;
72
    }
73
74
    /**
75
     * @param string $connectionName
76
     * @param Table[] $tables
77
     * @return ConnectionMigrator
78
     */
79
    public static function create(string $connectionName, array $tables)
80
    {
81
        return GeneralUtility::makeInstance(
82
            static::class,
83
            $connectionName,
84
            $tables
85
        );
86
    }
87
88
    /**
89
     * Return the raw Doctrine SchemaDiff object for the current connection.
90
     * This diff contains all changes without any pre-processing.
91
     *
92
     * @return SchemaDiff
93
     */
94
    public function getSchemaDiff(): SchemaDiff
95
    {
96
        return $this->buildSchemaDiff(false);
97
    }
98
99
    /**
100
     * Compare current and expected schema definitions and provide updates
101
     * suggestions in the form of SQL statements.
102
     *
103
     * @param bool $remove
104
     * @return array
105
     */
106
    public function getUpdateSuggestions(bool $remove = false): array
107
    {
108
        $schemaDiff = $this->buildSchemaDiff();
109
110
        if ($remove === false) {
111
            return array_merge_recursive(
112
                ['add' => [], 'create_table' => [], 'change' => [], 'change_currentValue' => []],
113
                $this->getNewFieldUpdateSuggestions($schemaDiff),
114
                $this->getNewTableUpdateSuggestions($schemaDiff),
115
                $this->getChangedFieldUpdateSuggestions($schemaDiff),
116
                $this->getChangedTableOptions($schemaDiff)
117
            );
118
        }
119
        return array_merge_recursive(
120
            ['change' => [], 'change_table' => [], 'drop' => [], 'drop_table' => [], 'tables_count' => []],
121
            $this->getUnusedFieldUpdateSuggestions($schemaDiff),
122
            $this->getUnusedTableUpdateSuggestions($schemaDiff),
123
            $this->getDropTableUpdateSuggestions($schemaDiff),
124
            $this->getDropFieldUpdateSuggestions($schemaDiff)
125
        );
126
    }
127
128
    /**
129
     * Perform add/change/create operations on tables and fields in an
130
     * optimized, non-interactive, mode using the original doctrine
131
     * SchemaManager ->toSaveSql() method.
132
     *
133
     * @param bool $createOnly
134
     * @return array
135
     */
136
    public function install(bool $createOnly = false): array
137
    {
138
        $result = [];
139
        $schemaDiff = $this->buildSchemaDiff(false);
140
141
        $schemaDiff->removedTables = [];
142
        foreach ($schemaDiff->changedTables as $key => $changedTable) {
143
            $schemaDiff->changedTables[$key]->removedColumns = [];
144
            $schemaDiff->changedTables[$key]->removedIndexes = [];
145
146
            // With partial ext_tables.sql files the SchemaManager is detecting
147
            // existing columns as false positives for a column rename. In this
148
            // context every rename is actually a new column.
149
            foreach ($changedTable->renamedColumns as $columnName => $renamedColumn) {
150
                $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance(
151
                    Column::class,
152
                    $renamedColumn->getName(),
153
                    $renamedColumn->getType(),
154
                    array_diff_key($renamedColumn->toArray(), ['name', 'type'])
155
                );
156
                unset($changedTable->renamedColumns[$columnName]);
157
            }
158
159
            if ($createOnly) {
160
                // Ignore new indexes that work on columns that need changes
161
                foreach ($changedTable->addedIndexes as $indexName => $addedIndex) {
162
                    $indexColumns = array_map(
163
                        function ($columnName) {
164
                            // Strip MySQL prefix length information to get real column names
165
                            $columnName = preg_replace('/\(\d+\)$/', '', $columnName);
166
                            // Strip mssql '[' and ']' from column names
167
                            $columnName = ltrim($columnName, '[');
168
                            $columnName = rtrim($columnName, ']');
169
                            // Strip sqlite '"' from column names
170
                            return trim($columnName, '"');
171
                        },
172
                        $addedIndex->getColumns()
173
                    );
174
                    $columnChanges = array_intersect($indexColumns, array_keys($changedTable->changedColumns));
175
                    if (!empty($columnChanges)) {
176
                        unset($schemaDiff->changedTables[$key]->addedIndexes[$indexName]);
177
                    }
178
                }
179
                $schemaDiff->changedTables[$key]->changedColumns = [];
180
                $schemaDiff->changedTables[$key]->changedIndexes = [];
181
                $schemaDiff->changedTables[$key]->renamedIndexes = [];
182
            }
183
        }
184
185
        $statements = $schemaDiff->toSaveSql(
186
            $this->connection->getDatabasePlatform()
187
        );
188
189
        foreach ($statements as $statement) {
190
            try {
191
                $this->connection->executeUpdate($statement);
192
                $result[$statement] = '';
193
            } catch (DBALException $e) {
194
                $result[$statement] = $e->getPrevious()->getMessage();
195
            }
196
        }
197
198
        return $result;
199
    }
200
201
    /**
202
     * If the schema is not for the Default connection remove all tables from the schema
203
     * that have no mapping in the TYPO3 configuration. This avoids update suggestions
204
     * for tables that are in the database but have no direct relation to the TYPO3 instance.
205
     *
206
     * @param bool $renameUnused
207
     * @throws \Doctrine\DBAL\DBALException
208
     * @return \Doctrine\DBAL\Schema\SchemaDiff
209
     * @throws \Doctrine\DBAL\Schema\SchemaException
210
     * @throws \InvalidArgumentException
211
     */
212
    protected function buildSchemaDiff(bool $renameUnused = true): SchemaDiff
213
    {
214
        // Build the schema definitions
215
        $fromSchema = $this->connection->getSchemaManager()->createSchema();
216
        $toSchema = $this->buildExpectedSchemaDefinitions($this->connectionName);
217
218
        // Add current table options to the fromSchema
219
        $tableOptions = $this->getTableOptions($fromSchema->getTableNames());
220
        foreach ($fromSchema->getTables() as $table) {
221
            $tableName = $table->getName();
222
            if (!array_key_exists($tableName, $tableOptions)) {
223
                continue;
224
            }
225
            foreach ($tableOptions[$tableName] as $optionName => $optionValue) {
226
                $table->addOption($optionName, $optionValue);
227
            }
228
        }
229
230
        // Build SchemaDiff and handle renames of tables and columns
231
        $comparator = GeneralUtility::makeInstance(Comparator::class, $this->connection->getDatabasePlatform());
232
        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
233
        $schemaDiff = $this->migrateColumnRenamesToDistinctActions($schemaDiff);
234
235
        if ($renameUnused) {
236
            $schemaDiff = $this->migrateUnprefixedRemovedTablesToRenames($schemaDiff);
237
            $schemaDiff = $this->migrateUnprefixedRemovedFieldsToRenames($schemaDiff);
238
        }
239
240
        // All tables in the default connection are managed by TYPO3
241
        if ($this->connectionName === ConnectionPool::DEFAULT_CONNECTION_NAME) {
242
            return $schemaDiff;
243
        }
244
245
        // If there are no mapped tables return a SchemaDiff without any changes
246
        // to avoid update suggestions for tables not related to TYPO3.
247
        if (empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'] ?? null)) {
248
            return GeneralUtility::makeInstance(SchemaDiff::class, [], [], [], $fromSchema);
249
        }
250
251
        // Collect the table names that have been mapped to this connection.
252
        $connectionName = $this->connectionName;
253
        $tablesForConnection = array_keys(
254
            array_filter(
255
                $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'],
256
                function ($tableConnectionName) use ($connectionName) {
257
                    return $tableConnectionName === $connectionName;
258
                }
259
            )
260
        );
261
262
        // Remove all tables that are not assigned to this connection from the diff
263
        $schemaDiff->newTables = $this->removeUnrelatedTables($schemaDiff->newTables, $tablesForConnection);
264
        $schemaDiff->changedTables = $this->removeUnrelatedTables($schemaDiff->changedTables, $tablesForConnection);
265
        $schemaDiff->removedTables = $this->removeUnrelatedTables($schemaDiff->removedTables, $tablesForConnection);
266
267
        return $schemaDiff;
268
    }
269
270
    /**
271
     * Build the expected schema definitions from raw SQL statements.
272
     *
273
     * @param string $connectionName
274
     * @return \Doctrine\DBAL\Schema\Schema
275
     * @throws \Doctrine\DBAL\DBALException
276
     * @throws \InvalidArgumentException
277
     */
278
    protected function buildExpectedSchemaDefinitions(string $connectionName): Schema
279
    {
280
        /** @var Table[] $tablesForConnection */
281
        $tablesForConnection = [];
282
        foreach ($this->tables as $table) {
283
            $tableName = $table->getName();
284
285
            // Skip tables for a different connection
286
            if ($connectionName !== $this->getConnectionNameForTable($tableName)) {
287
                continue;
288
            }
289
290
            if (!array_key_exists($tableName, $tablesForConnection)) {
291
                $tablesForConnection[$tableName] = $table;
292
                continue;
293
            }
294
295
            // Merge multiple table definitions. Later definitions overrule identical
296
            // columns, indexes and foreign_keys. Order of definitions is based on
297
            // extension load order.
298
            $currentTableDefinition = $tablesForConnection[$tableName];
299
            $tablesForConnection[$tableName] = GeneralUtility::makeInstance(
300
                Table::class,
301
                $tableName,
302
                array_merge($currentTableDefinition->getColumns(), $table->getColumns()),
303
                array_merge($currentTableDefinition->getIndexes(), $table->getIndexes()),
304
                array_merge($currentTableDefinition->getForeignKeys(), $table->getForeignKeys()),
305
                0,
306
                array_merge($currentTableDefinition->getOptions(), $table->getOptions())
307
            );
308
        }
309
310
        $tablesForConnection = $this->transformTablesForDatabasePlatform($tablesForConnection, $this->connection);
311
312
        $schemaConfig = GeneralUtility::makeInstance(SchemaConfig::class);
313
        $schemaConfig->setName($this->connection->getDatabase());
314
        if (isset($this->connection->getParams()['tableoptions'])) {
315
            $schemaConfig->setDefaultTableOptions($this->connection->getParams()['tableoptions']);
316
        }
317
318
        return GeneralUtility::makeInstance(Schema::class, $tablesForConnection, [], $schemaConfig);
319
    }
320
321
    /**
322
     * Extract the update suggestions (SQL statements) for newly added tables
323
     * from the complete schema diff.
324
     *
325
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
326
     * @return array
327
     * @throws \InvalidArgumentException
328
     */
329
    protected function getNewTableUpdateSuggestions(SchemaDiff $schemaDiff): array
330
    {
331
        // Build a new schema diff that only contains added tables
332
        $addTableSchemaDiff = GeneralUtility::makeInstance(
333
            SchemaDiff::class,
334
            $schemaDiff->newTables,
335
            [],
336
            [],
337
            $schemaDiff->fromSchema
338
        );
339
340
        $statements = $addTableSchemaDiff->toSql($this->connection->getDatabasePlatform());
341
342
        return ['create_table' => $this->calculateUpdateSuggestionsHashes($statements)];
343
    }
344
345
    /**
346
     * Extract the update suggestions (SQL statements) for newly added fields
347
     * from the complete schema diff.
348
     *
349
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
350
     * @return array
351
     * @throws \Doctrine\DBAL\Schema\SchemaException
352
     * @throws \InvalidArgumentException
353
     */
354
    protected function getNewFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
355
    {
356
        $changedTables = [];
357
358
        foreach ($schemaDiff->changedTables as $index => $changedTable) {
359
            $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
0 ignored issues
show
Bug introduced by
The method getTable() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

359
            $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->/** @scrutinizer ignore-call */ getTable($changedTable->name));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
360
361
            if (count($changedTable->addedColumns) !== 0) {
362
                // Treat each added column with a new diff to get a dedicated suggestions
363
                // just for this single column.
364
                foreach ($changedTable->addedColumns as $columnName => $addedColumn) {
365
                    $changedTables[$index . ':tbl_' . $addedColumn->getName()] = GeneralUtility::makeInstance(
366
                        TableDiff::class,
367
                        $changedTable->name,
368
                        [$columnName => $addedColumn],
369
                        [],
370
                        [],
371
                        [],
372
                        [],
373
                        [],
374
                        $fromTable
375
                    );
376
                }
377
            }
378
379
            if (count($changedTable->addedIndexes) !== 0) {
380
                // Treat each added index with a new diff to get a dedicated suggestions
381
                // just for this index.
382
                foreach ($changedTable->addedIndexes as $indexName => $addedIndex) {
383
                    $changedTables[$index . ':idx_' . $addedIndex->getName()] = GeneralUtility::makeInstance(
384
                        TableDiff::class,
385
                        $changedTable->name,
386
                        [],
387
                        [],
388
                        [],
389
                        [$indexName => $this->buildQuotedIndex($addedIndex)],
390
                        [],
391
                        [],
392
                        $fromTable
393
                    );
394
                }
395
            }
396
397
            if (count($changedTable->addedForeignKeys) !== 0) {
398
                // Treat each added foreign key with a new diff to get a dedicated suggestions
399
                // just for this foreign key.
400
                foreach ($changedTable->addedForeignKeys as $addedForeignKey) {
401
                    $fkIndex = $index . ':fk_' . $addedForeignKey->getName();
402
                    $changedTables[$fkIndex] = GeneralUtility::makeInstance(
403
                        TableDiff::class,
404
                        $changedTable->name,
405
                        [],
406
                        [],
407
                        [],
408
                        [],
409
                        [],
410
                        [],
411
                        $fromTable
412
                    );
413
                    $changedTables[$fkIndex]->addedForeignKeys = [$this->buildQuotedForeignKey($addedForeignKey)];
414
                }
415
            }
416
        }
417
418
        // Build a new schema diff that only contains added fields
419
        $addFieldSchemaDiff = GeneralUtility::makeInstance(
420
            SchemaDiff::class,
421
            [],
422
            $changedTables,
423
            [],
424
            $schemaDiff->fromSchema
425
        );
426
427
        $statements = $addFieldSchemaDiff->toSql($this->connection->getDatabasePlatform());
428
429
        return ['add' => $this->calculateUpdateSuggestionsHashes($statements)];
430
    }
431
432
    /**
433
     * Extract update suggestions (SQL statements) for changed options
434
     * (like ENGINE) from the complete schema diff.
435
     *
436
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
437
     * @return array
438
     * @throws \Doctrine\DBAL\Schema\SchemaException
439
     * @throws \InvalidArgumentException
440
     */
441
    protected function getChangedTableOptions(SchemaDiff $schemaDiff): array
442
    {
443
        $updateSuggestions = [];
444
445
        foreach ($schemaDiff->changedTables as $tableDiff) {
446
            // Skip processing if this is the base TableDiff class or has no table options set.
447
            if (!$tableDiff instanceof TableDiff || count($tableDiff->getTableOptions()) === 0) {
448
                continue;
449
            }
450
451
            $tableOptions = $tableDiff->getTableOptions();
452
            $tableOptionsDiff = GeneralUtility::makeInstance(
453
                TableDiff::class,
454
                $tableDiff->name,
455
                [],
456
                [],
457
                [],
458
                [],
459
                [],
460
                [],
461
                $tableDiff->fromTable
462
            );
463
            $tableOptionsDiff->setTableOptions($tableOptions);
464
465
            $tableOptionsSchemaDiff = GeneralUtility::makeInstance(
466
                SchemaDiff::class,
467
                [],
468
                [$tableOptionsDiff],
469
                [],
470
                $schemaDiff->fromSchema
471
            );
472
473
            $statements = $tableOptionsSchemaDiff->toSaveSql($this->connection->getDatabasePlatform());
474
            foreach ($statements as $statement) {
475
                $updateSuggestions['change'][md5($statement)] = $statement;
476
            }
477
        }
478
479
        return $updateSuggestions;
480
    }
481
482
    /**
483
     * Extract update suggestions (SQL statements) for changed fields
484
     * from the complete schema diff.
485
     *
486
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
487
     * @return array
488
     * @throws \Doctrine\DBAL\Schema\SchemaException
489
     * @throws \InvalidArgumentException
490
     */
491
    protected function getChangedFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
492
    {
493
        $databasePlatform = $this->connection->getDatabasePlatform();
494
        $updateSuggestions = [];
495
496
        foreach ($schemaDiff->changedTables as $index => $changedTable) {
497
            // Treat each changed index with a new diff to get a dedicated suggestions
498
            // just for this index.
499
            if (count($changedTable->changedIndexes) !== 0) {
500
                foreach ($changedTable->changedIndexes as $indexName => $changedIndex) {
501
                    $indexDiff = GeneralUtility::makeInstance(
502
                        TableDiff::class,
503
                        $changedTable->name,
504
                        [],
505
                        [],
506
                        [],
507
                        [],
508
                        [$indexName => $changedIndex],
509
                        [],
510
                        $schemaDiff->fromSchema->getTable($changedTable->name)
511
                    );
512
513
                    $temporarySchemaDiff = GeneralUtility::makeInstance(
514
                        SchemaDiff::class,
515
                        [],
516
                        [$indexDiff],
517
                        [],
518
                        $schemaDiff->fromSchema
519
                    );
520
521
                    $statements = $temporarySchemaDiff->toSql($databasePlatform);
522
                    foreach ($statements as $statement) {
523
                        $updateSuggestions['change'][md5($statement)] = $statement;
524
                    }
525
                }
526
            }
527
528
            // Treat renamed indexes as a field change as it's a simple rename operation
529
            if (count($changedTable->renamedIndexes) !== 0) {
530
                // Create a base table diff without any changes, there's no constructor
531
                // argument to pass in renamed indexes.
532
                $tableDiff = GeneralUtility::makeInstance(
533
                    TableDiff::class,
534
                    $changedTable->name,
535
                    [],
536
                    [],
537
                    [],
538
                    [],
539
                    [],
540
                    [],
541
                    $schemaDiff->fromSchema->getTable($changedTable->name)
542
                );
543
544
                // Treat each renamed index with a new diff to get a dedicated suggestions
545
                // just for this index.
546
                foreach ($changedTable->renamedIndexes as $key => $renamedIndex) {
547
                    $indexDiff = clone $tableDiff;
548
                    $indexDiff->renamedIndexes = [$key => $renamedIndex];
549
550
                    $temporarySchemaDiff = GeneralUtility::makeInstance(
551
                        SchemaDiff::class,
552
                        [],
553
                        [$indexDiff],
554
                        [],
555
                        $schemaDiff->fromSchema
556
                    );
557
558
                    $statements = $temporarySchemaDiff->toSql($databasePlatform);
559
                    foreach ($statements as $statement) {
560
                        $updateSuggestions['change'][md5($statement)] = $statement;
561
                    }
562
                }
563
            }
564
565
            if (count($changedTable->changedColumns) !== 0) {
566
                // Treat each changed column with a new diff to get a dedicated suggestions
567
                // just for this single column.
568
                $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
569
570
                foreach ($changedTable->changedColumns as $columnName => $changedColumn) {
571
                    // Field has been renamed and will be handled separately
572
                    if ($changedColumn->getOldColumnName()->getName() !== $changedColumn->column->getName()) {
573
                        continue;
574
                    }
575
576
                    $changedColumn->fromColumn = $this->buildQuotedColumn($changedColumn->fromColumn);
0 ignored issues
show
Bug introduced by
It seems like $changedColumn->fromColumn can also be of type null; however, parameter $column of TYPO3\CMS\Core\Database\...or::buildQuotedColumn() does only seem to accept Doctrine\DBAL\Schema\Column, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

576
                    $changedColumn->fromColumn = $this->buildQuotedColumn(/** @scrutinizer ignore-type */ $changedColumn->fromColumn);
Loading history...
577
578
                    // Get the current SQL declaration for the column
579
                    $currentColumn = $fromTable->getColumn($changedColumn->getOldColumnName()->getName());
580
                    $currentDeclaration = $databasePlatform->getColumnDeclarationSQL(
581
                        $currentColumn->getQuotedName($this->connection->getDatabasePlatform()),
582
                        $currentColumn->toArray()
583
                    );
584
585
                    // Build a dedicated diff just for the current column
586
                    $tableDiff = GeneralUtility::makeInstance(
587
                        TableDiff::class,
588
                        $changedTable->name,
589
                        [],
590
                        [$columnName => $changedColumn],
591
                        [],
592
                        [],
593
                        [],
594
                        [],
595
                        $fromTable
596
                    );
597
598
                    $temporarySchemaDiff = GeneralUtility::makeInstance(
599
                        SchemaDiff::class,
600
                        [],
601
                        [$tableDiff],
602
                        [],
603
                        $schemaDiff->fromSchema
604
                    );
605
606
                    $statements = $temporarySchemaDiff->toSql($databasePlatform);
607
                    foreach ($statements as $statement) {
608
                        $updateSuggestions['change'][md5($statement)] = $statement;
609
                        $updateSuggestions['change_currentValue'][md5($statement)] = $currentDeclaration;
610
                    }
611
                }
612
            }
613
614
            // Treat each changed foreign key with a new diff to get a dedicated suggestions
615
            // just for this foreign key.
616
            if (count($changedTable->changedForeignKeys) !== 0) {
617
                $tableDiff = GeneralUtility::makeInstance(
618
                    TableDiff::class,
619
                    $changedTable->name,
620
                    [],
621
                    [],
622
                    [],
623
                    [],
624
                    [],
625
                    [],
626
                    $schemaDiff->fromSchema->getTable($changedTable->name)
627
                );
628
629
                foreach ($changedTable->changedForeignKeys as $changedForeignKey) {
630
                    $foreignKeyDiff = clone $tableDiff;
631
                    $foreignKeyDiff->changedForeignKeys = [$this->buildQuotedForeignKey($changedForeignKey)];
632
633
                    $temporarySchemaDiff = GeneralUtility::makeInstance(
634
                        SchemaDiff::class,
635
                        [],
636
                        [$foreignKeyDiff],
637
                        [],
638
                        $schemaDiff->fromSchema
639
                    );
640
641
                    $statements = $temporarySchemaDiff->toSql($databasePlatform);
642
                    foreach ($statements as $statement) {
643
                        $updateSuggestions['change'][md5($statement)] = $statement;
644
                    }
645
                }
646
            }
647
        }
648
649
        return $updateSuggestions;
650
    }
651
652
    /**
653
     * Extract update suggestions (SQL statements) for tables that are
654
     * no longer present in the expected schema from the schema diff.
655
     * In this case the update suggestions are renames of the tables
656
     * with a prefix to mark them for deletion in a second sweep.
657
     *
658
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
659
     * @return array
660
     * @throws \Doctrine\DBAL\Schema\SchemaException
661
     * @throws \InvalidArgumentException
662
     */
663
    protected function getUnusedTableUpdateSuggestions(SchemaDiff $schemaDiff): array
664
    {
665
        $updateSuggestions = [];
666
        foreach ($schemaDiff->changedTables as $tableDiff) {
667
            // Skip tables that are not being renamed or where the new name isn't prefixed
668
            // with the deletion marker.
669
            if ($tableDiff->getNewName() === false
670
                || strpos($tableDiff->getNewName()->getName(), $this->deletedPrefix) !== 0
671
            ) {
672
                continue;
673
            }
674
            // Build a new schema diff that only contains this table
675
            $changedFieldDiff = GeneralUtility::makeInstance(
676
                SchemaDiff::class,
677
                [],
678
                [$tableDiff],
679
                [],
680
                $schemaDiff->fromSchema
681
            );
682
683
            $statements = $changedFieldDiff->toSql($this->connection->getDatabasePlatform());
684
685
            foreach ($statements as $statement) {
686
                $updateSuggestions['change_table'][md5($statement)] = $statement;
687
            }
688
            $updateSuggestions['tables_count'][md5($statements[0])] = $this->getTableRecordCount((string)$tableDiff->name);
689
        }
690
691
        return $updateSuggestions;
692
    }
693
694
    /**
695
     * Extract update suggestions (SQL statements) for fields that are
696
     * no longer present in the expected schema from the schema diff.
697
     * In this case the update suggestions are renames of the fields
698
     * with a prefix to mark them for deletion in a second sweep.
699
     *
700
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
701
     * @return array
702
     * @throws \Doctrine\DBAL\Schema\SchemaException
703
     * @throws \InvalidArgumentException
704
     */
705
    protected function getUnusedFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
706
    {
707
        $changedTables = [];
708
709
        foreach ($schemaDiff->changedTables as $index => $changedTable) {
710
            if (count($changedTable->changedColumns) === 0) {
711
                continue;
712
            }
713
714
            $isSqlite = $this->tableRunsOnSqlite($index);
715
716
            // Treat each changed column with a new diff to get a dedicated suggestions
717
            // just for this single column.
718
            foreach ($changedTable->changedColumns as $oldFieldName => $changedColumn) {
719
                // Field has not been renamed
720
                if ($changedColumn->getOldColumnName()->getName() === $changedColumn->column->getName()) {
721
                    continue;
722
                }
723
724
                $changedTables[$index . ':' . $changedColumn->column->getName()] = GeneralUtility::makeInstance(
725
                    TableDiff::class,
726
                    $changedTable->name,
727
                    [],
728
                    [$oldFieldName => $changedColumn],
729
                    [],
730
                    [],
731
                    [],
732
                    [],
733
                    $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name))
734
                );
735
                if ($isSqlite) {
736
                    break;
737
                }
738
            }
739
        }
740
741
        // Build a new schema diff that only contains unused fields
742
        $changedFieldDiff = GeneralUtility::makeInstance(
743
            SchemaDiff::class,
744
            [],
745
            $changedTables,
746
            [],
747
            $schemaDiff->fromSchema
748
        );
749
750
        $statements = $changedFieldDiff->toSql($this->connection->getDatabasePlatform());
751
752
        return ['change' => $this->calculateUpdateSuggestionsHashes($statements)];
753
    }
754
755
    /**
756
     * Extract update suggestions (SQL statements) for fields that can
757
     * be removed from the complete schema diff.
758
     * Fields that can be removed have been prefixed in a previous run
759
     * of the schema migration.
760
     *
761
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
762
     * @return array
763
     * @throws \Doctrine\DBAL\Schema\SchemaException
764
     * @throws \InvalidArgumentException
765
     */
766
    protected function getDropFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
767
    {
768
        $changedTables = [];
769
770
        foreach ($schemaDiff->changedTables as $index => $changedTable) {
771
            $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
772
773
            $isSqlite = $this->tableRunsOnSqlite($index);
774
            $addMoreOperations = true;
775
776
            if (count($changedTable->removedColumns) !== 0) {
777
                // Treat each changed column with a new diff to get a dedicated suggestions
778
                // just for this single column.
779
                foreach ($changedTable->removedColumns as $columnName => $removedColumn) {
780
                    $changedTables[$index . ':tbl_' . $removedColumn->getName()] = GeneralUtility::makeInstance(
781
                        TableDiff::class,
782
                        $changedTable->name,
783
                        [],
784
                        [],
785
                        [$columnName => $this->buildQuotedColumn($removedColumn)],
786
                        [],
787
                        [],
788
                        [],
789
                        $fromTable
790
                    );
791
                    if ($isSqlite) {
792
                        $addMoreOperations = false;
793
                        break;
794
                    }
795
                }
796
            }
797
798
            if ($addMoreOperations && count($changedTable->removedIndexes) !== 0) {
799
                // Treat each removed index with a new diff to get a dedicated suggestions
800
                // just for this index.
801
                foreach ($changedTable->removedIndexes as $indexName => $removedIndex) {
802
                    $changedTables[$index . ':idx_' . $removedIndex->getName()] = GeneralUtility::makeInstance(
803
                        TableDiff::class,
804
                        $changedTable->name,
805
                        [],
806
                        [],
807
                        [],
808
                        [],
809
                        [],
810
                        [$indexName => $this->buildQuotedIndex($removedIndex)],
811
                        $fromTable
812
                    );
813
                    if ($isSqlite) {
814
                        $addMoreOperations = false;
815
                        break;
816
                    }
817
                }
818
            }
819
820
            if ($addMoreOperations && count($changedTable->removedForeignKeys) !== 0) {
821
                // Treat each removed foreign key with a new diff to get a dedicated suggestions
822
                // just for this foreign key.
823
                foreach ($changedTable->removedForeignKeys as $removedForeignKey) {
824
                    $fkIndex = $index . ':fk_' . $removedForeignKey->getName();
825
                    $changedTables[$fkIndex] = GeneralUtility::makeInstance(
826
                        TableDiff::class,
827
                        $changedTable->name,
828
                        [],
829
                        [],
830
                        [],
831
                        [],
832
                        [],
833
                        [],
834
                        $fromTable
835
                    );
836
                    $changedTables[$fkIndex]->removedForeignKeys = [$this->buildQuotedForeignKey($removedForeignKey)];
837
                    if ($isSqlite) {
838
                        break;
839
                    }
840
                }
841
            }
842
        }
843
844
        // Build a new schema diff that only contains removable fields
845
        $removedFieldDiff = GeneralUtility::makeInstance(
846
            SchemaDiff::class,
847
            [],
848
            $changedTables,
849
            [],
850
            $schemaDiff->fromSchema
851
        );
852
853
        $statements = $removedFieldDiff->toSql($this->connection->getDatabasePlatform());
854
855
        return ['drop' => $this->calculateUpdateSuggestionsHashes($statements)];
856
    }
857
858
    /**
859
     * Extract update suggestions (SQL statements) for tables that can
860
     * be removed from the complete schema diff.
861
     * Tables that can be removed have been prefixed in a previous run
862
     * of the schema migration.
863
     *
864
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
865
     * @return array
866
     * @throws \Doctrine\DBAL\Schema\SchemaException
867
     * @throws \InvalidArgumentException
868
     */
869
    protected function getDropTableUpdateSuggestions(SchemaDiff $schemaDiff): array
870
    {
871
        $updateSuggestions = [];
872
        foreach ($schemaDiff->removedTables as $removedTable) {
873
            // Build a new schema diff that only contains this table
874
            $tableDiff = GeneralUtility::makeInstance(
875
                SchemaDiff::class,
876
                [],
877
                [],
878
                [$this->buildQuotedTable($removedTable)],
879
                $schemaDiff->fromSchema
880
            );
881
882
            $statements = $tableDiff->toSql($this->connection->getDatabasePlatform());
883
            foreach ($statements as $statement) {
884
                $updateSuggestions['drop_table'][md5($statement)] = $statement;
885
            }
886
887
            // Only store the record count for this table for the first statement,
888
            // assuming that this is the actual DROP TABLE statement.
889
            $updateSuggestions['tables_count'][md5($statements[0])] = $this->getTableRecordCount(
890
                $removedTable->getName()
891
            );
892
        }
893
894
        return $updateSuggestions;
895
    }
896
897
    /**
898
     * Move tables to be removed that are not prefixed with the deleted prefix to the list
899
     * of changed tables and set a new prefixed name.
900
     * Without this help the Doctrine SchemaDiff has no idea if a table has been renamed and
901
     * performs a drop of the old table and creates a new table, which leads to all data in
902
     * the old table being lost.
903
     *
904
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
905
     * @return \Doctrine\DBAL\Schema\SchemaDiff
906
     * @throws \InvalidArgumentException
907
     */
908
    protected function migrateUnprefixedRemovedTablesToRenames(SchemaDiff $schemaDiff): SchemaDiff
909
    {
910
        foreach ($schemaDiff->removedTables as $index => $removedTable) {
911
            if (strpos($removedTable->getName(), $this->deletedPrefix) === 0) {
912
                continue;
913
            }
914
            $tableDiff = GeneralUtility::makeInstance(
915
                TableDiff::class,
916
                $removedTable->getQuotedName($this->connection->getDatabasePlatform()),
917
                $addedColumns = [],
918
                $changedColumns = [],
919
                $removedColumns = [],
920
                $addedIndexes = [],
921
                $changedIndexes = [],
922
                $removedIndexes = [],
923
                $this->buildQuotedTable($removedTable)
924
            );
925
926
            $tableDiff->newName = $this->connection->getDatabasePlatform()->quoteIdentifier(
927
                substr(
928
                    $this->deletedPrefix . $removedTable->getName(),
929
                    0,
930
                    PlatformInformation::getMaxIdentifierLength($this->connection->getDatabasePlatform())
931
                )
932
            );
933
            $schemaDiff->changedTables[$index] = $tableDiff;
934
            unset($schemaDiff->removedTables[$index]);
935
        }
936
937
        return $schemaDiff;
938
    }
939
940
    /**
941
     * Scan the list of changed tables for fields that are going to be dropped. If
942
     * the name of the field does not start with the deleted prefix mark the column
943
     * for a rename instead of a drop operation.
944
     *
945
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
946
     * @return \Doctrine\DBAL\Schema\SchemaDiff
947
     * @throws \InvalidArgumentException
948
     */
949
    protected function migrateUnprefixedRemovedFieldsToRenames(SchemaDiff $schemaDiff): SchemaDiff
950
    {
951
        foreach ($schemaDiff->changedTables as $tableIndex => $changedTable) {
952
            if (count($changedTable->removedColumns) === 0) {
953
                continue;
954
            }
955
956
            foreach ($changedTable->removedColumns as $columnIndex => $removedColumn) {
957
                if (strpos($removedColumn->getName(), $this->deletedPrefix) === 0) {
958
                    continue;
959
                }
960
961
                // Build a new column object with the same properties as the removed column
962
                $renamedColumnName = substr(
963
                    $this->deletedPrefix . $removedColumn->getName(),
964
                    0,
965
                    PlatformInformation::getMaxIdentifierLength($this->connection->getDatabasePlatform())
966
                );
967
                $renamedColumn = new Column(
968
                    $this->connection->quoteIdentifier($renamedColumnName),
969
                    $removedColumn->getType(),
970
                    array_diff_key($removedColumn->toArray(), ['name', 'type'])
971
                );
972
973
                // Build the diff object for the column to rename
974
                $columnDiff = GeneralUtility::makeInstance(
975
                    ColumnDiff::class,
976
                    $removedColumn->getQuotedName($this->connection->getDatabasePlatform()),
977
                    $renamedColumn,
978
                    $changedProperties = [],
979
                    $this->buildQuotedColumn($removedColumn)
980
                );
981
982
                // Add the column with the required rename information to the changed column list
983
                $schemaDiff->changedTables[$tableIndex]->changedColumns[$columnIndex] = $columnDiff;
984
985
                // Remove the column from the list of columns to be dropped
986
                unset($schemaDiff->changedTables[$tableIndex]->removedColumns[$columnIndex]);
987
            }
988
        }
989
990
        return $schemaDiff;
991
    }
992
993
    /**
994
     * Revert the automatic rename optimization that Doctrine performs when it detects
995
     * a column being added and a column being dropped that only differ by name.
996
     *
997
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
998
     * @return SchemaDiff
999
     * @throws \Doctrine\DBAL\Schema\SchemaException
1000
     * @throws \InvalidArgumentException
1001
     */
1002
    protected function migrateColumnRenamesToDistinctActions(SchemaDiff $schemaDiff): SchemaDiff
1003
    {
1004
        foreach ($schemaDiff->changedTables as $index => $changedTable) {
1005
            if (count($changedTable->renamedColumns) === 0) {
1006
                continue;
1007
            }
1008
1009
            // Treat each renamed column with a new diff to get a dedicated
1010
            // suggestion just for this single column.
1011
            foreach ($changedTable->renamedColumns as $originalColumnName => $renamedColumn) {
1012
                $columnOptions = array_diff_key($renamedColumn->toArray(), ['name', 'type']);
1013
1014
                $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance(
1015
                    Column::class,
1016
                    $renamedColumn->getName(),
1017
                    $renamedColumn->getType(),
1018
                    $columnOptions
1019
                );
1020
                $changedTable->removedColumns[$originalColumnName] = GeneralUtility::makeInstance(
1021
                    Column::class,
1022
                    $originalColumnName,
1023
                    $renamedColumn->getType(),
1024
                    $columnOptions
1025
                );
1026
1027
                unset($changedTable->renamedColumns[$originalColumnName]);
1028
            }
1029
        }
1030
1031
        return $schemaDiff;
1032
    }
1033
1034
    /**
1035
     * Return the amount of records in the given table.
1036
     *
1037
     * @param string $tableName
1038
     * @return int
1039
     * @throws \InvalidArgumentException
1040
     */
1041
    protected function getTableRecordCount(string $tableName): int
1042
    {
1043
        return GeneralUtility::makeInstance(ConnectionPool::class)
1044
            ->getConnectionForTable($tableName)
1045
            ->count('*', $tableName, []);
1046
    }
1047
1048
    /**
1049
     * Determine the connection name for a table
1050
     *
1051
     * @param string $tableName
1052
     * @return string
1053
     * @throws \InvalidArgumentException
1054
     */
1055
    protected function getConnectionNameForTable(string $tableName): string
1056
    {
1057
        $connectionNames = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionNames();
1058
1059
        if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName])) {
1060
            return in_array($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName], $connectionNames, true)
1061
                ? $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName]
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
1062
                : ConnectionPool::DEFAULT_CONNECTION_NAME;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
1063
        }
1064
1065
        return ConnectionPool::DEFAULT_CONNECTION_NAME;
1066
    }
1067
1068
    /**
1069
     * Replace the array keys with a md5 sum of the actual SQL statement
1070
     *
1071
     * @param string[] $statements
1072
     * @return string[]
1073
     */
1074
    protected function calculateUpdateSuggestionsHashes(array $statements): array
1075
    {
1076
        return array_combine(array_map('md5', $statements), $statements);
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_combine(arr...atements), $statements) could return the type false which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
1077
    }
1078
1079
    /**
1080
     * Helper for buildSchemaDiff to filter an array of TableDiffs against a list of valid table names.
1081
     *
1082
     * @param TableDiff[]|Table[] $tableDiffs
1083
     * @param string[] $validTableNames
1084
     * @return TableDiff[]
1085
     * @throws \InvalidArgumentException
1086
     */
1087
    protected function removeUnrelatedTables(array $tableDiffs, array $validTableNames): array
1088
    {
1089
        return array_filter(
1090
            $tableDiffs,
1091
            function ($table) use ($validTableNames) {
1092
                if ($table instanceof Table) {
1093
                    $tableName = $table->getName();
1094
                } else {
1095
                    $tableName = $table->newName ?: $table->name;
1096
                }
1097
1098
                // If the tablename has a deleted prefix strip it of before comparing
1099
                // it against the list of valid table names so that drop operations
1100
                // don't get removed.
1101
                if (strpos($tableName, $this->deletedPrefix) === 0) {
1102
                    $tableName = substr($tableName, strlen($this->deletedPrefix));
1103
                }
1104
                return in_array($tableName, $validTableNames, true)
1105
                    || in_array($this->deletedPrefix . $tableName, $validTableNames, true);
1106
            }
1107
        );
1108
    }
1109
1110
    /**
1111
     * Transform the table information to conform to specific
1112
     * requirements of different database platforms like removing
1113
     * the index substring length for Non-MySQL Platforms.
1114
     *
1115
     * @param Table[] $tables
1116
     * @param \TYPO3\CMS\Core\Database\Connection $connection
1117
     * @return Table[]
1118
     * @throws \InvalidArgumentException
1119
     */
1120
    protected function transformTablesForDatabasePlatform(array $tables, Connection $connection): array
1121
    {
1122
        $defaultTableOptions = $connection->getParams()['tableoptions'] ?? [];
1123
        foreach ($tables as &$table) {
1124
            $indexes = [];
1125
            foreach ($table->getIndexes() as $key => $index) {
1126
                $indexName = $index->getName();
1127
                // PostgreSQL and sqlite require index names to be unique per database/schema.
1128
                if ($connection->getDatabasePlatform() instanceof PostgreSqlPlatform
1129
                    || $connection->getDatabasePlatform() instanceof SqlitePlatform
1130
                ) {
1131
                    $indexName = $indexName . '_' . hash('crc32b', $table->getName() . '_' . $indexName);
1132
                }
1133
1134
                // Remove the length information from column names for indexes if required.
1135
                $cleanedColumnNames = array_map(
1136
                    function (string $columnName) use ($connection) {
1137
                        if ($connection->getDatabasePlatform() instanceof MySqlPlatform) {
1138
                            // Returning the unquoted, unmodified version of the column name since
1139
                            // it can include the length information for BLOB/TEXT columns which
1140
                            // may not be quoted.
1141
                            return $columnName;
1142
                        }
1143
1144
                        return $connection->quoteIdentifier(preg_replace('/\(\d+\)$/', '', $columnName));
1145
                    },
1146
                    $index->getUnquotedColumns()
1147
                );
1148
1149
                $indexes[$key] = GeneralUtility::makeInstance(
1150
                    Index::class,
1151
                    $connection->quoteIdentifier($indexName),
1152
                    $cleanedColumnNames,
1153
                    $index->isUnique(),
1154
                    $index->isPrimary(),
1155
                    $index->getFlags(),
1156
                    $index->getOptions()
1157
                );
1158
            }
1159
1160
            $table = GeneralUtility::makeInstance(
1161
                Table::class,
1162
                $table->getQuotedName($connection->getDatabasePlatform()),
1163
                $table->getColumns(),
1164
                $indexes,
1165
                $table->getForeignKeys(),
1166
                0,
1167
                array_merge($defaultTableOptions, $table->getOptions())
1168
            );
1169
        }
1170
1171
        return $tables;
1172
    }
1173
1174
    /**
1175
     * Get COLLATION, ROW_FORMAT, COMMENT and ENGINE table options on MySQL connections.
1176
     *
1177
     * @param string[] $tableNames
1178
     * @return array[]
1179
     * @throws \InvalidArgumentException
1180
     */
1181
    protected function getTableOptions(array $tableNames): array
1182
    {
1183
        $tableOptions = [];
1184
        if (strpos($this->connection->getServerVersion(), 'MySQL') !== 0) {
1185
            foreach ($tableNames as $tableName) {
1186
                $tableOptions[$tableName] = [];
1187
            }
1188
1189
            return $tableOptions;
1190
        }
1191
1192
        $queryBuilder = $this->connection->createQueryBuilder();
1193
        $result = $queryBuilder
1194
            ->select(
1195
                'tables.TABLE_NAME AS table',
1196
                'tables.ENGINE AS engine',
1197
                'tables.ROW_FORMAT AS row_format',
1198
                'tables.TABLE_COLLATION AS collate',
1199
                'tables.TABLE_COMMENT AS comment',
1200
                'CCSA.character_set_name AS charset'
1201
            )
1202
            ->from('information_schema.TABLES', 'tables')
1203
            ->join(
1204
                'tables',
1205
                'information_schema.COLLATION_CHARACTER_SET_APPLICABILITY',
1206
                'CCSA',
1207
                $queryBuilder->expr()->eq(
1208
                    'CCSA.collation_name',
1209
                    $queryBuilder->quoteIdentifier('tables.table_collation')
1210
                )
1211
            )
1212
            ->where(
1213
                $queryBuilder->expr()->eq(
1214
                    'TABLE_TYPE',
1215
                    $queryBuilder->createNamedParameter('BASE TABLE', \PDO::PARAM_STR)
1216
                ),
1217
                $queryBuilder->expr()->eq(
1218
                    'TABLE_SCHEMA',
1219
                    $queryBuilder->createNamedParameter($this->connection->getDatabase(), \PDO::PARAM_STR)
1220
                )
1221
            )
1222
            ->execute();
1223
1224
        while ($row = $result->fetch()) {
1225
            $index = $row['table'];
1226
            unset($row['table']);
1227
            $tableOptions[$index] = $row;
1228
        }
1229
1230
        return $tableOptions;
1231
    }
1232
1233
    /**
1234
     * Helper function to build a table object that has the _quoted attribute set so that the SchemaManager
1235
     * will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine doesn't
1236
     * provide a method to set the flag after the object has been instantiated and there's no possibility to
1237
     * hook into the createSchema() method early enough to influence the original table object.
1238
     *
1239
     * @param \Doctrine\DBAL\Schema\Table $table
1240
     * @return \Doctrine\DBAL\Schema\Table
1241
     */
1242
    protected function buildQuotedTable(Table $table): Table
1243
    {
1244
        $databasePlatform = $this->connection->getDatabasePlatform();
1245
1246
        return GeneralUtility::makeInstance(
1247
            Table::class,
1248
            $databasePlatform->quoteIdentifier($table->getName()),
1249
            $table->getColumns(),
1250
            $table->getIndexes(),
1251
            $table->getForeignKeys(),
1252
            0,
1253
            $table->getOptions()
1254
        );
1255
    }
1256
1257
    /**
1258
     * Helper function to build a column object that has the _quoted attribute set so that the SchemaManager
1259
     * will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine doesn't
1260
     * provide a method to set the flag after the object has been instantiated and there's no possibility to
1261
     * hook into the createSchema() method early enough to influence the original column object.
1262
     *
1263
     * @param \Doctrine\DBAL\Schema\Column $column
1264
     * @return \Doctrine\DBAL\Schema\Column
1265
     */
1266
    protected function buildQuotedColumn(Column $column): Column
1267
    {
1268
        $databasePlatform = $this->connection->getDatabasePlatform();
1269
1270
        return GeneralUtility::makeInstance(
1271
            Column::class,
1272
            $databasePlatform->quoteIdentifier($column->getName()),
1273
            $column->getType(),
1274
            array_diff_key($column->toArray(), ['name', 'type'])
1275
        );
1276
    }
1277
1278
    /**
1279
     * Helper function to build an index 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\Index $index
1285
     * @return \Doctrine\DBAL\Schema\Index
1286
     */
1287
    protected function buildQuotedIndex(Index $index): Index
1288
    {
1289
        $databasePlatform = $this->connection->getDatabasePlatform();
1290
1291
        return GeneralUtility::makeInstance(
1292
            Index::class,
1293
            $databasePlatform->quoteIdentifier($index->getName()),
1294
            $index->getColumns(),
1295
            $index->isUnique(),
1296
            $index->isPrimary(),
1297
            $index->getFlags(),
1298
            $index->getOptions()
1299
        );
1300
    }
1301
1302
    /**
1303
     * Helper function to build a foreign key constraint object that has the _quoted attribute set so that the
1304
     * SchemaManager will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine
1305
     * doesn't provide a method to set the flag after the object has been instantiated and there's no possibility to
1306
     * hook into the createSchema() method early enough to influence the original column object.
1307
     *
1308
     * @param \Doctrine\DBAL\Schema\ForeignKeyConstraint $index
1309
     * @return \Doctrine\DBAL\Schema\ForeignKeyConstraint
1310
     */
1311
    protected function buildQuotedForeignKey(ForeignKeyConstraint $index): ForeignKeyConstraint
1312
    {
1313
        $databasePlatform = $this->connection->getDatabasePlatform();
1314
1315
        return GeneralUtility::makeInstance(
1316
            ForeignKeyConstraint::class,
1317
            $index->getLocalColumns(),
1318
            $databasePlatform->quoteIdentifier($index->getForeignTableName()),
1319
            $index->getForeignColumns(),
1320
            $databasePlatform->quoteIdentifier($index->getName()),
1321
            $index->getOptions()
1322
        );
1323
    }
1324
1325
    protected function tableRunsOnSqlite(string $tableName): bool
1326
    {
1327
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName);
1328
        return $connection->getDatabasePlatform() instanceof SqlitePlatform;
1329
    }
1330
}
1331