Fix: de-dup before the 0024 unique index + conflict-safe SPC dashboard inserts #109

Merged
claude-bot merged 1 commit from fix/v1.4.0-dashboard-dedup-migration into main 2026-07-19 04:01:03 +00:00
Contributor

Hotfix — the Phase 1 (#107) CD deploy to dev failed and rolled back (dev is back on v1.3.0). This unblocks it.

Root cause

Migration 0024 adds a partial UNIQUE index on sent_alerts (nws_alert_id, location_id) WHERE channel_id IS NULL. The real dev DB already held 18 groups of duplicate channel_id=NULL SPC dashboard rows (one group had 147 copies), so CREATE UNIQUE INDEX failed → the app couldn't start → the CD health check rolled back. (The original PR's throwaway-DB test passed because that DB was empty.)

The duplicates themselves are a real bug: poll_spc and poll_spc_dashboard are two independent 5-minute jobs that both create the same channel_id=NULL dashboard record via a SELECT-then-INSERT with no DB-level guard — when they race, both insert.

Fix

  1. Migration 0024 de-duplicates first. Before creating the index it keeps the newest row (greatest sent_at, ties broken by ctid) per (nws_alert_id, location_id) where channel_id IS NULL and deletes the rest (Postgres-only DDL, matching the index's scope). Verified against a restored copy of the real dev DB: 18 dup groups → 0, index created.
  2. Conflict-safe inserts. Both NULL-dashboard insert sites (_record_and_dispatch_outlook, _upsert_dashboard_record) now wrap the insert in a SAVEPOINT (begin_nested) and catch IntegrityError, treating a lost race as "already exists" instead of poisoning the poll cycle's transaction. Staged impact/transition/upgrade work is flushed into the outer transaction before the savepoint, so only the dashboard insert rolls back on conflict. The normal path is unchanged; the notification path still dispatches its per-channel record even if it loses the dashboard-row race. (The model's partial index also carries sqlite_where, so the SQLite bulk tier enforces the same semantics — the new tests exercise a genuine IntegrityError.)

Testing (dev server)

  • Bulk suite 660 passed; Postgres tier 4 passed.
  • Migration 0024 applies on a restored copy of the real dev DB with de-dup to zero remaining duplicate groups.

Follow-up to #57 / #107.

🤖 Generated with Claude Code

**Hotfix** — the Phase 1 (#107) CD deploy to dev failed and rolled back (dev is back on v1.3.0). This unblocks it. ## Root cause Migration 0024 adds a partial UNIQUE index on `sent_alerts (nws_alert_id, location_id) WHERE channel_id IS NULL`. The real dev DB already held **18 groups of duplicate `channel_id=NULL` SPC dashboard rows** (one group had 147 copies), so `CREATE UNIQUE INDEX` failed → the app couldn't start → the CD health check rolled back. (The original PR's throwaway-DB test passed because that DB was empty.) The duplicates themselves are a real bug: `poll_spc` and `poll_spc_dashboard` are two independent 5-minute jobs that both create the same `channel_id=NULL` dashboard record via a SELECT-then-INSERT with no DB-level guard — when they race, both insert. ## Fix 1. **Migration 0024 de-duplicates first.** Before creating the index it keeps the newest row (greatest `sent_at`, ties broken by `ctid`) per `(nws_alert_id, location_id)` where `channel_id IS NULL` and deletes the rest (Postgres-only DDL, matching the index's scope). **Verified against a restored copy of the real dev DB: 18 dup groups → 0, index created.** 2. **Conflict-safe inserts.** Both NULL-dashboard insert sites (`_record_and_dispatch_outlook`, `_upsert_dashboard_record`) now wrap the insert in a `SAVEPOINT` (`begin_nested`) and catch `IntegrityError`, treating a lost race as "already exists" instead of poisoning the poll cycle's transaction. Staged impact/transition/upgrade work is flushed into the outer transaction *before* the savepoint, so only the dashboard insert rolls back on conflict. The normal path is unchanged; the notification path still dispatches its per-channel record even if it loses the dashboard-row race. (The model's partial index also carries `sqlite_where`, so the SQLite bulk tier enforces the same semantics — the new tests exercise a genuine `IntegrityError`.) ## Testing (dev server) - Bulk suite **660 passed**; Postgres tier **4 passed**. - Migration 0024 applies on a **restored copy of the real dev DB** with de-dup to **zero** remaining duplicate groups. Follow-up to #57 / #107. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Fix dashboard-dedup migration + make SPC dashboard inserts conflict-safe
All checks were successful
CI / test (pull_request) Successful in 5m30s
1029c5210b
Follow-up to #57 (#107): the Phase 1 CD deploy failed and rolled back. Root
cause: the dev DB already held duplicate channel_id=NULL SPC dashboard rows
(18 groups, up to 147 copies), so migration 0024's partial UNIQUE index could
not be built. The duplicates come from poll_spc and poll_spc_dashboard — two
independent 5-minute jobs — racing the same SELECT-then-INSERT for the same
(dedup_id, location_id, channel_id IS NULL) row.

- Migration 0024 now de-duplicates first: before creating the unique index it
  keeps the newest row (greatest sent_at, ties by ctid) per (nws_alert_id,
  location_id) where channel_id IS NULL and deletes the rest (Postgres-only DDL,
  matching the index). Verified against a restored copy of the real dev DB:
  18 dup groups -> 0, index created.
- Both NULL-dashboard insert sites (_record_and_dispatch_outlook and
  _upsert_dashboard_record) now wrap the insert in a SAVEPOINT and catch
  IntegrityError, treating a lost race as "already exists" instead of poisoning
  the poll cycle's transaction. Prior staged work (impact/transition/upgrade) is
  flushed into the outer transaction before the savepoint so only the dashboard
  insert is rolled back on conflict. Normal (non-racing) path is unchanged; the
  notification path still dispatches its per-channel record on a lost dashboard
  race.

The model's partial index carries sqlite_where too, so the SQLite bulk tier
enforces the same semantics and the new tests exercise a real IntegrityError.

Verified on the dev server: bulk 660 passed, postgres tier 4 passed, and
migration 0024 applies on a real-DB copy with dedup to zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude-bot deleted branch fix/v1.4.0-dashboard-dedup-migration 2026-07-19 04:01:03 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/WeatherBot!109
No description provided.