fix(bookmarks): upsert on title conflict so position upgrades don't 500

Bookmark dedup is keyed on hash(title + position bucket), but the table
also enforces UNIQUE(media_item_id, user_id, title). When a client re-
saves the same bookmark title with a changed position form - e.g. the
Android app upgrading a percentage-only row to an EPUB CFI, or a web and
app bookmark landing on the same 'Bookmark at 44%' title - the dedup-key
lookup misses and the INSERT violates the title constraint, returning
HTTP 500 and failing the sync.

A title collision on the same (user, item) is by definition the same
bookmark slot, so take the LWW semantics all the way: ON CONFLICT DO
UPDATE replaces position/cfi_position/page/chapter/percentage, refreshes
dedup_key and timestamps, merges device_sync_data, and - matching
UpdateMediaBookmarkForSync - clears deleted/deleted_at so a re-create
resurrects a tombstoned title slot instead of leaving an invisible row
holding it.

Device sync flows are unaffected: KOReader/Kobo pushes that carry their
own dedup-key echoes never reach the INSERT, and same-key saves still go
through applyBookmarkLWW with its tombstone freshness checks.
This commit is contained in:
2026-08-30 20:57:15 -04:00
parent 5c5593644d
commit 27b3dcb69f
5 changed files with 50 additions and 6 deletions
+21 -1
View File
@@ -893,6 +893,10 @@ ORDER BY deleted ASC, deleted_at DESC NULLS LAST
LIMIT 1;
-- name: CreateMediaBookmarkFull :one
-- The dedup key can change while the title stays the same (e.g. a client
-- upgrading a bookmark from percentage-only to a CFI position), so the
-- title UNIQUE constraint races the dedup-key lookup. Upsert on the title
-- slot: the conflicting row is by definition the same bookmark.
INSERT INTO media_bookmarks (
media_item_id, user_id, page_number, chapter_number,
cfi_position, title, position, notes,
@@ -901,7 +905,23 @@ INSERT INTO media_bookmarks (
device_sync_data
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15
) RETURNING *;
) ON CONFLICT (media_item_id, user_id, title) DO UPDATE SET
page_number = EXCLUDED.page_number,
chapter_number = EXCLUDED.chapter_number,
cfi_position = EXCLUDED.cfi_position,
position = EXCLUDED.position,
notes = EXCLUDED.notes,
percentage_location = EXCLUDED.percentage_location,
epubcfi_location = EXCLUDED.epubcfi_location,
chapter_reference = EXCLUDED.chapter_reference,
dedup_key = EXCLUDED.dedup_key,
last_modified_at = EXCLUDED.last_modified_at,
last_modified_source = EXCLUDED.last_modified_source,
device_sync_data = EXCLUDED.device_sync_data,
created_at = created_at,
deleted = FALSE,
deleted_at = NULL
RETURNING *;
-- name: UpdateMediaBookmarkForSync :one
UPDATE media_bookmarks SET