diff --git a/core/Command/Db/ConvertType.php b/core/Command/Db/ConvertType.php
index f91949fe92afb..355d258a63f9f 100644
--- a/core/Command/Db/ConvertType.php
+++ b/core/Command/Db/ConvertType.php
@@ -200,6 +200,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
}
$intersectingTables = array_intersect($toTables, $fromTables);
+ $intersectingTables = $this->sortTablesByForeignKeys($toDB, $intersectingTables);
+
$this->convertDB($fromDB, $toDB, $intersectingTables, $input, $output);
return 0;
}
@@ -265,6 +267,9 @@ protected function clearSchema(Connection $db, InputInterface $input, OutputInte
if (!empty($toTables)) {
$output->writeln('Clearing schema in new database');
}
+
+ $toTables = $this->sortTablesByForeignKeys($db, $toTables, true);
+
foreach ($toTables as $table) {
$db->createSchemaManager()->dropTable($table);
}
@@ -340,7 +345,7 @@ protected function copyTable(Connection $fromDB, Connection $toDB, Table $table,
for ($chunk = 0; $chunk < $numChunks; $chunk++) {
$query->setFirstResult($chunk * $chunkSize);
- $result = $query->executeQuery();
+ = $query->executeQuery();
try {
$toDB->beginTransaction();
@@ -400,6 +405,97 @@ protected function getColumnType(Table $table, $columnName) {
return $this->columnTypes[$tableName][$columnName];
}
+ /**
+ * Sort tables so that tables referenced by foreign keys are copied
+ * before the tables containing those foreign keys.
+ *
+ * The dependency information is obtained from the target database,
+ * making this independent of the source/target database vendor.
+ *
+ * @param Connection $connection Target database connection
+ * @param array $tables Tables to sort
+ * @param bool $dependenciesFirst Whether to place FK dependencies before dependent tables
+ * @return array Tables in dependency order
+ */
+ protected function sortTablesByForeignKeys(Connection $connection, array $tables, bool $dependenciesFirst = false): array {
+ $tableSet = array_fill_keys($tables, true);
+
+ // dependencies[table] = tables that must be copied before it
+ $dependencies = array_fill_keys($tables, []);
+
+ // dependents[table] = tables that depend on it
+ $dependents = array_fill_keys($tables, []);
+
+ $schemaManager = $connection->createSchemaManager();
+
+ foreach ($tables as $table) {
+ foreach ($schemaManager->listTableForeignKeys($table) as $foreignKey) {
+ $foreignTable = $foreignKey->getForeignTableName();
+
+ // Ignore references to tables which aren't being converted.
+ if (!isset($tableSet[$foreignTable])) {
+ continue;
+ }
+
+ // Ignore self-references. They don't impose an ordering
+ // requirement on the table itself.
+ if ($foreignTable === $table) {
+ continue;
+ }
+
+ $dependencies[$table][$foreignTable] = true;
+ $dependents[$foreignTable][$table] = true;
+ }
+ }
+
+ /*
+ * Kahn's topological sort.
+ *
+ * Tables without dependencies can be copied immediately.
+ */
+ $readyTables = [];
+
+ foreach ($tables as $table) {
+ if ($dependencies[$table] === []) {
+ $readyTables[] = $table;
+ }
+ }
+
+ $sortedTables = [];
+
+ while ($readyTables !== []) {
+ $table = array_shift($ready);
+ $sortedTables[] = $table;
+
+ foreach (array_keys($dependents[$table]) as $dependent) {
+ unset($dependencies[$dependent][$table]);
+
+ if ($dependencies[$dependent] === []) {
+ $readyTables[] = $dependent;
+ }
+ }
+ }
+
+ /*
+ * A cycle means there is no valid topological ordering.
+ *
+ * Don't silently produce an invalid ordering. Keep the original
+ * order for the remaining tables; PostgreSQL may still reject
+ * the conversion, but the failure will accurately expose the
+ * cyclic dependency rather than being hidden by this sorter.
+ */
+ if (count($sortedTables) !== count($tables)) {
+ $remaining = array_diff($tables, $sortedTables);
+ $sortedTables = array_merge($sortedTables, $remaining);
+ }
+
+ if ($dependenciesFirst) {
+ $sortedTables = array_reverse($sortedTables);
+ }
+
+ return $sortedTables;
+ }
+
protected function convertDB(Connection $fromDB, Connection $toDB, array $tables, InputInterface $input, OutputInterface $output) {
$this->config->setSystemValue('maintenance', true);
$schema = $fromDB->createSchema();