DrinkMeAlice. Tx
(drink_me_alice v0.1.0)
Copy Markdown
Transaction scripts with a classifier-ready shape, and the one write path that maps transport facts plus the reply into a write outcome.
A script is BEGIN TRANSACTION; …; COMMIT TRANSACTION; RETURN {…} —
every decision executes inside the database transaction, and the
trailing RETURN carries a map with a decision string. build/2
records the positional context %{commit: i, decision: j, len: n}
that DrinkMeAlice.Outcome needs: the pinned 3.0.5 executor pushes
a COMMIT result only on success, so a failed reply is one entry
shorter and a shifted RETURN must never read as commit success.
String-typed params need care on 3.0.5, which coerces JSON params
that merely look typed: a <string>$p cast repairs UUID-shaped
strings and record-id-shaped strings, but NOT date-shaped strings
("2026-10-01" comes back rewritten even under the cast). For an
exact string at any depth, send DrinkMeAlice.exact_string/1 and
read it back with DrinkMeAlice.exact_string_sql/1.
Summary
Functions
Assemble a script from its statements (without trailing ;).
Classify an already-received reply for tx (exposed for callers with their own transport).
Submit tx through a Conn owner and classify the result.
Types
@type outcome() :: {:committed, map()} | {:existing, map()} | {:rejected, DrinkMeAlice.Error.t()} | {:unknown, t()} | {:unavailable, DrinkMeAlice.Error.t()}
@type t() :: %DrinkMeAlice.Tx{ ctx: %{ commit: non_neg_integer(), decision: non_neg_integer(), len: pos_integer() }, sql: binary(), vars: map() }
Functions
Assemble a script from its statements (without trailing ;).
The shape is checked, not assumed: the first statement is
BEGIN TRANSACTION, exactly one COMMIT TRANSACTION sits immediately
before the last statement, the last statement starts with RETURN,
and the joined SQL is at most 65536 bytes.
{:error, {:bad_tx, why}} otherwise.
At least TWO in-transaction statements are required (5 statements
total, at most 256). With a single in-tx statement the
pinned executor's commit-failure rewrite is indistinguishable from a
statement abort, so every failure would classify as unknown — a
script that can never be definitely rejected. Prepend a harmless
LET if the work is one statement.
Classify an already-received reply for tx (exposed for callers with their own transport).
@spec run(GenServer.server(), t(), verdicts(), keyword()) :: outcome()
Submit tx through a Conn owner and classify the result.
{:committed, decision_map}/{:existing, decision_map}— perverdicts, with the wholeRETURNmap;{:rejected, error}— a mapped rejection verdict or a qualified statement abort (definitely not committed);{:unknown, tx}— the request may have committed (lost reply, inconclusive reply shape). Reconcile by resubmitting the identical idempotent script or by reading; never treat as failure;{:unavailable, error}— certified not submitted (credit bound, connection down, deadline passed before send, session refused, latched clock uncertainty — gated at dispatch inside the owner), or the request was over the size bound.
The whole submission — admission, deadline and the clock gate — runs
under Conn.call/3's bounded protocol; there is no separate control
call, so a missing owner is a typed {:unavailable, _} and a blocked
owner is bounded by the caller's own deadline.
Options: deadline_ms, class.