The new tables (mig 136) and the dual-write that keeps them in sync
(B.2) have been steady-state in prod since mig 136 deployed at
13:24 UTC today. Drift verified clean before this commit:
deadline_rules=231, sequencing_rules=231, procedural_events=231 (153
codes + 78 synthetic), legal_sources=87, zero mismatches across
counts, FK integrity, lifecycle, is_active.
This commit flips READ paths to source data from the new tables via
a backwards-compatible view, leaving the dual-write WRITE paths
untouched for B.4 to retire alongside the destructive drop.
* internal/db/migrations/139_deadline_rules_unified_view.up.sql (new) —
CREATE VIEW paliad.deadline_rules_unified projecting sr+pe+ls
back into the legacy paliad.deadline_rules column shape. Same
column names + types so the Go-side change is a 1-token
substitution per query with no struct or scanner edits.
Post-apply DO block asserts view row count = sequencing_rules row
count (FK NOT NULL on procedural_event_id guarantees they match).
* 10 service / handler files — every SELECT FROM paliad.deadline_rules
(or JOIN paliad.deadline_rules) flipped to use the view:
- internal/handlers/submissions.go (Schriftsätze list)
- internal/services/deadline_rule_service.go (8 read sites)
- internal/services/rule_editor_service.go (3 read sites — ListRules, getByID, validateSpawnNoCycle)
- internal/services/rule_editor_orphans.go (candidate-rule lookup)
- internal/services/submission_vars.go (loadPublishedRule)
- internal/services/deadline_service.go (deadlines list join)
- internal/services/fristenrechner.go (calculator reads)
- internal/services/projection_service.go (projection reads)
- internal/services/event_deadline_service.go (event→rule join)
- internal/services/export_service.go (3 export sites — ref__deadline_rules)
Verified semantically safe on live (read-only smoke):
- 231 rows in view match 231 in legacy.
- name + event_type pair: 231/231 match.
- legal_source: 231/231 match (NULL on both sides treated as match).
- submission_code: 153 non-NULL codes match exactly; the 78
synthetic 'null.<8hex>' codes diverge from legacy NULL but no
reader filters on NULL submission_code (verified
handlers/submissions.go: synthetic-code rules all have NULL
event_type so the WHERE event_type = 'filing' filter excludes
them; the Schriftsätze surface returns the same 105 rows).
Scope decisions documented (deviation from design §5.3):
- B.3 ships the READ flip only. WRITE paths (RuleEditorService
Create / UpdateDraft / CloneAsDraft / Publish / flipLifecycle)
retain the dual-write from B.2 — they write to both legacy and
new tables. B.4 (destructive drop) will retire the legacy writes
in the same slice that drops the table, avoiding a transient
state where the legacy writes have no purpose.
- The B.2 drift-check ticker (StartDualWriteDriftCheckLoop) stays
active for the same reason: dual-write continues, so the
invariants the loop checks remain meaningful.
This shape is paliadin-approvable on a "good solution > strict
phase boundary" reading of m's greenlight. If paliadin pushes back
and wants the legacy writes removed in B.3, the refactor is ~300
LOC across the 5 RuleEditorService write methods + buildPatchSets
split into PE/SR sets — schedulable as B.3.5 before B.4.
Build + vet clean. TestMigrations_NoDuplicateSlot passes.