-- t-paliad-184 / Fristen Phase 3 Slice 3 Step C — data-move 77 rows -- from paliad.event_deadlines → paliad.deadline_rules so the Phase-3 -- unified backend can serve both pipelines. -- -- Source rows are PRESERVED (mig 086's read-only trigger blocks -- further writes; mig 090 in Slice 9 drops the table once every -- caller has cut over). The data-move is one-way; legacy callers -- continue reading event_deadlines via plain SELECTs until Slice 9. -- -- Mapping (per design §3.C): -- -- paliad.event_deadlines → paliad.deadline_rules -- ------------------------- ---------------------- -- id (new gen_random_uuid()) -- trigger_event_id trigger_event_id (Phase 3 column from mig 078) -- title (EN, NOT NULL) name_en (NOT NULL) -- title_de (DE, NOT NULL DEFAULT '') name (NOT NULL — every row has non-empty title_de in live data) -- duration_value duration_value -- duration_unit (days/weeks/months/working_days) duration_unit -- timing (before/after) timing -- notes (DE) deadline_notes (DE) -- notes_en (EN, nullable) deadline_notes_en (EN, nullable) -- alt_duration_value alt_duration_value -- alt_duration_unit alt_duration_unit -- combine_op (max/min, nullable) combine_op (Phase 3 column from mig 078) -- legal_source legal_source -- is_active is_active -- created_at published_at (preserves chronology — lifecycle_state='published' on every row) -- updated_at = now() (this is the publish event) -- -- Pipeline-A-only fields default: -- proceeding_type_id = NULL (event-rooted, no proceeding) -- parent_id = NULL (Pipeline C is flat, no chain) -- spawn_proceeding_type_id = NULL (no spawn) -- code = NULL (no local rule code in Pipeline C) -- primary_party = NULL (event_deadlines has no party column) -- event_type = NULL (filing/hearing/decision is a -- Pipeline-A category) -- is_court_set = false (no court-set Pipeline-C rules -- in the corpus; legal-review -- pass can flip Zustellung-* if -- those ever land here) -- is_spawn = false -- is_mandatory = true (Pipeline C has no mandatory -- bool; design §2.3 says default -- 'mandatory' is correct for -- statutory event-driven deadlines) -- is_optional = false -- priority = 'mandatory' -- condition_expr = NULL (Pipeline C has no flag gating) -- condition_flag = NULL -- sequence_order = 1000 + event_deadlines.id -- (large offset so Pipeline-C -- rows sort AFTER any future -- hand-edited Pipeline-A -- sequence_orders without -- colliding with the -- existing 0–171 range) -- lifecycle_state = 'published' -- -- Idempotency: WHERE NOT EXISTS guard on (trigger_event_id, name) skips -- rows that already exist in deadline_rules. Re-running the migration -- is a no-op. -- -- Hard assertion at end: COUNT(deadline_rules WHERE trigger_event_id -- IS NOT NULL) == COUNT(event_deadlines WHERE is_active = true) (77 = 77). -- RAISE EXCEPTION on mismatch so a partial move fails the migration -- loudly instead of poisoning Slice 4. -- -- Audit-reason cites design §3.C — the rationale persists in the -- paliad.deadline_rule_audit log forever via the mig 079 trigger. SELECT set_config( 'paliad.audit_reason', 'pipeline C migration 085: data-move event_deadlines → deadline_rules per design §3.C — ' || 'preserves source rows; mig 086 wraps the source table read-only', true); INSERT INTO paliad.deadline_rules ( id, proceeding_type_id, parent_id, trigger_event_id, spawn_proceeding_type_id, code, name, name_en, primary_party, event_type, is_mandatory, is_optional, is_court_set, is_spawn, duration_value, duration_unit, timing, alt_duration_value, alt_duration_unit, combine_op, rule_code, deadline_notes, deadline_notes_en, legal_source, condition_expr, condition_flag, sequence_order, is_active, priority, lifecycle_state, draft_of, published_at, created_at, updated_at ) SELECT gen_random_uuid() AS id, NULL::integer AS proceeding_type_id, NULL::uuid AS parent_id, ed.trigger_event_id AS trigger_event_id, NULL::integer AS spawn_proceeding_type_id, NULL::text AS code, ed.title_de AS name, ed.title AS name_en, NULL::text AS primary_party, NULL::text AS event_type, true AS is_mandatory, false AS is_optional, false AS is_court_set, false AS is_spawn, ed.duration_value AS duration_value, ed.duration_unit AS duration_unit, ed.timing AS timing, ed.alt_duration_value AS alt_duration_value, ed.alt_duration_unit AS alt_duration_unit, ed.combine_op AS combine_op, NULL::text AS rule_code, NULLIF(ed.notes, '') AS deadline_notes, ed.notes_en AS deadline_notes_en, ed.legal_source AS legal_source, NULL::jsonb AS condition_expr, NULL::text[] AS condition_flag, (1000 + ed.id)::integer AS sequence_order, ed.is_active AS is_active, 'mandatory' AS priority, 'published' AS lifecycle_state, NULL::uuid AS draft_of, ed.created_at AS published_at, ed.created_at AS created_at, now() AS updated_at FROM paliad.event_deadlines ed WHERE ed.is_active = true AND NOT EXISTS ( SELECT 1 FROM paliad.deadline_rules dr WHERE dr.trigger_event_id = ed.trigger_event_id AND dr.name = ed.title_de ); -- Hard assertion: every active event_deadlines row must have a matching -- deadline_rules row by (trigger_event_id, name). If the counts diverge, -- something in the WHERE NOT EXISTS clause (likely a stale duplicate) -- prevented a real insert — fail the migration rather than ship a -- partial Pipeline-C corpus. DO $$ DECLARE n_source int; n_target int; BEGIN SELECT count(*) INTO n_source FROM paliad.event_deadlines WHERE is_active = true; SELECT count(*) INTO n_target FROM paliad.deadline_rules WHERE trigger_event_id IS NOT NULL; RAISE NOTICE 'mig 085: event_deadlines(active)=%, deadline_rules(trigger_event_id IS NOT NULL)=%', n_source, n_target; IF n_target <> n_source THEN RAISE EXCEPTION 'mig 085: data-move incomplete — expected % unified rows, got %. ' 'Investigate event_deadlines (trigger_event_id, title_de) duplicates ' 'OR re-applied migration on dirtied target.', n_source, n_target; END IF; END $$;