multithreaded: chat message storage and retention
The storage half of MT's chat build (wiki livechat-design). No routes and
no trait impls yet; this is the layer they sit on.
038 creates chat_messages. BIGINT identity rather than UUID because the
reconnect cursor (?after=<id>) needs a total order and chat is log-shaped;
this is livechat::MessageId. Three indexes, one per access path that
exists: (community_id, id) for backlog replay, (expires_at) for the sweep,
(community_id, author_id) for the ban purge, which runs on a hot path at
the moment the room is busiest.
039 adds chat_retention_hours and chat_max_messages to communities. 037
gave a community a chat_policy but nothing to build a livechat::Retention
from, and the owner settings screen needs somewhere to write. Both NOT
NULL with defaults matching Retention::forum_default(), both CHECK-bounded
against the crate ceilings, and deliberately no NULL: that would be
exactly the "keep forever" sentinel Retention has no way to express.
Expiry is computed in SQL as now() + make_interval rather than bound as a
chrono timestamp. Not style: the server build unifies sqlx's time and
chrono features, so the compile-time macro infers a TIMESTAMPTZ bind as
time::OffsetDateTime, and unlike an output column a bind parameter's type
cannot be overridden. Computing it server-side keeps all nine statements
compile-time checked, which mutations/moderation.rs had to give up.
Both halves of retention are one statement each. The age sweep is a global
indexed delete, not per-room, since expiry is already on the row. The
count cap is one window-function delete across the whole table rather than
a query per community per tick, most of which would find nothing.
Recompute restamps from created_at, not now(), or an owner touching the
setting would silently extend every old message by a full window.
17 storage tests; 311 integration and 178 unit tests pass, clippy and fmt
clean.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-08 18:23 UTC
"query": "SELECT id,\n author_id,\n body_html,\n EXTRACT(EPOCH FROM created_at)::BIGINT AS \"created_at!\"\n FROM chat_messages\n WHERE community_id = $1\n ORDER BY id DESC\n LIMIT $2",