Skip to content

Use INSERT OR IGNORE for excluded_statuses markers - #2189

Merged
jocmp merged 2 commits into
jocmp:mainfrom
mjhfunctionalashtanga:fix/excluded-statuses-insert-or-ignore
Aug 2, 2026
Merged

Use INSERT OR IGNORE for excluded_statuses markers#2189
jocmp merged 2 commits into
jocmp:mainfrom
mjhfunctionalashtanga:fix/excluded-statuses-insert-or-ignore

Conversation

@mjhfunctionalashtanga

Copy link
Copy Markdown
Contributor

Problem

During refreshUnreadEntriesmarkAllUnread, syncing fails with:

android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed:
excluded_statuses.article_id, excluded_statuses.type (code 1555)
    at com.jocmp.capy.db.ArticlesQueries.upsertUnread

upsertUnread / upsertStarred insert a marker row into excluded_statuses(article_id, type) with no conflict clause (the sibling article_statuses insert correctly uses ON CONFLICT). markAllUnread runs all the marker inserts and the updateStaleUnreads() cleanup — which deletes those markers — in a single transaction. So if any marker already exists (a duplicate ID in the batch, or a marker stranded by an earlier interrupted sync), the insert throws, the whole transaction rolls back, the cleanup never runs, and the leftover markers guarantee the same failure on every subsequent sync. Unread refresh never completes and the client churns.

Fix

Make the marker insert idempotent with INSERT OR IGNORE. Duplicates/leftovers are skipped, the transaction completes, and updateStaleUnreads / updateStaleStars clean up the markers as intended — self-healing.

Verified on a Boox device (Android 11): the recurring db_error disappears and unread refresh completes cleanly.

🤖 Generated with Claude Code

https://claude.ai/code/session_015yVKgdxd3ZSrFeY5eWpFuA

upsertUnread/upsertStarred insert into excluded_statuses(article_id, type) with no conflict clause. markAllUnread runs the marker inserts and the updateStaleUnreads cleanup in one transaction, so a duplicate or stranded marker makes the insert throw, rolls back the transaction, skips the cleanup, and strands the markers -- failing identically on every subsequent sync, so unread refresh never completes. Use INSERT OR IGNORE so duplicate/leftover markers are skipped and the cleanup self-heals.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yVKgdxd3ZSrFeY5eWpFuA
@jocmp jocmp closed this Jul 15, 2026
@jocmp jocmp reopened this Aug 2, 2026
@jocmp

jocmp commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Pushed a follow-up commit to this branch (maintainer edit) rather than opening a competing PR.

Two additions on top of the INSERT OR IGNORE:

  • .distinct() at the markAllUnread/markAllStarred boundary. The duplicate originates upstream — fetchAllEntryIDs (Miniflux) and fetchAllItemIDs (Reader) paginate by offset against total with no dedup, so a set shifting mid-pagination can yield the same ID on two pages. Worth stopping there too, not only at the insert.
  • Three regression tests: duplicate IDs through both markAll* entry points, plus one driving upsertUnread twice inside a transaction so the SQL change stays covered independently of the .distinct().

Verified the tests fail with either fix reverted and pass with both — :capy:test --tests ArticleRecordsTest, 27 tests.

One note on the description: the "marker stranded by an earlier interrupted sync" mechanism doesn't hold, since the marker inserts and updateStaleUnreads() share a transaction and a rollback takes the markers with it. The duplicate-ID-in-batch path is real on its own and is what the fix addresses.

@jocmp
jocmp merged commit 07a4a20 into jocmp:main Aug 2, 2026
2 checks passed
ron159 pushed a commit to ron159/MeerkatReader that referenced this pull request Aug 13, 2026
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.

2 participants