-- t-paliad-174 — SmartTimeline Slice 3. -- Two structural additions for the counterclaim sub-project shape -- (§4 of docs/design-smart-timeline-2026-05-08.md): -- -- 1. paliad.projects.counterclaim_of — nullable FK referencing -- paliad.projects(id) ON DELETE SET NULL. When non-NULL the row -- represents the CCR (counterclaim) sub-project filed against the -- target row. Standard parent_id keeps governing the project tree; -- counterclaim_of is the *additional* relation describing the CCR -- link. parent_id of the CCR child is set to the target's parent -- (sibling-under-patent placement, §4.4) — that placement is owned -- by ProjectService.CreateCounterclaim, not the schema. -- -- 2. Two-level-CCR rejection trigger — UPC practice does NOT have -- counterclaim-of-a-counterclaim chains. Reject the malformed shape -- at the schema level so the application can never write it. CHECK -- can't reference other rows; trigger function raises explicitly. -- -- Idempotent: re-applying is a no-op. Tracker advances 76 → 77. -- 1. paliad.projects.counterclaim_of --------------------------------------- ALTER TABLE paliad.projects ADD COLUMN IF NOT EXISTS counterclaim_of uuid NULL REFERENCES paliad.projects(id) ON DELETE SET NULL; COMMENT ON COLUMN paliad.projects.counterclaim_of IS 'When non-NULL this project is the CCR (counterclaim) filed against ' 'the referenced parent project. parent_id continues to govern the ' 'project tree (CCR is placed as a sibling under the same patent — ' 'see ProjectService.CreateCounterclaim). ON DELETE SET NULL keeps ' 'the CCR row alive when the parent is hard-deleted (rare; default ' 'is archival) so the audit trail survives.'; CREATE INDEX IF NOT EXISTS projects_counterclaim_of_idx ON paliad.projects (counterclaim_of) WHERE counterclaim_of IS NOT NULL; -- 2. Two-level-CCR rejection trigger --------------------------------------- CREATE OR REPLACE FUNCTION paliad.projects_no_two_level_ccr() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN -- A project that is itself a CCR may NOT be the target of another CCR. -- Two cases to reject: -- -- (a) NEW row points at a parent that is itself a CCR: -- NEW.counterclaim_of -> some row with counterclaim_of NOT NULL. -- -- (b) NEW row claims to be a CCR (NEW.counterclaim_of IS NOT NULL) -- but already has another CCR pointing AT it (NEW.id is the -- target of some other row's counterclaim_of). The cleaner -- phrasing: "no row may simultaneously have a CCR child AND -- a CCR parent". IF NEW.counterclaim_of IS NOT NULL THEN IF EXISTS ( SELECT 1 FROM paliad.projects p WHERE p.id = NEW.counterclaim_of AND p.counterclaim_of IS NOT NULL ) THEN RAISE EXCEPTION 'two-level counterclaim chains are not allowed: parent project % is itself a counterclaim', NEW.counterclaim_of; END IF; IF EXISTS ( SELECT 1 FROM paliad.projects p WHERE p.counterclaim_of = NEW.id ) THEN RAISE EXCEPTION 'project % already has a counterclaim child and cannot itself be a counterclaim', NEW.id; END IF; END IF; RETURN NEW; END; $$; COMMENT ON FUNCTION paliad.projects_no_two_level_ccr() IS 'Rejects two-level counterclaim chains. UPC practice does not have ' 'CCR-of-a-CCR; reject the malformed shape at write time so the app ' 'layer never has to defend against it. See migration 077.'; DROP TRIGGER IF EXISTS projects_no_two_level_ccr ON paliad.projects; CREATE TRIGGER projects_no_two_level_ccr BEFORE INSERT OR UPDATE OF counterclaim_of ON paliad.projects FOR EACH ROW EXECUTE FUNCTION paliad.projects_no_two_level_ccr();