-- Hankan sync schema. -- -- Mirrors the client's syncable tables, plus the two columns the client does -- not have: change_seq, which the server assigns and clients use as a -- cursor, and user_id, which is one value today but keeps a second device or -- person from being a migration. -- -- Rows are stored generically: the primary key as text, the row body as -- JSONB. The alternative — six typed tables kept in lockstep with the -- client's migrations — buys nothing here, because the server never reads -- inside a row. It stores and orders them; the client interprets them. CREATE TABLE IF NOT EXISTS sync_row ( user_id TEXT NOT NULL, tbl TEXT NOT NULL, pk TEXT NOT NULL, data JSONB NOT NULL, -- The client's wall clock, and the field last-write-wins compares. updated_at BIGINT NOT NULL, -- A delete. Kept as a row so it can be handed to a device that was -- offline when it happened. deleted BOOLEAN NOT NULL DEFAULT FALSE, -- Server-assigned and monotonic. The cursor a client pages from. change_seq BIGSERIAL NOT NULL, PRIMARY KEY (user_id, tbl, pk) ); -- The pull query is exactly this: everything newer than the client's cursor, -- in assignment order. CREATE INDEX IF NOT EXISTS sync_row_cursor ON sync_row (user_id, change_seq); -- change_seq must advance on every update, or a row edited after a client -- last pulled would sit below that client's cursor and never be delivered. -- Doing it in a trigger means no write path can forget. CREATE OR REPLACE FUNCTION sync_row_bump() RETURNS trigger AS $$ BEGIN NEW.change_seq := nextval('sync_row_change_seq_seq'); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS sync_row_bump_trg ON sync_row; CREATE TRIGGER sync_row_bump_trg BEFORE UPDATE ON sync_row FOR EACH ROW EXECUTE FUNCTION sync_row_bump();