A compile-time-derived whole-record builder: A ⇒ GenericData.Record, leaf by leaf, with no codec composition on the hot path (issue #95).
'''The use case.''' Building a fresh generic record from a typed value on a hot path — an ingest side, a replay, a batch flush. The full vulcan Codec[A].encode pays its composition once per FIELD, per LEVEL: a FreeApplicative.analyze, an Either + Chain.one per field, and a put(name, value) hash probe — and a nested sub-record field redoes all of it inside Codec[Sub].encode, which is what the filer measured as ~384–468 B/field on their real ClickInfo. A hand-built .put(pos, value) builder avoids all of it but costs one hand-maintained line per leaf — the exact complaint the filer opened the issue with.
'''What a derived builder is.''' AvroVulcan.recordBuilder walks A's case fields at COMPILE time (the WholeRecordBuilder.RecordShape IR) and emits one runtime assembly call; construction resolves every case field's schema slot by NAME (all-or-nothing, issue #105's doctrine) and validates every arm against the schema it will write into — so toRecord itself is nothing but positional puts: new GenericData.Record(schema), then per field either the value itself (a primitive leaf), a recursive sub-record build (a nested case class — the recursion the filer's positional prototype lacked, which is what made nested shapes pay vulcan's per-sub-record composition), null (a None), or the field type's own leaf codec (everything the fast arms don't cover). Schema-only fields (computed/derived columns the case class doesn't hold) stay at their in-record default.
'''Allocation is the gate, and it is hand-built-equal.''' Per record: the GenericData.Record values array plus one boxed value per primitive leaf — exactly what the hand-built builder allocates; the plans and slots are construction-time. ns/op stays within a small multiple of the hand-built form (one erasure-level dispatch per non-primitive leaf; the primitive bulk is a tight positional loop) and far below the codec composition it replaces — the benchmarks ClickRecordBench measures all of it side by side.
'''Construction is total.''' Every way assembly can fail — a case field no schema column answers for, two case fields claiming one column, an arm disagreeing with its schema field's shape, a non-record schema — comes back as the Exception half of the recordBuilder / derive result, naming the field and the record, BEFORE any record is built. toRecord itself is total for values matching A (a codec-leaf arm that fails encode still throws, per AvroCodec's total-encode convention — that is a codec-definition bug, not a construction condition).
'''The one behavioural difference from Codec[A].encode,''' stated because a wire-compat claim without it would be false precision: a schema-only (computed/derived) column. The codec fills it during encode; the builder leaves the slot at its in-record value (null on a fresh GenericData.Record) — identical to the hand-built .put builder the issue benchmarked, and round-trip-safe through the codec's own decode (which reconstructs A from the fields it knows).
Value parameters
- schema
-
the record schema the builder writes into — the codec's own schema object, so a record built by the builder and a record decoded by the codec share one identity.
Attributes
- Companion
- object
- Source
- WholeRecordBuilder.scala
- Graph
-
- Supertypes
-
class Objecttrait Matchableclass Any