DrinkMeAlice.Outcome (drink_me_alice v0.1.0)

Copy Markdown

Classifies raw SurrealDB replies for possibly-submitted mutations and reads into facts.

The core rule: once a request may have entered the write path, a lost or malformed reply is {:unknown, request} — never silently rejected or committed. {:refused, …} facts exist only where the server gave qualified evidence the request could not have executed through a request-level or session-level RPC refusal. Incomplete replies remain inconclusive.

SurrealDB 3.0.5 statement-result semantics used here (pinned dbs/executor.rs:675-1035 — review-44 correction):

  • A query reply's result is a statement array. COMMIT has its own slot ONLY on success: the inner executor pushes the COMMIT result after txn.commit() returns ok (~:917), and consumes COMMIT with NO result entry on every failure path. A failed reply is therefore one entry SHORTER than the script — the trailing RETURN executes at the nominal COMMIT index. The classifier keys on the expected total length (ctx.len from DrinkMeAlice.Tx), never on the COMMIT index alone.
  • Full-length reply (len == ctx.len): results[ci] is the real COMMIT slot. OK is observed commit success — a post-commit error cannot un-commit it, so a missing/failed decision is :committed_no_decision, never a false rejection. Any ERR at ci in a full-length array is an unqualified shape (the pinned executor never pushes a COMMIT error) — inconclusive.
  • Shortened reply (len == ctx.len - 1): the COMMIT entry was omitted; results[ci] is the shifted RETURN and is NEVER commit evidence. Qualified shapes over the in-tx entries at 1..ci-1 (statement errors carry kind/details at ENTRY level with result a message STRING — pinned dbs/response.rs:78-90,113-164, confirmed by the live capture memory-reply-shapes-live-02.txt):
    • statement abort: BEGIN {status: "OK", result: nil} at 0 (outside the rewrite's [start_results..] range), entries before the failing statement are NotExecuted markers ({status: "ERR", kind: "Query", details: {kind: "NotExecuted"}, result: <string>}), the failing statement carries the real error (a non-marker ERR with a message-string result), and every statement after it is a Cancelled marker — exactly ONE real error, marker order NotExecuted-before / Cancelled-after → definite non-commit ({:rejected, _}).
    • homogeneous cancellation: ALL in-tx entries are Cancelled markers (ctx-done path) or ALL are NotExecuted markers (tx-creation failure) — definite non-commit. Mixed marker sets are not produced → unqualified. A shortened reply needs ≥2 in-tx entries (ci ≥ 3) for the single-error abort qualification: with one in-tx statement the commit-failure rewrite produces exactly one Internal error — indistinguishable from a lone abort error without message substrings.
  • Commit failure (txn.commit() error, ~:930): every in-tx entry is rewritten to Internal("Query not executed: {e}") — MULTIPLE real errors — and surrealkv/mod.rs commits the storage transaction before commit_coordinator.wait_for_sync, so the write may already be durable. No rollback certificate exists on this wire shape — always inconclusive. Earlier "real" errors in that reply are rewrites, not abort causes.
  • A full-length reply must have OK status on every pre-COMMIT entry, result: nil (NONE) at BEGIN and COMMIT — a contradictory pre-commit error or a non-nil COMMIT cannot establish success.
  • THROW surfaces as entry-level kind: "Thrown" with the thrown tag inside the message-string result.
  • Any other length, an OK mid-tx entry inside a shortened reply, a non-OK BEGIN, or any silhouette not listed above is unsupported — inconclusive. A shifted RETURN is never read as COMMIT success.

Every statement entry must match the {status: "OK"|"ERR", result: _} shape — unknown statuses, scalars or malformed entries make the whole reply inconclusive rather than letting a valid-looking decision overshadow corrupt neighbours.

Summary

Functions

Classify a read reply — single RETURN statement. Returns {:ok, map} | {:error, :session | :rpc_error | :bad_shape}.

Classify a mutation reply.

True when the error CODE ALONE proves a session refusal — only -32002. -32000 requires structured evidence; use session_refusal?/1 on the whole error object.

True when the wire error object is a session refusal: -32002 InvalidAuth outright, or -32000 carrying the exact expired-session wrapper chain the pinned 3.0.5 server emits — kind: "NotAllowed", details.kind: "Auth", details.details.kind: "SessionExpired". A SessionExpired kind under any other wrapper, a malformed chain, or a bare -32000 is generic internal evidence: NOT a session refusal (review-13).

Types

fact()

@type fact() ::
  {:committed, map()}
  | {:existing, map()}
  | {:rejected, DrinkMeAlice.Error.t()}
  | {:session_refused}
  | {:refused, integer()}
  | {:inconclusive, term()}

Functions

classify_read(arg1)

@spec classify_read(map()) :: {:ok, map()} | {:error, atom()}

Classify a read reply — single RETURN statement. Returns {:ok, map} | {:error, :session | :rpc_error | :bad_shape}.

classify_tx(reply, verdicts, ctx \\ nil)

@spec classify_tx(map(), map(), map() | nil) :: fact()

Classify a mutation reply.

verdicts maps the script's decision strings to outcome atoms used by the caller. ctx carries %{commit: i, decision: j, len: n} — the nominal COMMIT index, the trailing RETURN index, and the script's statement count, emitted by DrinkMeAlice.Tx builders (len defaults to j + 1: RETURN is the last statement). When ctx is absent (tests, migrations-side replies), the strict fallback runs: a decision is honoured only if no entry carries any error and the complete array is well-shaped.

session_code?(code)

@spec session_code?(integer()) :: boolean()

True when the error CODE ALONE proves a session refusal — only -32002. -32000 requires structured evidence; use session_refusal?/1 on the whole error object.

session_refusal?(err)

@spec session_refusal?(map()) :: boolean()

True when the wire error object is a session refusal: -32002 InvalidAuth outright, or -32000 carrying the exact expired-session wrapper chain the pinned 3.0.5 server emits — kind: "NotAllowed", details.kind: "Auth", details.details.kind: "SessionExpired". A SessionExpired kind under any other wrapper, a malformed chain, or a bare -32000 is generic internal evidence: NOT a session refusal (review-13).