Fix: de-dup before the 0024 unique index + conflict-safe SPC dashboard inserts #109
No reviewers
Labels
No labels
area:ai
area:ci-cd
area:notifications
area:observability
area:public-pages
backlog
bug
duplicate
enhancement
help wanted
invalid
question
type:decision
type:feature
type:infra
type:maintenance
type:security
v1.0.1
v1.1.0
v1.2.0
v1.3.0
v2.0.0
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/WeatherBot!109
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/v1.4.0-dashboard-dedup-migration"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 duplicatechannel_id=NULLSPC dashboard rows (one group had 147 copies), soCREATE UNIQUE INDEXfailed → 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_spcandpoll_spc_dashboardare two independent 5-minute jobs that both create the samechannel_id=NULLdashboard record via a SELECT-then-INSERT with no DB-level guard — when they race, both insert.Fix
sent_at, ties broken byctid) per(nws_alert_id, location_id)wherechannel_id IS NULLand 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._record_and_dispatch_outlook,_upsert_dashboard_record) now wrap the insert in aSAVEPOINT(begin_nested) and catchIntegrityError, 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 carriessqlite_where, so the SQLite bulk tier enforces the same semantics — the new tests exercise a genuineIntegrityError.)Testing (dev server)
Follow-up to #57 / #107.
🤖 Generated with Claude Code