Capstan.Casting.Types (Capstan v0.1.0)

Copy Markdown View Source

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_MAP optional-metadata TLV type 1. cast/4 takes the resolved signedness bit as an explicit argument; a BIGINT UNSIGNED of 18446744073709551615 must never decode to -1.
  • SET masquerading as ENUM. Both are wire type 254 (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 defers SET (roadmap row C4): parse_col_meta/2 unpacks the pair to detect it and cast/4 fails 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/2 enumerates every supported type's meta width explicitly and fails closed on the rest, rather than guessing with a 1-byte fallback (the bug in probe/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

col_meta()

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

unsupported()

@type unsupported() :: {:unsupported_column_type, map()}

The fail-closed signal: a type/meta/value C1 refuses rather than mis-decode.

Functions

cast(col_meta, signed?, str_values, bytes)

@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's col_meta/0 from parse_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 an ENUM column, [] 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.

parse_col_meta(column_types, column_metadata)

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