‪TYPO3CMS  9.5
ConnectionMigrator.php
Go to the documentation of this file.
1 <?php
2 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 use Doctrine\DBAL\DBALException;
19 use Doctrine\DBAL\Platforms\MySqlPlatform;
20 use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
21 use Doctrine\DBAL\Platforms\SqlitePlatform;
22 use Doctrine\DBAL\Schema\Column;
23 use Doctrine\DBAL\Schema\ColumnDiff;
24 use Doctrine\DBAL\Schema\ForeignKeyConstraint;
25 use Doctrine\DBAL\Schema\Index;
26 use Doctrine\DBAL\Schema\Schema;
27 use Doctrine\DBAL\Schema\SchemaConfig;
28 use Doctrine\DBAL\Schema\SchemaDiff;
29 use Doctrine\DBAL\Schema\Table;
34 
41 {
45  protected ‪$deletedPrefix = 'zzz_deleted_';
46 
50  protected ‪$connection;
51 
55  protected ‪$connectionName;
56 
60  protected ‪$tables;
61 
66  public function ‪__construct(string ‪$connectionName, array ‪$tables)
67  {
68  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
69  $this->connection = $connectionPool->getConnectionByName(‪$connectionName);
70  $this->connectionName = ‪$connectionName;
71  $this->tables = ‪$tables;
72  }
73 
79  public static function ‪create(string ‪$connectionName, array ‪$tables)
80  {
81  return GeneralUtility::makeInstance(
82  static::class,
85  );
86  }
87 
94  public function ‪getSchemaDiff(): SchemaDiff
95  {
96  return $this->‪buildSchemaDiff(false);
97  }
98 
106  public function ‪getUpdateSuggestions(bool $remove = false): array
107  {
108  $schemaDiff = $this->‪buildSchemaDiff();
109 
110  if ($remove === false) {
111  return array_merge_recursive(
112  ['add' => [], 'create_table' => [], 'change' => [], 'change_currentValue' => []],
113  $this->‪getNewFieldUpdateSuggestions($schemaDiff),
114  $this->‪getNewTableUpdateSuggestions($schemaDiff),
115  $this->‪getChangedFieldUpdateSuggestions($schemaDiff),
116  $this->‪getChangedTableOptions($schemaDiff)
117  );
118  }
119  return array_merge_recursive(
120  ['change' => [], 'change_table' => [], 'drop' => [], 'drop_table' => [], 'tables_count' => []],
121  $this->‪getUnusedFieldUpdateSuggestions($schemaDiff),
122  $this->‪getUnusedTableUpdateSuggestions($schemaDiff),
123  $this->‪getDropTableUpdateSuggestions($schemaDiff),
124  $this->‪getDropFieldUpdateSuggestions($schemaDiff)
125  );
126  }
127 
136  public function ‪install(bool $createOnly = false): array
137  {
138  $result = [];
139  $schemaDiff = $this->‪buildSchemaDiff(false);
140 
141  $schemaDiff->removedTables = [];
142  foreach ($schemaDiff->changedTables as $key => $changedTable) {
143  $schemaDiff->changedTables[$key]->removedColumns = [];
144  $schemaDiff->changedTables[$key]->removedIndexes = [];
145 
146  // With partial ext_tables.sql files the SchemaManager is detecting
147  // existing columns as false positives for a column rename. In this
148  // context every rename is actually a new column.
149  foreach ($changedTable->renamedColumns as $columnName => $renamedColumn) {
150  $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance(
151  Column::class,
152  $renamedColumn->getName(),
153  $renamedColumn->getType(),
154  array_diff_key($renamedColumn->toArray(), ['name', 'type'])
155  );
156  unset($changedTable->renamedColumns[$columnName]);
157  }
158 
159  if ($createOnly) {
160  // Ignore new indexes that work on columns that need changes
161  foreach ($changedTable->addedIndexes as $indexName => $addedIndex) {
162  $indexColumns = array_map(
163  function ($columnName) {
164  // Strip MySQL prefix length information to get real column names
165  $columnName = preg_replace('/\‍(\d+\‍)$/', '', $columnName);
166  // Strip mssql '[' and ']' from column names
167  $columnName = ltrim($columnName, '[');
168  $columnName = rtrim($columnName, ']');
169  // Strip sqlite '"' from column names
170  return trim($columnName, '"');
171  },
172  $addedIndex->getColumns()
173  );
174  $columnChanges = array_intersect($indexColumns, array_keys($changedTable->changedColumns));
175  if (!empty($columnChanges)) {
176  unset($schemaDiff->changedTables[$key]->addedIndexes[$indexName]);
177  }
178  }
179  $schemaDiff->changedTables[$key]->changedColumns = [];
180  $schemaDiff->changedTables[$key]->changedIndexes = [];
181  $schemaDiff->changedTables[$key]->renamedIndexes = [];
182  }
183  }
184 
185  $statements = $schemaDiff->toSaveSql(
186  $this->connection->getDatabasePlatform()
187  );
188 
189  foreach ($statements as $statement) {
190  try {
191  $this->connection->executeUpdate($statement);
192  $result[$statement] = '';
193  } catch (DBALException $e) {
194  $result[$statement] = $e->getPrevious()->getMessage();
195  }
196  }
197 
198  return $result;
199  }
200 
212  protected function ‪buildSchemaDiff(bool $renameUnused = true): SchemaDiff
213  {
214  // Build the schema definitions
215  $fromSchema = $this->connection->getSchemaManager()->createSchema();
216  $toSchema = $this->‪buildExpectedSchemaDefinitions($this->connectionName);
217 
218  // Add current table options to the fromSchema
219  $tableOptions = $this->‪getTableOptions($fromSchema->getTableNames());
220  foreach ($fromSchema->getTables() as $table) {
221  $tableName = $table->getName();
222  if (!array_key_exists($tableName, $tableOptions)) {
223  continue;
224  }
225  foreach ($tableOptions[$tableName] as $optionName => $optionValue) {
226  $table->addOption($optionName, $optionValue);
227  }
228  }
229 
230  // Build SchemaDiff and handle renames of tables and colums
231  $comparator = GeneralUtility::makeInstance(Comparator::class, $this->connection->getDatabasePlatform());
232  $schemaDiff = $comparator->compare($fromSchema, $toSchema);
233  $schemaDiff = $this->‪migrateColumnRenamesToDistinctActions($schemaDiff);
234 
235  if ($renameUnused) {
236  $schemaDiff = $this->‪migrateUnprefixedRemovedTablesToRenames($schemaDiff);
237  $schemaDiff = $this->‪migrateUnprefixedRemovedFieldsToRenames($schemaDiff);
238  }
239 
240  // All tables in the default connection are managed by TYPO3
241  if ($this->connectionName === ‪ConnectionPool::DEFAULT_CONNECTION_NAME) {
242  return $schemaDiff;
243  }
244 
245  // If there are no mapped tables return a SchemaDiff without any changes
246  // to avoid update suggestions for tables not related to TYPO3.
247  if (empty(‪$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'] ?? null)) {
248  return GeneralUtility::makeInstance(SchemaDiff::class, [], [], [], $fromSchema);
249  }
250 
251  // Collect the table names that have been mapped to this connection.
253  $tablesForConnection = array_keys(
254  array_filter(
255  ‪$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'],
256  function ($tableConnectionName) use (‪$connectionName) {
257  return $tableConnectionName === ‪$connectionName;
258  }
259  )
260  );
261 
262  // Remove all tables that are not assigned to this connection from the diff
263  $schemaDiff->newTables = $this->‪removeUnrelatedTables($schemaDiff->newTables, $tablesForConnection);
264  $schemaDiff->changedTables = $this->‪removeUnrelatedTables($schemaDiff->changedTables, $tablesForConnection);
265  $schemaDiff->removedTables = $this->‪removeUnrelatedTables($schemaDiff->removedTables, $tablesForConnection);
266 
267  return $schemaDiff;
268  }
269 
278  protected function ‪buildExpectedSchemaDefinitions(string ‪$connectionName): Schema
279  {
281  $tablesForConnection = [];
282  foreach ($this->tables as $table) {
283  $tableName = $table->getName();
284 
285  // Skip tables for a different connection
286  if (‪$connectionName !== $this->‪getConnectionNameForTable($tableName)) {
287  continue;
288  }
289 
290  if (!array_key_exists($tableName, $tablesForConnection)) {
291  $tablesForConnection[$tableName] = $table;
292  continue;
293  }
294 
295  // Merge multiple table definitions. Later definitions overrule identical
296  // columns, indexes and foreign_keys. Order of definitions is based on
297  // extension load order.
298  $currentTableDefinition = $tablesForConnection[$tableName];
299  $tablesForConnection[$tableName] = GeneralUtility::makeInstance(
300  Table::class,
301  $tableName,
302  array_merge($currentTableDefinition->getColumns(), $table->getColumns()),
303  array_merge($currentTableDefinition->getIndexes(), $table->getIndexes()),
304  array_merge($currentTableDefinition->getForeignKeys(), $table->getForeignKeys()),
305  0,
306  array_merge($currentTableDefinition->getOptions(), $table->getOptions())
307  );
308  }
309 
310  $tablesForConnection = $this->‪transformTablesForDatabasePlatform($tablesForConnection, $this->connection);
311 
312  $schemaConfig = GeneralUtility::makeInstance(SchemaConfig::class);
313  $schemaConfig->setName($this->connection->getDatabase());
314  if (isset($this->connection->getParams()['tableoptions'])) {
315  $schemaConfig->setDefaultTableOptions($this->connection->getParams()['tableoptions']);
316  }
317 
318  return GeneralUtility::makeInstance(Schema::class, $tablesForConnection, [], $schemaConfig);
319  }
320 
329  protected function ‪getNewTableUpdateSuggestions(SchemaDiff $schemaDiff): array
330  {
331  // Build a new schema diff that only contains added tables
332  $addTableSchemaDiff = GeneralUtility::makeInstance(
333  SchemaDiff::class,
334  $schemaDiff->newTables,
335  [],
336  [],
337  $schemaDiff->fromSchema
338  );
339 
340  $statements = $addTableSchemaDiff->toSql($this->connection->getDatabasePlatform());
341 
342  return ['create_table' => $this->‪calculateUpdateSuggestionsHashes($statements)];
343  }
344 
354  protected function ‪getNewFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
355  {
356  $changedTables = [];
357 
358  foreach ($schemaDiff->changedTables as $index => $changedTable) {
359  $fromTable = $this->‪buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
360 
361  if (count($changedTable->addedColumns) !== 0) {
362  // Treat each added column with a new diff to get a dedicated suggestions
363  // just for this single column.
364  foreach ($changedTable->addedColumns as $columnName => $addedColumn) {
365  $changedTables[$index . ':tbl_' . $addedColumn->getName()] = GeneralUtility::makeInstance(
366  TableDiff::class,
367  $changedTable->name,
368  [$columnName => $addedColumn],
369  [],
370  [],
371  [],
372  [],
373  [],
374  $fromTable
375  );
376  }
377  }
378 
379  if (count($changedTable->addedIndexes) !== 0) {
380  // Treat each added index with a new diff to get a dedicated suggestions
381  // just for this index.
382  foreach ($changedTable->addedIndexes as $indexName => $addedIndex) {
383  $changedTables[$index . ':idx_' . $addedIndex->getName()] = GeneralUtility::makeInstance(
384  TableDiff::class,
385  $changedTable->name,
386  [],
387  [],
388  [],
389  [$indexName => $this->buildQuotedIndex($addedIndex)],
390  [],
391  [],
392  $fromTable
393  );
394  }
395  }
396 
397  if (count($changedTable->addedForeignKeys) !== 0) {
398  // Treat each added foreign key with a new diff to get a dedicated suggestions
399  // just for this foreign key.
400  foreach ($changedTable->addedForeignKeys as $addedForeignKey) {
401  $fkIndex = $index . ':fk_' . $addedForeignKey->getName();
402  $changedTables[$fkIndex] = GeneralUtility::makeInstance(
403  TableDiff::class,
404  $changedTable->name,
405  [],
406  [],
407  [],
408  [],
409  [],
410  [],
411  $fromTable
412  );
413  $changedTables[$fkIndex]->addedForeignKeys = [$this->‪buildQuotedForeignKey($addedForeignKey)];
414  }
415  }
416  }
417 
418  // Build a new schema diff that only contains added fields
419  $addFieldSchemaDiff = GeneralUtility::makeInstance(
420  SchemaDiff::class,
421  [],
422  $changedTables,
423  [],
424  $schemaDiff->fromSchema
425  );
426 
427  $statements = $addFieldSchemaDiff->toSql($this->connection->getDatabasePlatform());
428 
429  return ['add' => $this->‪calculateUpdateSuggestionsHashes($statements)];
430  }
431 
441  protected function ‪getChangedTableOptions(SchemaDiff $schemaDiff): array
442  {
443  $updateSuggestions = [];
444 
445  foreach ($schemaDiff->changedTables as $tableDiff) {
446  // Skip processing if this is the base TableDiff class or has no table options set.
447  if (!$tableDiff instanceof TableDiff || count($tableDiff->getTableOptions()) === 0) {
448  continue;
449  }
450 
451  $tableOptions = $tableDiff->getTableOptions();
452  $tableOptionsDiff = GeneralUtility::makeInstance(
453  TableDiff::class,
454  $tableDiff->name,
455  [],
456  [],
457  [],
458  [],
459  [],
460  [],
461  $tableDiff->fromTable
462  );
463  $tableOptionsDiff->setTableOptions($tableOptions);
464 
465  $tableOptionsSchemaDiff = GeneralUtility::makeInstance(
466  SchemaDiff::class,
467  [],
468  [$tableOptionsDiff],
469  [],
470  $schemaDiff->fromSchema
471  );
472 
473  $statements = $tableOptionsSchemaDiff->toSaveSql($this->connection->getDatabasePlatform());
474  foreach ($statements as $statement) {
475  $updateSuggestions['change'][md5($statement)] = $statement;
476  }
477  }
478 
479  return $updateSuggestions;
480  }
481 
491  protected function ‪getChangedFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
492  {
493  $databasePlatform = $this->connection->getDatabasePlatform();
494  $updateSuggestions = [];
495 
496  foreach ($schemaDiff->changedTables as $index => $changedTable) {
497  // Treat each changed index with a new diff to get a dedicated suggestions
498  // just for this index.
499  if (count($changedTable->changedIndexes) !== 0) {
500  foreach ($changedTable->changedIndexes as $indexName => $changedIndex) {
501  $indexDiff = GeneralUtility::makeInstance(
502  TableDiff::class,
503  $changedTable->name,
504  [],
505  [],
506  [],
507  [],
508  [$indexName => $changedIndex],
509  [],
510  $schemaDiff->fromSchema->getTable($changedTable->name)
511  );
512 
513  $temporarySchemaDiff = GeneralUtility::makeInstance(
514  SchemaDiff::class,
515  [],
516  [$indexDiff],
517  [],
518  $schemaDiff->fromSchema
519  );
520 
521  $statements = $temporarySchemaDiff->toSql($databasePlatform);
522  foreach ($statements as $statement) {
523  $updateSuggestions['change'][md5($statement)] = $statement;
524  }
525  }
526  }
527 
528  // Treat renamed indexes as a field change as it's a simple rename operation
529  if (count($changedTable->renamedIndexes) !== 0) {
530  // Create a base table diff without any changes, there's no constructor
531  // argument to pass in renamed indexes.
532  $tableDiff = GeneralUtility::makeInstance(
533  TableDiff::class,
534  $changedTable->name,
535  [],
536  [],
537  [],
538  [],
539  [],
540  [],
541  $schemaDiff->fromSchema->getTable($changedTable->name)
542  );
543 
544  // Treat each renamed index with a new diff to get a dedicated suggestions
545  // just for this index.
546  foreach ($changedTable->renamedIndexes as $key => $renamedIndex) {
547  $indexDiff = clone $tableDiff;
548  $indexDiff->renamedIndexes = [$key => $renamedIndex];
549 
550  $temporarySchemaDiff = GeneralUtility::makeInstance(
551  SchemaDiff::class,
552  [],
553  [$indexDiff],
554  [],
555  $schemaDiff->fromSchema
556  );
557 
558  $statements = $temporarySchemaDiff->toSql($databasePlatform);
559  foreach ($statements as $statement) {
560  $updateSuggestions['change'][md5($statement)] = $statement;
561  }
562  }
563  }
564 
565  if (count($changedTable->changedColumns) !== 0) {
566  // Treat each changed column with a new diff to get a dedicated suggestions
567  // just for this single column.
568  $fromTable = $this->‪buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
569 
570  foreach ($changedTable->changedColumns as $columnName => $changedColumn) {
571  // Field has been renamed and will be handled separately
572  if ($changedColumn->getOldColumnName()->getName() !== $changedColumn->column->getName()) {
573  continue;
574  }
575 
576  $changedColumn->fromColumn = $this->‪buildQuotedColumn($changedColumn->fromColumn);
577 
578  // Get the current SQL declaration for the column
579  $currentColumn = $fromTable->getColumn($changedColumn->getOldColumnName()->getName());
580  $currentDeclaration = $databasePlatform->getColumnDeclarationSQL(
581  $currentColumn->getQuotedName($this->connection->getDatabasePlatform()),
582  $currentColumn->toArray()
583  );
584 
585  // Build a dedicated diff just for the current column
586  $tableDiff = GeneralUtility::makeInstance(
587  TableDiff::class,
588  $changedTable->name,
589  [],
590  [$columnName => $changedColumn],
591  [],
592  [],
593  [],
594  [],
595  $fromTable
596  );
597 
598  $temporarySchemaDiff = GeneralUtility::makeInstance(
599  SchemaDiff::class,
600  [],
601  [$tableDiff],
602  [],
603  $schemaDiff->fromSchema
604  );
605 
606  $statements = $temporarySchemaDiff->toSql($databasePlatform);
607  foreach ($statements as $statement) {
608  $updateSuggestions['change'][md5($statement)] = $statement;
609  $updateSuggestions['change_currentValue'][md5($statement)] = $currentDeclaration;
610  }
611  }
612  }
613 
614  // Treat each changed foreign key with a new diff to get a dedicated suggestions
615  // just for this foreign key.
616  if (count($changedTable->changedForeignKeys) !== 0) {
617  $tableDiff = GeneralUtility::makeInstance(
618  TableDiff::class,
619  $changedTable->name,
620  [],
621  [],
622  [],
623  [],
624  [],
625  [],
626  $schemaDiff->fromSchema->getTable($changedTable->name)
627  );
628 
629  foreach ($changedTable->changedForeignKeys as $changedForeignKey) {
630  $foreignKeyDiff = clone $tableDiff;
631  $foreignKeyDiff->changedForeignKeys = [$this->‪buildQuotedForeignKey($changedForeignKey)];
632 
633  $temporarySchemaDiff = GeneralUtility::makeInstance(
634  SchemaDiff::class,
635  [],
636  [$foreignKeyDiff],
637  [],
638  $schemaDiff->fromSchema
639  );
640 
641  $statements = $temporarySchemaDiff->toSql($databasePlatform);
642  foreach ($statements as $statement) {
643  $updateSuggestions['change'][md5($statement)] = $statement;
644  }
645  }
646  }
647  }
648 
649  return $updateSuggestions;
650  }
651 
663  protected function ‪getUnusedTableUpdateSuggestions(SchemaDiff $schemaDiff): array
664  {
665  $updateSuggestions = [];
666  foreach ($schemaDiff->changedTables as $tableDiff) {
667  // Skip tables that are not being renamed or where the new name isn't prefixed
668  // with the deletion marker.
669  if ($tableDiff->getNewName() === false
670  || strpos($tableDiff->getNewName()->getName(), $this->deletedPrefix) !== 0
671  ) {
672  continue;
673  }
674  // Build a new schema diff that only contains this table
675  $changedFieldDiff = GeneralUtility::makeInstance(
676  SchemaDiff::class,
677  [],
678  [$tableDiff],
679  [],
680  $schemaDiff->fromSchema
681  );
682 
683  $statements = $changedFieldDiff->toSql($this->connection->getDatabasePlatform());
684 
685  foreach ($statements as $statement) {
686  $updateSuggestions['change_table'][md5($statement)] = $statement;
687  }
688  $updateSuggestions['tables_count'][md5($statements[0])] = $this->‪getTableRecordCount((string)$tableDiff->name);
689  }
690 
691  return $updateSuggestions;
692  }
693 
705  protected function ‪getUnusedFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
706  {
707  $changedTables = [];
708 
709  foreach ($schemaDiff->changedTables as $index => $changedTable) {
710  if (count($changedTable->changedColumns) === 0) {
711  continue;
712  }
713 
714  $isSqlite = $this->‪tableRunsOnSqlite($index);
715 
716  // Treat each changed column with a new diff to get a dedicated suggestions
717  // just for this single column.
718  foreach ($changedTable->changedColumns as $oldFieldName => $changedColumn) {
719  // Field has not been renamed
720  if ($changedColumn->getOldColumnName()->getName() === $changedColumn->column->getName()) {
721  continue;
722  }
723 
724  $changedTables[$index . ':' . $changedColumn->column->getName()] = GeneralUtility::makeInstance(
725  TableDiff::class,
726  $changedTable->name,
727  [],
728  [$oldFieldName => $changedColumn],
729  [],
730  [],
731  [],
732  [],
733  $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name))
734  );
735  if ($isSqlite) {
736  break;
737  }
738  }
739  }
740 
741  // Build a new schema diff that only contains unused fields
742  $changedFieldDiff = GeneralUtility::makeInstance(
743  SchemaDiff::class,
744  [],
745  $changedTables,
746  [],
747  $schemaDiff->fromSchema
748  );
749 
750  $statements = $changedFieldDiff->toSql($this->connection->getDatabasePlatform());
751 
752  return ['change' => $this->‪calculateUpdateSuggestionsHashes($statements)];
753  }
754 
766  protected function ‪getDropFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
767  {
768  $changedTables = [];
769 
770  foreach ($schemaDiff->changedTables as $index => $changedTable) {
771  $fromTable = $this->‪buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
772 
773  $isSqlite = $this->‪tableRunsOnSqlite($index);
774  $addMoreOperations = true;
775 
776  if (count($changedTable->removedColumns) !== 0) {
777  // Treat each changed column with a new diff to get a dedicated suggestions
778  // just for this single column.
779  foreach ($changedTable->removedColumns as $columnName => $removedColumn) {
780  $changedTables[$index . ':tbl_' . $removedColumn->getName()] = GeneralUtility::makeInstance(
781  TableDiff::class,
782  $changedTable->name,
783  [],
784  [],
785  [$columnName => $this->buildQuotedColumn($removedColumn)],
786  [],
787  [],
788  [],
789  $fromTable
790  );
791  if ($isSqlite) {
792  $addMoreOperations = false;
793  break;
794  }
795  }
796  }
797 
798  if ($addMoreOperations && count($changedTable->removedIndexes) !== 0) {
799  // Treat each removed index with a new diff to get a dedicated suggestions
800  // just for this index.
801  foreach ($changedTable->removedIndexes as $indexName => $removedIndex) {
802  $changedTables[$index . ':idx_' . $removedIndex->getName()] = GeneralUtility::makeInstance(
803  TableDiff::class,
804  $changedTable->name,
805  [],
806  [],
807  [],
808  [],
809  [],
810  [$indexName => $this->buildQuotedIndex($removedIndex)],
811  $fromTable
812  );
813  if ($isSqlite) {
814  $addMoreOperations = false;
815  break;
816  }
817  }
818  }
819 
820  if ($addMoreOperations && count($changedTable->removedForeignKeys) !== 0) {
821  // Treat each removed foreign key with a new diff to get a dedicated suggestions
822  // just for this foreign key.
823  foreach ($changedTable->removedForeignKeys as $removedForeignKey) {
824  $fkIndex = $index . ':fk_' . $removedForeignKey->getName();
825  $changedTables[$fkIndex] = GeneralUtility::makeInstance(
826  TableDiff::class,
827  $changedTable->name,
828  [],
829  [],
830  [],
831  [],
832  [],
833  [],
834  $fromTable
835  );
836  $changedTables[$fkIndex]->removedForeignKeys = [$this->‪buildQuotedForeignKey($removedForeignKey)];
837  if ($isSqlite) {
838  break;
839  }
840  }
841  }
842  }
843 
844  // Build a new schema diff that only contains removable fields
845  $removedFieldDiff = GeneralUtility::makeInstance(
846  SchemaDiff::class,
847  [],
848  $changedTables,
849  [],
850  $schemaDiff->fromSchema
851  );
852 
853  $statements = $removedFieldDiff->toSql($this->connection->getDatabasePlatform());
854 
855  return ['drop' => $this->‪calculateUpdateSuggestionsHashes($statements)];
856  }
857 
869  protected function ‪getDropTableUpdateSuggestions(SchemaDiff $schemaDiff): array
870  {
871  $updateSuggestions = [];
872  foreach ($schemaDiff->removedTables as $removedTable) {
873  // Build a new schema diff that only contains this table
874  $tableDiff = GeneralUtility::makeInstance(
875  SchemaDiff::class,
876  [],
877  [],
878  [$this->‪buildQuotedTable($removedTable)],
879  $schemaDiff->fromSchema
880  );
881 
882  $statements = $tableDiff->toSql($this->connection->getDatabasePlatform());
883  foreach ($statements as $statement) {
884  $updateSuggestions['drop_table'][md5($statement)] = $statement;
885  }
886 
887  // Only store the record count for this table for the first statement,
888  // assuming that this is the actual DROP TABLE statement.
889  $updateSuggestions['tables_count'][md5($statements[0])] = $this->‪getTableRecordCount(
890  $removedTable->getName()
891  );
892  }
893 
894  return $updateSuggestions;
895  }
896 
908  protected function ‪migrateUnprefixedRemovedTablesToRenames(SchemaDiff $schemaDiff): SchemaDiff
909  {
910  foreach ($schemaDiff->removedTables as $index => $removedTable) {
911  if (strpos($removedTable->getName(), $this->deletedPrefix) === 0) {
912  continue;
913  }
914  $tableDiff = GeneralUtility::makeInstance(
915  TableDiff::class,
916  $removedTable->getQuotedName($this->connection->getDatabasePlatform()),
917  $addedColumns = [],
918  $changedColumns = [],
919  $removedColumns = [],
920  $addedIndexes = [],
921  $changedIndexes = [],
922  $removedIndexes = [],
923  $this->buildQuotedTable($removedTable)
924  );
925 
926  $tableDiff->newName = $this->connection->getDatabasePlatform()->quoteIdentifier(
927  substr(
928  $this->deletedPrefix . $removedTable->getName(),
929  0,
930  ‪PlatformInformation::getMaxIdentifierLength($this->connection->getDatabasePlatform())
931  )
932  );
933  $schemaDiff->changedTables[$index] = $tableDiff;
934  unset($schemaDiff->removedTables[$index]);
935  }
936 
937  return $schemaDiff;
938  }
939 
949  protected function ‪migrateUnprefixedRemovedFieldsToRenames(SchemaDiff $schemaDiff): SchemaDiff
950  {
951  foreach ($schemaDiff->changedTables as $tableIndex => $changedTable) {
952  if (count($changedTable->removedColumns) === 0) {
953  continue;
954  }
955 
956  foreach ($changedTable->removedColumns as $columnIndex => $removedColumn) {
957  if (strpos($removedColumn->getName(), $this->deletedPrefix) === 0) {
958  continue;
959  }
960 
961  // Build a new column object with the same properties as the removed column
962  $renamedColumnName = substr(
963  $this->deletedPrefix . $removedColumn->getName(),
964  0,
965  ‪PlatformInformation::getMaxIdentifierLength($this->connection->getDatabasePlatform())
966  );
967  $renamedColumn = new Column(
968  $this->connection->quoteIdentifier($renamedColumnName),
969  $removedColumn->getType(),
970  array_diff_key($removedColumn->toArray(), ['name', 'type'])
971  );
972 
973  // Build the diff object for the column to rename
974  $columnDiff = GeneralUtility::makeInstance(
975  ColumnDiff::class,
976  $removedColumn->getQuotedName($this->connection->getDatabasePlatform()),
977  $renamedColumn,
978  $changedProperties = [],
979  $this->buildQuotedColumn($removedColumn)
980  );
981 
982  // Add the column with the required rename information to the changed column list
983  $schemaDiff->changedTables[$tableIndex]->changedColumns[$columnIndex] = $columnDiff;
984 
985  // Remove the column from the list of columns to be dropped
986  unset($schemaDiff->changedTables[$tableIndex]->removedColumns[$columnIndex]);
987  }
988  }
989 
990  return $schemaDiff;
991  }
992 
1002  protected function ‪migrateColumnRenamesToDistinctActions(SchemaDiff $schemaDiff): SchemaDiff
1003  {
1004  foreach ($schemaDiff->changedTables as $index => $changedTable) {
1005  if (count($changedTable->renamedColumns) === 0) {
1006  continue;
1007  }
1008 
1009  // Treat each renamed column with a new diff to get a dedicated
1010  // suggestion just for this single column.
1011  foreach ($changedTable->renamedColumns as $originalColumnName => $renamedColumn) {
1012  $columnOptions = array_diff_key($renamedColumn->toArray(), ['name', 'type']);
1013 
1014  $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance(
1015  Column::class,
1016  $renamedColumn->getName(),
1017  $renamedColumn->getType(),
1018  $columnOptions
1019  );
1020  $changedTable->removedColumns[$originalColumnName] = GeneralUtility::makeInstance(
1021  Column::class,
1022  $originalColumnName,
1023  $renamedColumn->getType(),
1024  $columnOptions
1025  );
1026 
1027  unset($changedTable->renamedColumns[$originalColumnName]);
1028  }
1029  }
1030 
1031  return $schemaDiff;
1032  }
1033 
1041  protected function ‪getTableRecordCount(string $tableName): int
1042  {
1043  return GeneralUtility::makeInstance(ConnectionPool::class)
1044  ->getConnectionForTable($tableName)
1045  ->count('*', $tableName, []);
1046  }
1047 
1055  protected function ‪getConnectionNameForTable(string $tableName): string
1056  {
1057  $connectionNames = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionNames();
1058 
1059  if (isset(‪$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName])) {
1060  return in_array(‪$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName], $connectionNames, true)
1061  ? ‪$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName]
1063  }
1064 
1066  }
1067 
1074  protected function ‪calculateUpdateSuggestionsHashes(array $statements): array
1075  {
1076  return array_combine(array_map('md5', $statements), $statements);
1077  }
1078 
1087  protected function ‪removeUnrelatedTables(array $tableDiffs, array $validTableNames): array
1088  {
1089  return array_filter(
1090  $tableDiffs,
1091  function ($table) use ($validTableNames) {
1092  if ($table instanceof Table) {
1093  $tableName = $table->getName();
1094  } else {
1095  $tableName = $table->newName ?: $table->name;
1096  }
1097 
1098  // If the tablename has a deleted prefix strip it of before comparing
1099  // it against the list of valid table names so that drop operations
1100  // don't get removed.
1101  if (strpos($tableName, $this->deletedPrefix) === 0) {
1102  $tableName = substr($tableName, strlen($this->deletedPrefix));
1103  }
1104  return in_array($tableName, $validTableNames, true)
1105  || in_array($this->deletedPrefix . $tableName, $validTableNames, true);
1106  }
1107  );
1108  }
1109 
1120  protected function ‪transformTablesForDatabasePlatform(array ‪$tables, Connection ‪$connection): array
1121  {
1122  $defaultTableOptions = ‪$connection->getParams()['tableoptions'] ?? [];
1123  foreach (‪$tables as &$table) {
1124  $indexes = [];
1125  foreach ($table->getIndexes() as $key => $index) {
1126  $indexName = $index->getName();
1127  // PostgreSQL and sqlite require index names to be unique per database/schema.
1128  if (‪$connection->getDatabasePlatform() instanceof PostgreSqlPlatform
1129  || ‪$connection->getDatabasePlatform() instanceof SqlitePlatform
1130  ) {
1131  $indexName = $indexName . '_' . hash('crc32b', $table->getName() . '_' . $indexName);
1132  }
1133 
1134  // Remove the length information from column names for indexes if required.
1135  $cleanedColumnNames = array_map(
1136  function (string $columnName) use (‪$connection) {
1137  if (‪$connection->getDatabasePlatform() instanceof MySqlPlatform) {
1138  // Returning the unquoted, unmodified version of the column name since
1139  // it can include the length information for BLOB/TEXT columns which
1140  // may not be quoted.
1141  return $columnName;
1142  }
1143 
1144  return ‪$connection->‪quoteIdentifier(preg_replace('/\‍(\d+\‍)$/', '', $columnName));
1145  },
1146  $index->getUnquotedColumns()
1147  );
1148 
1149  $indexes[$key] = GeneralUtility::makeInstance(
1150  Index::class,
1151  ‪$connection->‪quoteIdentifier($indexName),
1152  $cleanedColumnNames,
1153  $index->isUnique(),
1154  $index->isPrimary(),
1155  $index->getFlags(),
1156  $index->getOptions()
1157  );
1158  }
1159 
1160  $table = GeneralUtility::makeInstance(
1161  Table::class,
1162  $table->getQuotedName(‪$connection->getDatabasePlatform()),
1163  $table->getColumns(),
1164  $indexes,
1165  $table->getForeignKeys(),
1166  0,
1167  array_merge($defaultTableOptions, $table->getOptions())
1168  );
1169  }
1170 
1171  return ‪$tables;
1172  }
1173 
1181  protected function ‪getTableOptions(array $tableNames): array
1182  {
1183  $tableOptions = [];
1184  if (strpos($this->connection->getServerVersion(), 'MySQL') !== 0) {
1185  foreach ($tableNames as $tableName) {
1186  $tableOptions[$tableName] = [];
1187  }
1188 
1189  return $tableOptions;
1190  }
1191 
1192  $queryBuilder = $this->connection->createQueryBuilder();
1193  $result = $queryBuilder
1194  ->select(
1195  'tables.TABLE_NAME AS table',
1196  'tables.ENGINE AS engine',
1197  'tables.ROW_FORMAT AS row_format',
1198  'tables.TABLE_COLLATION AS collate',
1199  'tables.TABLE_COMMENT AS comment',
1200  'CCSA.character_set_name AS charset'
1201  )
1202  ->from('information_schema.TABLES', 'tables')
1203  ->join(
1204  'tables',
1205  'information_schema.COLLATION_CHARACTER_SET_APPLICABILITY',
1206  'CCSA',
1207  $queryBuilder->expr()->eq(
1208  'CCSA.collation_name',
1209  $queryBuilder->quoteIdentifier('tables.table_collation')
1210  )
1211  )
1212  ->where(
1213  $queryBuilder->expr()->eq(
1214  'TABLE_TYPE',
1215  $queryBuilder->createNamedParameter('BASE TABLE', \PDO::PARAM_STR)
1216  ),
1217  $queryBuilder->expr()->eq(
1218  'TABLE_SCHEMA',
1219  $queryBuilder->createNamedParameter($this->connection->getDatabase(), \PDO::PARAM_STR)
1220  )
1221  )
1222  ->execute();
1223 
1224  while ($row = $result->fetch()) {
1225  $index = $row['table'];
1226  unset($row['table']);
1227  $tableOptions[$index] = $row;
1228  }
1229 
1230  return $tableOptions;
1231  }
1232 
1242  protected function ‪buildQuotedTable(Table $table): Table
1243  {
1244  $databasePlatform = $this->connection->getDatabasePlatform();
1245 
1246  return GeneralUtility::makeInstance(
1247  Table::class,
1248  $databasePlatform->quoteIdentifier($table->getName()),
1249  $table->getColumns(),
1250  $table->getIndexes(),
1251  $table->getForeignKeys(),
1252  0,
1253  $table->getOptions()
1254  );
1255  }
1256 
1266  protected function ‪buildQuotedColumn(Column $column): Column
1267  {
1268  $databasePlatform = $this->connection->getDatabasePlatform();
1269 
1270  return GeneralUtility::makeInstance(
1271  Column::class,
1272  $databasePlatform->quoteIdentifier($column->getName()),
1273  $column->getType(),
1274  array_diff_key($column->toArray(), ['name', 'type'])
1275  );
1276  }
1277 
1287  protected function ‪buildQuotedIndex(Index $index): Index
1288  {
1289  $databasePlatform = $this->connection->getDatabasePlatform();
1290 
1291  return GeneralUtility::makeInstance(
1292  Index::class,
1293  $databasePlatform->quoteIdentifier($index->getName()),
1294  $index->getColumns(),
1295  $index->isUnique(),
1296  $index->isPrimary(),
1297  $index->getFlags(),
1298  $index->getOptions()
1299  );
1300  }
1301 
1311  protected function ‪buildQuotedForeignKey(ForeignKeyConstraint $index): ForeignKeyConstraint
1312  {
1313  $databasePlatform = $this->connection->getDatabasePlatform();
1314 
1315  return GeneralUtility::makeInstance(
1316  ForeignKeyConstraint::class,
1317  $index->getLocalColumns(),
1318  $databasePlatform->quoteIdentifier($index->getForeignTableName()),
1319  $index->getForeignColumns(),
1320  $databasePlatform->quoteIdentifier($index->getName()),
1321  $index->getOptions()
1322  );
1323  }
1324 
1325  protected function ‪tableRunsOnSqlite(string $tableName): bool
1326  {
1327  ‪$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName);
1328  return ‪$connection->getDatabasePlatform() instanceof SqlitePlatform;
1329  }
1330 }
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\buildQuotedColumn
‪Doctrine DBAL Schema Column buildQuotedColumn(Column $column)
Definition: ConnectionMigrator.php:1262
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\create
‪static ConnectionMigrator create(string $connectionName, array $tables)
Definition: ConnectionMigrator.php:75
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\migrateUnprefixedRemovedTablesToRenames
‪Doctrine DBAL Schema SchemaDiff migrateUnprefixedRemovedTablesToRenames(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:904
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\buildQuotedTable
‪Doctrine DBAL Schema Table buildQuotedTable(Table $table)
Definition: ConnectionMigrator.php:1238
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\buildQuotedIndex
‪Doctrine DBAL Schema Index buildQuotedIndex(Index $index)
Definition: ConnectionMigrator.php:1283
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getUnusedFieldUpdateSuggestions
‪array getUnusedFieldUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:701
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\buildExpectedSchemaDefinitions
‪Doctrine DBAL Schema Schema buildExpectedSchemaDefinitions(string $connectionName)
Definition: ConnectionMigrator.php:274
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getUpdateSuggestions
‪array getUpdateSuggestions(bool $remove=false)
Definition: ConnectionMigrator.php:102
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\install
‪array install(bool $createOnly=false)
Definition: ConnectionMigrator.php:132
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getUnusedTableUpdateSuggestions
‪array getUnusedTableUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:659
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\calculateUpdateSuggestionsHashes
‪string[] calculateUpdateSuggestionsHashes(array $statements)
Definition: ConnectionMigrator.php:1070
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getSchemaDiff
‪SchemaDiff getSchemaDiff()
Definition: ConnectionMigrator.php:90
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getChangedFieldUpdateSuggestions
‪array getChangedFieldUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:487
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\transformTablesForDatabasePlatform
‪Table[] transformTablesForDatabasePlatform(array $tables, Connection $connection)
Definition: ConnectionMigrator.php:1116
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\tableRunsOnSqlite
‪tableRunsOnSqlite(string $tableName)
Definition: ConnectionMigrator.php:1321
‪TYPO3\CMS\Core\Database\Connection\quoteIdentifier
‪string quoteIdentifier($identifier)
Definition: Connection.php:126
‪TYPO3\CMS\Core\Database\Schema
Definition: Comparator.php:3
‪TYPO3\CMS\Core\Database\ConnectionPool\DEFAULT_CONNECTION_NAME
‪const DEFAULT_CONNECTION_NAME
Definition: ConnectionPool.php:48
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\$tables
‪Table[] $tables
Definition: ConnectionMigrator.php:56
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\buildSchemaDiff
‪Doctrine DBAL Schema SchemaDiff buildSchemaDiff(bool $renameUnused=true)
Definition: ConnectionMigrator.php:208
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator
Definition: ConnectionMigrator.php:41
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getTableOptions
‪array[] getTableOptions(array $tableNames)
Definition: ConnectionMigrator.php:1177
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getTableRecordCount
‪int getTableRecordCount(string $tableName)
Definition: ConnectionMigrator.php:1037
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\$deletedPrefix
‪string $deletedPrefix
Definition: ConnectionMigrator.php:44
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\buildQuotedForeignKey
‪Doctrine DBAL Schema ForeignKeyConstraint buildQuotedForeignKey(ForeignKeyConstraint $index)
Definition: ConnectionMigrator.php:1307
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getDropTableUpdateSuggestions
‪array getDropTableUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:865
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\migrateColumnRenamesToDistinctActions
‪SchemaDiff migrateColumnRenamesToDistinctActions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:998
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:31
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\removeUnrelatedTables
‪TableDiff[] removeUnrelatedTables(array $tableDiffs, array $validTableNames)
Definition: ConnectionMigrator.php:1083
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getNewFieldUpdateSuggestions
‪array getNewFieldUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:350
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\$connection
‪Connection $connection
Definition: ConnectionMigrator.php:48
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getConnectionNameForTable
‪string getConnectionNameForTable(string $tableName)
Definition: ConnectionMigrator.php:1051
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation
Definition: PlatformInformation.php:31
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\__construct
‪__construct(string $connectionName, array $tables)
Definition: ConnectionMigrator.php:62
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getNewTableUpdateSuggestions
‪array getNewTableUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:325
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\$connectionName
‪string $connectionName
Definition: ConnectionMigrator.php:52
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getChangedTableOptions
‪array getChangedTableOptions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:437
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getDropFieldUpdateSuggestions
‪array getDropFieldUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:762
‪TYPO3\CMS\Core\Database\Schema\TableDiff
Definition: TableDiff.php:25
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation\getMaxIdentifierLength
‪static int getMaxIdentifierLength(AbstractPlatform $platform)
Definition: PlatformInformation.php:57
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\migrateUnprefixedRemovedFieldsToRenames
‪Doctrine DBAL Schema SchemaDiff migrateUnprefixedRemovedFieldsToRenames(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:945