Passed
Push — master ( abe621...79303b )
by
unknown
13:58
created

ConnectionMigrator::tableRunsOnSqlite()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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