feat(db): queries for the deleted-annotation history

ListDeletedAnnotationsForBook unions tombstoned highlights, notes, and
bookmarks for a user+book regardless of the sync TTL cutoff (the history
must show everything still restorable, not just recent deletes), with
display text, secondary text, color, and both timestamps.

Restore queries clear deleted/deleted_at (lossless — the row was soft-
deleted, never removed) and are scoped to the owning user and media item
so a restore can never touch another user's annotation.

Purge queries hard-delete an already-tombstoned row: the user-driven
counterpart of the TTL maintenance sweep, for explicit 'delete
permanently' actions from the history.

All six write queries are :execrows so callers can distinguish 'restored'
from 'nothing matched' without a follow-up read.
This commit is contained in:
2026-08-22 13:16:31 -04:00
parent 91c8be8562
commit acbb6c7981
3 changed files with 314 additions and 0 deletions
+81
View File
@@ -1027,6 +1027,87 @@ FROM media_bookmarks mb
WHERE mb.media_item_id = $1 AND mb.user_id = $2 AND mb.deleted = TRUE AND mb.deleted_at > $3
ORDER BY deleted_at DESC;
-- ============================================
-- ANNOTATION HISTORY (deleted-annotation archive)
-- ============================================
-- Lists every currently-tombstoned annotation for a book regardless of the
-- tombstone TTL: this backs the book page's "recently deleted" history where
-- users can restore or permanently remove entries. Rows whose tombstones have
-- been purged by the daily maintenance sweep no longer exist at all.
-- name: ListDeletedAnnotationsForBook :many
SELECT
mh.id,
mh.dedup_key,
'highlight' as annotation_type,
mh.selection_text as display_text,
mh.note_text as secondary_text,
mh.color,
mh.deleted_at,
mh.created_at
FROM media_highlights mh
WHERE mh.media_item_id = $1 AND mh.user_id = $2 AND mh.deleted = TRUE
UNION ALL
SELECT
mn.id,
mn.dedup_key,
'note' as annotation_type,
mn.content as display_text,
NULL::text as secondary_text,
NULL::text as color,
mn.deleted_at,
mn.created_at
FROM media_notes mn
WHERE mn.media_item_id = $1 AND mn.user_id = $2 AND mn.deleted = TRUE
UNION ALL
SELECT
mb.id,
mb.dedup_key,
'bookmark' as annotation_type,
mb.title as display_text,
mb.notes as secondary_text,
NULL::text as color,
mb.deleted_at,
mb.created_at
FROM media_bookmarks mb
WHERE mb.media_item_id = $1 AND mb.user_id = $2 AND mb.deleted = TRUE
ORDER BY deleted_at DESC;
-- name: RestoreMediaHighlightByID :execrows
UPDATE media_highlights SET
deleted = FALSE,
deleted_at = NULL,
last_modified_at = NOW()
WHERE id = $1 AND user_id = $2 AND media_item_id = $3 AND deleted = TRUE;
-- name: RestoreMediaNoteByID :execrows
UPDATE media_notes SET
deleted = FALSE,
deleted_at = NULL,
last_modified_at = NOW()
WHERE id = $1 AND user_id = $2 AND media_item_id = $3 AND deleted = TRUE;
-- name: RestoreMediaBookmarkByID :execrows
UPDATE media_bookmarks SET
deleted = FALSE,
deleted_at = NULL,
last_modified_at = NOW()
WHERE id = $1 AND user_id = $2 AND media_item_id = $3 AND deleted = TRUE;
-- Permanent removal from the history (distinct from the TTL-driven purge,
-- which is maintenance). Scoped to the owning user and book.
-- name: PurgeMediaHighlightByID :execrows
DELETE FROM media_highlights
WHERE id = $1 AND user_id = $2 AND media_item_id = $3 AND deleted = TRUE;
-- name: PurgeMediaNoteByID :execrows
DELETE FROM media_notes
WHERE id = $1 AND user_id = $2 AND media_item_id = $3 AND deleted = TRUE;
-- name: PurgeMediaBookmarkByID :execrows
DELETE FROM media_bookmarks
WHERE id = $1 AND user_id = $2 AND media_item_id = $3 AND deleted = TRUE;
-- Refresh Tokens queries
-- name: CreateRefreshToken :one
INSERT INTO refresh_tokens (user_id, token, expires_at)