WholeRecordBuilder

dev.constructive.eo.avro.vulcan.WholeRecordBuilder
See theWholeRecordBuilder companion object
final class WholeRecordBuilder[A]

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 Object
trait Matchable
class Any

Members list

Value members

Concrete methods

def asAvroCodec(using c: Codec[A]): AvroCodec[A]

The AvroCodec with THIS builder as its encode and the in-scope vulcan codec as its decode over THIS builder's schema — the one-line replacement for the hand-written given AvroCodec[A] with { def encode(a) = buildRecord(a) … } the issue's recommendation asked for. Decode errors surface as Left; encode is total (see AvroVulcan for the error mapping).

The AvroCodec with THIS builder as its encode and the in-scope vulcan codec as its decode over THIS builder's schema — the one-line replacement for the hand-written given AvroCodec[A] with { def encode(a) = buildRecord(a) … } the issue's recommendation asked for. Decode errors surface as Left; encode is total (see AvroVulcan for the error mapping).

Attributes

Source
WholeRecordBuilder.scala
def toRecord(a: A): Record

Build the whole record from a. Total for values matching A; every case field's slot is written, schema-only columns keep their in-record default.

Build the whole record from a. Total for values matching A; every case field's slot is written, schema-only columns keep their in-record default.

Attributes

Source
WholeRecordBuilder.scala

Concrete fields

val schema: Schema

Attributes

Source
WholeRecordBuilder.scala