-- 135_primary_party_check — Slice B3, m/paliad#124 §18.3 -- -- Tightens paliad.deadline_rules.primary_party from free-text to a -- CHECK constraint over the canonical four-value vocabulary -- (claimant / defendant / court / both). NULL stays valid for the -- 78 cross-cutting orphan concept seeds (Wiedereinsetzung, -- Versäumnisurteil-Einspruch, Schriftsatznachreichung, -- Weiterbehandlung) — they have no proceeding_type_id binding so -- they're outside the calculator's path; loosening the CHECK to -- "IS NULL OR IN (…)" keeps them valid without backfill gymnastics. -- -- Audit-first: the DO block RAISEs NOTICE for every non-conforming -- row before adding the CHECK, and RAISEs EXCEPTION if any dirty -- rows are found so the operator can decide a manual cleanup path. -- Live audit (Supabase, 2026-05-26 §18.0) confirmed zero dirty rows -- on the current corpus: 26 claimant + 26 defendant + 38 court + -- 63 both + 78 NULL = 231 total, all in the canonical vocab. The -- audit pass stays in the migration for safety against future drift -- (e.g. a rule editor write that bypassed the application-layer -- validation hook this slice also adds). DO $$ DECLARE rec record; dirty_count int := 0; BEGIN RAISE NOTICE '[mig 135] primary_party audit pass — non-conforming rows:'; FOR rec IN SELECT dr.id, dr.name, dr.primary_party, pt.code AS proceeding_code FROM paliad.deadline_rules dr LEFT JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id WHERE dr.is_active = true AND dr.primary_party IS NOT NULL AND dr.primary_party NOT IN ('claimant', 'defendant', 'court', 'both') ORDER BY pt.code NULLS LAST, dr.name LOOP RAISE NOTICE '[mig 135] % % primary_party=% (rule=%)', COALESCE(rec.proceeding_code, ''), rec.name, rec.primary_party, rec.id; dirty_count := dirty_count + 1; END LOOP; IF dirty_count > 0 THEN RAISE EXCEPTION '[mig 135] FAILED — % rule(s) carry non-canonical primary_party values. ' 'Manual cleanup required: update each row to one of ' '''claimant'', ''defendant'', ''court'', ''both'', or NULL. ' 'See the NOTICE lines above for the offending rows.', dirty_count; END IF; RAISE NOTICE '[mig 135] audit clean — proceeding with CHECK constraint'; END $$; -- --------------------------------------------------------------- -- Add the CHECK constraint. NULL stays valid; the four canonical -- values are the only allowed non-NULL forms. -- --------------------------------------------------------------- ALTER TABLE paliad.deadline_rules ADD CONSTRAINT deadline_rules_primary_party_chk CHECK ( primary_party IS NULL OR primary_party IN ('claimant', 'defendant', 'court', 'both') ); COMMENT ON CONSTRAINT deadline_rules_primary_party_chk ON paliad.deadline_rules IS 'Slice B3 (mig 135, m/paliad#124 §18.3) — canonical four-value ' 'vocab for primary_party (claimant / defendant / court / both). ' 'NULL allowed for cross-cutting orphan concept seeds (78 rows in ' 'live corpus as of mig 135). See pkg/litigationplanner.PrimaryParties ' 'for the in-code vocabulary.'; -- --------------------------------------------------------------- -- Post-migration distribution check — informational NOTICE only. -- --------------------------------------------------------------- DO $$ DECLARE rec record; BEGIN RAISE NOTICE '[mig 135] post: primary_party distribution after constraint add:'; FOR rec IN SELECT COALESCE(primary_party, '') AS party, COUNT(*) AS n FROM paliad.deadline_rules WHERE is_active = true GROUP BY primary_party ORDER BY party LOOP RAISE NOTICE '[mig 135] % count=%', rec.party, rec.n; END LOOP; END $$;