From a01e1524f5394dce9e8bdd36fc510a34690bd031 Mon Sep 17 00:00:00 2001 From: Joeri Colman Date: Tue, 22 Sep 2026 08:32:11 +0200 Subject: [PATCH 1/5] Add sorting function for tables by foreign key dependencies Signed-off-by: Joeri Colman --- core/Command/Db/ConvertType.php | 96 +++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/core/Command/Db/ConvertType.php b/core/Command/Db/ConvertType.php index f91949fe92afb..0b2575642f2b3 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); } @@ -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 $reverse Reverse the sorted tables for dropping tables + * @return array Tables in dependency order + */ + protected function sortTablesByForeignKeys(Connection $connection, array $tables, bool $reverse = 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) { + $parent = $foreignKey->getForeignTableName(); + + // Ignore references to tables which aren't being converted. + if (!isset($tableSet[$parent])) { + continue; + } + + // Ignore self-references. They don't impose an ordering + // requirement on the table itself. + if ($parent === $table) { + continue; + } + + $dependencies[$table][$parent] = true; + $dependents[$parent][$table] = true; + } + } + + /* + * Kahn's topological sort. + * + * Tables without dependencies can be copied immediately. + */ + $ready = []; + + foreach ($tables as $table) { + if ($dependencies[$table] === []) { + $ready[] = $table; + } + } + + $result = []; + + while ($ready !== []) { + $table = array_shift($ready); + $result[] = $table; + + foreach ($dependents[$table] as $dependent => $_) { + unset($dependencies[$dependent][$table]); + + if ($dependencies[$dependent] === []) { + $ready[] = $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($result) !== count($tables)) { + $remaining = array_diff($tables, $result); + $result = array_merge($result, $remaining); + } + + if ($reverse) { + $result = array_reverse($result); + } + + return $result; + } + protected function convertDB(Connection $fromDB, Connection $toDB, array $tables, InputInterface $input, OutputInterface $output) { $this->config->setSystemValue('maintenance', true); $schema = $fromDB->createSchema(); From e1ad32cb88a84c7be67b57fe2574ae8d7b94555d Mon Sep 17 00:00:00 2001 From: Joeri Colman Date: Tue, 22 Sep 2026 09:51:29 +0200 Subject: [PATCH 2/5] Small formatting Co-authored-by: Kate <26026535+provokateurin@users.noreply.github.com> Signed-off-by: Joeri Colman --- core/Command/Db/ConvertType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Command/Db/ConvertType.php b/core/Command/Db/ConvertType.php index 0b2575642f2b3..88300824d2baa 100644 --- a/core/Command/Db/ConvertType.php +++ b/core/Command/Db/ConvertType.php @@ -200,7 +200,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } $intersectingTables = array_intersect($toTables, $fromTables); - $intersectingTables = $this->sortTablesByForeignKeys( $toDB, $intersectingTables); + $intersectingTables = $this->sortTablesByForeignKeys($toDB, $intersectingTables); $this->convertDB($fromDB, $toDB, $intersectingTables, $input, $output); return 0; From 0e28aa3e0a315aeb2f747b84f79e8b46020c292d Mon Sep 17 00:00:00 2001 From: Joeri Colman Date: Thu, 24 Sep 2026 16:35:21 +0200 Subject: [PATCH 3/5] Refactor sortTablesByForeignKeys parameters and logic Signed-off-by: Joeri Colman --- core/Command/Db/ConvertType.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/core/Command/Db/ConvertType.php b/core/Command/Db/ConvertType.php index 88300824d2baa..10970d44ef6ba 100644 --- a/core/Command/Db/ConvertType.php +++ b/core/Command/Db/ConvertType.php @@ -414,10 +414,10 @@ protected function getColumnType(Table $table, $columnName) { * * @param Connection $connection Target database connection * @param array $tables Tables to sort - * @param bool $reverse Reverse the sorted tables for dropping tables + * @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 $reverse = false): array { + 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 @@ -430,21 +430,21 @@ protected function sortTablesByForeignKeys(Connection $connection, array $tables foreach ($tables as $table) { foreach ($schemaManager->listTableForeignKeys($table) as $foreignKey) { - $parent = $foreignKey->getForeignTableName(); + $foreignTable = $foreignKey->getForeignTableName(); // Ignore references to tables which aren't being converted. - if (!isset($tableSet[$parent])) { + if (!isset($tableSet[$foreignTable])) { continue; } // Ignore self-references. They don't impose an ordering // requirement on the table itself. - if ($parent === $table) { + if ($foreignTable === $table) { continue; } - $dependencies[$table][$parent] = true; - $dependents[$parent][$table] = true; + $dependencies[$table][$foreignTable] = true; + $dependents[$foreignTable][$table] = true; } } @@ -453,17 +453,17 @@ protected function sortTablesByForeignKeys(Connection $connection, array $tables * * Tables without dependencies can be copied immediately. */ - $ready = []; + $readyTables = []; foreach ($tables as $table) { if ($dependencies[$table] === []) { - $ready[] = $table; + $readyTables[] = $table; } } $result = []; - while ($ready !== []) { + while ($readyTables !== []) { $table = array_shift($ready); $result[] = $table; @@ -471,7 +471,7 @@ protected function sortTablesByForeignKeys(Connection $connection, array $tables unset($dependencies[$dependent][$table]); if ($dependencies[$dependent] === []) { - $ready[] = $dependent; + $readyTables[] = $dependent; } } } @@ -489,7 +489,7 @@ protected function sortTablesByForeignKeys(Connection $connection, array $tables $result = array_merge($result, $remaining); } - if ($reverse) { + if ($dependenciesFirst) { $result = array_reverse($result); } From dbfe336fff2cd8e72fe62879525bbc5d4d9a4d8d Mon Sep 17 00:00:00 2001 From: Joeri Colman Date: Thu, 24 Sep 2026 16:37:12 +0200 Subject: [PATCH 4/5] Refactor result handling to use sortedTables Signed-off-by: Joeri Colman --- core/Command/Db/ConvertType.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/Command/Db/ConvertType.php b/core/Command/Db/ConvertType.php index 10970d44ef6ba..5b3f7ab471cdc 100644 --- a/core/Command/Db/ConvertType.php +++ b/core/Command/Db/ConvertType.php @@ -345,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(); @@ -461,11 +461,11 @@ protected function sortTablesByForeignKeys(Connection $connection, array $tables } } - $result = []; + $sortedTables = []; while ($readyTables !== []) { $table = array_shift($ready); - $result[] = $table; + $sortedTables[] = $table; foreach ($dependents[$table] as $dependent => $_) { unset($dependencies[$dependent][$table]); @@ -484,16 +484,16 @@ protected function sortTablesByForeignKeys(Connection $connection, array $tables * the conversion, but the failure will accurately expose the * cyclic dependency rather than being hidden by this sorter. */ - if (count($result) !== count($tables)) { - $remaining = array_diff($tables, $result); - $result = array_merge($result, $remaining); + if (count($sortedTables) !== count($tables)) { + $remaining = array_diff($tables, $sortedTables); + $sortedTables = array_merge($sortedTables, $remaining); } if ($dependenciesFirst) { - $result = array_reverse($result); + $sortedTables = array_reverse($sortedTables); } - return $result; + return $sortedTables; } protected function convertDB(Connection $fromDB, Connection $toDB, array $tables, InputInterface $input, OutputInterface $output) { From de52d6440ac86778c7c4d73aebab1c2d7da0c91d Mon Sep 17 00:00:00 2001 From: Joeri Colman Date: Thu, 24 Sep 2026 16:47:03 +0200 Subject: [PATCH 5/5] Update core/Command/Db/ConvertType.php Co-authored-by: Kate <26026535+provokateurin@users.noreply.github.com> Signed-off-by: Joeri Colman --- core/Command/Db/ConvertType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Command/Db/ConvertType.php b/core/Command/Db/ConvertType.php index 5b3f7ab471cdc..355d258a63f9f 100644 --- a/core/Command/Db/ConvertType.php +++ b/core/Command/Db/ConvertType.php @@ -467,7 +467,7 @@ protected function sortTablesByForeignKeys(Connection $connection, array $tables $table = array_shift($ready); $sortedTables[] = $table; - foreach ($dependents[$table] as $dependent => $_) { + foreach (array_keys($dependents[$table]) as $dependent) { unset($dependencies[$dependent][$table]); if ($dependencies[$dependent] === []) {