Repository navigation
Description
Describe the bug
When writing an Arrow struct field tagged ARROW:extension:name = arrow.parquet.variant, the Parquet writer annotates the group with VARIANT but leaves specification_version unset:
arrow-rs/parquet/src/arrow/schema/extension.rs
Lines 127 to 134 in 5b02c08
| pub(crate) fn logical_type_for_struct(field: &Field) -> Option<LogicalType> { | |
| use parquet_variant_compute::VariantType; | |
| if field.has_valid_extension_type::<VariantType>() { | |
| Some(LogicalType::variant(None)) | |
| } else { | |
| None | |
| } | |
| } |
pub(crate) fn logical_type_for_struct(field: &Field) -> Option<LogicalType> {
use parquet_variant_compute::VariantType;
if field.has_valid_extension_type::<VariantType>() {
Some(LogicalType::variant(None))This is the only place the writer produces the annotation (arrow_to_parquet_type).
The thrift field is optional i8 specification_version (parquet.thrift L416-L420), so the file is well-formed thrift. But the Parquet spec requires the version in the declaration: "The Variant group must be annotated with the VARIANT logical type, with the version number included in the declaration" (LogicalTypes.md L602-L603), and every example in LogicalTypes.md and VariantShredding.md is VARIANT(1).
Spark 4.1 enforces this. It reads the annotation through parquet-java, whose thrift getter returns 0 when the field is unset (ParquetMetadataConverter L1430-L1432), and Spark only accepts version 1:
case v: VariantLogicalTypeAnnotation if v.getSpecVersion == 1 =>Any other value falls through to unrecognizedParquetTypeError and the read fails with [PARQUET_TYPE_NOT_RECOGNIZED] Unrecognized Parquet type: .... Spark's own writer always sets 1 (L860).
The arrow-rs reader is lenient (try_add_extension_type L50-L55 matches LogicalType::Variant(_)), so arrow-rs round-trips its own files and the gap only shows when another engine reads them.
To Reproduce
Schema conversion alone shows it (feature variant_experimental):
use arrow_schema::{DataType, Field, Schema};
use parquet::arrow::ArrowSchemaConverter;
use parquet::basic::LogicalType;
use parquet_variant_compute::VariantType;
let fields = vec![
Field::new("metadata", DataType::BinaryView, false),
Field::new("value", DataType::BinaryView, true),
];
let v = Field::new("v", DataType::Struct(fields.into()), true)
.with_extension_type(VariantType);
let schema = ArrowSchemaConverter::new()
.convert(&Schema::new(vec![v]))
.unwrap();
let root = schema.root_schema();
assert_eq!(
root.get_fields()[0].get_basic_info().logical_type_ref(),
Some(&LogicalType::variant(Some(1))),
);
// left: Some(Variant(VariantType { specification_version: None }))End to end:
- Write a file with one
arrow.parquet.variantcolumn usingArrowWriter. parquet-schema <file>prints the group asVARIANT(None)(printer.rsL322-L324); parquet-java tools printVARIANT(0).spark.read.parquet(path)on Spark 4.1 fails withPARQUET_TYPE_NOT_RECOGNIZED.
Reproduced 2026-09-25 on Spark 4.1 (AWS Glue) with arrow-rs 56.1.0. Code unchanged on main at 5b02c08 (60.1.0 dev, 2026-10-26).
Expected behavior
The writer sets specification_version = 1, the only version the spec defines, so the file matches VARIANT(1) in LogicalTypes.md and reads in Spark.
Possible fix
Some(LogicalType::variant(Some(1))) in logical_type_for_struct, plus a round-trip test asserting the version on the written schema. Happy to open the PR.
Additional context
Found while working on DataFusion Variant support (apache/datafusion#26517).
Activity
raghuvanshraj commentedon Oct 28, 2026
take
fix(parquet): write `specification_version = 1` on the VARIANT logica…