-- 007_remove_duplicate_recipes_by_name.sql -- Removes duplicate recipes that share the same "Name" (trimmed, exact match). -- Keeps the row with the lowest "Id" for each name. -- -- Analysis of Recipes-2.csv export (196 rows): -- 168 unique names, 28 duplicate groups, 28 rows to delete (Ids 113–140). -- Those duplicate Ids mirror Ids 29–56 one-to-one. -- -- Run inside a transaction. Review the preview queries before COMMIT. BEGIN; -- --------------------------------------------------------------------------- -- 1) Preview duplicates -- --------------------------------------------------------------------------- -- SELECT -- r."Name", -- array_agg(r."Id" ORDER BY r."Id") AS ids, -- min(r."Id") AS keep_id, -- array_agg(r."Id" ORDER BY r."Id") FILTER (WHERE r."Id" <> min(r."Id")) AS remove_ids -- FROM diet."Recipes" r -- GROUP BY r."Name" -- HAVING count(*) > 1 -- ORDER BY min(r."Id"); -- --------------------------------------------------------------------------- -- 2) Build duplicate map: remove_id -> keep_id -- --------------------------------------------------------------------------- CREATE TEMP TABLE recipe_dedup_map ON COMMIT DROP AS SELECT r."Id" AS remove_id, d.keep_id FROM diet."Recipes" r INNER JOIN ( SELECT btrim("Name") AS name_key, MIN("Id") AS keep_id FROM diet."Recipes" GROUP BY btrim("Name") HAVING COUNT(*) > 1 ) d ON btrim(r."Name") = d.name_key WHERE r."Id" <> d.keep_id; -- Optional: restrict to known duplicate ids from export -- DELETE FROM recipe_dedup_map WHERE remove_id NOT BETWEEN 113 AND 140; DO $$ DECLARE dup_count int; BEGIN SELECT count(*) INTO dup_count FROM recipe_dedup_map; RAISE NOTICE 'Duplicate recipe rows to remove: %', dup_count; END $$; -- --------------------------------------------------------------------------- -- 3) Re-point foreign keys to the canonical (lowest-id) recipe -- --------------------------------------------------------------------------- -- Meal planner / diet generator drafts UPDATE diet."MealPlanEntries" e SET "RecipeId" = m.keep_id, "UpdatedAt" = now() FROM recipe_dedup_map m WHERE e."RecipeId" = m.remove_id; UPDATE diet."DietGeneratorDraftMeals" d SET "RecipeId" = m.keep_id FROM recipe_dedup_map m WHERE d."RecipeId" = m.remove_id; -- AI recipe generator saved draft pointer UPDATE diet."RecipeGeneratorSessions" s SET "SavedRecipeId" = m.keep_id, "UpdatedAt" = now() FROM recipe_dedup_map m WHERE s."SavedRecipeId" = m.remove_id; -- Diet tracking (nullable FK) UPDATE diet."MealConsumptions" c SET "RecipeId" = m.keep_id FROM recipe_dedup_map m WHERE c."RecipeId" = m.remove_id; -- Favorites: drop rows that would collide after merge, then re-point the rest DELETE FROM diet."UserFavoriteRecipes" f USING recipe_dedup_map m WHERE f."RecipeId" = m.remove_id AND EXISTS ( SELECT 1 FROM diet."UserFavoriteRecipes" f2 WHERE f2."UserId" = f."UserId" AND f2."RecipeId" = m.keep_id ); UPDATE diet."UserFavoriteRecipes" f SET "RecipeId" = m.keep_id FROM recipe_dedup_map m WHERE f."RecipeId" = m.remove_id; -- --------------------------------------------------------------------------- -- 4) Delete duplicate recipes (Ingredients + RecipeSteps cascade) -- --------------------------------------------------------------------------- DELETE FROM diet."Recipes" r USING recipe_dedup_map m WHERE r."Id" = m.remove_id; -- --------------------------------------------------------------------------- -- 5) Verify -- --------------------------------------------------------------------------- DO $$ DECLARE remaining_dup_names int; recipe_count int; BEGIN SELECT count(*) INTO remaining_dup_names FROM ( SELECT btrim("Name") FROM diet."Recipes" GROUP BY btrim("Name") HAVING count(*) > 1 ) d; SELECT count(*) INTO recipe_count FROM diet."Recipes"; IF remaining_dup_names > 0 THEN RAISE EXCEPTION 'Duplicate recipe names still present: % groups', remaining_dup_names; END IF; RAISE NOTICE 'Done. Active recipe count: %', recipe_count; END $$; COMMIT; -- Optional hardening (uncomment if every recipe name must be unique going forward): -- CREATE UNIQUE INDEX IF NOT EXISTS "UX_Recipes_Name" -- ON diet."Recipes" (btrim("Name")) -- WHERE "IsActive" AND NOT "IsDraft";