diff --git a/internal/db/migrations/085_pipeline_c_data_move.down.sql b/internal/db/migrations/085_pipeline_c_data_move.down.sql new file mode 100644 index 0000000..c7becc7 --- /dev/null +++ b/internal/db/migrations/085_pipeline_c_data_move.down.sql @@ -0,0 +1,17 @@ +-- t-paliad-184 down — reverts the Pipeline-C data-move from +-- 085_pipeline_c_data_move.up.sql. Deletes every paliad.deadline_rules +-- row carrying a non-NULL trigger_event_id (those are exactly the rows +-- the up-migration created — before mig 085 no Pipeline-A rule ever +-- carried trigger_event_id, and Slice 9 hasn't dropped the source +-- table yet so the rows can be regenerated). +-- +-- Audit-reason set so the mig 079 trigger captures the rollback +-- rationale and doesn't raise on DELETE. + +SELECT set_config( + 'paliad.audit_reason', + 'rollback 085: delete Pipeline-C unified rows (source preserved in event_deadlines)', + true); + +DELETE FROM paliad.deadline_rules + WHERE trigger_event_id IS NOT NULL; diff --git a/internal/db/migrations/085_pipeline_c_data_move.up.sql b/internal/db/migrations/085_pipeline_c_data_move.up.sql new file mode 100644 index 0000000..243b6d6 --- /dev/null +++ b/internal/db/migrations/085_pipeline_c_data_move.up.sql @@ -0,0 +1,184 @@ +-- 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 $$;