The previous post-check compared unfiltered counts (snapshot 493 vs
sequencing_rules 231) and false-positived as "dual-write drift". Reality:
B.2 dual-write was scoped to is_active=true + lifecycle_state='published'
(the read-path universe). Archived + draft rows in deadline_rules were
never replicated to sequencing_rules because nothing read them.
Patch: filter both counts to active+published before comparison — the
invariant B.2 actually maintained. Archived/draft rows survive in
deadline_rules_pre_140 for forensic / future-backfill.
Third hotfix on mig 140 today (1: missing matview drop; 2: wrong post-check
comparand; 3: post-check missing lifecycle filter). The slice itself is
sound — every failure was in the verification path, not the data.
The previous post-check compared paliad.deadline_rules_pre_140 row count
to paliad.deadline_rules_unified row count and failed with
"snapshot has 493 rows, view has 231 rows — drift". That's a false
positive: the snapshot has every row (all lifecycle states + is_active),
the view filters to is_active+published. They're not supposed to match.
The right invariant: snapshot row count == sequencing_rules row count
(B.2 dual-write keeps them 1:1 across all lifecycle states). Patched.
View count stays in the RAISE NOTICE line as informational.
Refs t-paliad-305 / m/paliad#93 Slice B.4 hotfix #2.
prod-down: mig 140 fails with `cannot drop table deadline_rules because
other objects depend on it (2BP01)`. The dependent object is the
deadline_search materialized view (mig 077) — curie's brief listed FK
re-pointing but missed the matview.
Fix: drop the matview before DROP TABLE deadline_rules, recreate it at
the end of mig 140 against deadline_rules_unified (same column shape).
All 11 indexes restored. REFRESH at end so search keeps working.
Single-TX atomicity preserved — if anything past step 6a fails, the
whole drop-and-recreate rolls back. The pre_140 snapshot from step 1
remains as the forensic backstop.
Refs t-paliad-305 / m/paliad#93 Slice B.4.
Drops the legacy paliad.deadline_rules table after 3 weeks of dual-write
shadowing (mig 136 → B.2 dual-write → B.3 read cutover via view). The
new tables — paliad.procedural_events, paliad.sequencing_rules,
paliad.legal_sources — are the sole source of truth from this commit
forward.
Pre-flip drift verified clean against prod:
deadline_rules=231 == sequencing_rules=231 == procedural_events=231
legal_sources=87
missing_sr=0, orphaned_sr=0, mismatched_lifecycle=0
* internal/db/migrations/140_drop_deadline_rules.up.sql (new) —
Single TX, audit-first:
1. CREATE TABLE paliad.deadline_rules_pre_140 AS TABLE paliad.deadline_rules
(precedent migs 091/093/095/098 — snapshot in same TX as destructive op).
2. Final reconciliation UPDATE on paliad.deadlines (no-op when
drift is already 0; defensive against last-minute writes).
3. DROP TRIGGER deadline_rules_audit_aiud.
4. Re-point FKs to sequencing_rules:
- paliad.appointments.deadline_rule_id → paliad.sequencing_rules(id)
- paliad.deadline_rule_backfill_orphans.resolved_rule_id → paliad.sequencing_rules(id)
(the id values are identical — sr.id inherited dr.id at mig 136.)
5. DROP COLUMN paliad.deadlines.rule_id.
6. DROP TABLE paliad.deadline_rules.
7. CREATE INSTEAD OF INSERT + INSTEAD OF UPDATE triggers on
paliad.deadline_rules_unified. Triggers route writes into the
three new tables in the same TX, preserving the legacy column
shape on the wire so RuleEditorService SQL only needs a
table-name swap, not a structural rewrite. Synthetic-code mint
expression is byte-identical to mig 136 + the B.2 dual-write
helper. POST assertions confirm the table is gone, the column
is gone, and the snapshot matches.
Trigger design notes (1:N caveat documented in-trigger):
- PE identity columns (code, name, name_en, description, event_kind,
primary_party_default, legal_source_id, concept_id) mirror from
the writing sequencing-rule.
- PE lifecycle columns (lifecycle_state, published_at, is_active)
deliberately do NOT mirror — a draft sequencing-rule cloned from
a published source shares the source's PE; we don't want the
clone's 'draft' lifecycle to leak back onto the source's PE.
Practical bound today (1:1 corpus); explicit comment in-trigger
for the eventual 1:N pattern.
* internal/db/migrations/140_drop_deadline_rules.down.sql (new) —
Best-effort restore from the snapshot. Triggers / indexes /
CHECK constraints from historical migrations are NOT replayed;
operator must reapply 078/079/091/095/098/122/128/134/135 to
bring the restored table to working shape. The down path is for
catastrophic recovery, not casual revert.
* internal/services/rule_editor_service.go —
Six syncDualWriteFromDeadlineRule(...) calls removed (the
INSTEAD OF triggers now do the fan-out). Five
INSERT/UPDATE paliad.deadline_rules statements (Create,
UpdateDraft, CloneAsDraft INSERT+SELECT, Publish, peer-archive,
flipLifecycle) renamed to paliad.deadline_rules_unified —
trigger handles the routing.
* internal/services/rule_editor_orphans.go — ResolveOrphan no
longer writes deadlines.rule_id (column dropped). Sets
sequencing_rule_id directly + derives procedural_event_id from
the matching sequencing_rules row in the same UPDATE statement.
* internal/services/deadline_service.go — deadlineColumns now
lists sequencing_rule_id (Deadline.RuleID still binds to it via
the db tag rename below). Update path's appendSet("rule_id",…)
flipped to appendSet("sequencing_rule_id",…) and post-write
derivation moved to the renamed syncDeadlineProceduralEventID
helper.
* internal/services/projection_service.go,
internal/services/submission_vars.go — `WHERE rule_id = $X`
reads on paliad.deadlines flipped to sequencing_rule_id.
* internal/models/models.go — Deadline.RuleID db tag changed from
"rule_id" to "sequencing_rule_id". Field name + JSON name kept
for backward compat with the frontend and existing Go callers;
semantic value is identical (same UUID).
* internal/services/dual_write.go — Massively trimmed.
Removed: syncDualWriteFromDeadlineRule, syncDeadlineDualLinks,
CheckDualWriteDrift, DualWriteDriftReport, HasDrift,
StartDualWriteDriftCheckLoop. All referenced
paliad.deadline_rules which no longer exists.
Kept (renamed): syncDeadlineProceduralEventID — derives
procedural_event_id from sequencing_rule_id after any
DeadlineService.Update that touched the back-link.
* cmd/server/main.go — Removed the StartDualWriteDriftCheckLoop
bootstrap call (and its `time` import that only that call
needed). Comment notes the retirement.
* internal/services/dual_write_test.go — Removed the final
CheckDualWriteDrift assertion in
TestDualWrite_RuleEditorLifecycle (function deleted). The
per-step asserts against procedural_events / sequencing_rules
/ legal_sources cover the same contract by direct query.
Hard rules followed:
- Audit-first: snapshot precedes destructive ops in the same TX.
- No silent data loss: pre-drop drift was zero; snapshot captures
the final state; FK re-points use identical UUIDs.
- INSTEAD OF triggers documented in mig 140 — single source of
truth for the legacy→new mapping.
- Down migration is honest about its scope (catastrophic recovery
only).
Build + vet clean. TestMigrations_NoDuplicateSlot passes. Live-DB
tests skipped (no TEST_DATABASE_URL in this env) — they'll exercise
the full mig 140 + INSTEAD OF triggers in CI.