Files
projax/db/migrations/0013_orphan_item_links_cleanup.sql
mAi 0e490bb600 feat(phase 3d auto-tag): backfill area tags, bulk-edit UI, soft-delete cleanup
- migration 0012: one-shot populate empty tags from each item's area-roots
  (so chips on /?tag=work etc. actually filter the 40+ mai-backfilled rows)
- migration 0013: cleanup 12 orphan item_links + BEFORE-UPDATE trigger that
  cascades soft-delete to item_links going forward — closes the data drift
  that made TestItemsUnifiedSurfacesMaiPointer fail since 3c
- /admin/bulk page: flat filter+checkbox list with one-tx Apply for add/
  remove tag, set management, set status. Per-row inline chip add/remove
  via /admin/bulk/chip. Reuses tree_filter URL params 1:1.
- design.md §3.2 + §4.1 updated; tag+management section notes 0012
- bulk + tag-backfill + soft-delete-cascade tests cover the new surface
2026-05-15 18:49:58 +02:00

51 lines
2.2 KiB
PL/PgSQL

-- 0013_orphan_item_links_cleanup.sql
--
-- Two-step fix for the orphan-link drift that was masking source_ref_id
-- counts in the items_unified view:
--
-- 1. One-shot cleanup: every projax.item_links row whose item_id references
-- a soft-deleted projax.items row is removed. There are ~12 of these in
-- production, all leftover 'mai-project' pointers from items that were
-- soft-deleted before 0008's sync trigger covered every path.
--
-- 2. Going-forward trigger: when projax.items.deleted_at flips NULL → non-NULL
-- (i.e. a soft-delete), drop every item_links row that points at the
-- item in the same statement. This keeps `count(item_links where ref_type=X)`
-- and `count(items_unified where source_ref_id is not null)` in lock-step
-- for every future soft-delete, without taxing the items_unified view
-- with a JOIN.
--
-- Rationale for the trigger approach over a view-side JOIN:
-- The view is on the hot path (every tree-page render). A trigger
-- pays the cost once at delete time, not once per read. And the alternative
-- (orphan links lingering) is itself a correctness bug — a CalDAV/Gitea
-- integration that follows the link from items_unified -> external system
-- would try to resolve a pointer for an item that no longer exists.
--
-- The trigger is BEFORE UPDATE so the DELETE on item_links happens inside the
-- same transaction as the soft-delete UPDATE — rollback safety preserved.
-- 1. One-shot cleanup of existing orphan link rows.
DELETE FROM projax.item_links l
USING projax.items i
WHERE i.id = l.item_id
AND i.deleted_at IS NOT NULL;
-- 2. Trigger function: cascade soft-delete to item_links.
CREATE OR REPLACE FUNCTION projax.items_cascade_softdelete_links()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL THEN
DELETE FROM projax.item_links WHERE item_id = NEW.id;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS items_cascade_softdelete_links ON projax.items;
CREATE TRIGGER items_cascade_softdelete_links
BEFORE UPDATE OF deleted_at ON projax.items
FOR EACH ROW EXECUTE FUNCTION projax.items_cascade_softdelete_links();