From ffe5febfd625e1f31fe7c4b1bc5da0c808f12356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 11:04:52 +0100 Subject: [PATCH] fix: use LEFT JOIN for MySQL batch deletes in search FK migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback: replace NOT IN subquery with LEFT JOIN for MySQL batch deletes. Since MySQL doesn't support LIMIT in multi-table DELETE, orphan entity_ids are found via SELECT first, then deleted in a separate statement. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- ...20260214000000_search_fk_final_entities.js | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js index 917a4e36b2..1b483268b0 100644 --- a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -69,13 +69,21 @@ exports.up = async function up(knex) { ); } else if (client.includes('mysql')) { // Batch-delete orphaned rows before DDL to reduce lock time. + // Uses LEFT JOIN to find orphans efficiently, then deletes by entity_id. + // MySQL doesn't support LIMIT in multi-table DELETE, so we find the + // orphaned entity_ids first, then delete in a separate statement. for (;;) { - const [result] = await knex.raw( - `DELETE FROM \`search\` WHERE \`entity_id\` NOT IN (SELECT \`entity_id\` FROM \`final_entities\`) LIMIT 10000`, - ); - if (result.affectedRows === 0) { + const [orphanIds] = await knex.raw(` + SELECT DISTINCT s.\`entity_id\` FROM \`search\` s + LEFT JOIN \`final_entities\` fe ON s.\`entity_id\` = fe.\`entity_id\` + WHERE fe.\`entity_id\` IS NULL + LIMIT 10000 + `); + if (orphanIds.length === 0) { break; } + const ids = orphanIds.map(r => r.entity_id); + await knex('search').whereIn('entity_id', ids).delete(); } // Drop old FK and add new one. MySQL does not support NOT VALID, but @@ -148,12 +156,17 @@ exports.down = async function down(knex) { ); } else if (client.includes('mysql')) { for (;;) { - const [result] = await knex.raw( - `DELETE FROM \`search\` WHERE \`entity_id\` NOT IN (SELECT \`entity_id\` FROM \`refresh_state\`) LIMIT 10000`, - ); - if (result.affectedRows === 0) { + const [orphanIds] = await knex.raw(` + SELECT DISTINCT s.\`entity_id\` FROM \`search\` s + LEFT JOIN \`refresh_state\` rs ON s.\`entity_id\` = rs.\`entity_id\` + WHERE rs.\`entity_id\` IS NULL + LIMIT 10000 + `); + if (orphanIds.length === 0) { break; } + const ids = orphanIds.map(r => r.entity_id); + await knex('search').whereIn('entity_id', ids).delete(); } await knex.schema.alterTable('search', table => {