# `Capstan.Binlog.Decoder`
[🔗](https://github.com/baselabs/capstan/blob/v1.3.1/lib/capstan/binlog/decoder.ex#L74)

Per-type binlog event body decoder — the second decode layer, over
`Capstan.Binlog.Event`.

`Capstan.Binlog.Event.parse/1` has already parsed the 19-byte header and verified
and stripped the CRC32 trailer. This module dispatches on the event **type byte**
and decodes `event.body` into a per-type term. It does **not** re-parse the header
or re-verify the CRC.

## Return contract

`decode/1` returns one of three shapes:

  * `{:ok, decoded}` — a recognised event, decoded to its per-type term.
  * `{:halt, :unsupported_transaction_shape}` — an `XA_PREPARE_LOG_EVENT` (type 38).
    This is a **fail-closed control signal**, distinct from both success and a
    decode error, so a consumer that omits a `{:halt, _}` clause crashes loudly
    rather than silently treating XA rows as a normal event (ADR-0003).
  * `{:error, reason}` — an event C1 refuses: a malformed/unconsumable
    transaction payload, or an unknown type byte. An unknown type **fails
    closed** and is never silently skipped — a dropped event of an
    unrecognised shape could hide a condition capstan must halt on.

## Two silent-failure safety branches (the reason this module is high-Risk)

  * **`ROWS_QUERY_LOG_EVENT` (29)** carries the complete original SQL of the row
    change with every literal value — a total Rule-1 leak if surfaced. The header
    is recognised and the body is **discarded**: `decode/1` returns
    `{:rows_query, :discarded}` and the SQL text never enters the returned term.
  * **`XA_PREPARE_LOG_EVENT` (38)** halts fail-closed. Its row images ride the
    *prepare* event and are committed later by a separate `XID` — or discarded by
    `XA ROLLBACK` — so treating them as committed delivers rolled-back rows
    effect-once. The halt is keyed purely on the type byte (ADR-0003).

## Rule-1 note on QUERY text

For `QUERY` (2) the SQL text **is** returned — `Capstan.Assembler` inspects it to
detect `BEGIN`/`COMMIT` and self-committing DDL terminators. That raw text is
Rule-1-sensitive (DDL routinely embeds literals like `DEFAULT 'secret'`) and must
never be logged. DDL redaction happens later, at the `%Capstan.SchemaChange{}`
boundary (Rule 1), not in this module.

# `decoded`

```elixir
@type decoded() ::
  {:rotate, String.t(), non_neg_integer()}
  | {:format_description, non_neg_integer(), String.t()}
  | :previous_gtids
  | {:gtid, gtid()}
  | {:query, %{schema: String.t(), sql: String.t()}}
  | Capstan.Binlog.TableMap.t()
  | rows()
  | {:xid, non_neg_integer()}
  | :stop
  | :heartbeat
  | {:rows_query, :discarded}
  | {:xa_prepare, Capstan.Xa.Id.xid()}
  | {:transaction_payload, [Capstan.Binlog.Event.t()]}
```

A successfully decoded event body.

# `error_reason`

```elixir
@type error_reason() :: {:unknown_event_type, byte()} | term()
```

A reason C1 refuses an event outright.

# `gtid`

```elixir
@type gtid() :: {String.t(), pos_integer()}
```

A single GTID: `{server_uuid, transaction_sequence_number}`.

# `rows`

```elixir
@type rows() ::
  {:write_rows, non_neg_integer(), binary(), binary()}
  | {:delete_rows, non_neg_integer(), binary(), binary()}
  | {:update_rows, non_neg_integer(), binary(), binary(), binary()}
```

The structural decode of a row event body: identity, present bitmap(s), raw image bytes.

# `decode`

```elixir
@spec decode(Capstan.Binlog.Event.t()) ::
  {:ok, decoded()}
  | {:halt, :unsupported_transaction_shape}
  | {:error, error_reason()}
```

Decodes one `%Capstan.Binlog.Event{}` body to its per-type term.

See the module doc for the `{:ok, _}` / `{:halt, _}` / `{:error, _}` contract and
the two safety branches (ROWS_QUERY discard, XA_PREPARE halt).

---

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