Skip to content

[Variant] Parquet writer omits specification_version on the VARIANT logical type #11917

Closed

Description

@raghuvanshraj

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:

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:

https://github.com/apache/spark/blob/branch-4.1/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaConverter.scala#L395

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:

  1. Write a file with one arrow.parquet.variant column using ArrowWriter.
  2. parquet-schema <file> prints the group as VARIANT(None) (printer.rs L322-L324); parquet-java tools print VARIANT(0).
  3. spark.read.parquet(path) on Spark 4.1 fails with PARQUET_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

  1. raghuvanshraj commented on Oct 28, 2026

    @raghuvanshraj
    ContributorAuthor

    take

  2. added a commit that references this issue on Oct 29, 2026
    5629836
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions