max / makenotwork
| 1 | -- Chat message storage for the livechat crate (wiki livechat-design). |
| 2 | -- |
| 3 | -- Ephemeral, but written down. Chat expires in both consumers, and messages are |
| 4 | -- stored anyway for two reasons: a deploy is a symlink swap plus a restart with |
| 5 | -- no connection draining, so every client reconnects and needs its backlog |
| 6 | -- replayed from a cursor; and moderation has to reach a message that already |
| 7 | -- scrolled past. `expires_at` is what keeps that from becoming an archive. |
| 8 | -- |
| 9 | -- One room per community, so `community_id` is the room id the crate calls |
| 10 | -- `RoomId`. There is no rooms table: a community with `chat_policy <> 'off'` |
| 11 | -- is the room. |
| 12 | |
| 13 | ( |
| 14 | -- BIGINT identity, not UUID. The reconnect cursor (`?after=<id>`) needs a |
| 15 | -- total order, which a v4 UUID cannot give, and chat is log-shaped. This is |
| 16 | -- `livechat::MessageId`. |
| 17 | id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, |
| 18 | community_id UUID NOT NULL REFERENCES communities(id) ON DELETE CASCADE, |
| 19 | author_id UUID NOT NULL REFERENCES users(mnw_account_id) ON DELETE CASCADE, |
| 20 | |
| 21 | -- Rendered once at insert through docengine's chat preset, never |
| 22 | -- re-rendered: there is no edit path anywhere in the system. Storing the |
| 23 | -- HTML rather than the source is safe for the same reason it is elsewhere |
| 24 | -- in this schema, sanitization happens in docengine and nowhere else. |
| 25 | body_html TEXT NOT NULL, |
| 26 | |
| 27 | created_at TIMESTAMPTZ NOT NULL DEFAULT now, |
| 28 | |
| 29 | -- Computed at insert from the room's retention policy rather than joined at |
| 30 | -- sweep time, which is what lets the sweep be one indexed delete instead of |
| 31 | -- a join against every community's policy. Shortening a room's retention is |
| 32 | -- therefore a recompute UPDATE over that room, which is also what makes a |
| 33 | -- shortened window apply retroactively to messages already sent. |
| 34 | expires_at TIMESTAMPTZ NOT NULL |
| 35 | ); |
| 36 | |
| 37 | -- Backlog replay: `WHERE community_id = $1 AND id > $2 ORDER BY id`, and the |
| 38 | -- newest-window variant a fresh connection takes, `ORDER BY id DESC LIMIT n`. |
| 39 | -- Both are index-only scans on the hottest path chat has. |
| 40 | ON chat_messages (community_id, id); |
| 41 | |
| 42 | -- The retention sweep, run on MT's scheduler: one `DELETE WHERE expires_at < |
| 43 | -- now()` across every room at once. Deliberately not keyed by community: the |
| 44 | -- sweep is global, and making it per-room would turn one statement into one per |
| 45 | -- community. |
| 46 | ON chat_messages (expires_at); |
| 47 | |
| 48 | -- Ban purge: one statement keyed by (room, author). Without this index a ban |
| 49 | -- sequentially scans the room, and a ban is issued on a hot path at exactly the |
| 50 | -- moment the room is busiest. |
| 51 | ON chat_messages (community_id, author_id); |
| 52 |