Capstan.Binlog.TableRegistry (Capstan v0.1.0)

Copy Markdown View Source

The table_id%Capstan.Binlog.TableMap{} registry — the schema-resolution core (ADR-0002).

A row event names its table only by a numeric table_id. This registry is the one place that id is resolved to a decoded %TableMap{}, and resolution is by the row event's OWN table_id — never by "the most recent TABLE_MAP". A single multi-table statement interleaves TABLE_MAP(ta) → TABLE_MAP(tb) → ROWS(ta) → ROWS(tb) (live-verified), so both maps are live at once; "last map wins" would cast ta's rows against tb's schema and emit silently wrong values.

Storage and resolution only

This module keys a plain map by table_id and hands back the stored struct verbatim. It parses no bytesCapstan.Binlog.Decoder already owns the entire TABLE_MAP body, including the optional-metadata TLVs, and produces the %TableMap{} structs this registry stores. Adding any parsing here would cross that ownership boundary.

Invalidation

table_id is unstable: it moves across DDL (ta observed moving 92 → 94 across an ALTER) and a freed id is later reused for a different table. Within a binlog file the server re-emits a TABLE_MAP for a table_id before the row events that use it, so put/2 overwriting a reused id keeps the binding current. Across a file boundary the ids reset wholesale, so the owner calls invalidate/1 on ROTATE and FORMAT_DESCRIPTION to drop every binding — a table_id carried over from the previous file must never resolve to a stale map. The owner is the pure Capstan.Assembler fold, which threads this registry and invalidates it inline; Capstan.AssemblerServer drives that fold.

Unmapped ids fail closed

A table_id with no live binding resolves to {:error, :unmapped_table_id} — never a guess and never the "nearest" map. The owner turns this into a halt; silently dropping the row would be exactly the loss C1 refuses.

Summary

Types

t()

The registry: a map from table_id to its currently-bound %TableMap{}. Build and inspect it only through this module — treat it as opaque.

Functions

Drops every binding, returning an empty registry. The owner calls this on ROTATE and FORMAT_DESCRIPTION, where table_ids reset and any retained binding would be stale.

An empty registry with no table bindings.

Binds table_map under its own table_id, overwriting any prior binding for that id. A table_id reused after DDL therefore takes the newest map (see the moduledoc).

Resolves a row event's table_id to its bound %TableMap{}, or {:error, :unmapped_table_id} when nothing is bound (fail closed).

Types

t()

@opaque t()

The registry: a map from table_id to its currently-bound %TableMap{}. Build and inspect it only through this module — treat it as opaque.

Functions

invalidate(registry)

@spec invalidate(t()) :: t()

Drops every binding, returning an empty registry. The owner calls this on ROTATE and FORMAT_DESCRIPTION, where table_ids reset and any retained binding would be stale.

new()

@spec new() :: t()

An empty registry with no table bindings.

put(registry, table_map)

@spec put(t(), Capstan.Binlog.TableMap.t()) :: t()

Binds table_map under its own table_id, overwriting any prior binding for that id. A table_id reused after DDL therefore takes the newest map (see the moduledoc).

resolve(registry, table_id)

@spec resolve(t(), non_neg_integer()) ::
  {:ok, Capstan.Binlog.TableMap.t()} | {:error, :unmapped_table_id}

Resolves a row event's table_id to its bound %TableMap{}, or {:error, :unmapped_table_id} when nothing is bound (fail closed).