Passed
Push — master ( 8fe902...154606 )
by
unknown
31:52 queued 14:07
created

ConnectionMigrator::getTableOptions()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 50
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

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

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