Enforce a per-account session limit by evicting the oldest sessions.
Keeps the newest max_sessions sessions and deletes the rest.
Race safety: this function must run inside a transaction alongside the
INSERT that created the new session. All callers satisfy this requirement:
POST /login uses the default transaction: true (framework-managed
transaction wrapping in apply_route_specs)- The
account_token_create RPC handler runs under the dispatcher's
transaction path because its spec declares side_effects: true POST /bootstrap and POST /signup manage their own transactions
and pass the transaction-scoped deps to create_session_and_set_cookie
The transaction makes one creator's INSERT + enforce_limit pair atomic, but it
does not serialize concurrent creators. Under Read Committed, two
transactions can't see each other's uncommitted session row, so each computes
its OFFSET eviction against a stale count, each preserves its own row, and
both commit above max_sessions. A transaction is necessary here but not
sufficient. Closing this needs one serialization point per
(account_id, credential_kind) — a locked parent row or a transaction-scoped
advisory lock — taken before count/evict/insert.
Ordering is created_at DESC, id DESC. The id leg is a stability
tie-breaker, not a recency one: created_at defaults to NOW(), the
*transaction* timestamp, so two sessions born in one transaction — or in two
transactions that started in the same microsecond — tie, and an untied
OFFSET may keep a different set on two evaluations of the same rows. id is
the blake3 token hash, so the tie-break is arbitrary but deterministic; it
makes the survivors reproducible, it does not make the row just inserted a
guaranteed survivor. Only the serialization point above does that.
Expired-but-unreaped rows count toward the cap (the predicate is account_id
alone). Matches the Rust twin query_session_enforce_limit.