feat(db): add annotation sync schema, queries, and tombstone support

Add migration columns to media_highlights, media_notes, and media_bookmarks
for cross-device annotation sync:

- dedup_key: SHA-1 of normalized selection text + bucketed position, used
  as the stable cross-device identity for annotations
- last_modified_at / last_modified_source: edit clock for LWW resolution
  and cross-source conflict detection
- deleted / deleted_at: sticky tombstone columns for delete-wins semantics
  with a 30-day TTL before rows are physically purged
- note_text on highlights: stores attached notes from KOReader entries that
  have both selected text and a user note
- Location columns on bookmarks (cfi_position, percentage_location,
  epubcfi_location, chapter_reference, paragraph_reference)
- device_sync_data JSONB on all three tables: stores per-device native
  identifiers (e.g. KOReader pos0/datetime, Kobo bookmark_id) so each
  device can locate and manipulate its own copy of an annotation

Add partial unique indexes on (user_id, media_item_id, dedup_key) where
deleted = FALSE to enforce one active annotation per dedup key.

Add tombstone purge indexes on (deleted, deleted_at) for efficient GC.

New queries:
- GetByDedupKey for all three tables (returns active or most-recent tombstone)
- CreateFull / UpdateForSync for all three tables (populate sync columns)
- TombstoneByDedupKey / TombstoneByID for all three tables
- PurgeExpired* for all three tables (GC past TTL)
- GetActiveAnnotationsForBook (filtered union of highlights + notes)
- GetTombstonedAnnotationsForBook (union of all 3 deleted within TTL)
- GetMediaBookmarks with deleted filter
- GetMediaBookmark (singular) with deleted filter
- Added deleted=FALSE filter to GetMediaHighlights, GetAnnotationsForBook
- CreateAutoResolvedSyncConflict (INSERT with resolution_status='auto_resolved')
This commit is contained in:
2026-07-29 14:48:30 -04:00
parent 78176c57a5
commit 2a15effc3e
6 changed files with 1452 additions and 54 deletions
+52 -32
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// sqlc v1.30.0
package database
@@ -155,40 +155,55 @@ type LibraryVisibility struct {
}
type MediaBookmarks struct {
ID pgtype.UUID `db:"id" json:"id"`
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
PageNumber pgtype.Int4 `db:"page_number" json:"page_number"`
ChapterNumber pgtype.Int4 `db:"chapter_number" json:"chapter_number"`
CfiPosition pgtype.Text `db:"cfi_position" json:"cfi_position"`
Title string `db:"title" json:"title"`
Position pgtype.Text `db:"position" json:"position"`
Notes pgtype.Text `db:"notes" json:"notes"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
ID pgtype.UUID `db:"id" json:"id"`
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
PageNumber pgtype.Int4 `db:"page_number" json:"page_number"`
ChapterNumber pgtype.Int4 `db:"chapter_number" json:"chapter_number"`
CfiPosition pgtype.Text `db:"cfi_position" json:"cfi_position"`
Title string `db:"title" json:"title"`
Position pgtype.Text `db:"position" json:"position"`
Notes pgtype.Text `db:"notes" json:"notes"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
DedupKey pgtype.Text `db:"dedup_key" json:"dedup_key"`
LastModifiedAt pgtype.Timestamptz `db:"last_modified_at" json:"last_modified_at"`
LastModifiedSource pgtype.Text `db:"last_modified_source" json:"last_modified_source"`
DeviceSyncData []byte `db:"device_sync_data" json:"device_sync_data"`
PercentageLocation pgtype.Float8 `db:"percentage_location" json:"percentage_location"`
EpubcfiLocation pgtype.Text `db:"epubcfi_location" json:"epubcfi_location"`
ChapterReference pgtype.Int4 `db:"chapter_reference" json:"chapter_reference"`
Deleted pgtype.Bool `db:"deleted" json:"deleted"`
DeletedAt pgtype.Timestamptz `db:"deleted_at" json:"deleted_at"`
}
type MediaHighlights struct {
ID pgtype.UUID `db:"id" json:"id"`
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
SelectionText string `db:"selection_text" json:"selection_text"`
StartPosition pgtype.Text `db:"start_position" json:"start_position"`
EndPosition pgtype.Text `db:"end_position" json:"end_position"`
Color pgtype.Text `db:"color" json:"color"`
NoteID pgtype.UUID `db:"note_id" json:"note_id"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
PercentageStart pgtype.Float8 `db:"percentage_start" json:"percentage_start"`
PercentageEnd pgtype.Float8 `db:"percentage_end" json:"percentage_end"`
CharacterStart pgtype.Int4 `db:"character_start" json:"character_start"`
CharacterEnd pgtype.Int4 `db:"character_end" json:"character_end"`
EpubcfiStart pgtype.Text `db:"epubcfi_start" json:"epubcfi_start"`
EpubcfiEnd pgtype.Text `db:"epubcfi_end" json:"epubcfi_end"`
ChapterReference pgtype.Int4 `db:"chapter_reference" json:"chapter_reference"`
ParagraphStart pgtype.Int4 `db:"paragraph_start" json:"paragraph_start"`
ParagraphEnd pgtype.Int4 `db:"paragraph_end" json:"paragraph_end"`
PanelNumber pgtype.Int4 `db:"panel_number" json:"panel_number"`
DeviceSyncData []byte `db:"device_sync_data" json:"device_sync_data"`
ID pgtype.UUID `db:"id" json:"id"`
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
SelectionText string `db:"selection_text" json:"selection_text"`
StartPosition pgtype.Text `db:"start_position" json:"start_position"`
EndPosition pgtype.Text `db:"end_position" json:"end_position"`
Color pgtype.Text `db:"color" json:"color"`
NoteID pgtype.UUID `db:"note_id" json:"note_id"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
PercentageStart pgtype.Float8 `db:"percentage_start" json:"percentage_start"`
PercentageEnd pgtype.Float8 `db:"percentage_end" json:"percentage_end"`
CharacterStart pgtype.Int4 `db:"character_start" json:"character_start"`
CharacterEnd pgtype.Int4 `db:"character_end" json:"character_end"`
EpubcfiStart pgtype.Text `db:"epubcfi_start" json:"epubcfi_start"`
EpubcfiEnd pgtype.Text `db:"epubcfi_end" json:"epubcfi_end"`
ChapterReference pgtype.Int4 `db:"chapter_reference" json:"chapter_reference"`
ParagraphStart pgtype.Int4 `db:"paragraph_start" json:"paragraph_start"`
ParagraphEnd pgtype.Int4 `db:"paragraph_end" json:"paragraph_end"`
PanelNumber pgtype.Int4 `db:"panel_number" json:"panel_number"`
DeviceSyncData []byte `db:"device_sync_data" json:"device_sync_data"`
DedupKey pgtype.Text `db:"dedup_key" json:"dedup_key"`
LastModifiedAt pgtype.Timestamptz `db:"last_modified_at" json:"last_modified_at"`
LastModifiedSource pgtype.Text `db:"last_modified_source" json:"last_modified_source"`
NoteText pgtype.Text `db:"note_text" json:"note_text"`
Deleted pgtype.Bool `db:"deleted" json:"deleted"`
DeletedAt pgtype.Timestamptz `db:"deleted_at" json:"deleted_at"`
}
type MediaItemFormats struct {
@@ -304,6 +319,11 @@ type MediaNotes struct {
ChapterReference pgtype.Int4 `db:"chapter_reference" json:"chapter_reference"`
ParagraphReference pgtype.Int4 `db:"paragraph_reference" json:"paragraph_reference"`
DeviceSyncData []byte `db:"device_sync_data" json:"device_sync_data"`
DedupKey pgtype.Text `db:"dedup_key" json:"dedup_key"`
LastModifiedAt pgtype.Timestamptz `db:"last_modified_at" json:"last_modified_at"`
LastModifiedSource pgtype.Text `db:"last_modified_source" json:"last_modified_source"`
Deleted pgtype.Bool `db:"deleted" json:"deleted"`
DeletedAt pgtype.Timestamptz `db:"deleted_at" json:"deleted_at"`
}
type MediaRatings struct {