Files
paliad/internal/db/migrations/077_projects_counterclaim_of.up.sql
m 306bb11618 feat(t-paliad-174): SmartTimeline Slice 3 — counterclaim sub-project schema + service
Migration 077 adds paliad.projects.counterclaim_of (nullable FK ON DELETE
SET NULL) plus a partial index. A trigger function rejects two-level CCR
chains: a project with counterclaim_of NOT NULL cannot be the target of
another CCR — UPC practice has no CCR-of-a-CCR shape, so reject it at
the schema level rather than defending in the application layer.

ProjectService gains LoadCounterclaimChildrenVisible (list visible CCR
sub-projects against a parent) and CreateCounterclaim (atomic: project
row + creator-as-lead team membership + audit rows on parent AND child).
The CCR child is placed as a sibling under the same patent (§4.4), our
side flips claimant↔defendant by default with a "Stimmt nicht?" override
for the R.49.2.b CCI edge case, and the proceeding type defaults to
UPC_REV. Title auto-suggests from the patent ancestor's patent_number
when available.

Tracker advances 76 → 77.
2026-05-09 16:07:17 +02:00

90 lines
3.8 KiB
PL/PgSQL

-- 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();