- Status: Implemented
- Date: 2026-06-18
- Deciders: project maintainer
- Supersedes: —
- Superseded by: —
Java could already decode the canonical variant container (vortex.variant) but
VariantEncodingEncoder threw "encode not yet implemented", so no Java-written file
could carry a variant column. Variant is the biggest external-facing gap: every
JSON-shaped ingest pipeline maps to it.
The Rust reference has two distinct encodings:
vortex.variant— the canonical container. Purely structural:nbuffers = 0, slots[core_storage, shredded]. It stores no bytes itself.core_storagemust be aDType::Variantarray but "may be chunked, constant, or otherwise encoded".shreddedis an optional row-aligned typed tree for selected paths, its dtype recorded inVariantMetadata.shredded_dtype.vortex.parquet.variant— a separate physical encoding (id literally"vortex.parquet.variant"), slots[validity, metadata, value, typed_value], wheremetadataandvalueare opaque Binary columns holding the Apache Variant binary (metadata dictionary + value bytes) per row. This is what Rust writes for real, arbitrary JSON-shaped columns.
A row-varying variant column therefore has (at least) two valid physical layouts. The
Rust test suite itself builds a non-constant column without parquet.variant: a
ChunkedArray of one-row ConstantArrays of variant scalars, wrapped in the canonical
VariantArray.
Encode variant columns using only the canonical vortex.variant container over
existing encodings — no new physical encoding for now.
core_storage is built from per-row inner scalars (Scalar::variant(inner)):
- all rows equal → a single
vortex.constantchild (the constant broadcasts); - varying rows →
vortex.chunked, child 0 = cumulativeu64run offsets, then onevortex.constantper run of equal adjacent values.
Adjacent-equal values are coalesced into runs so an all-equal column collapses to one
constant. Shredding is expressed through the container's shredded child plus
VariantMetadata.shredded_dtype.
Defer vortex.parquet.variant (the efficient Apache-Variant-binary physical
encoding) until a concrete need for arbitrary JSON-shaped object columns arrives.
- No new
EncodingId, decoder, encoder, proto message, or reader array type. Reusesvortex.variant+vortex.chunked+vortex.constant, all already round-tripped. - Matches a layout the Rust reference produces and reads, so Java↔Rust interop holds (verified via JNI: rowCount + arrow schema for constant and row-varying columns).
- Keeps the variant surface small and reviewable; ships value now.
- Per-row values are limited to typed scalars wrapped as variants, not arbitrary
Apache Variant binary objects (nested JSON). Real object columns need
parquet.variant. - One
vortex.constantper distinct run is space-inefficient for high-cardinality columns versus two packed Binary columns.
- Heterogeneous inner dtypes across rows: the chunked-of-constants read path assumes a
consistent inner dtype. Mixed-type variant columns are out of scope until
parquet.variant. - When
parquet.variantlands it becomes a second physical layout for the same logical column; readers must handle both (the canonical container already abstracts this).
- Implement
vortex.parquet.variantnow. Rejected for the first milestone: new encoding id + decoder + encoder +ParquetVariantMetadata{has_value, typed_value_dtype, value_nullable}proto (regeneration) + reader array type — a much larger surface, not needed until arbitrary object columns are required. Tracked as the next step.
- Rust:
vortex-array/src/arrays/variant/vtable/mod.rs(vortex.variant),encodings/parquet-variant/src/vtable.rs(vortex.parquet.variant),vortex-array/src/arrays/chunked/array.rs(cumulativechunk_offsets). - Commits
35438c72(Layer A constant),3b1be436(Layer B chunked constants).