‪TYPO3CMS  10.4
ConnectionMigrator.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Doctrine\DBAL\DBALException;
21 use Doctrine\DBAL\Platforms\MySqlPlatform;
22 use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
23 use Doctrine\DBAL\Platforms\SqlitePlatform;
24 use Doctrine\DBAL\Platforms\SQLServer2012Platform as SQLServerPlatform;
25 use Doctrine\DBAL\Schema\Column;
26 use Doctrine\DBAL\Schema\ColumnDiff;
27 use Doctrine\DBAL\Schema\ForeignKeyConstraint;
28 use Doctrine\DBAL\Schema\Index;
29 use Doctrine\DBAL\Schema\Schema;
30 use Doctrine\DBAL\Schema\SchemaConfig;
31 use Doctrine\DBAL\Schema\SchemaDiff;
32 use Doctrine\DBAL\Schema\Table;
37 
44 {
48  protected ‪$deletedPrefix = 'zzz_deleted_';
49 
53  protected ‪$connection;
54 
58  protected ‪$connectionName;
59 
63  protected ‪$tables;
64 
69  public function ‪__construct(string ‪$connectionName, array ‪$tables)
70  {
71  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
72  $this->connection = $connectionPool->getConnectionByName(‪$connectionName);
73  $this->connectionName = ‪$connectionName;
74  $this->tables = ‪$tables;
75  }
76 
82  public static function ‪create(string ‪$connectionName, array ‪$tables)
83  {
84  return GeneralUtility::makeInstance(
85  static::class,
88  );
89  }
90 
97  public function ‪getSchemaDiff(): SchemaDiff
98  {
99  return $this->‪buildSchemaDiff(false);
100  }
101 
109  public function ‪getUpdateSuggestions(bool $remove = false): array
110  {
111  $schemaDiff = $this->‪buildSchemaDiff();
112 
113  if ($remove === false) {
114  return array_merge_recursive(
115  ['add' => [], 'create_table' => [], 'change' => [], 'change_currentValue' => []],
116  $this->‪getNewFieldUpdateSuggestions($schemaDiff),
117  $this->‪getNewTableUpdateSuggestions($schemaDiff),
118  $this->‪getChangedFieldUpdateSuggestions($schemaDiff),
119  $this->‪getChangedTableOptions($schemaDiff)
120  );
121  }
122  return array_merge_recursive(
123  ['change' => [], 'change_table' => [], 'drop' => [], 'drop_table' => [], 'tables_count' => []],
124  $this->‪getUnusedFieldUpdateSuggestions($schemaDiff),
125  $this->‪getUnusedTableUpdateSuggestions($schemaDiff),
126  $this->‪getDropTableUpdateSuggestions($schemaDiff),
127  $this->‪getDropFieldUpdateSuggestions($schemaDiff)
128  );
129  }
130 
139  public function ‪install(bool $createOnly = false): array
140  {
141  $result = [];
142  $schemaDiff = $this->‪buildSchemaDiff(false);
143 
144  $schemaDiff->removedTables = [];
145  foreach ($schemaDiff->changedTables as $key => $changedTable) {
146  $schemaDiff->changedTables[$key]->removedColumns = [];
147  $schemaDiff->changedTables[$key]->removedIndexes = [];
148 
149  // With partial ext_tables.sql files the SchemaManager is detecting
150  // existing columns as false positives for a column rename. In this
151  // context every rename is actually a new column.
152  foreach ($changedTable->renamedColumns as $columnName => $renamedColumn) {
153  $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance(
154  Column::class,
155  $renamedColumn->getName(),
156  $renamedColumn->getType(),
157  array_diff_key($renamedColumn->toArray(), ['name', 'type'])
158  );
159  unset($changedTable->renamedColumns[$columnName]);
160  }
161 
162  if ($createOnly) {
163  // Ignore new indexes that work on columns that need changes
164  foreach ($changedTable->addedIndexes as $indexName => $addedIndex) {
165  $indexColumns = array_map(
166  function ($columnName) {
167  // Strip MySQL prefix length information to get real column names
168  $columnName = preg_replace('/\‍(\d+\‍)$/', '', $columnName) ?? '';
169  // Strip mssql '[' and ']' from column names
170  $columnName = ltrim($columnName, '[');
171  $columnName = rtrim($columnName, ']');
172  // Strip sqlite '"' from column names
173  return trim($columnName, '"');
174  },
175  $addedIndex->getColumns()
176  );
177  $columnChanges = array_intersect($indexColumns, array_keys($changedTable->changedColumns));
178  if (!empty($columnChanges)) {
179  unset($schemaDiff->changedTables[$key]->addedIndexes[$indexName]);
180  }
181  }
182  $schemaDiff->changedTables[$key]->changedColumns = [];
183  $schemaDiff->changedTables[$key]->changedIndexes = [];
184  $schemaDiff->changedTables[$key]->renamedIndexes = [];
185  }
186  }
187 
188  $statements = $schemaDiff->toSaveSql(
189  $this->connection->getDatabasePlatform()
190  );
191 
192  foreach ($statements as $statement) {
193  try {
194  $this->connection->executeUpdate($statement);
195  $result[$statement] = '';
196  } catch (DBALException $e) {
197  $result[$statement] = $e->getPrevious()->getMessage();
198  }
199  }
200 
201  return $result;
202  }
203 
215  protected function ‪buildSchemaDiff(bool $renameUnused = true): SchemaDiff
216  {
217  // Build the schema definitions
218  $fromSchema = $this->connection->getSchemaManager()->createSchema();
219  $toSchema = $this->‪buildExpectedSchemaDefinitions($this->connectionName);
220 
221  // Add current table options to the fromSchema
222  $tableOptions = $this->‪getTableOptions($fromSchema->getTableNames());
223  foreach ($fromSchema->getTables() as $table) {
224  $tableName = $table->getName();
225  if (!array_key_exists($tableName, $tableOptions)) {
226  continue;
227  }
228  foreach ($tableOptions[$tableName] as $optionName => $optionValue) {
229  $table->addOption($optionName, $optionValue);
230  }
231  }
232 
233  // Build SchemaDiff and handle renames of tables and columns
234  $comparator = GeneralUtility::makeInstance(Comparator::class, $this->connection->getDatabasePlatform());
235  $schemaDiff = $comparator->compare($fromSchema, $toSchema);
236  $schemaDiff = $this->‪migrateColumnRenamesToDistinctActions($schemaDiff);
237 
238  if ($renameUnused) {
239  $schemaDiff = $this->‪migrateUnprefixedRemovedTablesToRenames($schemaDiff);
240  $schemaDiff = $this->‪migrateUnprefixedRemovedFieldsToRenames($schemaDiff);
241  }
242 
243  // All tables in the default connection are managed by TYPO3
244  if ($this->connectionName === ‪ConnectionPool::DEFAULT_CONNECTION_NAME) {
245  return $schemaDiff;
246  }
247 
248  // If there are no mapped tables return a SchemaDiff without any changes
249  // to avoid update suggestions for tables not related to TYPO3.
250  if (empty(‪$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'] ?? null)) {
251  return GeneralUtility::makeInstance(SchemaDiff::class, [], [], [], $fromSchema);
252  }
253 
254  // Collect the table names that have been mapped to this connection.
257  $tablesForConnection = array_keys(
258  array_filter(
259  ‪$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'],
260  function ($tableConnectionName) use (‪$connectionName) {
261  return $tableConnectionName === ‪$connectionName;
262  }
263  )
264  );
265 
266  // Remove all tables that are not assigned to this connection from the diff
267  $schemaDiff->newTables = $this->‪removeUnrelatedTables($schemaDiff->newTables, $tablesForConnection);
268  $schemaDiff->changedTables = $this->‪removeUnrelatedTables($schemaDiff->changedTables, $tablesForConnection);
269  $schemaDiff->removedTables = $this->‪removeUnrelatedTables($schemaDiff->removedTables, $tablesForConnection);
270 
271  return $schemaDiff;
272  }
273 
282  protected function ‪buildExpectedSchemaDefinitions(string ‪$connectionName): Schema
283  {
285  $tablesForConnection = [];
286  foreach ($this->tables as $table) {
287  $tableName = $table->getName();
288 
289  // Skip tables for a different connection
290  if (‪$connectionName !== $this->‪getConnectionNameForTable($tableName)) {
291  continue;
292  }
293 
294  if (!array_key_exists($tableName, $tablesForConnection)) {
295  $tablesForConnection[$tableName] = $table;
296  continue;
297  }
298 
299  // Merge multiple table definitions. Later definitions overrule identical
300  // columns, indexes and foreign_keys. Order of definitions is based on
301  // extension load order.
302  $currentTableDefinition = $tablesForConnection[$tableName];
303  $tablesForConnection[$tableName] = GeneralUtility::makeInstance(
304  Table::class,
305  $tableName,
306  array_merge($currentTableDefinition->getColumns(), $table->getColumns()),
307  array_merge($currentTableDefinition->getIndexes(), $table->getIndexes()),
308  array_merge($currentTableDefinition->getForeignKeys(), $table->getForeignKeys()),
309  0,
310  array_merge($currentTableDefinition->getOptions(), $table->getOptions())
311  );
312  }
313 
314  $tablesForConnection = $this->‪transformTablesForDatabasePlatform($tablesForConnection, $this->connection);
315 
316  $schemaConfig = GeneralUtility::makeInstance(SchemaConfig::class);
317  $schemaConfig->setName($this->connection->getDatabase());
318  if (isset($this->connection->getParams()['tableoptions'])) {
319  $schemaConfig->setDefaultTableOptions($this->connection->getParams()['tableoptions']);
320  }
321 
322  return GeneralUtility::makeInstance(Schema::class, $tablesForConnection, [], $schemaConfig);
323  }
324 
333  protected function ‪getNewTableUpdateSuggestions(SchemaDiff $schemaDiff): array
334  {
335  // Build a new schema diff that only contains added tables
336  $addTableSchemaDiff = GeneralUtility::makeInstance(
337  SchemaDiff::class,
338  $schemaDiff->newTables,
339  [],
340  [],
341  $schemaDiff->fromSchema
342  );
343 
344  $statements = $addTableSchemaDiff->toSql($this->connection->getDatabasePlatform());
345 
346  return ['create_table' => $this->‪calculateUpdateSuggestionsHashes($statements)];
347  }
348 
358  protected function ‪getNewFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
359  {
360  $changedTables = [];
361 
362  foreach ($schemaDiff->changedTables as $index => $changedTable) {
363  $fromTable = $this->‪buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
364 
365  if (count($changedTable->addedColumns) !== 0) {
366  // Treat each added column with a new diff to get a dedicated suggestions
367  // just for this single column.
368  foreach ($changedTable->addedColumns as $columnName => $addedColumn) {
369  $changedTables[$index . ':tbl_' . $addedColumn->getName()] = GeneralUtility::makeInstance(
370  TableDiff::class,
371  $changedTable->name,
372  [$columnName => $addedColumn],
373  [],
374  [],
375  [],
376  [],
377  [],
378  $fromTable
379  );
380  }
381  }
382 
383  if (count($changedTable->addedIndexes) !== 0) {
384  // Treat each added index with a new diff to get a dedicated suggestions
385  // just for this index.
386  foreach ($changedTable->addedIndexes as $indexName => $addedIndex) {
387  $changedTables[$index . ':idx_' . $addedIndex->getName()] = GeneralUtility::makeInstance(
388  TableDiff::class,
389  $changedTable->name,
390  [],
391  [],
392  [],
393  [$indexName => $this->buildQuotedIndex($addedIndex)],
394  [],
395  [],
396  $fromTable
397  );
398  }
399  }
400 
401  if (count($changedTable->addedForeignKeys) !== 0) {
402  // Treat each added foreign key with a new diff to get a dedicated suggestions
403  // just for this foreign key.
404  foreach ($changedTable->addedForeignKeys as $addedForeignKey) {
405  $fkIndex = $index . ':fk_' . $addedForeignKey->getName();
406  $changedTables[$fkIndex] = GeneralUtility::makeInstance(
407  TableDiff::class,
408  $changedTable->name,
409  [],
410  [],
411  [],
412  [],
413  [],
414  [],
415  $fromTable
416  );
417  $changedTables[$fkIndex]->addedForeignKeys = [$this->‪buildQuotedForeignKey($addedForeignKey)];
418  }
419  }
420  }
421 
422  // Build a new schema diff that only contains added fields
423  $addFieldSchemaDiff = GeneralUtility::makeInstance(
424  SchemaDiff::class,
425  [],
426  $changedTables,
427  [],
428  $schemaDiff->fromSchema
429  );
430 
431  $statements = $addFieldSchemaDiff->toSql($this->connection->getDatabasePlatform());
432 
433  return ['add' => $this->‪calculateUpdateSuggestionsHashes($statements)];
434  }
435 
445  protected function ‪getChangedTableOptions(SchemaDiff $schemaDiff): array
446  {
447  $updateSuggestions = [];
448 
449  foreach ($schemaDiff->changedTables as $tableDiff) {
450  // Skip processing if this is the base TableDiff class or has no table options set.
451  if (!$tableDiff instanceof TableDiff || count($tableDiff->getTableOptions()) === 0) {
452  continue;
453  }
454 
455  $tableOptions = $tableDiff->getTableOptions();
456  $tableOptionsDiff = GeneralUtility::makeInstance(
457  TableDiff::class,
458  $tableDiff->name,
459  [],
460  [],
461  [],
462  [],
463  [],
464  [],
465  $tableDiff->fromTable
466  );
467  $tableOptionsDiff->setTableOptions($tableOptions);
468 
469  $tableOptionsSchemaDiff = GeneralUtility::makeInstance(
470  SchemaDiff::class,
471  [],
472  [$tableOptionsDiff],
473  [],
474  $schemaDiff->fromSchema
475  );
476 
477  $statements = $tableOptionsSchemaDiff->toSaveSql($this->connection->getDatabasePlatform());
478  foreach ($statements as $statement) {
479  $updateSuggestions['change'][md5($statement)] = $statement;
480  }
481  }
482 
483  return $updateSuggestions;
484  }
485 
495  protected function ‪getChangedFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
496  {
497  $databasePlatform = $this->connection->getDatabasePlatform();
498  $updateSuggestions = [];
499 
500  foreach ($schemaDiff->changedTables as $index => $changedTable) {
501  // Treat each changed index with a new diff to get a dedicated suggestions
502  // just for this index.
503  if (count($changedTable->changedIndexes) !== 0) {
504  foreach ($changedTable->changedIndexes as $indexName => $changedIndex) {
505  $indexDiff = GeneralUtility::makeInstance(
506  TableDiff::class,
507  $changedTable->name,
508  [],
509  [],
510  [],
511  [],
512  [$indexName => $changedIndex],
513  [],
514  $schemaDiff->fromSchema->getTable($changedTable->name)
515  );
516 
517  $temporarySchemaDiff = GeneralUtility::makeInstance(
518  SchemaDiff::class,
519  [],
520  [$indexDiff],
521  [],
522  $schemaDiff->fromSchema
523  );
524 
525  $statements = $temporarySchemaDiff->toSql($databasePlatform);
526  foreach ($statements as $statement) {
527  $updateSuggestions['change'][md5($statement)] = $statement;
528  }
529  }
530  }
531 
532  // Treat renamed indexes as a field change as it's a simple rename operation
533  if (count($changedTable->renamedIndexes) !== 0) {
534  // Create a base table diff without any changes, there's no constructor
535  // argument to pass in renamed indexes.
536  $tableDiff = GeneralUtility::makeInstance(
537  TableDiff::class,
538  $changedTable->name,
539  [],
540  [],
541  [],
542  [],
543  [],
544  [],
545  $schemaDiff->fromSchema->getTable($changedTable->name)
546  );
547 
548  // Treat each renamed index with a new diff to get a dedicated suggestions
549  // just for this index.
550  foreach ($changedTable->renamedIndexes as $key => $renamedIndex) {
551  $indexDiff = clone $tableDiff;
552  $indexDiff->renamedIndexes = [$key => $renamedIndex];
553 
554  $temporarySchemaDiff = GeneralUtility::makeInstance(
555  SchemaDiff::class,
556  [],
557  [$indexDiff],
558  [],
559  $schemaDiff->fromSchema
560  );
561 
562  $statements = $temporarySchemaDiff->toSql($databasePlatform);
563  foreach ($statements as $statement) {
564  $updateSuggestions['change'][md5($statement)] = $statement;
565  }
566  }
567  }
568 
569  if (count($changedTable->changedColumns) !== 0) {
570  // Treat each changed column with a new diff to get a dedicated suggestions
571  // just for this single column.
572  $fromTable = $this->‪buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
573 
574  foreach ($changedTable->changedColumns as $columnName => $changedColumn) {
575  // Field has been renamed and will be handled separately
576  if ($changedColumn->getOldColumnName()->getName() !== $changedColumn->column->getName()) {
577  continue;
578  }
579 
580  if ($changedColumn->fromColumn !== null) {
581  $changedColumn->fromColumn = $this->‪buildQuotedColumn($changedColumn->fromColumn);
582  }
583 
584  // Get the current SQL declaration for the column
585  $currentColumn = $fromTable->getColumn($changedColumn->getOldColumnName()->getName());
586  $currentDeclaration = $databasePlatform->getColumnDeclarationSQL(
587  $currentColumn->getQuotedName($this->connection->getDatabasePlatform()),
588  $currentColumn->toArray()
589  );
590 
591  // Build a dedicated diff just for the current column
592  $tableDiff = GeneralUtility::makeInstance(
593  TableDiff::class,
594  $changedTable->name,
595  [],
596  [$columnName => $changedColumn],
597  [],
598  [],
599  [],
600  [],
601  $fromTable
602  );
603 
604  $temporarySchemaDiff = GeneralUtility::makeInstance(
605  SchemaDiff::class,
606  [],
607  [$tableDiff],
608  [],
609  $schemaDiff->fromSchema
610  );
611 
612  $statements = $temporarySchemaDiff->toSql($databasePlatform);
613  foreach ($statements as $statement) {
614  $updateSuggestions['change'][md5($statement)] = $statement;
615  $updateSuggestions['change_currentValue'][md5($statement)] = $currentDeclaration;
616  }
617  }
618  }
619 
620  // Treat each changed foreign key with a new diff to get a dedicated suggestions
621  // just for this foreign key.
622  if (count($changedTable->changedForeignKeys) !== 0) {
623  $tableDiff = GeneralUtility::makeInstance(
624  TableDiff::class,
625  $changedTable->name,
626  [],
627  [],
628  [],
629  [],
630  [],
631  [],
632  $schemaDiff->fromSchema->getTable($changedTable->name)
633  );
634 
635  foreach ($changedTable->changedForeignKeys as $changedForeignKey) {
636  $foreignKeyDiff = clone $tableDiff;
637  $foreignKeyDiff->changedForeignKeys = [$this->‪buildQuotedForeignKey($changedForeignKey)];
638 
639  $temporarySchemaDiff = GeneralUtility::makeInstance(
640  SchemaDiff::class,
641  [],
642  [$foreignKeyDiff],
643  [],
644  $schemaDiff->fromSchema
645  );
646 
647  $statements = $temporarySchemaDiff->toSql($databasePlatform);
648  foreach ($statements as $statement) {
649  $updateSuggestions['change'][md5($statement)] = $statement;
650  }
651  }
652  }
653  }
654 
655  return $updateSuggestions;
656  }
657 
669  protected function ‪getUnusedTableUpdateSuggestions(SchemaDiff $schemaDiff): array
670  {
671  $updateSuggestions = [];
672  foreach ($schemaDiff->changedTables as $tableDiff) {
673  // Skip tables that are not being renamed or where the new name isn't prefixed
674  // with the deletion marker.
675  if ($tableDiff->getNewName() === false
676  || strpos($tableDiff->getNewName()->getName(), $this->deletedPrefix) !== 0
677  ) {
678  continue;
679  }
680  // Build a new schema diff that only contains this table
681  $changedFieldDiff = GeneralUtility::makeInstance(
682  SchemaDiff::class,
683  [],
684  [$tableDiff],
685  [],
686  $schemaDiff->fromSchema
687  );
688 
689  $statements = $changedFieldDiff->toSql($this->connection->getDatabasePlatform());
690 
691  foreach ($statements as $statement) {
692  $updateSuggestions['change_table'][md5($statement)] = $statement;
693  }
694  $updateSuggestions['tables_count'][md5($statements[0])] = $this->‪getTableRecordCount((string)$tableDiff->name);
695  }
696 
697  return $updateSuggestions;
698  }
699 
711  protected function ‪getUnusedFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
712  {
713  $changedTables = [];
714 
715  foreach ($schemaDiff->changedTables as $index => $changedTable) {
716  if (count($changedTable->changedColumns) === 0) {
717  continue;
718  }
719 
720  $databasePlatform = $this->‪getDatabasePlatform($index);
721 
722  // Treat each changed column with a new diff to get a dedicated suggestions
723  // just for this single column.
724  foreach ($changedTable->changedColumns as $oldFieldName => $changedColumn) {
725  // Field has not been renamed
726  if ($changedColumn->getOldColumnName()->getName() === $changedColumn->column->getName()) {
727  continue;
728  }
729 
730  $renameColumnTableDiff = GeneralUtility::makeInstance(
731  TableDiff::class,
732  $changedTable->name,
733  [],
734  [$oldFieldName => $changedColumn],
735  [],
736  [],
737  [],
738  [],
739  $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name))
740  );
741  if ($databasePlatform === 'postgresql') {
742  $renameColumnTableDiff->renamedColumns[$oldFieldName] = $changedColumn->column;
743  }
744  $changedTables[$index . ':' . $changedColumn->column->getName()] = $renameColumnTableDiff;
745 
746  if ($databasePlatform === 'sqlite') {
747  break;
748  }
749  }
750  }
751 
752  // Build a new schema diff that only contains unused fields
753  $changedFieldDiff = GeneralUtility::makeInstance(
754  SchemaDiff::class,
755  [],
756  $changedTables,
757  [],
758  $schemaDiff->fromSchema
759  );
760 
761  $statements = $changedFieldDiff->toSql($this->connection->getDatabasePlatform());
762 
763  return ['change' => $this->‪calculateUpdateSuggestionsHashes($statements)];
764  }
765 
777  protected function ‪getDropFieldUpdateSuggestions(SchemaDiff $schemaDiff): array
778  {
779  $changedTables = [];
780 
781  foreach ($schemaDiff->changedTables as $index => $changedTable) {
782  $fromTable = $this->‪buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name));
783 
784  $isSqlite = $this->‪getDatabasePlatform($index) === 'sqlite';
785  $addMoreOperations = true;
786 
787  if (count($changedTable->removedColumns) !== 0) {
788  // Treat each changed column with a new diff to get a dedicated suggestions
789  // just for this single column.
790  foreach ($changedTable->removedColumns as $columnName => $removedColumn) {
791  $changedTables[$index . ':tbl_' . $removedColumn->getName()] = GeneralUtility::makeInstance(
792  TableDiff::class,
793  $changedTable->name,
794  [],
795  [],
796  [$columnName => $this->buildQuotedColumn($removedColumn)],
797  [],
798  [],
799  [],
800  $fromTable
801  );
802  if ($isSqlite) {
803  $addMoreOperations = false;
804  break;
805  }
806  }
807  }
808 
809  if ($addMoreOperations && count($changedTable->removedIndexes) !== 0) {
810  // Treat each removed index with a new diff to get a dedicated suggestions
811  // just for this index.
812  foreach ($changedTable->removedIndexes as $indexName => $removedIndex) {
813  $changedTables[$index . ':idx_' . $removedIndex->getName()] = GeneralUtility::makeInstance(
814  TableDiff::class,
815  $changedTable->name,
816  [],
817  [],
818  [],
819  [],
820  [],
821  [$indexName => $this->buildQuotedIndex($removedIndex)],
822  $fromTable
823  );
824  if ($isSqlite) {
825  $addMoreOperations = false;
826  break;
827  }
828  }
829  }
830 
831  if ($addMoreOperations && count($changedTable->removedForeignKeys) !== 0) {
832  // Treat each removed foreign key with a new diff to get a dedicated suggestions
833  // just for this foreign key.
834  foreach ($changedTable->removedForeignKeys as $removedForeignKey) {
835  if (is_string($removedForeignKey)) {
836  continue;
837  }
838  $fkIndex = $index . ':fk_' . $removedForeignKey->getName();
839  $changedTables[$fkIndex] = GeneralUtility::makeInstance(
840  TableDiff::class,
841  $changedTable->name,
842  [],
843  [],
844  [],
845  [],
846  [],
847  [],
848  $fromTable
849  );
850  $changedTables[$fkIndex]->removedForeignKeys = [$this->‪buildQuotedForeignKey($removedForeignKey)];
851  if ($isSqlite) {
852  break;
853  }
854  }
855  }
856  }
857 
858  // Build a new schema diff that only contains removable fields
859  $removedFieldDiff = GeneralUtility::makeInstance(
860  SchemaDiff::class,
861  [],
862  $changedTables,
863  [],
864  $schemaDiff->fromSchema
865  );
866 
867  $statements = $removedFieldDiff->toSql($this->connection->getDatabasePlatform());
868 
869  return ['drop' => $this->‪calculateUpdateSuggestionsHashes($statements)];
870  }
871 
883  protected function ‪getDropTableUpdateSuggestions(SchemaDiff $schemaDiff): array
884  {
885  $updateSuggestions = [];
886  foreach ($schemaDiff->removedTables as $removedTable) {
887  // Build a new schema diff that only contains this table
888  $tableDiff = GeneralUtility::makeInstance(
889  SchemaDiff::class,
890  [],
891  [],
892  [$this->‪buildQuotedTable($removedTable)],
893  $schemaDiff->fromSchema
894  );
895 
896  $statements = $tableDiff->toSql($this->connection->getDatabasePlatform());
897  foreach ($statements as $statement) {
898  $updateSuggestions['drop_table'][md5($statement)] = $statement;
899  }
900 
901  // Only store the record count for this table for the first statement,
902  // assuming that this is the actual DROP TABLE statement.
903  $updateSuggestions['tables_count'][md5($statements[0])] = $this->‪getTableRecordCount(
904  $removedTable->getName()
905  );
906  }
907 
908  return $updateSuggestions;
909  }
910 
922  protected function ‪migrateUnprefixedRemovedTablesToRenames(SchemaDiff $schemaDiff): SchemaDiff
923  {
924  foreach ($schemaDiff->removedTables as $index => $removedTable) {
925  if (strpos($removedTable->getName(), $this->deletedPrefix) === 0) {
926  continue;
927  }
928  $tableDiff = GeneralUtility::makeInstance(
929  TableDiff::class,
930  $removedTable->getQuotedName($this->connection->getDatabasePlatform()),
931  $addedColumns = [],
932  $changedColumns = [],
933  $removedColumns = [],
934  $addedIndexes = [],
935  $changedIndexes = [],
936  $removedIndexes = [],
937  $this->buildQuotedTable($removedTable)
938  );
939 
940  $tableDiff->newName = $this->connection->getDatabasePlatform()->quoteIdentifier(
941  substr(
942  $this->deletedPrefix . $removedTable->getName(),
943  0,
944  ‪PlatformInformation::getMaxIdentifierLength($this->connection->getDatabasePlatform())
945  )
946  );
947  $schemaDiff->changedTables[$index] = $tableDiff;
948  unset($schemaDiff->removedTables[$index]);
949  }
950 
951  return $schemaDiff;
952  }
953 
963  protected function ‪migrateUnprefixedRemovedFieldsToRenames(SchemaDiff $schemaDiff): SchemaDiff
964  {
965  foreach ($schemaDiff->changedTables as $tableIndex => $changedTable) {
966  if (count($changedTable->removedColumns) === 0) {
967  continue;
968  }
969 
970  foreach ($changedTable->removedColumns as $columnIndex => $removedColumn) {
971  if (strpos($removedColumn->getName(), $this->deletedPrefix) === 0) {
972  continue;
973  }
974 
975  // Build a new column object with the same properties as the removed column
976  $renamedColumnName = substr(
977  $this->deletedPrefix . $removedColumn->getName(),
978  0,
979  ‪PlatformInformation::getMaxIdentifierLength($this->connection->getDatabasePlatform())
980  );
981  $renamedColumn = new Column(
982  $this->connection->quoteIdentifier($renamedColumnName),
983  $removedColumn->getType(),
984  array_diff_key($removedColumn->toArray(), ['name', 'type'])
985  );
986 
987  // Build the diff object for the column to rename
988  $columnDiff = GeneralUtility::makeInstance(
989  ColumnDiff::class,
990  $removedColumn->getQuotedName($this->connection->getDatabasePlatform()),
991  $renamedColumn,
992  $changedProperties = [],
993  $this->buildQuotedColumn($removedColumn)
994  );
995 
996  // Add the column with the required rename information to the changed column list
997  $schemaDiff->changedTables[$tableIndex]->changedColumns[$columnIndex] = $columnDiff;
998 
999  // Remove the column from the list of columns to be dropped
1000  unset($schemaDiff->changedTables[$tableIndex]->removedColumns[$columnIndex]);
1001  }
1002  }
1003 
1004  return $schemaDiff;
1005  }
1006 
1016  protected function ‪migrateColumnRenamesToDistinctActions(SchemaDiff $schemaDiff): SchemaDiff
1017  {
1018  foreach ($schemaDiff->changedTables as $index => $changedTable) {
1019  if (count($changedTable->renamedColumns) === 0) {
1020  continue;
1021  }
1022 
1023  // Treat each renamed column with a new diff to get a dedicated
1024  // suggestion just for this single column.
1025  foreach ($changedTable->renamedColumns as $originalColumnName => $renamedColumn) {
1026  $columnOptions = array_diff_key($renamedColumn->toArray(), ['name', 'type']);
1027 
1028  $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance(
1029  Column::class,
1030  $renamedColumn->getName(),
1031  $renamedColumn->getType(),
1032  $columnOptions
1033  );
1034  $changedTable->removedColumns[$originalColumnName] = GeneralUtility::makeInstance(
1035  Column::class,
1036  $originalColumnName,
1037  $renamedColumn->getType(),
1038  $columnOptions
1039  );
1040 
1041  unset($changedTable->renamedColumns[$originalColumnName]);
1042  }
1043  }
1044 
1045  return $schemaDiff;
1046  }
1047 
1055  protected function ‪getTableRecordCount(string $tableName): int
1056  {
1057  return GeneralUtility::makeInstance(ConnectionPool::class)
1058  ->getConnectionForTable($tableName)
1059  ->count('*', $tableName, []);
1060  }
1061 
1069  protected function ‪getConnectionNameForTable(string $tableName): string
1070  {
1071  $connectionNames = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionNames();
1072 
1073  if (isset(‪$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName])) {
1074  return in_array(‪$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName], $connectionNames, true)
1075  ? ‪$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName]
1077  }
1078 
1080  }
1081 
1088  protected function ‪calculateUpdateSuggestionsHashes(array $statements): array
1089  {
1090  return array_combine(array_map('md5', $statements), $statements);
1091  }
1092 
1101  protected function ‪removeUnrelatedTables(array $tableDiffs, array $validTableNames): array
1102  {
1103  return array_filter(
1104  $tableDiffs,
1105  function ($table) use ($validTableNames) {
1106  if ($table instanceof Table) {
1107  $tableName = $table->getName();
1108  } else {
1109  $tableName = $table->newName ?: $table->name;
1110  }
1111 
1112  // If the tablename has a deleted prefix strip it of before comparing
1113  // it against the list of valid table names so that drop operations
1114  // don't get removed.
1115  if (strpos($tableName, $this->deletedPrefix) === 0) {
1116  $tableName = substr($tableName, strlen($this->deletedPrefix));
1117  }
1118  return in_array($tableName, $validTableNames, true)
1119  || in_array($this->deletedPrefix . $tableName, $validTableNames, true);
1120  }
1121  );
1122  }
1123 
1134  protected function ‪transformTablesForDatabasePlatform(array ‪$tables, Connection ‪$connection): array
1135  {
1136  $defaultTableOptions = ‪$connection->getParams()['tableoptions'] ?? [];
1137  foreach (‪$tables as &$table) {
1138  $indexes = [];
1139  foreach ($table->getIndexes() as $key => $index) {
1140  $indexName = $index->getName();
1141  // PostgreSQL and sqlite require index names to be unique per database/schema.
1142  if (‪$connection->getDatabasePlatform() instanceof PostgreSqlPlatform
1143  || ‪$connection->getDatabasePlatform() instanceof SqlitePlatform
1144  ) {
1145  $indexName = $indexName . '_' . hash('crc32b', $table->getName() . '_' . $indexName);
1146  }
1147 
1148  // Remove the length information from column names for indexes if required.
1149  $cleanedColumnNames = array_map(
1150  function (string $columnName) use (‪$connection) {
1151  if (‪$connection->getDatabasePlatform() instanceof MySqlPlatform) {
1152  // Returning the unquoted, unmodified version of the column name since
1153  // it can include the length information for BLOB/TEXT columns which
1154  // may not be quoted.
1155  return $columnName;
1156  }
1157 
1158  return ‪$connection->‪quoteIdentifier(preg_replace('/\‍(\d+\‍)$/', '', $columnName));
1159  },
1160  $index->getUnquotedColumns()
1161  );
1162 
1163  $indexes[$key] = GeneralUtility::makeInstance(
1164  Index::class,
1165  ‪$connection->‪quoteIdentifier($indexName),
1166  $cleanedColumnNames,
1167  $index->isUnique(),
1168  $index->isPrimary(),
1169  $index->getFlags(),
1170  $index->getOptions()
1171  );
1172  }
1173 
1174  $table = GeneralUtility::makeInstance(
1175  Table::class,
1176  $table->getQuotedName(‪$connection->getDatabasePlatform()),
1177  $table->getColumns(),
1178  $indexes,
1179  $table->getForeignKeys(),
1180  0,
1181  array_merge($defaultTableOptions, $table->getOptions())
1182  );
1183  }
1184 
1185  return ‪$tables;
1186  }
1187 
1195  protected function ‪getTableOptions(array $tableNames): array
1196  {
1197  $tableOptions = [];
1198  if (strpos($this->connection->getServerVersion(), 'MySQL') !== 0) {
1199  foreach ($tableNames as $tableName) {
1200  $tableOptions[$tableName] = [];
1201  }
1202 
1203  return $tableOptions;
1204  }
1205 
1206  $queryBuilder = $this->connection->createQueryBuilder();
1207  $result = $queryBuilder
1208  ->select(
1209  'tables.TABLE_NAME AS table',
1210  'tables.ENGINE AS engine',
1211  'tables.ROW_FORMAT AS row_format',
1212  'tables.TABLE_COLLATION AS collate',
1213  'tables.TABLE_COMMENT AS comment',
1214  'CCSA.character_set_name AS charset'
1215  )
1216  ->from('information_schema.TABLES', 'tables')
1217  ->join(
1218  'tables',
1219  'information_schema.COLLATION_CHARACTER_SET_APPLICABILITY',
1220  'CCSA',
1221  $queryBuilder->expr()->eq(
1222  'CCSA.collation_name',
1223  $queryBuilder->quoteIdentifier('tables.table_collation')
1224  )
1225  )
1226  ->where(
1227  $queryBuilder->expr()->eq(
1228  'TABLE_TYPE',
1229  $queryBuilder->createNamedParameter('BASE TABLE', \PDO::PARAM_STR)
1230  ),
1231  $queryBuilder->expr()->eq(
1232  'TABLE_SCHEMA',
1233  $queryBuilder->createNamedParameter($this->connection->getDatabase(), \PDO::PARAM_STR)
1234  )
1235  )
1236  ->execute();
1237 
1238  while ($row = $result->fetch()) {
1239  $index = $row['table'];
1240  unset($row['table']);
1241  $tableOptions[$index] = $row;
1242  }
1243 
1244  return $tableOptions;
1245  }
1246 
1256  protected function ‪buildQuotedTable(Table $table): Table
1257  {
1258  $databasePlatform = $this->connection->getDatabasePlatform();
1259 
1260  return GeneralUtility::makeInstance(
1261  Table::class,
1262  $databasePlatform->quoteIdentifier($table->getName()),
1263  $table->getColumns(),
1264  $table->getIndexes(),
1265  $table->getForeignKeys(),
1266  0,
1267  $table->getOptions()
1268  );
1269  }
1270 
1280  protected function ‪buildQuotedColumn(Column $column): Column
1281  {
1282  $databasePlatform = $this->connection->getDatabasePlatform();
1283 
1284  return GeneralUtility::makeInstance(
1285  Column::class,
1286  $databasePlatform->quoteIdentifier($column->getName()),
1287  $column->getType(),
1288  array_diff_key($column->toArray(), ['name', 'type'])
1289  );
1290  }
1291 
1301  protected function ‪buildQuotedIndex(Index $index): Index
1302  {
1303  $databasePlatform = $this->connection->getDatabasePlatform();
1304 
1305  return GeneralUtility::makeInstance(
1306  Index::class,
1307  $databasePlatform->quoteIdentifier($index->getName()),
1308  $index->getColumns(),
1309  $index->isUnique(),
1310  $index->isPrimary(),
1311  $index->getFlags(),
1312  $index->getOptions()
1313  );
1314  }
1315 
1325  protected function ‪buildQuotedForeignKey(ForeignKeyConstraint $index): ForeignKeyConstraint
1326  {
1327  $databasePlatform = $this->connection->getDatabasePlatform();
1328 
1329  return GeneralUtility::makeInstance(
1330  ForeignKeyConstraint::class,
1331  $index->getLocalColumns(),
1332  $databasePlatform->quoteIdentifier($index->getForeignTableName()),
1333  $index->getForeignColumns(),
1334  $databasePlatform->quoteIdentifier($index->getName()),
1335  $index->getOptions()
1336  );
1337  }
1338 
1339  protected function ‪getDatabasePlatform(string $tableName): string
1340  {
1341  $databasePlatform = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName)->getDatabasePlatform();
1342  if ($databasePlatform instanceof PostgreSqlPlatform) {
1343  return 'postgresql';
1344  }
1345  if ($databasePlatform instanceof SQLServerPlatform) {
1346  return 'mssql';
1347  }
1348  if ($databasePlatform instanceof SqlitePlatform) {
1349  return 'sqlite';
1350  }
1351 
1352  return 'mysql';
1353  }
1354 }
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\buildQuotedColumn
‪Doctrine DBAL Schema Column buildQuotedColumn(Column $column)
Definition: ConnectionMigrator.php:1276
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\create
‪static ConnectionMigrator create(string $connectionName, array $tables)
Definition: ConnectionMigrator.php:78
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\migrateUnprefixedRemovedTablesToRenames
‪Doctrine DBAL Schema SchemaDiff migrateUnprefixedRemovedTablesToRenames(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:918
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\buildQuotedTable
‪Doctrine DBAL Schema Table buildQuotedTable(Table $table)
Definition: ConnectionMigrator.php:1252
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\buildQuotedIndex
‪Doctrine DBAL Schema Index buildQuotedIndex(Index $index)
Definition: ConnectionMigrator.php:1297
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getUnusedFieldUpdateSuggestions
‪array getUnusedFieldUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:707
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\buildExpectedSchemaDefinitions
‪Doctrine DBAL Schema Schema buildExpectedSchemaDefinitions(string $connectionName)
Definition: ConnectionMigrator.php:278
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getUpdateSuggestions
‪array getUpdateSuggestions(bool $remove=false)
Definition: ConnectionMigrator.php:105
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\install
‪array install(bool $createOnly=false)
Definition: ConnectionMigrator.php:135
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getUnusedTableUpdateSuggestions
‪array getUnusedTableUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:665
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\calculateUpdateSuggestionsHashes
‪string[] calculateUpdateSuggestionsHashes(array $statements)
Definition: ConnectionMigrator.php:1084
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getSchemaDiff
‪SchemaDiff getSchemaDiff()
Definition: ConnectionMigrator.php:93
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getChangedFieldUpdateSuggestions
‪array getChangedFieldUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:491
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\removeUnrelatedTables
‪Doctrine DBAL Schema TableDiff[] removeUnrelatedTables(array $tableDiffs, array $validTableNames)
Definition: ConnectionMigrator.php:1097
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\transformTablesForDatabasePlatform
‪Table[] transformTablesForDatabasePlatform(array $tables, Connection $connection)
Definition: ConnectionMigrator.php:1130
‪TYPO3\CMS\Core\Database\Connection\quoteIdentifier
‪string quoteIdentifier($identifier)
Definition: Connection.php:133
‪TYPO3\CMS\Core\Database\Schema
Definition: Comparator.php:18
‪TYPO3\CMS\Core\Database\ConnectionPool\DEFAULT_CONNECTION_NAME
‪const DEFAULT_CONNECTION_NAME
Definition: ConnectionPool.php:50
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getDatabasePlatform
‪getDatabasePlatform(string $tableName)
Definition: ConnectionMigrator.php:1335
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\$tables
‪Table[] $tables
Definition: ConnectionMigrator.php:59
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\buildSchemaDiff
‪Doctrine DBAL Schema SchemaDiff buildSchemaDiff(bool $renameUnused=true)
Definition: ConnectionMigrator.php:211
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator
Definition: ConnectionMigrator.php:44
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getTableOptions
‪array[] getTableOptions(array $tableNames)
Definition: ConnectionMigrator.php:1191
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getTableRecordCount
‪int getTableRecordCount(string $tableName)
Definition: ConnectionMigrator.php:1051
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\$deletedPrefix
‪string $deletedPrefix
Definition: ConnectionMigrator.php:47
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\buildQuotedForeignKey
‪Doctrine DBAL Schema ForeignKeyConstraint buildQuotedForeignKey(ForeignKeyConstraint $index)
Definition: ConnectionMigrator.php:1321
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getDropTableUpdateSuggestions
‪array getDropTableUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:879
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\migrateColumnRenamesToDistinctActions
‪SchemaDiff migrateColumnRenamesToDistinctActions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:1012
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:36
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getNewFieldUpdateSuggestions
‪array getNewFieldUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:354
‪$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:51
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getConnectionNameForTable
‪string getConnectionNameForTable(string $tableName)
Definition: ConnectionMigrator.php:1065
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation
Definition: PlatformInformation.php:33
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\__construct
‪__construct(string $connectionName, array $tables)
Definition: ConnectionMigrator.php:65
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getNewTableUpdateSuggestions
‪array getNewTableUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:329
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\$connectionName
‪string $connectionName
Definition: ConnectionMigrator.php:55
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getChangedTableOptions
‪array getChangedTableOptions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:441
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\getDropFieldUpdateSuggestions
‪array getDropFieldUpdateSuggestions(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:773
‪TYPO3\CMS\Core\Database\Schema\TableDiff
Definition: TableDiff.php:27
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation\getMaxIdentifierLength
‪static int getMaxIdentifierLength(AbstractPlatform $platform)
Definition: PlatformInformation.php:111
‪TYPO3\CMS\Core\Database\Schema\ConnectionMigrator\migrateUnprefixedRemovedFieldsToRenames
‪Doctrine DBAL Schema SchemaDiff migrateUnprefixedRemovedFieldsToRenames(SchemaDiff $schemaDiff)
Definition: ConnectionMigrator.php:959