-- The sync read model: the streams /sync reports that are not room events, -- and the filters a client uploads. -- -- A room event's position in the event stream is its stream_ordering in -- roomserver_events, and nothing here duplicates it. What is here is -- everything else a sync response carries, each with a stream of its own: -- read receipts, to-device messages, presence and account data all change -- without any event being sent, and a client that has seen receipt 7 but -- only event 4 has to be able to say so in one token. -- -- Typing notifications are deliberately absent. They expire after seconds, -- they are worthless after a restart, and writing them to disk would mean a -- row per keystroke-burst per room; they live in memory in the syncapi. -- One counter row per stream, incremented inside the writing transaction. -- Same reasoning as roomserver_sequences: an UPDATE takes a row lock in -- both engines and hands out gapless positions, which a sequence does not. CREATE TABLE IF NOT EXISTS sync_sequences ( name TEXT NOT NULL PRIMARY KEY, value BIGINT NOT NULL ); INSERT INTO sync_sequences (name, value) VALUES ('receipt', 0) ON CONFLICT (name) DO NOTHING; INSERT INTO sync_sequences (name, value) VALUES ('to_device', 0) ON CONFLICT (name) DO NOTHING; INSERT INTO sync_sequences (name, value) VALUES ('account_data', 0) ON CONFLICT (name) DO NOTHING; INSERT INTO sync_sequences (name, value) VALUES ('presence', 0) ON CONFLICT (name) DO NOTHING; -- Account data gets a stream position so that an incremental sync can ask -- for the blobs that changed rather than re-sending every one of them. The -- column goes on the existing table rather than into a parallel one: there -- is exactly one current value per (user, room, type), and a second table -- would be a second thing to keep in step with it. ALTER TABLE user_account_data ADD COLUMN stream_pos BIGINT NOT NULL DEFAULT 0; CREATE INDEX IF NOT EXISTS user_account_data_stream_idx ON user_account_data (localpart, stream_pos); -- Read receipts. Only the latest receipt of each (user, type, thread) in a -- room is kept: a receipt supersedes the one before it, and /sync reports -- the current one rather than a history. CREATE TABLE IF NOT EXISTS sync_receipts ( room_id TEXT NOT NULL, user_id TEXT NOT NULL, -- m.read or m.read.private. receipt_type TEXT NOT NULL, -- The thread the receipt is for, or '' for the room's main timeline. -- Threads are Phase 10; the column is here because the key would -- otherwise have to change when they arrive, and a receipt table is -- not something to migrate twice. thread_id TEXT NOT NULL DEFAULT '', event_id TEXT NOT NULL, -- When the receipt was sent, which clients show as "read at". ts_ms BIGINT NOT NULL, stream_pos BIGINT NOT NULL, PRIMARY KEY (room_id, user_id, receipt_type, thread_id) ); CREATE INDEX IF NOT EXISTS sync_receipts_stream_idx ON sync_receipts (stream_pos); CREATE INDEX IF NOT EXISTS sync_receipts_room_stream_idx ON sync_receipts (room_id, stream_pos); -- To-device messages: a queue per device, drained by the device -- acknowledging a later sync token rather than by reading it. CREATE TABLE IF NOT EXISTS sync_to_device ( stream_pos BIGINT NOT NULL PRIMARY KEY, localpart TEXT NOT NULL, device_id TEXT NOT NULL, sender TEXT NOT NULL, event_type TEXT NOT NULL, content TEXT NOT NULL, sent_at_ms BIGINT NOT NULL ); CREATE INDEX IF NOT EXISTS sync_to_device_device_idx ON sync_to_device (localpart, device_id, stream_pos); -- Presence. One row per user, the current state and nothing historical. CREATE TABLE IF NOT EXISTS sync_presence ( user_id TEXT NOT NULL PRIMARY KEY, presence TEXT NOT NULL, status_msg TEXT NOT NULL DEFAULT '', last_active_ms BIGINT NOT NULL, stream_pos BIGINT NOT NULL ); CREATE INDEX IF NOT EXISTS sync_presence_stream_idx ON sync_presence (stream_pos); -- Uploaded filters. The JSON is stored as the client sent it: a filter is -- returned verbatim by GET /user/{userID}/filter/{filterID}, and clients -- compare what they get back with what they uploaded. CREATE TABLE IF NOT EXISTS sync_filters ( localpart TEXT NOT NULL, filter_id TEXT NOT NULL, filter_json TEXT NOT NULL, created_at_ms BIGINT NOT NULL, PRIMARY KEY (localpart, filter_id) );