Files
readest/docker/volumes/db/init/schema.sql
T
Huang Xin 082edc204b fix(sync): sync updated book covers across devices (#4544) (#4731)
* docs: design for syncing updated book data (cover + file) (#4544)

Cover-change sync via a content hash (coverHash = partial MD5 of cover.png)
plus a cover_updated_at field-level merge timestamp; file updates ride the
existing re-import / metaHash dedupe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(sync): sync updated book covers across devices (#4544)

Editing a book's cover wrote cover.png locally but changed no hash (the
cover is keyed by the file hash), so peers had no signal to re-download it
and the change never propagated.

Give the cover its own content-addressed version:
- coverHash = partial MD5 of cover.png; a peer re-downloads the cover iff
  the synced hash differs from the local one (idempotent, no churn on
  identical/re-extracted covers — compatible with the metaHash dedupe).
- coverUpdatedAt = field-level LWW timestamp so a page-turn that wins
  whole-row LWW on updated_at can't clobber a cover edit (mirrors the
  reading_status_updated_at fix for #4634).

Editing a cover recomputes the hash, bumps coverUpdatedAt, and re-uploads
only the cover; the server merges cover fields independently; peers
re-download on a hash diff. File updates continue to ride the existing
re-import / metaHash dedupe (changed file -> changed hash -> re-key).

Migration 016 adds cover_hash / cover_updated_at to books.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:31:03 +02:00

136 lines
5.8 KiB
PL/PgSQL

ALTER FUNCTION auth.uid() OWNER TO supabase_auth_admin;
ALTER FUNCTION auth.role() OWNER TO supabase_auth_admin;
CREATE TABLE public.books (
user_id uuid NOT NULL,
book_hash text NOT NULL,
meta_hash text NULL,
format text NULL,
title text NULL,
source_title text NULL,
author text NULL,
"group" text NULL,
tags text[] NULL,
created_at timestamp with time zone NULL DEFAULT now(),
updated_at timestamp with time zone NULL DEFAULT now(),
deleted_at timestamp with time zone NULL,
uploaded_at timestamp with time zone NULL,
synced_at timestamp with time zone NOT NULL DEFAULT now(),
progress integer[] NULL,
reading_status text NULL,
reading_status_updated_at timestamp with time zone NULL,
cover_hash text NULL,
cover_updated_at timestamp with time zone NULL,
group_id text NULL,
group_name text NULL,
metadata json NULL,
CONSTRAINT books_pkey PRIMARY KEY (user_id, book_hash),
CONSTRAINT books_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE
);
-- Server-assigned incremental-pull cursor, decoupled from updated_at (the
-- client event time / sort key). A trigger stamps it on every write so a
-- server-resolved merge propagates without reordering the date-read library.
-- See migration 016_add_books_synced_at.sql (issue #4678).
CREATE INDEX idx_books_user_synced ON public.books (user_id, synced_at);
CREATE OR REPLACE FUNCTION public.set_books_synced_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.synced_at := now();
RETURN NEW;
END;
$$;
CREATE TRIGGER books_set_synced_at
BEFORE INSERT OR UPDATE ON public.books
FOR EACH ROW
EXECUTE FUNCTION public.set_books_synced_at();
ALTER TABLE public.books ENABLE ROW LEVEL SECURITY;
CREATE POLICY select_books ON public.books FOR SELECT TO authenticated USING ((SELECT auth.uid()) = user_id);
CREATE POLICY insert_books ON public.books FOR INSERT TO authenticated WITH CHECK ((SELECT auth.uid()) = user_id);
CREATE POLICY update_books ON public.books FOR UPDATE TO authenticated USING ((SELECT auth.uid()) = user_id);
CREATE POLICY delete_books ON public.books FOR DELETE TO authenticated USING ((SELECT auth.uid()) = user_id);
CREATE TABLE public.book_configs (
user_id uuid NOT NULL,
book_hash text NOT NULL,
meta_hash text NULL,
location text NULL,
xpointer text NULL,
progress jsonb NULL,
rsvp_position text NULL,
search_config jsonb NULL,
view_settings jsonb NULL,
created_at timestamp with time zone NULL DEFAULT now(),
updated_at timestamp with time zone NULL DEFAULT now(),
deleted_at timestamp with time zone NULL,
CONSTRAINT book_configs_pkey PRIMARY KEY (user_id, book_hash),
CONSTRAINT book_configs_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE
);
ALTER TABLE public.book_configs ENABLE ROW LEVEL SECURITY;
CREATE POLICY select_book_configs ON public.book_configs FOR SELECT TO authenticated USING ((SELECT auth.uid()) = user_id);
CREATE POLICY insert_book_configs ON public.book_configs FOR INSERT TO authenticated WITH CHECK ((SELECT auth.uid()) = user_id);
CREATE POLICY update_book_configs ON public.book_configs FOR UPDATE TO authenticated USING ((SELECT auth.uid()) = user_id);
CREATE POLICY delete_book_configs ON public.book_configs FOR DELETE TO authenticated USING ((SELECT auth.uid()) = user_id);
CREATE TABLE public.book_notes (
user_id uuid NOT NULL,
book_hash text NOT NULL,
meta_hash text NULL,
id text NOT NULL,
type text NULL,
cfi text NULL,
xpointer0 text NULL,
xpointer1 text NULL,
text text NULL,
style text NULL,
color text NULL,
note text NULL,
page integer NULL,
global boolean NULL,
created_at timestamp with time zone NULL DEFAULT now(),
updated_at timestamp with time zone NULL DEFAULT now(),
deleted_at timestamp with time zone NULL,
CONSTRAINT book_notes_pkey PRIMARY KEY (user_id, book_hash, id),
CONSTRAINT book_notes_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE
);
ALTER TABLE public.book_notes ENABLE ROW LEVEL SECURITY;
CREATE POLICY select_book_notes ON public.book_notes FOR SELECT TO authenticated USING ((SELECT auth.uid()) = user_id);
CREATE POLICY insert_book_notes ON public.book_notes FOR INSERT TO authenticated WITH CHECK ((SELECT auth.uid()) = user_id);
CREATE POLICY update_book_notes ON public.book_notes FOR UPDATE TO authenticated USING ((SELECT auth.uid()) = user_id);
CREATE POLICY delete_book_notes ON public.book_notes FOR DELETE TO authenticated USING ((SELECT auth.uid()) = user_id);
CREATE TABLE public.files (
id uuid NOT NULL DEFAULT gen_random_uuid(),
user_id uuid NOT NULL,
book_hash text NULL,
file_key text NOT NULL,
file_size bigint NOT NULL,
created_at timestamp with time zone NULL DEFAULT now(),
updated_at timestamp with time zone NULL DEFAULT now(),
deleted_at timestamp with time zone NULL,
CONSTRAINT files_pkey PRIMARY KEY (id),
CONSTRAINT files_file_key_key UNIQUE (file_key),
CONSTRAINT files_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE
);
CREATE INDEX idx_files_user_id_deleted_at ON public.files (user_id, deleted_at);
CREATE INDEX idx_files_file_key ON public.files (file_key);
CREATE INDEX idx_files_file_key_deleted_at ON public.files (file_key, deleted_at);
ALTER TABLE public.files ENABLE ROW LEVEL SECURITY;
CREATE POLICY files_insert ON public.files FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY files_select ON public.files FOR SELECT USING (auth.uid() = user_id AND deleted_at IS NULL);
CREATE POLICY files_update ON public.files FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (deleted_at IS NULL OR deleted_at > now());
CREATE POLICY files_delete ON public.files FOR DELETE USING (auth.uid() = user_id);
GRANT ALL ON public.books TO authenticated;
GRANT ALL ON public.book_configs TO authenticated;
GRANT ALL ON public.book_notes TO authenticated;
GRANT ALL ON public.files TO authenticated;