From 65364716da916c9724523d1eb5b87aa13277d178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 11:53:28 +0100 Subject: [PATCH] fix: add NULL safety and idempotent DROP CONSTRAINT to search FK migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent orphan cleanup queries from incorrectly matching rows with NULL entity_id via LEFT JOIN, and use DROP CONSTRAINT IF EXISTS for safer partial re-runs given transaction: false. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- .../migrations/20260214000000_search_fk_final_entities.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 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 9c90567dfc..6c2136de4d 100644 --- a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -41,6 +41,7 @@ exports.up = async function up(knex) { SELECT s.ctid FROM "search" s LEFT JOIN "final_entities" fe ON s."entity_id" = fe."entity_id" WHERE fe."entity_id" IS NULL + AND s."entity_id" IS NOT NULL LIMIT 10000 ) `); @@ -52,7 +53,7 @@ exports.up = async function up(knex) { // Drop old FK and add new one with NOT VALID (minimal lock time). // NOT VALID skips the full table scan — we already cleaned up orphans above. await knex.raw( - `ALTER TABLE "search" DROP CONSTRAINT "search_entity_id_foreign"`, + `ALTER TABLE "search" DROP CONSTRAINT IF EXISTS "search_entity_id_foreign"`, ); await knex.raw(` ALTER TABLE "search" @@ -77,6 +78,7 @@ exports.up = async function up(knex) { 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 + AND s.\`entity_id\` IS NOT NULL LIMIT 10000 `); if (orphanIds.length === 0) { @@ -134,6 +136,7 @@ exports.down = async function down(knex) { SELECT s.ctid FROM "search" s LEFT JOIN "refresh_state" rs ON s."entity_id" = rs."entity_id" WHERE rs."entity_id" IS NULL + AND s."entity_id" IS NOT NULL LIMIT 10000 ) `); @@ -143,7 +146,7 @@ exports.down = async function down(knex) { } await knex.raw( - `ALTER TABLE "search" DROP CONSTRAINT "search_entity_id_foreign"`, + `ALTER TABLE "search" DROP CONSTRAINT IF EXISTS "search_entity_id_foreign"`, ); await knex.raw(` ALTER TABLE "search" @@ -162,6 +165,7 @@ exports.down = async function down(knex) { 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 + AND s.\`entity_id\` IS NOT NULL LIMIT 10000 `); if (orphanIds.length === 0) {