A sync button press racing a page-reload sync could run two sync
requests concurrently. create_feed_item used a check-then-insert
pattern (query for an existing item, then insert if none found), so
both requests could pass the check before either inserted, creating
duplicate items.
- Add a UNIQUE (feed_id, url) constraint on feed_item, keyed on the
article's link rather than its title since that's RSS's stable
article identity (a feed editing a headline after publishing, same
link, would otherwise dupe under title-based matching).
- The migration first collapses any duplicates already created by the
race, propagating read=true onto the surviving row when any
duplicate in its group was already read, so the cleanup can't make
an already-read article look unread.
- create_feed_item now does a single atomic
INSERT ... ON CONFLICT (feed_id, url) DO NOTHING instead of
select-then-insert, closing the race entirely.
- Add a test that races two threads on separate connections inserting
the same item and asserts only one row survives.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CHcxDSPbHhe7sLJQBRVC9