Two Avro files, both manifest_file/manifest_entry records:
- the manifest list (
snap-<id>-<n>-<uuid>.avro) names every manifest in a
snapshot, with the counts and per-partition-field summaries a scan uses to
skip manifests without opening them;
- each manifest lists data or delete files with their partition tuple,
per-column metrics, and the entry status that drives inheritance.
Three inheritance rules are implemented here, because getting them wrong
silently mis-plans a scan:
- Sequence numbers. A
null sequence_number / file_sequence_number is
inherited from the manifest's entry in the manifest list — but only for
ADDED entries. EXISTING and DELETED entries must carry both explicitly, and a
v1 manifest with no sequence column defaults every file to 0.
- Snapshot id. A
null snapshot_id is inherited from the manifest's
added_snapshot_id.
first_row_id (v3). A null value is the manifest's first_row_id plus
the record counts of every preceding data file in that manifest that also had
a null first_row_id.
Fields this build does not recognise are ignored rather than fatal, and the
reserved id 141 and the v4-only content_stats (146) are simply not read.
Both files are read with avro.mojo's RecordCursor rather than its Value
API. Manifests sit on the scan-planning critical path and a Value costs
roughly sixty heap allocations per entry, plus a string comparison per field
lookup. The cursor compiles the file's own schema into a decode plan once,
and every field this module wants is then a slot number resolved once per
file (_ListSlots / _EntrySlots) — the per-entry work is integer indexing
and, for a path or a bound, a span into the block buffer. A field the
manifest's schema does not have resolves to slot -1, which is how the
version differences below stay expressible.
Once per file is still once too many. A snapshot's five hundred
manifests were written by one writer and carry five hundred byte-identical
copies of the same avro.schema, the same Iceberg schema and the same
partition-spec. ManifestCache keys everything those three determine —
the compiled decode plan, the parsed schema and spec, the partition type and
the resolved slot table — on the raw metadata bytes, before any of it is
parsed. Byte equality is the cheap sufficient condition, and the hash over
those bytes is only a filter: a hit is confirmed by comparing them. A scan
owns one cache and drops it when it is done; there is no global state, and a
plan handed out stays alive on its own because a cursor holds its own
reference to it.