refactor(arrow/avro): migrate from hamba/avro to twmb/avro#830
Conversation
Switch the avro reader from hamba/avro to twmb/avro for schema parsing
and value decoding. The new library hands back typed Go values (e.g.
*big.Rat for decimals, [16]byte for fixed-uuid) instead of []byte, which
shortens the data-side helpers and lets us fix a stack of bugs the old
[]byte arms were masking.
The exported arrow/avro surface is preserved: NewOCFReader, OCFReader
and its methods, the Option/With… helpers, and ArrowSchemaFromAvro
all keep their signatures. A new ArrowSchemaFromAvroJSON is added
alongside as the recommended entry point — it doesn't couple callers
to a particular Avro library through its type signature.
ArrowSchemaFromAvro is now marked Deprecated and serializes the hamba
schema via json.Marshal before re-parsing with twmb; the
github.com/hamba/avro/v2 dependency is kept so downstream callers
continue to compile.
Newly supported logical types (the mapping had them commented out
under hamba):
- local-timestamp-millis
- local-timestamp-micros
- local-timestamp-nanos
- timestamp-nanos
Fixes:
- fixed(16)+uuid rows are no longer silently dropped — the
UUIDBuilder now handles [16]byte directly.
- ArrowSchemaFromAvro (deprecated wrapper) preserves logical-type
annotations on fixed (uuid/decimal/duration). The wrapper now uses
json.Marshal, which dispatches to each schema type's MarshalJSON;
hambaAvro.Schema.String() returns Avro Parsing Canonical Form,
which strips logical types by spec, and was the wrong serializer
to bridge between libraries.
- Heterogeneous and non-nullable multi-branch unions (e.g.
["null", A, B], ["int", "string"]) used to silently mis-decode
against the wrong branch or fall through to an opaque downstream
nil-field error. They now fail upfront with "unsupported avro
union at <path>".
- Nullable unions written as ["T", "null"] (with null in the second
position) now resolve to T as expected; the old code always took
Types()[1] and produced a Null-typed column for that ordering.
- Map-of-primitive schemas (e.g. {"type":"map","values":"int"}) now
parse; the old code asserted the value type to NamedSchema and
panicked on primitive values.
- Unknown SchemaNode types used to leave a nameless nil-typed field
that exploded in the record builder; now they fail with
"unhandled avro type %q", surfaced as "invalid avro schema".
- Duration's []byte arm read Uint16 with gaps and overflowed uint32
on the milli multiply — gone; only the avro.Duration arm remains.
- append*Data helpers no longer silently no-op or fmt.Sprint-coerce
unknown inputs; they return "unsupported value of type %T".
- Enum-symbol mismatches in appendBinaryDictData surface a clear
error instead of a generic dictionary builder failure.
- Schema-conversion errors wrap with %w so callers can
errors.Unwrap past arrow.ErrInvalid.
OCF wire format is unchanged: twmb/avro/ocf supports the same codecs
(null, deflate, snappy, zstd) and files written by hamba remain
readable.
zeroshade
left a comment
There was a problem hiding this comment.
Can you rebase and resolve the conflicts please?
| node.namedCache[s.Name] = s | ||
| if s.Namespace != "" { | ||
| node.namedCache[s.Namespace+"."+s.Name] = s | ||
| } |
There was a problem hiding this comment.
It's worth noting that the hamba SchemaCache keyed on full name only, so for schemas with two records named Foo in different namespaces a bare {"type": "Foo"} will resolve to whichever was defined latest as opposed to being an error (the previous behavior under hamba).
We should either restrict to full-name lookups or document the change in behavior here that where a malformed schema may now silently produce something where hamba would have errored.
| case []byte: | ||
| buf := bytes.NewBuffer(dt) | ||
| if len(dt) <= 38 { | ||
| var intData int64 | ||
| err := binary.Read(buf, binary.BigEndian, &intData) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| b.Append(decimal128.FromI64(intData)) | ||
| } else { | ||
| var bigIntData big.Int | ||
| b.Append(decimal128.FromBigInt(bigIntData.SetBytes(buf.Bytes()))) | ||
| } | ||
| case map[string]any: |
There was a problem hiding this comment.
If anyone was using custom-decoders that emitted []byte or map[string]any etc. they are now going to see "unsupported value of type X" errors where old code would have accepted the input. Let's update the description on the PR (and therefore the resulting changelog later) to note this change in behavior for anyone who was using custom-decoders.
| case 36: | ||
| return b.AppendValueFromString(string(dt)) |
There was a problem hiding this comment.
let's add a comment here explaining why we have a case for 36 (i guess, hex-dash UUID string?)
| for i := range s.Branches { | ||
| b := s.Branches[i] |
There was a problem hiding this comment.
| for i := range s.Branches { | |
| b := s.Branches[i] | |
| for _, b := range s.Branches { |
| root := schema.Root() | ||
| n := newSchemaNode() | ||
| n.schema = schema | ||
| c := n.newChild(n.schema.(avro.NamedSchema).Name(), n.schema) | ||
| n.node = root | ||
| c := n.newChild(root.Name, root) |
There was a problem hiding this comment.
OCF requires a named record at the top, we should probably add a guard or explicit "must be named record" check here (the hamba code would have panic'd).
Rationale for this change
Closes #813
What changes are included in this PR?
Primarily, the lib was changed. New UUID type support was added. Parsing simplified, that's thanks to the new lib that doesn't use
map[string]anywrapper.Are these changes tested?
All tests pass, new test for the UUID type was added
Are there any user-facing changes?
The
ArrowSchemaFromAvrofunction was deprecated, as it causes coupling with 3rd-party Avro module