Capstan.Binlog.Decoder (Capstan v0.1.0)

Copy Markdown View Source

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

Summary

Types

A successfully decoded event body.

A reason C1 refuses an event outright.

A single GTID: {server_uuid, transaction_sequence_number}.

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

Functions

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

Types

decoded()

@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}

A successfully decoded event body.

error_reason()

@type error_reason() ::
  :compressed_payload_unsupported | {:unknown_event_type, byte()}

A reason C1 refuses an event outright.

gtid()

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

A single GTID: {server_uuid, transaction_sequence_number}.

rows()

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

Functions

decode(event)

@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).