Passed
Branch master (6c65a4)
by Christian
27:15 queued 11:09
created

getTableAndFieldNameMaxLengths()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 8
nop 1
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
namespace TYPO3\CMS\Core\Database\Schema;
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
use Doctrine\DBAL\DBALException;
19
use Doctrine\DBAL\Platforms\MySqlPlatform;
20
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
21
use Doctrine\DBAL\Schema\Column;
22
use Doctrine\DBAL\Schema\ColumnDiff;
23
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
24
use Doctrine\DBAL\Schema\Index;
25
use Doctrine\DBAL\Schema\Schema;
26
use Doctrine\DBAL\Schema\SchemaConfig;
27
use Doctrine\DBAL\Schema\SchemaDiff;
28
use Doctrine\DBAL\Schema\Table;
29
use TYPO3\CMS\Core\Database\Connection;
30
use TYPO3\CMS\Core\Database\ConnectionPool;
31
use TYPO3\CMS\Core\Utility\GeneralUtility;
32
33
/**
34
 * Handling schema migrations per connection.
35
 *
36
 * @internal
37
 */
38
class ConnectionMigrator
39
{
40
    /**
41
     * @var string Prefix of deleted tables
42
     */
43
    protected $deletedPrefix = 'zzz_deleted_';
44
45
    /**
46
     * @var array
47
     */
48
    protected $tableAndFieldMaxNameLengthsPerDbPlatform = [
49
        'default' => [
50
            'tables' => 30,
51
            'columns' => 30
52
        ],
53
        'mysql' => [
54
            'tables' => 64,
55
            'columns' => 64
56
        ],
57
        'drizzle_pdo_mysql' => 'mysql',
58
        'mysqli' => 'mysql',
59
        'pdo_mysql' => 'mysql',
60
        'pdo_sqlite' => 'mysql',
61
        'postgresql' => [
62
            'tables' => 63,
63
            'columns' => 63
64
        ],
65
        'sqlserver' => [
66
            'tables' => 128,
67
            'columns' => 128
68
        ],
69
        'pdo_sqlsrv' => 'sqlserver',
70
        'sqlsrv' => 'sqlserver',
71
        'ibm' => [
72
            'tables' => 30,
73
            'columns' => 30
74
        ],
75
        'ibm_db2' => 'ibm',
76
        'pdo_ibm' => 'ibm',
77
        'oci8' => [
78
            'tables' => 30,
79
            'columns' => 30
80
        ],
81
        'sqlanywhere' => [
82
            'tables' => 128,
83
            'columns' => 128
84
        ]
85
    ];
86
87
    /**
88
     * @var Connection
89
     */
90
    protected $connection;
91
92
    /**
93
     * @var string
94
     */
95
    protected $connectionName;
96
97
    /**
98
     * @var Table[]
99
     */
100
    protected $tables;
101
102
    /**
103
     * @param string $connectionName
104
     * @param Table[] $tables
105
     */
106
    public function __construct(string $connectionName, array $tables)
107
    {
108
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
109
        $this->connection = $connectionPool->getConnectionByName($connectionName);
110
        $this->connectionName = $connectionName;
111
        $this->tables = $tables;
112
    }
113
114
    /**
115
     * @param string $connectionName
116
     * @param Table[] $tables
117
     * @return ConnectionMigrator
118
     */
119
    public static function create(string $connectionName, array $tables)
120
    {
121
        return GeneralUtility::makeInstance(
122
            static::class,
123
            $connectionName,
0 ignored issues
show
Bug introduced by
$connectionName of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

123
            /** @scrutinizer ignore-type */ $connectionName,
Loading history...
124
            $tables
125
        );
126
    }
127
128
    /**
129
     * Return the raw Doctrine SchemaDiff object for the current connection.
130
     * This diff contains all changes without any pre-processing.
131
     *
132
     * @return SchemaDiff
133
     */
134
    public function getSchemaDiff(): SchemaDiff
135
    {
136
        return $this->buildSchemaDiff(false);
137
    }
138
139
    /**
140
     * Compare current and expected schema definitions and provide updates
141
     * suggestions in the form of SQL statements.
142
     *
143
     * @param bool $remove
144
     * @return array
145
     */
146
    public function getUpdateSuggestions(bool $remove = false): array
147
    {
148
        $schemaDiff = $this->buildSchemaDiff();
149
150
        if ($remove === false) {
151
            return array_merge_recursive(
152
                ['add' => [], 'create_table' => [], 'change' => [], 'change_currentValue' => []],
153
                $this->getNewFieldUpdateSuggestions($schemaDiff),
154
                $this->getNewTableUpdateSuggestions($schemaDiff),
155
                $this->getChangedFieldUpdateSuggestions($schemaDiff),
156
                $this->getChangedTableOptions($schemaDiff)
157
            );
158
        }
159
        return array_merge_recursive(
160
                ['change' => [], 'change_table' => [], 'drop' => [], 'drop_table' => [], 'tables_count' => []],
161
                $this->getUnusedFieldUpdateSuggestions($schemaDiff),
162
                $this->getUnusedTableUpdateSuggestions($schemaDiff),
163
                $this->getDropTableUpdateSuggestions($schemaDiff),
164
                $this->getDropFieldUpdateSuggestions($schemaDiff)
165
            );
166
    }
167
168
    /**
169
     * Perform add/change/create operations on tables and fields in an
170
     * optimized, non-interactive, mode using the original doctrine
171
     * SchemaManager ->toSaveSql() method.
172
     *
173
     * @param bool $createOnly
174
     * @return array
175
     */
176
    public function install(bool $createOnly = false): array
177
    {
178
        $result = [];
179
        $schemaDiff = $this->buildSchemaDiff(false);
180
181
        $schemaDiff->removedTables = [];
182
        foreach ($schemaDiff->changedTables as $key => $changedTable) {
183
            $schemaDiff->changedTables[$key]->removedColumns = [];
184
            $schemaDiff->changedTables[$key]->removedIndexes = [];
185
186
            // With partial ext_tables.sql files the SchemaManager is detecting
187
            // existing columns as false positives for a column rename. In this
188
            // context every rename is actually a new column.
189
            foreach ($changedTable->renamedColumns as $columnName => $renamedColumn) {
190
                $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance(
191
                    Column::class,
192
                    $renamedColumn->getName(),
0 ignored issues
show
Bug introduced by
$renamedColumn->getName() of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

192
                    /** @scrutinizer ignore-type */ $renamedColumn->getName(),
Loading history...
193
                    $renamedColumn->getType(),
0 ignored issues
show
Bug introduced by
$renamedColumn->getType() of type Doctrine\DBAL\Types\Type is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

193
                    /** @scrutinizer ignore-type */ $renamedColumn->getType(),
Loading history...
194
                    array_diff_key($renamedColumn->toArray(), ['name', 'type'])
195
                );
196
                unset($changedTable->renamedColumns[$columnName]);
197
            }
198
199
            if ($createOnly) {
200
                // Ignore new indexes that work on columns that need changes
201
                foreach ($changedTable->addedIndexes as $indexName => $addedIndex) {
202
                    $indexColumns = array_map(
203
                        function ($columnName) {
204
                            // Strip MySQL prefix length information to get real column names
205
                            $columnName = preg_replace('/\(\d+\)$/', '', $columnName);
206
                            // Strip mssql '[' and ']' from column names
207
                            $columnName = ltrim($columnName, '[');
208
                            return rtrim($columnName, ']');
209
                        },
210
                        $addedIndex->getColumns()
211
                    );
212
                    $columnChanges = array_intersect($indexColumns, array_keys($changedTable->changedColumns));
213
                    if (!empty($columnChanges)) {
214
                        unset($schemaDiff->changedTables[$key]->addedIndexes[$indexName]);
215
                    }
216
                }
217
                $schemaDiff->changedTables[$key]->changedColumns = [];
218
                $schemaDiff->changedTables[$key]->changedIndexes = [];
219
                $schemaDiff->changedTables[$key]->renamedIndexes = [];
220
            }
221
        }
222
223
        $statements = $schemaDiff->toSaveSql(
224
            $this->connection->getDatabasePlatform()
0 ignored issues
show
Bug introduced by
$this->connection->getDatabasePlatform() of type array is incompatible with the type Doctrine\DBAL\Platforms\AbstractPlatform expected by parameter $platform of Doctrine\DBAL\Schema\SchemaDiff::toSaveSql(). ( Ignorable by Annotation )

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

224
            /** @scrutinizer ignore-type */ $this->connection->getDatabasePlatform()
Loading history...
225
        );
226
227
        foreach ($statements as $statement) {
228
            try {
229
                $this->connection->executeUpdate($statement);
230
                $result[$statement] = '';
231
            } catch (DBALException $e) {
232
                $result[$statement] = $e->getPrevious()->getMessage();
233
            }
234
        }
235
236
        return $result;
237
    }
238
239
    /**
240
     * If the schema is not for the Default connection remove all tables from the schema
241
     * that have no mapping in the TYPO3 configuration. This avoids update suggestions
242
     * for tables that are in the database but have no direct relation to the TYPO3 instance.
243
     *
244
     * @param bool $renameUnused
245
     * @throws \Doctrine\DBAL\DBALException
246
     * @return \Doctrine\DBAL\Schema\SchemaDiff
247
     * @throws \Doctrine\DBAL\Schema\SchemaException
248
     * @throws \InvalidArgumentException
249
     */
250
    protected function buildSchemaDiff(bool $renameUnused = true): SchemaDiff
251
    {
252
        // Build the schema definitions
253
        $fromSchema = $this->connection->getSchemaManager()->createSchema();
254
        $toSchema = $this->buildExpectedSchemaDefinitions($this->connectionName);
255
256
        // Add current table options to the fromSchema
257
        $tableOptions = $this->getTableOptions($fromSchema->getTableNames());
258
        foreach ($fromSchema->getTables() as $table) {
259
            $tableName = $table->getName();
260
            if (!array_key_exists($tableName, $tableOptions)) {
261
                continue;
262
            }
263
            foreach ($tableOptions[$tableName] as $optionName => $optionValue) {
264
                $table->addOption($optionName, $optionValue);
265
            }
266
        }
267
268
        // Build SchemaDiff and handle renames of tables and colums
269
        $comparator = GeneralUtility::makeInstance(Comparator::class, $this->connection->getDatabasePlatform());
0 ignored issues
show
Bug introduced by
$this->connection->getDatabasePlatform() of type Doctrine\DBAL\Platforms\AbstractPlatform is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

269
        $comparator = GeneralUtility::makeInstance(Comparator::class, /** @scrutinizer ignore-type */ $this->connection->getDatabasePlatform());
Loading history...
270
        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
271
        $schemaDiff = $this->migrateColumnRenamesToDistinctActions($schemaDiff);
272
273
        if ($renameUnused) {
274
            $schemaDiff = $this->migrateUnprefixedRemovedTablesToRenames($schemaDiff);
275
            $schemaDiff = $this->migrateUnprefixedRemovedFieldsToRenames($schemaDiff);
276
        }
277
278
        // All tables in the default connection are managed by TYPO3
279
        if ($this->connectionName === ConnectionPool::DEFAULT_CONNECTION_NAME) {
280
            return $schemaDiff;
281
        }
282
283
        // If there are no mapped tables return a SchemaDiff without any changes
284
        // to avoid update suggestions for tables not related to TYPO3.
285
        if (empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'] ?? null)) {
286
            return GeneralUtility::makeInstance(SchemaDiff::class, [], [], [], $fromSchema);
0 ignored issues
show
Bug introduced by
$fromSchema of type Doctrine\DBAL\Schema\Schema is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

286
            return GeneralUtility::makeInstance(SchemaDiff::class, [], [], [], /** @scrutinizer ignore-type */ $fromSchema);
Loading history...
287
        }
288
289
        // Collect the table names that have been mapped to this connection.
290
        $connectionName = $this->connectionName;
291
        $tablesForConnection = array_keys(
292
            array_filter(
293
                $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'],
294
                function ($tableConnectionName) use ($connectionName) {
295
                    return $tableConnectionName === $connectionName;
296
                }
297
            )
298
        );
299
300
        // Remove all tables that are not assigned to this connection from the diff
301
        $schemaDiff->newTables = $this->removeUnrelatedTables($schemaDiff->newTables, $tablesForConnection);
302
        $schemaDiff->changedTables = $this->removeUnrelatedTables($schemaDiff->changedTables, $tablesForConnection);
303
        $schemaDiff->removedTables = $this->removeUnrelatedTables($schemaDiff->removedTables, $tablesForConnection);
304
305
        return $schemaDiff;
306
    }
307
308
    /**
309
     * Build the expected schema definitons from raw SQL statements.
310
     *
311
     * @param string $connectionName
312
     * @return \Doctrine\DBAL\Schema\Schema
313
     * @throws \Doctrine\DBAL\DBALException
314
     * @throws \InvalidArgumentException
315
     */
316
    protected function buildExpectedSchemaDefinitions(string $connectionName): Schema
317
    {
318
        /** @var Table[] $tablesForConnection */
319
        $tablesForConnection = [];
320
        foreach ($this->tables as $table) {
321
            $tableName = $table->getName();
322
323
            // Skip tables for a different connection
324
            if ($connectionName !== $this->getConnectionNameForTable($tableName)) {
325
                continue;
326
            }
327
328
            if (!array_key_exists($tableName, $tablesForConnection)) {
329
                $tablesForConnection[$tableName] = $table;
330
                continue;
331
            }
332
333
            // Merge multiple table definitions. Later definitions overrule identical
334
            // columns, indexes and foreign_keys. Order of definitions is based on
335
            // extension load order.
336
            $currentTableDefinition = $tablesForConnection[$tableName];
337
            $tablesForConnection[$tableName] = GeneralUtility::makeInstance(
338
                Table::class,
339
                $tableName,
0 ignored issues
show
Bug introduced by
$tableName of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

339
                /** @scrutinizer ignore-type */ $tableName,
Loading history...
340
                array_merge($currentTableDefinition->getColumns(), $table->getColumns()),
341
                array_merge($currentTableDefinition->getIndexes(), $table->getIndexes()),
342
                array_merge($currentTableDefinition->getForeignKeys(), $table->getForeignKeys()),
343
                0,
0 ignored issues
show
Bug introduced by
0 of type integer is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

343
                /** @scrutinizer ignore-type */ 0,
Loading history...
344
                array_merge($currentTableDefinition->getOptions(), $table->getOptions())
345
            );
346
        }
347
348
        $tablesForConnection = $this->transformTablesForDatabasePlatform($tablesForConnection, $this->connection);
349
350
        $schemaConfig = GeneralUtility::makeInstance(SchemaConfig::class);
351
        $schemaConfig->setName($this->connection->getDatabase());
352
353
        return GeneralUtility::makeInstance(Schema::class, $tablesForConnection, [], $schemaConfig);
0 ignored issues
show
Bug introduced by
$schemaConfig of type object is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

353
        return GeneralUtility::makeInstance(Schema::class, $tablesForConnection, [], /** @scrutinizer ignore-type */ $schemaConfig);
Loading history...
354
    }
355
356
    /**
357
     * Extract the update suggestions (SQL statements) for newly added tables
358
     * from the complete schema diff.
359
     *
360
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
361
     * @return array
362
     * @throws \InvalidArgumentException
363
     */
364
    protected function getNewTableUpdateSuggestions(SchemaDiff $schemaDiff): array
365
    {
366
        // Build a new schema diff that only contains added tables
367
        $addTableSchemaDiff = GeneralUtility::makeInstance(
368
            SchemaDiff::class,
369
            $schemaDiff->newTables,
370
            [],
371
            [],
372
            $schemaDiff->fromSchema
0 ignored issues
show
Bug introduced by
$schemaDiff->fromSchema of type Doctrine\DBAL\Schema\Schema is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

372
            /** @scrutinizer ignore-type */ $schemaDiff->fromSchema
Loading history...
373
        );
374
375
        $statements = $addTableSchemaDiff->toSql($this->connection->getDatabasePlatform());
376
377
        return ['create_table' => $this->calculateUpdateSuggestionsHashes($statements)];
378
    }
379
380
    /**
381
     * Extract the update suggestions (SQL statements) for newly added fields
382
     * from the complete schema diff.
383
     *
384
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
385
     * @return array
386
     * @throws \Doctrine\DBAL\Schema\SchemaException
387
     * @throws \InvalidArgumentException
388
     */
389
    protected function getNewFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
390
    {
391
        $changedTables = [];
392
393
        foreach ($schemaDiff->changedTables as $index => $changedTable) {
394
            $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
395
396
            if (count($changedTable->addedColumns) !== 0) {
397
                // Treat each added column with a new diff to get a dedicated suggestions
398
                // just for this single column.
399
                foreach ($changedTable->addedColumns as $addedColumn) {
400
                    $changedTables[$index . ':tbl_' . $addedColumn->getName()] = GeneralUtility::makeInstance(
401
                        TableDiff::class,
402
                        $changedTable->name,
403
                        [$addedColumn],
404
                        [],
405
                        [],
406
                        [],
407
                        [],
408
                        [],
409
                        $fromTable
0 ignored issues
show
Bug introduced by
$fromTable of type Doctrine\DBAL\Schema\Table is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

409
                        /** @scrutinizer ignore-type */ $fromTable
Loading history...
410
                    );
411
                }
412
            }
413
414
            if (count($changedTable->addedIndexes) !== 0) {
415
                // Treat each added index with a new diff to get a dedicated suggestions
416
                // just for this index.
417
                foreach ($changedTable->addedIndexes as $addedIndex) {
418
                    $changedTables[$index . ':idx_' . $addedIndex->getName()] = GeneralUtility::makeInstance(
419
                        TableDiff::class,
420
                        $changedTable->name,
421
                        [],
422
                        [],
423
                        [],
424
                        [$this->buildQuotedIndex($addedIndex)],
425
                        [],
426
                        [],
427
                        $fromTable
428
                    );
429
                }
430
            }
431
432
            if (count($changedTable->addedForeignKeys) !== 0) {
433
                // Treat each added foreign key with a new diff to get a dedicated suggestions
434
                // just for this foreign key.
435
                foreach ($changedTable->addedForeignKeys as $addedForeignKey) {
436
                    $fkIndex = $index . ':fk_' . $addedForeignKey->getName();
437
                    $changedTables[$fkIndex] = GeneralUtility::makeInstance(
438
                        TableDiff::class,
439
                        $changedTable->name,
440
                        [],
441
                        [],
442
                        [],
443
                        [],
444
                        [],
445
                        [],
446
                        $fromTable
447
                    );
448
                    $changedTables[$fkIndex]->addedForeignKeys = [$this->buildQuotedForeignKey($addedForeignKey)];
449
                }
450
            }
451
        }
452
453
        // Build a new schema diff that only contains added fields
454
        $addFieldSchemaDiff = GeneralUtility::makeInstance(
455
            SchemaDiff::class,
456
            [],
457
            $changedTables,
458
            [],
459
            $schemaDiff->fromSchema
0 ignored issues
show
Bug introduced by
$schemaDiff->fromSchema of type Doctrine\DBAL\Schema\Schema is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

459
            /** @scrutinizer ignore-type */ $schemaDiff->fromSchema
Loading history...
460
        );
461
462
        $statements = $addFieldSchemaDiff->toSql($this->connection->getDatabasePlatform());
463
464
        return ['add' => $this->calculateUpdateSuggestionsHashes($statements)];
465
    }
466
467
    /**
468
     * Extract update suggestions (SQL statements) for changed options
469
     * (like ENGINE) from the complete schema diff.
470
     *
471
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
472
     * @return array
473
     * @throws \Doctrine\DBAL\Schema\SchemaException
474
     * @throws \InvalidArgumentException
475
     */
476
    protected function getChangedTableOptions(SchemaDiff $schemaDiff): array
477
    {
478
        $updateSuggestions = [];
479
480
        foreach ($schemaDiff->changedTables as $tableDiff) {
481
            // Skip processing if this is the base TableDiff class or has no table options set.
482
            if (!$tableDiff instanceof TableDiff || count($tableDiff->getTableOptions()) === 0) {
483
                continue;
484
            }
485
486
            $tableOptions = $tableDiff->getTableOptions();
487
            $tableOptionsDiff = GeneralUtility::makeInstance(
488
                TableDiff::class,
489
                $tableDiff->name,
0 ignored issues
show
Bug introduced by
$tableDiff->name of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

489
                /** @scrutinizer ignore-type */ $tableDiff->name,
Loading history...
490
                [],
491
                [],
492
                [],
493
                [],
494
                [],
495
                [],
496
                $tableDiff->fromTable
0 ignored issues
show
Bug introduced by
$tableDiff->fromTable of type Doctrine\DBAL\Schema\Table is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

496
                /** @scrutinizer ignore-type */ $tableDiff->fromTable
Loading history...
497
            );
498
            $tableOptionsDiff->setTableOptions($tableOptions);
499
500
            $tableOptionsSchemaDiff = GeneralUtility::makeInstance(
501
                SchemaDiff::class,
502
                [],
503
                [$tableOptionsDiff],
504
                [],
505
                $schemaDiff->fromSchema
0 ignored issues
show
Bug introduced by
$schemaDiff->fromSchema of type Doctrine\DBAL\Schema\Schema is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

505
                /** @scrutinizer ignore-type */ $schemaDiff->fromSchema
Loading history...
506
            );
507
508
            $statements = $tableOptionsSchemaDiff->toSaveSql($this->connection->getDatabasePlatform());
509
            foreach ($statements as $statement) {
510
                $updateSuggestions['change'][md5($statement)] = $statement;
511
            }
512
        }
513
514
        return $updateSuggestions;
515
    }
516
517
    /**
518
     * Extract update suggestions (SQL statements) for changed fields
519
     * from the complete schema diff.
520
     *
521
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
522
     * @return array
523
     * @throws \Doctrine\DBAL\Schema\SchemaException
524
     * @throws \InvalidArgumentException
525
     */
526
    protected function getChangedFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
527
    {
528
        $databasePlatform = $this->connection->getDatabasePlatform();
529
        $updateSuggestions = [];
530
531
        foreach ($schemaDiff->changedTables as $index => $changedTable) {
532
            if (count($changedTable->changedColumns) !== 0) {
533
                // Treat each changed column with a new diff to get a dedicated suggestions
534
                // just for this single column.
535
                $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
536
537
                foreach ($changedTable->changedColumns as $changedColumn) {
538
                    // Field has been renamed and will be handled separately
539
                    if ($changedColumn->getOldColumnName()->getName() !== $changedColumn->column->getName()) {
540
                        continue;
541
                    }
542
543
                    $changedColumn->fromColumn = $this->buildQuotedColumn($changedColumn->fromColumn);
544
545
                    // Get the current SQL declaration for the column
546
                    $currentColumn = $fromTable->getColumn($changedColumn->getOldColumnName()->getName());
547
                    $currentDeclaration = $databasePlatform->getColumnDeclarationSQL(
548
                        $currentColumn->getQuotedName($this->connection->getDatabasePlatform()),
549
                        $currentColumn->toArray()
550
                    );
551
552
                    // Build a dedicated diff just for the current column
553
                    $tableDiff = GeneralUtility::makeInstance(
554
                        TableDiff::class,
555
                        $changedTable->name,
556
                        [],
557
                        [$changedColumn],
558
                        [],
559
                        [],
560
                        [],
561
                        [],
562
                        $fromTable
0 ignored issues
show
Bug introduced by
$fromTable of type Doctrine\DBAL\Schema\Table is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

562
                        /** @scrutinizer ignore-type */ $fromTable
Loading history...
563
                    );
564
565
                    $temporarySchemaDiff = GeneralUtility::makeInstance(
566
                        SchemaDiff::class,
567
                        [],
568
                        [$tableDiff],
569
                        [],
570
                        $schemaDiff->fromSchema
0 ignored issues
show
Bug introduced by
$schemaDiff->fromSchema of type Doctrine\DBAL\Schema\Schema is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

570
                        /** @scrutinizer ignore-type */ $schemaDiff->fromSchema
Loading history...
571
                    );
572
573
                    $statements = $temporarySchemaDiff->toSql($databasePlatform);
574
                    foreach ($statements as $statement) {
575
                        $updateSuggestions['change'][md5($statement)] = $statement;
576
                        $updateSuggestions['change_currentValue'][md5($statement)] = $currentDeclaration;
577
                    }
578
                }
579
            }
580
581
            // Treat each changed index with a new diff to get a dedicated suggestions
582
            // just for this index.
583
            if (count($changedTable->changedIndexes) !== 0) {
584
                foreach ($changedTable->renamedIndexes as $key => $changedIndex) {
585
                    $indexDiff = GeneralUtility::makeInstance(
586
                        TableDiff::class,
587
                        $changedTable->name,
588
                        [],
589
                        [],
590
                        [],
591
                        [],
592
                        [$changedIndex],
593
                        [],
594
                        $schemaDiff->fromSchema->getTable($changedTable->name)
0 ignored issues
show
Bug introduced by
$changedTable->name of type array is incompatible with the type string expected by parameter $tableName of Doctrine\DBAL\Schema\Schema::getTable(). ( Ignorable by Annotation )

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

594
                        $schemaDiff->fromSchema->getTable(/** @scrutinizer ignore-type */ $changedTable->name)
Loading history...
595
                    );
596
597
                    $temporarySchemaDiff = GeneralUtility::makeInstance(
598
                        SchemaDiff::class,
599
                        [],
600
                        [$indexDiff],
601
                        [],
602
                        $schemaDiff->fromSchema
603
                    );
604
605
                    $statements = $temporarySchemaDiff->toSql($databasePlatform);
606
                    foreach ($statements as $statement) {
607
                        $updateSuggestions['change'][md5($statement)] = $statement;
608
                    }
609
                }
610
            }
611
612
            // Treat renamed indexes as a field change as it's a simple rename operation
613
            if (count($changedTable->renamedIndexes) !== 0) {
614
                // Create a base table diff without any changes, there's no constructor
615
                // argument to pass in renamed indexes.
616
                $tableDiff = GeneralUtility::makeInstance(
617
                    TableDiff::class,
618
                    $changedTable->name,
619
                    [],
620
                    [],
621
                    [],
622
                    [],
623
                    [],
624
                    [],
625
                    $schemaDiff->fromSchema->getTable($changedTable->name)
626
                );
627
628
                // Treat each renamed index with a new diff to get a dedicated suggestions
629
                // just for this index.
630
                foreach ($changedTable->renamedIndexes as $key => $renamedIndex) {
631
                    $indexDiff = clone $tableDiff;
632
                    $indexDiff->renamedIndexes = [$key => $renamedIndex];
633
634
                    $temporarySchemaDiff = GeneralUtility::makeInstance(
635
                        SchemaDiff::class,
636
                        [],
637
                        [$indexDiff],
638
                        [],
639
                        $schemaDiff->fromSchema
640
                    );
641
642
                    $statements = $temporarySchemaDiff->toSql($databasePlatform);
643
                    foreach ($statements as $statement) {
644
                        $updateSuggestions['change'][md5($statement)] = $statement;
645
                    }
646
                }
647
            }
648
649
            // Treat each changed foreign key with a new diff to get a dedicated suggestions
650
            // just for this foreign key.
651
            if (count($changedTable->changedForeignKeys) !== 0) {
652
                $tableDiff = GeneralUtility::makeInstance(
653
                    TableDiff::class,
654
                    $changedTable->name,
655
                    [],
656
                    [],
657
                    [],
658
                    [],
659
                    [],
660
                    [],
661
                    $schemaDiff->fromSchema->getTable($changedTable->name)
662
                );
663
664
                foreach ($changedTable->changedForeignKeys as $changedForeignKey) {
665
                    $foreignKeyDiff = clone $tableDiff;
666
                    $foreignKeyDiff->changedForeignKeys = [$this->buildQuotedForeignKey($changedForeignKey)];
667
668
                    $temporarySchemaDiff = GeneralUtility::makeInstance(
669
                        SchemaDiff::class,
670
                        [],
671
                        [$foreignKeyDiff],
672
                        [],
673
                        $schemaDiff->fromSchema
674
                    );
675
676
                    $statements = $temporarySchemaDiff->toSql($databasePlatform);
677
                    foreach ($statements as $statement) {
678
                        $updateSuggestions['change'][md5($statement)] = $statement;
679
                    }
680
                }
681
            }
682
        }
683
684
        return $updateSuggestions;
685
    }
686
687
    /**
688
     * Extract update suggestions (SQL statements) for tables that are
689
     * no longer present in the expected schema from the schema diff.
690
     * In this case the update suggestions are renames of the tables
691
     * with a prefix to mark them for deletion in a second sweep.
692
     *
693
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
694
     * @return array
695
     * @throws \Doctrine\DBAL\Schema\SchemaException
696
     * @throws \InvalidArgumentException
697
     */
698
    protected function getUnusedTableUpdateSuggestions(SchemaDiff $schemaDiff): array
699
    {
700
        $updateSuggestions = [];
701
        foreach ($schemaDiff->changedTables as $tableDiff) {
702
            // Skip tables that are not being renamed or where the new name isn't prefixed
703
            // with the deletion marker.
704
            if ($tableDiff->getNewName() === false
705
                || strpos($tableDiff->getNewName()->getName(), $this->deletedPrefix) !== 0
706
            ) {
707
                continue;
708
            }
709
            // Build a new schema diff that only contains this table
710
            $changedFieldDiff = GeneralUtility::makeInstance(
711
                SchemaDiff::class,
712
                [],
713
                [$tableDiff],
714
                [],
715
                $schemaDiff->fromSchema
0 ignored issues
show
Bug introduced by
$schemaDiff->fromSchema of type Doctrine\DBAL\Schema\Schema is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

715
                /** @scrutinizer ignore-type */ $schemaDiff->fromSchema
Loading history...
716
            );
717
718
            $statements = $changedFieldDiff->toSql($this->connection->getDatabasePlatform());
719
720
            foreach ($statements as $statement) {
721
                $updateSuggestions['change_table'][md5($statement)] = $statement;
722
            }
723
            $updateSuggestions['tables_count'][md5($statements[0])] = $this->getTableRecordCount((string)$tableDiff->name);
724
        }
725
726
        return $updateSuggestions;
727
    }
728
729
    /**
730
     * Extract update suggestions (SQL statements) for fields that are
731
     * no longer present in the expected schema from the schema diff.
732
     * In this case the update suggestions are renames of the fields
733
     * with a prefix to mark them for deletion in a second sweep.
734
     *
735
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
736
     * @return array
737
     * @throws \Doctrine\DBAL\Schema\SchemaException
738
     * @throws \InvalidArgumentException
739
     */
740
    protected function getUnusedFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
741
    {
742
        $changedTables = [];
743
744
        foreach ($schemaDiff->changedTables as $index => $changedTable) {
745
            if (count($changedTable->changedColumns) === 0) {
746
                continue;
747
            }
748
749
            // Treat each changed column with a new diff to get a dedicated suggestions
750
            // just for this single column.
751
            foreach ($changedTable->changedColumns as $changedColumn) {
752
                // Field has not been renamed
753
                if ($changedColumn->getOldColumnName()->getName() === $changedColumn->column->getName()) {
754
                    continue;
755
                }
756
757
                $changedTables[$index . ':' . $changedColumn->column->getName()] = GeneralUtility::makeInstance(
758
                    TableDiff::class,
759
                    $changedTable->name,
760
                    [],
761
                    [$changedColumn],
762
                    [],
763
                    [],
764
                    [],
765
                    [],
766
                    $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name))
0 ignored issues
show
Bug introduced by
$this->buildQuotedTable(...e($changedTable->name)) of type Doctrine\DBAL\Schema\Table is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

766
                    /** @scrutinizer ignore-type */ $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name))
Loading history...
Bug introduced by
$changedTable->name of type array is incompatible with the type string expected by parameter $tableName of Doctrine\DBAL\Schema\Schema::getTable(). ( Ignorable by Annotation )

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

766
                    $this->buildQuotedTable($schemaDiff->fromSchema->getTable(/** @scrutinizer ignore-type */ $changedTable->name))
Loading history...
767
                );
768
            }
769
        }
770
771
        // Build a new schema diff that only contains unused fields
772
        $changedFieldDiff = GeneralUtility::makeInstance(
773
            SchemaDiff::class,
774
            [],
775
            $changedTables,
776
            [],
777
            $schemaDiff->fromSchema
0 ignored issues
show
Bug introduced by
$schemaDiff->fromSchema of type Doctrine\DBAL\Schema\Schema is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

777
            /** @scrutinizer ignore-type */ $schemaDiff->fromSchema
Loading history...
778
        );
779
780
        $statements = $changedFieldDiff->toSql($this->connection->getDatabasePlatform());
781
782
        return ['change' => $this->calculateUpdateSuggestionsHashes($statements)];
783
    }
784
785
    /**
786
     * Extract update suggestions (SQL statements) for fields that can
787
     * be removed from the complete schema diff.
788
     * Fields that can be removed have been prefixed in a previous run
789
     * of the schema migration.
790
     *
791
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
792
     * @return array
793
     * @throws \Doctrine\DBAL\Schema\SchemaException
794
     * @throws \InvalidArgumentException
795
     */
796
    protected function getDropFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
797
    {
798
        $changedTables = [];
799
800
        foreach ($schemaDiff->changedTables as $index => $changedTable) {
801
            $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
802
803
            if (count($changedTable->removedColumns) !== 0) {
804
                // Treat each changed column with a new diff to get a dedicated suggestions
805
                // just for this single column.
806
                foreach ($changedTable->removedColumns as $removedColumn) {
807
                    $changedTables[$index . ':tbl_' . $removedColumn->getName()] = GeneralUtility::makeInstance(
808
                        TableDiff::class,
809
                        $changedTable->name,
810
                        [],
811
                        [],
812
                        [$this->buildQuotedColumn($removedColumn)],
813
                        [],
814
                        [],
815
                        [],
816
                        $fromTable
0 ignored issues
show
Bug introduced by
$fromTable of type Doctrine\DBAL\Schema\Table is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

816
                        /** @scrutinizer ignore-type */ $fromTable
Loading history...
817
                    );
818
                }
819
            }
820
821
            if (count($changedTable->removedIndexes) !== 0) {
822
                // Treat each removed index with a new diff to get a dedicated suggestions
823
                // just for this index.
824
                foreach ($changedTable->removedIndexes as $removedIndex) {
825
                    $changedTables[$index . ':idx_' . $removedIndex->getName()] = GeneralUtility::makeInstance(
826
                        TableDiff::class,
827
                        $changedTable->name,
828
                        [],
829
                        [],
830
                        [],
831
                        [],
832
                        [],
833
                        [$this->buildQuotedIndex($removedIndex)],
834
                        $fromTable
835
                    );
836
                }
837
            }
838
839
            if (count($changedTable->removedForeignKeys) !== 0) {
840
                // Treat each removed foreign key with a new diff to get a dedicated suggestions
841
                // just for this foreign key.
842
                foreach ($changedTable->removedForeignKeys as $removedForeignKey) {
843
                    $fkIndex = $index . ':fk_' . $removedForeignKey->getName();
844
                    $changedTables[$fkIndex] = GeneralUtility::makeInstance(
845
                        TableDiff::class,
846
                        $changedTable->name,
847
                        [],
848
                        [],
849
                        [],
850
                        [],
851
                        [],
852
                        [],
853
                        $fromTable
854
                    );
855
                    $changedTables[$fkIndex]->removedForeignKeys = [$this->buildQuotedForeignKey($removedForeignKey)];
856
                }
857
            }
858
        }
859
860
        // Build a new schema diff that only contains removable fields
861
        $removedFieldDiff = GeneralUtility::makeInstance(
862
            SchemaDiff::class,
863
            [],
864
            $changedTables,
865
            [],
866
            $schemaDiff->fromSchema
0 ignored issues
show
Bug introduced by
$schemaDiff->fromSchema of type Doctrine\DBAL\Schema\Schema is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

866
            /** @scrutinizer ignore-type */ $schemaDiff->fromSchema
Loading history...
867
        );
868
869
        $statements = $removedFieldDiff->toSql($this->connection->getDatabasePlatform());
870
871
        return ['drop' => $this->calculateUpdateSuggestionsHashes($statements)];
872
    }
873
874
    /**
875
     * Extract update suggestions (SQL statements) for tables that can
876
     * be removed from the complete schema diff.
877
     * Tables that can be removed have been prefixed in a previous run
878
     * of the schema migration.
879
     *
880
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
881
     * @return array
882
     * @throws \Doctrine\DBAL\Schema\SchemaException
883
     * @throws \InvalidArgumentException
884
     */
885
    protected function getDropTableUpdateSuggestions(SchemaDiff $schemaDiff): array
886
    {
887
        $updateSuggestions = [];
888
        foreach ($schemaDiff->removedTables as $removedTable) {
889
            // Build a new schema diff that only contains this table
890
            $tableDiff = GeneralUtility::makeInstance(
891
                SchemaDiff::class,
892
                [],
893
                [],
894
                [$this->buildQuotedTable($removedTable)],
895
                $schemaDiff->fromSchema
0 ignored issues
show
Bug introduced by
$schemaDiff->fromSchema of type Doctrine\DBAL\Schema\Schema is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

895
                /** @scrutinizer ignore-type */ $schemaDiff->fromSchema
Loading history...
896
            );
897
898
            $statements = $tableDiff->toSql($this->connection->getDatabasePlatform());
899
            foreach ($statements as $statement) {
900
                $updateSuggestions['drop_table'][md5($statement)] = $statement;
901
            }
902
903
            // Only store the record count for this table for the first statement,
904
            // assuming that this is the actual DROP TABLE statement.
905
            $updateSuggestions['tables_count'][md5($statements[0])] = $this->getTableRecordCount(
906
                $removedTable->getName()
907
            );
908
        }
909
910
        return $updateSuggestions;
911
    }
912
913
    /**
914
     * Move tables to be removed that are not prefixed with the deleted prefix to the list
915
     * of changed tables and set a new prefixed name.
916
     * Without this help the Doctrine SchemaDiff has no idea if a table has been renamed and
917
     * performs a drop of the old table and creates a new table, which leads to all data in
918
     * the old table being lost.
919
     *
920
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
921
     * @return \Doctrine\DBAL\Schema\SchemaDiff
922
     * @throws \InvalidArgumentException
923
     */
924
    protected function migrateUnprefixedRemovedTablesToRenames(SchemaDiff $schemaDiff): SchemaDiff
925
    {
926
        foreach ($schemaDiff->removedTables as $index => $removedTable) {
927
            if (strpos($removedTable->getName(), $this->deletedPrefix) === 0) {
928
                continue;
929
            }
930
            $tableDiff = GeneralUtility::makeInstance(
931
                TableDiff::class,
932
                $removedTable->getQuotedName($this->connection->getDatabasePlatform()),
0 ignored issues
show
Bug introduced by
$removedTable->getQuoted...>getDatabasePlatform()) of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

932
                /** @scrutinizer ignore-type */ $removedTable->getQuotedName($this->connection->getDatabasePlatform()),
Loading history...
933
                $addedColumns = [],
934
                $changedColumns = [],
935
                $removedColumns = [],
936
                $addedIndexes = [],
937
                $changedIndexes = [],
938
                $removedIndexes = [],
939
                $this->buildQuotedTable($removedTable)
0 ignored issues
show
Bug introduced by
$this->buildQuotedTable($removedTable) of type Doctrine\DBAL\Schema\Table is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

939
                /** @scrutinizer ignore-type */ $this->buildQuotedTable($removedTable)
Loading history...
940
            );
941
942
            $tableDiff->newName = $this->connection->getDatabasePlatform()->quoteIdentifier(
943
                substr($this->deletedPrefix . $removedTable->getName(), 0, $this->getMaxTableNameLength())
0 ignored issues
show
Bug introduced by
$this->getMaxTableNameLength() of type string is incompatible with the type integer expected by parameter $length of substr(). ( Ignorable by Annotation )

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

943
                substr($this->deletedPrefix . $removedTable->getName(), 0, /** @scrutinizer ignore-type */ $this->getMaxTableNameLength())
Loading history...
944
            );
945
            $schemaDiff->changedTables[$index] = $tableDiff;
946
            unset($schemaDiff->removedTables[$index]);
947
        }
948
949
        return $schemaDiff;
950
    }
951
952
    /**
953
     * Scan the list of changed tables for fields that are going to be dropped. If
954
     * the name of the field does not start with the deleted prefix mark the column
955
     * for a rename instead of a drop operation.
956
     *
957
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
958
     * @return \Doctrine\DBAL\Schema\SchemaDiff
959
     * @throws \InvalidArgumentException
960
     */
961
    protected function migrateUnprefixedRemovedFieldsToRenames(SchemaDiff $schemaDiff): SchemaDiff
962
    {
963
        foreach ($schemaDiff->changedTables as $tableIndex => $changedTable) {
964
            if (count($changedTable->removedColumns) === 0) {
965
                continue;
966
            }
967
968
            foreach ($changedTable->removedColumns as $columnIndex => $removedColumn) {
969
                if (strpos($removedColumn->getName(), $this->deletedPrefix) === 0) {
970
                    continue;
971
                }
972
973
                // Build a new column object with the same properties as the removed column
974
                $renamedColumnName = substr(
975
                    $this->deletedPrefix . $removedColumn->getName(),
976
                    0,
977
                    $this->getMaxColumnNameLength()
0 ignored issues
show
Bug introduced by
$this->getMaxColumnNameLength() of type string is incompatible with the type integer expected by parameter $length of substr(). ( Ignorable by Annotation )

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

977
                    /** @scrutinizer ignore-type */ $this->getMaxColumnNameLength()
Loading history...
978
                );
979
                $renamedColumn = new Column(
980
                    $this->connection->quoteIdentifier($renamedColumnName),
981
                    $removedColumn->getType(),
982
                    array_diff_key($removedColumn->toArray(), ['name', 'type'])
983
                );
984
985
                // Build the diff object for the column to rename
986
                $columnDiff = GeneralUtility::makeInstance(
987
                    ColumnDiff::class,
988
                    $removedColumn->getQuotedName($this->connection->getDatabasePlatform()),
0 ignored issues
show
Bug introduced by
$removedColumn->getQuote...>getDatabasePlatform()) of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

988
                    /** @scrutinizer ignore-type */ $removedColumn->getQuotedName($this->connection->getDatabasePlatform()),
Loading history...
989
                    $renamedColumn,
0 ignored issues
show
Bug introduced by
$renamedColumn of type Doctrine\DBAL\Schema\Column is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

989
                    /** @scrutinizer ignore-type */ $renamedColumn,
Loading history...
990
                    $changedProperties = [],
991
                    $this->buildQuotedColumn($removedColumn)
992
                );
993
994
                // Add the column with the required rename information to the changed column list
995
                $schemaDiff->changedTables[$tableIndex]->changedColumns[$columnIndex] = $columnDiff;
996
997
                // Remove the column from the list of columns to be dropped
998
                unset($schemaDiff->changedTables[$tableIndex]->removedColumns[$columnIndex]);
999
            }
1000
        }
1001
1002
        return $schemaDiff;
1003
    }
1004
1005
    /**
1006
     * Revert the automatic rename optimization that Doctrine performs when it detects
1007
     * a column being added and a column being dropped that only differ by name.
1008
     *
1009
     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
1010
     * @return SchemaDiff
1011
     * @throws \Doctrine\DBAL\Schema\SchemaException
1012
     * @throws \InvalidArgumentException
1013
     */
1014
    protected function migrateColumnRenamesToDistinctActions(SchemaDiff $schemaDiff): SchemaDiff
1015
    {
1016
        foreach ($schemaDiff->changedTables as $index => $changedTable) {
1017
            if (count($changedTable->renamedColumns) === 0) {
1018
                continue;
1019
            }
1020
1021
            // Treat each renamed column with a new diff to get a dedicated
1022
            // suggestion just for this single column.
1023
            foreach ($changedTable->renamedColumns as $originalColumnName => $renamedColumn) {
1024
                $columnOptions = array_diff_key($renamedColumn->toArray(), ['name', 'type']);
1025
1026
                $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance(
1027
                    Column::class,
1028
                    $renamedColumn->getName(),
0 ignored issues
show
Bug introduced by
$renamedColumn->getName() of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1028
                    /** @scrutinizer ignore-type */ $renamedColumn->getName(),
Loading history...
1029
                    $renamedColumn->getType(),
0 ignored issues
show
Bug introduced by
$renamedColumn->getType() of type Doctrine\DBAL\Types\Type is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1029
                    /** @scrutinizer ignore-type */ $renamedColumn->getType(),
Loading history...
1030
                    $columnOptions
1031
                );
1032
                $changedTable->removedColumns[$originalColumnName] = GeneralUtility::makeInstance(
1033
                    Column::class,
1034
                    $originalColumnName,
1035
                    $renamedColumn->getType(),
1036
                    $columnOptions
1037
                );
1038
1039
                unset($changedTable->renamedColumns[$originalColumnName]);
1040
            }
1041
        }
1042
1043
        return $schemaDiff;
1044
    }
1045
1046
    /**
1047
     * Retrieve the database platform-specific limitations on column and schema name sizes as
1048
     * defined in the tableAndFieldMaxNameLengthsPerDbPlatform property.
1049
     *
1050
     * @param string $databasePlatform
1051
     * @return array
1052
     */
1053
    protected function getTableAndFieldNameMaxLengths(string $databasePlatform = '')
1054
    {
1055
        if ($databasePlatform === '') {
1056
            $databasePlatform = $this->connection->getDatabasePlatform()->getName();
1057
        }
1058
        $databasePlatform = strtolower($databasePlatform);
1059
1060
        if (isset($this->tableAndFieldMaxNameLengthsPerDbPlatform[$databasePlatform])) {
1061
            $nameLengthRestrictions = $this->tableAndFieldMaxNameLengthsPerDbPlatform[$databasePlatform];
1062
        } else {
1063
            $nameLengthRestrictions = $this->tableAndFieldMaxNameLengthsPerDbPlatform['default'];
1064
        }
1065
1066
        if (is_string($nameLengthRestrictions)) {
1067
            return $this->getTableAndFieldNameMaxLengths($nameLengthRestrictions);
1068
        }
1069
        return $nameLengthRestrictions;
1070
    }
1071
1072
    /**
1073
     * Get the maximum table name length possible for the given DB platform.
1074
     *
1075
     * @param string $databasePlatform
1076
     * @return string
1077
     */
1078
    protected function getMaxTableNameLength(string $databasePlatform = '')
1079
    {
1080
        $nameLengthRestrictions = $this->getTableAndFieldNameMaxLengths($databasePlatform);
1081
        return $nameLengthRestrictions['tables'];
1082
    }
1083
1084
    /**
1085
     * Get the maximum column name length possible for the given DB platform.
1086
     *
1087
     * @param string $databasePlatform
1088
     * @return string
1089
     */
1090
    protected function getMaxColumnNameLength(string $databasePlatform = '')
1091
    {
1092
        $nameLengthRestrictions = $this->getTableAndFieldNameMaxLengths($databasePlatform);
1093
        return $nameLengthRestrictions['columns'];
1094
    }
1095
1096
    /**
1097
     * Return the amount of records in the given table.
1098
     *
1099
     * @param string $tableName
1100
     * @return int
1101
     * @throws \InvalidArgumentException
1102
     */
1103
    protected function getTableRecordCount(string $tableName): int
1104
    {
1105
        return GeneralUtility::makeInstance(ConnectionPool::class)
1106
            ->getConnectionForTable($tableName)
1107
            ->count('*', $tableName, []);
1108
    }
1109
1110
    /**
1111
     * Determine the connection name for a table
1112
     *
1113
     * @param string $tableName
1114
     * @return string
1115
     * @throws \InvalidArgumentException
1116
     */
1117
    protected function getConnectionNameForTable(string $tableName): string
1118
    {
1119
        $connectionNames = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionNames();
1120
1121
        if (array_key_exists($tableName, (array)$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])) {
1122
            return in_array($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName], $connectionNames, true)
1123
                ? $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName]
1124
                : ConnectionPool::DEFAULT_CONNECTION_NAME;
1125
        }
1126
1127
        return ConnectionPool::DEFAULT_CONNECTION_NAME;
1128
    }
1129
1130
    /**
1131
     * Replace the array keys with a md5 sum of the actual SQL statement
1132
     *
1133
     * @param string[] $statements
1134
     * @return string[]
1135
     */
1136
    protected function calculateUpdateSuggestionsHashes(array $statements): array
1137
    {
1138
        return array_combine(array_map('md5', $statements), $statements);
1139
    }
1140
1141
    /**
1142
     * Helper for buildSchemaDiff to filter an array of TableDiffs against a list of valid table names.
1143
     *
1144
     * @param TableDiff[]|Table[] $tableDiffs
1145
     * @param string[] $validTableNames
1146
     * @return TableDiff[]
1147
     * @throws \InvalidArgumentException
1148
     */
1149
    protected function removeUnrelatedTables(array $tableDiffs, array $validTableNames): array
1150
    {
1151
        return array_filter(
1152
            $tableDiffs,
1153
            function ($table) use ($validTableNames) {
1154
                if ($table instanceof Table) {
1155
                    $tableName = $table->getName();
1156
                } else {
1157
                    $tableName = $table->newName ?: $table->name;
1158
                }
1159
1160
                // If the tablename has a deleted prefix strip it of before comparing
1161
                // it against the list of valid table names so that drop operations
1162
                // don't get removed.
1163
                if (strpos($tableName, $this->deletedPrefix) === 0) {
1164
                    $tableName = substr($tableName, strlen($this->deletedPrefix));
1165
                }
1166
                return in_array($tableName, $validTableNames, true)
1167
                    || in_array($this->deletedPrefix . $tableName, $validTableNames, true);
1168
            }
1169
        );
1170
    }
1171
1172
    /**
1173
     * Transform the table information to conform to specific
1174
     * requirements of different database platforms like removing
1175
     * the index substring length for Non-MySQL Platforms.
1176
     *
1177
     * @param Table[] $tables
1178
     * @param \TYPO3\CMS\Core\Database\Connection $connection
1179
     * @return Table[]
1180
     * @throws \InvalidArgumentException
1181
     */
1182
    protected function transformTablesForDatabasePlatform(array $tables, Connection $connection): array
1183
    {
1184
        foreach ($tables as &$table) {
1185
            $indexes = [];
1186
            foreach ($table->getIndexes() as $key => $index) {
1187
                $indexName = $index->getName();
1188
                // PostgreSQL requires index names to be unique per database/schema.
1189
                if ($connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
1190
                    $indexName = $indexName . '_' . hash('crc32b', $table->getName() . '_' . $indexName);
1191
                }
1192
1193
                // Remove the length information from column names for indexes if required.
1194
                $cleanedColumnNames = array_map(
1195
                    function (string $columnName) use ($connection) {
1196
                        if ($connection->getDatabasePlatform() instanceof MySqlPlatform) {
1197
                            // Returning the unquoted, unmodified version of the column name since
1198
                            // it can include the length information for BLOB/TEXT columns which
1199
                            // may not be quoted.
1200
                            return $columnName;
1201
                        }
1202
1203
                        return $connection->quoteIdentifier(preg_replace('/\(\d+\)$/', '', $columnName));
1204
                    },
1205
                    $index->getUnquotedColumns()
1206
                );
1207
1208
                $indexes[$key] = GeneralUtility::makeInstance(
1209
                    Index::class,
1210
                    $connection->quoteIdentifier($indexName),
0 ignored issues
show
Bug introduced by
$connection->quoteIdentifier($indexName) of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1210
                    /** @scrutinizer ignore-type */ $connection->quoteIdentifier($indexName),
Loading history...
1211
                    $cleanedColumnNames,
1212
                    $index->isUnique(),
0 ignored issues
show
Bug introduced by
$index->isUnique() of type boolean is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1212
                    /** @scrutinizer ignore-type */ $index->isUnique(),
Loading history...
1213
                    $index->isPrimary(),
1214
                    $index->getFlags(),
1215
                    $index->getOptions()
1216
                );
1217
            }
1218
1219
            $table = GeneralUtility::makeInstance(
1220
                Table::class,
1221
                $table->getQuotedName($connection->getDatabasePlatform()),
1222
                $table->getColumns(),
1223
                $indexes,
1224
                $table->getForeignKeys(),
1225
                0,
0 ignored issues
show
Bug introduced by
0 of type integer is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1225
                /** @scrutinizer ignore-type */ 0,
Loading history...
1226
                $table->getOptions()
1227
            );
1228
        }
1229
1230
        return $tables;
1231
    }
1232
1233
    /**
1234
     * Get COLLATION, ROW_FORMAT, COMMENT and ENGINE table options on MySQL connections.
1235
     *
1236
     * @param string[] $tableNames
1237
     * @return array[]
1238
     * @throws \InvalidArgumentException
1239
     */
1240
    protected function getTableOptions(array $tableNames): array
1241
    {
1242
        $tableOptions = [];
1243
        if (strpos($this->connection->getServerVersion(), 'MySQL') !== 0) {
1244
            foreach ($tableNames as $tableName) {
1245
                $tableOptions[$tableName] = [];
1246
            }
1247
1248
            return $tableOptions;
1249
        }
1250
1251
        $queryBuilder = $this->connection->createQueryBuilder();
1252
        $result = $queryBuilder
1253
            ->select(
1254
                'TABLE_NAME AS table',
1255
                'ENGINE AS engine',
1256
                'ROW_FORMAT AS row_format',
1257
                'TABLE_COLLATION AS collate',
1258
                'TABLE_COMMENT AS comment'
1259
            )
1260
            ->from('information_schema.TABLES')
1261
            ->where(
1262
                $queryBuilder->expr()->eq(
1263
                    'TABLE_TYPE',
1264
                    $queryBuilder->createNamedParameter('BASE TABLE', \PDO::PARAM_STR)
1265
                ),
1266
                $queryBuilder->expr()->eq(
1267
                    'TABLE_SCHEMA',
1268
                    $queryBuilder->createNamedParameter($this->connection->getDatabase(), \PDO::PARAM_STR)
1269
                )
1270
            )
1271
            ->execute();
1272
1273
        while ($row = $result->fetch()) {
1274
            $index = $row['table'];
1275
            unset($row['table']);
1276
            $tableOptions[$index] = $row;
1277
        }
1278
1279
        return $tableOptions;
1280
    }
1281
1282
    /**
1283
     * Helper function to build a table object that has the _quoted attribute set so that the SchemaManager
1284
     * will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine doesn't
1285
     * provide a method to set the flag after the object has been instantiated and there's no possibility to
1286
     * hook into the createSchema() method early enough to influence the original table object.
1287
     *
1288
     * @param \Doctrine\DBAL\Schema\Table $table
1289
     * @return \Doctrine\DBAL\Schema\Table
1290
     */
1291
    protected function buildQuotedTable(Table $table): Table
1292
    {
1293
        $databasePlatform = $this->connection->getDatabasePlatform();
1294
1295
        return GeneralUtility::makeInstance(
1296
            Table::class,
1297
            $databasePlatform->quoteIdentifier($table->getName()),
0 ignored issues
show
Bug introduced by
$databasePlatform->quote...fier($table->getName()) of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1297
            /** @scrutinizer ignore-type */ $databasePlatform->quoteIdentifier($table->getName()),
Loading history...
1298
            $table->getColumns(),
1299
            $table->getIndexes(),
1300
            $table->getForeignKeys(),
1301
            0,
0 ignored issues
show
Bug introduced by
0 of type integer is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1301
            /** @scrutinizer ignore-type */ 0,
Loading history...
1302
            $table->getOptions()
1303
        );
1304
    }
1305
1306
    /**
1307
     * Helper function to build a column object that has the _quoted attribute set so that the SchemaManager
1308
     * will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine doesn't
1309
     * provide a method to set the flag after the object has been instantiated and there's no possibility to
1310
     * hook into the createSchema() method early enough to influence the original column object.
1311
     *
1312
     * @param \Doctrine\DBAL\Schema\Column $column
1313
     * @return \Doctrine\DBAL\Schema\Column
1314
     */
1315
    protected function buildQuotedColumn(Column $column): Column
1316
    {
1317
        $databasePlatform = $this->connection->getDatabasePlatform();
1318
1319
        return GeneralUtility::makeInstance(
1320
            Column::class,
1321
            $databasePlatform->quoteIdentifier($column->getName()),
0 ignored issues
show
Bug introduced by
$databasePlatform->quote...ier($column->getName()) of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1321
            /** @scrutinizer ignore-type */ $databasePlatform->quoteIdentifier($column->getName()),
Loading history...
1322
            $column->getType(),
0 ignored issues
show
Bug introduced by
$column->getType() of type Doctrine\DBAL\Types\Type is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1322
            /** @scrutinizer ignore-type */ $column->getType(),
Loading history...
1323
            array_diff_key($column->toArray(), ['name', 'type'])
1324
        );
1325
    }
1326
1327
    /**
1328
     * Helper function to build an index object that has the _quoted attribute set so that the SchemaManager
1329
     * will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine doesn't
1330
     * provide a method to set the flag after the object has been instantiated and there's no possibility to
1331
     * hook into the createSchema() method early enough to influence the original column object.
1332
     *
1333
     * @param \Doctrine\DBAL\Schema\Index $index
1334
     * @return \Doctrine\DBAL\Schema\Index
1335
     */
1336
    protected function buildQuotedIndex(Index $index): Index
1337
    {
1338
        $databasePlatform = $this->connection->getDatabasePlatform();
1339
1340
        return GeneralUtility::makeInstance(
1341
            Index::class,
1342
            $databasePlatform->quoteIdentifier($index->getName()),
0 ignored issues
show
Bug introduced by
$databasePlatform->quote...fier($index->getName()) of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1342
            /** @scrutinizer ignore-type */ $databasePlatform->quoteIdentifier($index->getName()),
Loading history...
1343
            $index->getColumns(),
1344
            $index->isUnique(),
0 ignored issues
show
Bug introduced by
$index->isUnique() of type boolean is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1344
            /** @scrutinizer ignore-type */ $index->isUnique(),
Loading history...
1345
            $index->isPrimary(),
1346
            $index->getFlags(),
1347
            $index->getOptions()
1348
        );
1349
    }
1350
1351
    /**
1352
     * Helper function to build a foreign key constraint object that has the _quoted attribute set so that the
1353
     * SchemaManager will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine
1354
     * doesn't provide a method to set the flag after the object has been instantiated and there's no possibility to
1355
     * hook into the createSchema() method early enough to influence the original column object.
1356
     *
1357
     * @param \Doctrine\DBAL\Schema\ForeignKeyConstraint $index
1358
     * @return \Doctrine\DBAL\Schema\ForeignKeyConstraint
1359
     */
1360
    protected function buildQuotedForeignKey(ForeignKeyConstraint $index): ForeignKeyConstraint
1361
    {
1362
        $databasePlatform = $this->connection->getDatabasePlatform();
1363
1364
        return GeneralUtility::makeInstance(
1365
            ForeignKeyConstraint::class,
1366
            $index->getLocalColumns(),
1367
            $databasePlatform->quoteIdentifier($index->getForeignTableName()),
0 ignored issues
show
Bug introduced by
$databasePlatform->quote...>getForeignTableName()) of type string is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

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

1367
            /** @scrutinizer ignore-type */ $databasePlatform->quoteIdentifier($index->getForeignTableName()),
Loading history...
1368
            $index->getForeignColumns(),
1369
            $databasePlatform->quoteIdentifier($index->getName()),
1370
            $index->getOptions()
1371
        );
1372
    }
1373
}
1374