Decodes a full row image from the Capstan.Binlog.Decoder's raw tuple and the
resolved %Capstan.Binlog.TableMap{}.
The decoder splits a ROWS event into {op, table_id, present_bitmap(s), raw} but
leaves every value byte untouched. This module turns raw into decoded rows:
WRITE/DELETEcarry one image per row;UPDATEcarries a before/after image pair per row. A single event carries many rows (e.g. a multi-rowINSERTor a multi-tableUPDATE), sorawis walked until it is exhausted.- Each image starts with a per-row NULL bitmap over the columns present in the
image (its width is driven by the columns-PRESENT bitmap, not the full column
count —
probe:339-351assumes all columns present, which is wrong in general). - Each present, non-null column is cast via
Capstan.Casting.Types.cast/4; a present-null column is recorded asniland consumes no value bytes.
Rows are returned as column_name => value maps, the shape Capstan.Change consumes.
Fail closed, never silently wrong
The single biggest failure mode here is a plausible-but-wrong value, so the decode is built to fail closed at every point one could arise:
- Wrong schema (ADR-0002). A
table_idis resolved to a%TableMap{}by the owner; this module additionally asserts the event'stable_idequals the map's, so a caller that pairsta's rows withtb's map is refused rather than casting against the wrong columns. - Signedness. The per-column signedness bit is resolved from TLV type 1 and passed to the caster. A numeric column with no signedness bitmap fails closed rather than defaulting to signed.
- Unsupported column (
SET, JSON opaque, unknown type/meta). Any caster error aborts the whole decode; a partial or raw-passthrough row is never emitted. - Truncated image. If the raw bytes are too short to hold even a row's NULL
bitmap, the decode returns
{:error, {:truncated_row_image, _}}rather than raising — a clean halt the pipeline can dispatch (Event.parse/1's CRC check makes a truncation here a decode-desync signal, not silent corruption).
All error tuples carry only schema-level facts, never a column value (Rule 1).
Summary
Types
The decoded event: the semantic op plus its rows (UPDATE as before/after pairs).
A fail-closed refusal — an unsupported column, a schema mismatch, missing metadata, or a truncated image.
A decoded row: present columns keyed by name, present-null columns as nil.
Functions
Decodes a Capstan.Binlog.Decoder row tuple against its resolved %TableMap{}.
Types
The decoded event: the semantic op plus its rows (UPDATE as before/after pairs).
@type error() :: {:unsupported_column_type, map()} | {:table_id_mismatch, map()} | {:missing_column_names, map()} | {:truncated_row_image, map()}
A fail-closed refusal — an unsupported column, a schema mismatch, missing metadata, or a truncated image.
A decoded row: present columns keyed by name, present-null columns as nil.
Functions
@spec decode(Capstan.Binlog.Decoder.rows(), Capstan.Binlog.TableMap.t()) :: {:ok, decoded()} | {:error, error()}
Decodes a Capstan.Binlog.Decoder row tuple against its resolved %TableMap{}.
Returns {:ok, {:insert | :delete, [row]}}, {:ok, {:update, [{before, after}]}}, or
{:error, reason} — an unsupported column, a schema mismatch, missing metadata, or a
NULL-bitmap-truncated image all fail closed to an {:error, _} tuple (see the module
doc). A value-image so short that a value runs off the end raises rather than
returning a tuple: Capstan.Binlog.Event.parse/1 CRC-verifies the whole event before
the decoder runs, so that is a decode-desync assertion on already-validated bytes, not
an input-validation path.