- 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
51 lines
2.2 KiB
PL/PgSQL
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();
|