# `DrinkMeAlice.Tx`

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`.

# `outcome`

```elixir
@type outcome() ::
  {:committed, map()}
  | {:existing, map()}
  | {:rejected, DrinkMeAlice.Error.t()}
  | {:unknown, t()}
  | {:unavailable, DrinkMeAlice.Error.t()}
```

# `t`

```elixir
@type t() :: %DrinkMeAlice.Tx{
  ctx: %{
    commit: non_neg_integer(),
    decision: non_neg_integer(),
    len: pos_integer()
  },
  sql: binary(),
  vars: map()
}
```

# `verdicts`

```elixir
@type verdicts() :: %{
  required(binary()) =&gt;
    :committed
    | :existing
    | {:rejected, binary()}
    | {:rejected, binary(), term()}
}
```

# `build`

```elixir
@spec build([binary()], map()) :: {:ok, t()} | {:error, {:bad_tx, atom()}}
```

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`

```elixir
@spec classify(map(), verdicts(), t()) :: outcome()
```

Classify an already-received reply for `tx` (exposed for callers with their own transport).

# `run`

```elixir
@spec run(GenServer.server(), t(), verdicts(), keyword()) :: outcome()
```

Submit `tx` through a `Conn` owner and classify the result.

- `{:committed, decision_map}` / `{:existing, decision_map}` — per
  `verdicts`, with the whole `RETURN` map;
- `{: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`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
