2 Commits

Author SHA1 Message Date
mAi
1129baba7a feat(db,services): Slice B.4 destructive drop — paliad.deadline_rules retired, INSTEAD OF triggers on view route writes (mig 140, t-paliad-305 / m/paliad#93)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
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.
2026-05-26 19:53:24 +02:00
mAi
38ebccc907 feat(services): Slice B.2 dual-write — RuleEditorService writes deadline_rules AND procedural_events / sequencing_rules / legal_sources (t-paliad-305 / m/paliad#93)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
Keeps the parallel new tables (mig 136, Slice B.1) in lock-step with
the legacy paliad.deadline_rules table through every write path on
RuleEditorService. Read paths stay on deadline_rules in B.2 — B.3
flips them and stops legacy writes.

* internal/services/dual_write.go (new) —
  - syncDualWriteFromDeadlineRule(ctx, tx, id): idempotent UPSERT of
    legal_sources + procedural_events + sequencing_rules from the
    just-written deadline_rules row. Pure SQL projection, no Go-side
    struct mapping. Synthetic-code mint expression is byte-identical
    to mig 136 ('null.' || first 8 hex of stripped uuid).
  - syncDeadlineDualLinks(ctx, tx, deadlineID): mirrors a deadline's
    legacy rule_id back-link onto deadlines.procedural_event_id +
    sequencing_rule_id. Handles NULL rule_id naturally (collapses both
    new columns to NULL).
  - CheckDualWriteDrift(ctx, conn): nine read-only count queries +
    integrity joins. Returns DualWriteDriftReport. HasDrift() bool for
    log routing.
  - StartDualWriteDriftCheckLoop(ctx, conn, interval): goroutine ticker
    that runs CheckDualWriteDrift every `interval` (default 6h) for
    the lifetime of ctx. Clean run logs at INFO; drift at WARN with
    full report.

* internal/services/rule_editor_service.go —
  - Create / UpdateDraft / CloneAsDraft / Publish / flipLifecycle
    each call syncDualWriteFromDeadlineRule(ctx, tx, id) after the
    deadline_rules mutation, before tx.Commit. Publish syncs BOTH the
    published draft AND the cloned-from peer it just archived as a
    cascade. The audit_reason already set via setAuditReasonTx
    propagates to the new-table writes (same TX, same session).

* internal/services/rule_editor_orphans.go —
  - ResolveOrphan calls syncDeadlineDualLinks after UPDATE
    paliad.deadlines SET rule_id = $1, so the parallel new columns
    follow the legacy back-link.

* internal/services/deadline_service.go —
  - DeadlineService.Update calls syncDeadlineDualLinks when
    input.RuleSet is true (auto/custom rule swap from t-paliad-258).

* cmd/server/main.go —
  - Spawns StartDualWriteDriftCheckLoop alongside CalDAV sync and
    reminder scanner. Inherits bgCtx so the goroutine stops on
    SIGTERM. Interval 6h.

* internal/services/dual_write_test.go (new) —
  - TestDualWrite_RuleEditorLifecycle: Create → UpdateDraft → Publish
    → Archive, asserts the new tables mirror at each step. Final
    CheckDualWriteDrift returns zero drift.
  - TestDualWrite_SyntheticCodeForNullSubmission: rule created with
    submission_code=NULL gets a 'null.<8hex>' procedural_events row
    matching mig 136's mint expression byte-for-byte.

Scope decisions documented in the commit:

- B.2 keeps read paths on deadline_rules. paliadin's "Read paths fall
  back to legacy" reads as "reads stay on legacy as the safety net
  while drift-check validates the new tables". B.3 swaps reads to
  new tables only AND stops writing to deadline_rules — that's a
  separate slice per the design's §5.2/§5.3 split.

- B.2 does NOT modify submission_drafts, projection_service, the
  Fristenrechner calculator, the SubmissionVarsService, the
  Schriftsätze list query, or any other reader. They keep reading
  deadline_rules unchanged. The new tables are populated in parallel
  for B.3's cutover.

- Audit triggers on deadline_rules continue to fire as before. The
  new tables have no audit triggers yet (a later slice can add
  parallel audit rows once the new tables are authoritative).

- Drift-check uses default 6h interval — short enough that a broken
  dual-write surfaces within the same business day, long enough that
  the count-COUNTs don't churn the pool. Override via the caller in
  cmd/server.

Hard rules followed:
- audit_reason set on every TX before any deadline_rules mutation
  (existing pattern; new-table writes share the same reason).
- No destructive op (B.2 is strictly additive in behaviour).
- New helpers idempotent (UPSERT ON CONFLICT DO UPDATE) — safe to
  call twice, safe to re-run after a partial failure.

Build + vet clean. TestMigrations_NoDuplicateSlot passes.
2026-05-26 17:49:48 +02:00