Casts one column's raw row-image bytes to an Elixir term, and owns the per-column
metadata-width split of a TABLE_MAP's raw column_metadata blob.
This is the silent-wrong-value surface of C1, so the whole module is built around never emitting a plausible-but-wrong value. Three failure modes get first-class defence:
- Signedness. Integer width comes from the wire type byte, but signedness
does not — it rides
TABLE_MAPoptional-metadata TLV type 1.cast/4takes the resolved signedness bit as an explicit argument; aBIGINT UNSIGNEDof18446744073709551615must never decode to-1. SETmasquerading asENUM. Both are wire type254(MYSQL_TYPE_STRING) and are distinguishable only by the STRING metadata pair (byte 0 real-type:247=ENUM,248=SET).SET's row image is packed as a bitfield, not an index, so an enum-style decode would be silently wrong. C1 defersSET(roadmap row C4):parse_col_meta/2unpacks the pair to detect it andcast/4fails closed with:unsupported_column_type.- Meta-driven widths. Fractional-second width, decimal precision/scale, blob
length-prefix width and string length all come from the metadata, not the type
byte. A fixed-width assumption both mis-values the column and desynchronises
every column after it.
parse_col_meta/2enumerates every supported type's meta width explicitly and fails closed on the rest, rather than guessing with a 1-byte fallback (the bug inprobe/mysql_binlog_probe.exs:301-311).
Fail-closed contract
Both entry points return {:error, {:unsupported_column_type, detail}} rather than a
value whenever the type, its metadata, or a decoded temporal component is one C1 does
not faithfully support. The owner (Capstan.Assembler) turns that into a stream halt, exactly as
it does for the registry's {:error, :unmapped_table_id} — a column C1 cannot decode
is never silently dropped or passed through raw. detail carries only schema-level
facts (wire type byte, column position); never a column value (Rule 1).
Summary
Types
A single column's parsed metadata — self-describing enough for cast/4 to decode the
value, produced by parse_col_meta/2.
The fail-closed signal: a type/meta/value C1 refuses rather than mis-decode.
Functions
Casts one column's leading bytes of a row image to an Elixir term.
Splits a TABLE_MAP's raw column_metadata blob into one col_meta/0 per column.
Types
@type col_meta() :: {:int, 1 | 2 | 3 | 4 | 8} | {:decimal, non_neg_integer(), non_neg_integer()} | {:varstring, non_neg_integer()} | {:char, non_neg_integer()} | {:enum, 1 | 2} | {:set, 1 | 2} | {:blob, 1..4} | {:datetime, 0..6} | {:timestamp, 0..6} | {:time, 0..6} | :date | {:json, 1..4}
A single column's parsed metadata — self-describing enough for cast/4 to decode the
value, produced by parse_col_meta/2.
@type unsupported() :: {:unsupported_column_type, map()}
The fail-closed signal: a type/meta/value C1 refuses rather than mis-decode.
Functions
@spec cast(col_meta(), boolean(), [String.t()], binary()) :: {:ok, term(), binary()} | {:error, unsupported()}
Casts one column's leading bytes of a row image to an Elixir term.
Arguments:
col_meta— the column'scol_meta/0fromparse_col_meta/2.signed?— the resolved signedness bit (true= signed). Consumed only by integer casts; supplied by the caller from TLV type 1.str_values— the allowed member strings for anENUMcolumn,[]otherwise.bytes— the remaining row-image bytes, positioned at this column's value.
Returns {:ok, value, rest} with the bytes this column consumed removed, or
{:error, {:unsupported_column_type, detail}} (SET, JSON opaque, or an
out-of-range temporal/enum) — never a wrong value and never a raw passthrough.
@spec parse_col_meta([byte()], binary()) :: {:ok, [col_meta()]} | {:error, unsupported()}
Splits a TABLE_MAP's raw column_metadata blob into one col_meta/0 per column.
column_types is the wire type byte per column (in order); column_metadata is the
raw, unsplit blob the decoder stored verbatim. Each type consumes a type-specific
number of metadata bytes — enumerated explicitly here. The first column whose type C1
does not support fails closed: an unknown meta width cannot be skipped without
desynchronising every later column, so the whole table is refused.
Returns {:ok, [col_meta]} aligned with column_types, or
{:error, {:unsupported_column_type, detail}}.