Skip to content

fix(etl): upsert explicit subscription writes#335

Merged
raymondjacobson merged 1 commit into
mainfrom
fix/etl-0030-dedupe-current
Jun 2, 2026
Merged

fix(etl): upsert explicit subscription writes#335
raymondjacobson merged 1 commit into
mainfrom
fix/etl-0030-dedupe-current

Conversation

@raymondjacobson
Copy link
Copy Markdown
Contributor

@raymondjacobson raymondjacobson commented Jun 2, 2026

Root cause

subscriptions is the only one of the four social tables (reposts/saves/follows/subscriptions) with two independent writers:

  1. Explicit Subscribe/Unsubscribe → social_subscription.godemote-then-insert (UPDATE … is_current=false then a plain INSERT).
  2. Implicit Follow/Unfollow auto-subscribe → social_follow.goON CONFLICT upsert.

The other three tables have a single writer each. Demote-then-insert is a two-statement write: between the UPDATE and the INSERT, the other subscription writer can land a second is_current row — and the table had no uniqueness constraint to stop it. Over time that accumulated duplicate is_current rows in subscriptions (92 of them in prod), which the partial unique index added in #331 (subscriptions_current_uniq_idx) was not equipped to coexist with.

#331 (24084d2) converted follow/repost/save to upserts and added the four arbiter indexes, but never touched social_subscription.go (last modified by an unrelated rename in #300). So subscriptions got an arbiter index while one of its two writers still used the demote-then-insert pattern that can violate it.

Fix

Convert insertSubscription to the same single-statement ON CONFLICT upsert used by the Follow path. Both subscription writers now go through the arbiter atomically, so the unique index fully enforces one-current-row-per-identity and duplicates can't recur.

Scope

Code-only. The pre-existing duplicate rows were already cleaned up out-of-band in the single deployment that had them, and golang-migrate tracks version number only (it won't re-run 0030), so no migration-side dedupe/backfill is included here — 0030 is unchanged.

Test plan

  • go build ./... and go vet in pkg/etl
  • go test ./processors/entity_manager/ (DB-backed: runs migrations + exercises subscribe/follow/contest paths) — pass

🤖 Generated with Claude Code

@raymondjacobson raymondjacobson changed the title fix(etl): dedupe duplicate is_current rows before social upsert indexes fix(etl): upsert explicit subscription writes + dedupe legacy duplicates Jun 2, 2026
social_subscription.go was the one social handler #331 left on
demote-then-insert; follow/repost/save were converted to ON CONFLICT
upserts. subscriptions is also the only social table with two writers
(explicit Subscribe + Follow auto-subscribe), and demote-then-insert is a
two-statement write: between the demote and the insert the other writer can
land a second is_current row. With no uniqueness constraint, that accumulated
the 92 duplicate current rows that later failed the 0030 index build.

Convert insertSubscription to the same single-statement ON CONFLICT upsert as
the Follow path so the arbiter index fully enforces one-current-row-per-
identity for both writers and dupes can't recur. The migration dedupe remains
required to clean pre-existing rows so the unique index can build.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@raymondjacobson raymondjacobson force-pushed the fix/etl-0030-dedupe-current branch from 128fe7e to 6d39f11 Compare June 2, 2026 05:51
@raymondjacobson raymondjacobson changed the title fix(etl): upsert explicit subscription writes + dedupe legacy duplicates fix(etl): upsert explicit subscription writes Jun 2, 2026
@raymondjacobson raymondjacobson merged commit 5d2e19a into main Jun 2, 2026
6 checks passed
@raymondjacobson raymondjacobson deleted the fix/etl-0030-dedupe-current branch June 2, 2026 06:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant