This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 562983649f fix(parquet): write `specification_version = 1` on the
VARIANT logical type (#11961)
562983649f is described below
commit 562983649f933bb7709ce60fe295ff29cf5361ea
Author: Raghuvansh Raj <[email protected]>
AuthorDate: Thu Oct 29 13:14:05 2026 +0000
fix(parquet): write `specification_version = 1` on the VARIANT logical type
(#11961)
# Which issue does this PR close?
- Closes #11917
# Rationale for this change
The Parquet writer annotates Variant columns with `VARIANT` but leaves
`specification_version` unset. The Parquet spec requires the version in
the declaration ([`LogicalTypes.md`
L602-L603](https://github.com/apache/parquet-format/blob/e365c6f7c05eea69c5aa11f1ead6c3db261ca9f9/LogicalTypes.md#L602-L603)),
and Spark 4.1 only reads `VARIANT(1)`
([`ParquetSchemaConverter.scala#L395`](https://github.com/apache/spark/blob/branch-4.1/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetSchemaConverter.scala#L395)),
so every Variant file written by arrow-rs today fails in Spark with
`PARQUET_TYPE_NOT_RECOGNIZED`.
# What changes are included in this PR?
- `logical_type_for_struct` now writes
`LogicalType::variant(Some(VARIANT_SPEC_VERSION as i8))`. The constant
is the existing header-version constant in `parquet-variant`
(`CORRECT_VERSION_VALUE`), renamed to `VARIANT_SPEC_VERSION` and made
`pub`, so the annotation and the metadata header share one source of
truth.
- Existing `write_logical_type` test in `parquet/src/variant.rs` updated
to assert the version.
- New `arrow_variant_to_parquet_variant` schema-conversion test in
`parquet/src/arrow/schema/mod.rs`, next to the UUID and JSON ones.
The reader is unchanged: `try_add_extension_type` already matches
`LogicalType::Variant(_)`, so files written before this change still
read.
# Are these changes tested?
Yes. Both tests fail on `main` and pass with the change. `cargo test -p
parquet --features
variant_experimental,arrow,arrow_canonical_extension_types,serde --lib`,
`--test variant_integration` and `--test arrow_writer` all pass.
# Are there any significant user-facing changes?
Variant files written with `variant_experimental` now carry `VARIANT(1)`
and are readable by Spark. One public addition:
`parquet_variant::VARIANT_SPEC_VERSION` (`pub const u8 = 1`), the
version shared by the metadata header and the logical type annotation.
# AI Use
Prepared with AI assistance; code, tests and links reviewed and run by
the author.
---
parquet-variant/src/variant.rs | 4 +++-
parquet-variant/src/variant/metadata.rs | 16 ++++++++++------
parquet/src/arrow/schema/extension.rs | 10 +++++++++-
parquet/src/arrow/schema/mod.rs | 33 +++++++++++++++++++++++++++++++++
parquet/src/variant.rs | 6 ++++--
5 files changed, 59 insertions(+), 10 deletions(-)
diff --git a/parquet-variant/src/variant.rs b/parquet-variant/src/variant.rs
index 6e57e0bee5..48f38d662d 100644
--- a/parquet-variant/src/variant.rs
+++ b/parquet-variant/src/variant.rs
@@ -17,7 +17,9 @@
pub use self::decimal::{VariantDecimal4, VariantDecimal8, VariantDecimal16,
VariantDecimalType};
pub use self::list::VariantList;
-pub use self::metadata::{EMPTY_VARIANT_METADATA, EMPTY_VARIANT_METADATA_BYTES,
VariantMetadata};
+pub use self::metadata::{
+ EMPTY_VARIANT_METADATA, EMPTY_VARIANT_METADATA_BYTES,
VARIANT_SPEC_VERSION, VariantMetadata,
+};
pub use self::object::VariantObject;
// Publicly export types used in the API
diff --git a/parquet-variant/src/variant/metadata.rs
b/parquet-variant/src/variant/metadata.rs
index 99de7b6447..da45757729 100644
--- a/parquet-variant/src/variant/metadata.rs
+++ b/parquet-variant/src/variant/metadata.rs
@@ -32,9 +32,13 @@ pub(crate) struct VariantMetadataHeader {
offset_size: OffsetSizeBytes,
}
-// According to the spec this is currently always = 1, and so we store this
const for validation
-// purposes and to make that visible.
-const CORRECT_VERSION_VALUE: u8 = 1;
+/// The version of the Variant specification implemented by this crate.
+///
+/// The spec currently defines only version 1. It appears in two places on
+/// disk: the 4-bit version field of every metadata header, and the
+/// `specification_version` of the Parquet `VARIANT` logical type annotation.
+/// Both must carry this value.
+pub const VARIANT_SPEC_VERSION: u8 = 1;
// The metadata header occupies one byte; use a named constant for readability
const NUM_HEADER_BYTES: u32 = 1;
@@ -68,9 +72,9 @@ impl VariantMetadataHeader {
/// - The actual number of bytes, offset_size, is offset_size_minus_one + 1
pub(crate) fn try_new(header_byte: u8) -> Result<Self, ArrowError> {
let version = header_byte & 0x0F; // First four bits
- if version != CORRECT_VERSION_VALUE {
+ if version != VARIANT_SPEC_VERSION {
let err_msg = format!(
- "The version bytes in the header is not
{CORRECT_VERSION_VALUE}, got {version:b}",
+ "The version bytes in the header is not
{VARIANT_SPEC_VERSION}, got {version:b}",
);
return Err(ArrowError::InvalidArgumentError(err_msg));
}
@@ -170,7 +174,7 @@ pub const EMPTY_VARIANT_METADATA_BYTES: &[u8] = &[1, 0, 0];
pub const EMPTY_VARIANT_METADATA: VariantMetadata = VariantMetadata {
bytes: EMPTY_VARIANT_METADATA_BYTES,
header: VariantMetadataHeader {
- version: CORRECT_VERSION_VALUE,
+ version: VARIANT_SPEC_VERSION,
is_sorted: false,
offset_size: OffsetSizeBytes::One,
},
diff --git a/parquet/src/arrow/schema/extension.rs
b/parquet/src/arrow/schema/extension.rs
index 4e1ec75084..11bb97eb59 100644
--- a/parquet/src/arrow/schema/extension.rs
+++ b/parquet/src/arrow/schema/extension.rs
@@ -123,11 +123,19 @@ pub(crate) fn has_extension_type(parquet_type: &Type) ->
bool {
}
/// Return the Parquet logical type to use for the specified Arrow Struct
field, if any.
+///
+/// The Parquet specification requires the `VARIANT` logical type annotation to
+/// carry the version number (`VARIANT(1)`); readers such as Apache Spark
reject
+/// files whose annotation does not carry version 1. The value written is
+/// [`parquet_variant::VARIANT_SPEC_VERSION`], the same version stamped into
+/// every metadata header.
#[cfg(feature = "variant_experimental")]
pub(crate) fn logical_type_for_struct(field: &Field) -> Option<LogicalType> {
+ use parquet_variant::VARIANT_SPEC_VERSION;
use parquet_variant_compute::VariantType;
if field.has_valid_extension_type::<VariantType>() {
- Some(LogicalType::variant(None))
+ // The 4-bit header version always fits the thrift `i8` field.
+ Some(LogicalType::variant(Some(VARIANT_SPEC_VERSION as i8)))
} else {
None
}
diff --git a/parquet/src/arrow/schema/mod.rs b/parquet/src/arrow/schema/mod.rs
index ce993f0872..0c315040ba 100644
--- a/parquet/src/arrow/schema/mod.rs
+++ b/parquet/src/arrow/schema/mod.rs
@@ -2803,6 +2803,39 @@ mod tests {
Ok(())
}
+ #[test]
+ #[cfg(feature = "variant_experimental")]
+ fn arrow_variant_to_parquet_variant() -> Result<()> {
+ use parquet_variant_compute::VariantType;
+ let variant_fields = Fields::from(vec![
+ Field::new("metadata", DataType::BinaryView, false),
+ Field::new("value", DataType::BinaryView, true),
+ ]);
+ let arrow_schema = Schema::new(vec![
+ Field::new("v", DataType::Struct(variant_fields), true)
+ .with_extension_type(VariantType),
+ ]);
+
+ let parquet_schema =
ArrowSchemaConverter::new().convert(&arrow_schema)?;
+
+ // The Parquet spec requires the VARIANT annotation to carry the
+ // specification version (`VARIANT(1)`); Spark rejects files without it
+ let root = parquet_schema.root_schema();
+ assert_eq!(
+ root.get_fields()[0].get_basic_info().logical_type_ref(),
+ Some(&LogicalType::variant(Some(1)))
+ );
+
+ let arrow_schema = parquet_to_arrow_schema(&parquet_schema, None)?;
+ assert!(
+ arrow_schema
+ .field(0)
+ .has_valid_extension_type::<VariantType>()
+ );
+
+ Ok(())
+ }
+
#[test]
fn test_parquet_to_arrow_field_levels_with_virtual_rejects_non_virtual() {
let message_type = "
diff --git a/parquet/src/variant.rs b/parquet/src/variant.rs
index 23f45b10ee..967595ad0d 100644
--- a/parquet/src/variant.rs
+++ b/parquet/src/variant.rs
@@ -247,10 +247,12 @@ mod tests {
assert_eq!(fields.len(), 1);
let field = &fields[0];
assert_eq!(field.name(), "data");
- // data should have been written with the Variant logical type
+ // data should have been written with the Variant logical type, with
the
+ // specification version set (required by the Parquet spec and by
readers
+ // such as Spark, which only accept `VARIANT(1)`)
assert_eq!(
field.get_basic_info().logical_type_ref(),
- Some(&crate::basic::LogicalType::variant(None))
+ Some(&crate::basic::LogicalType::variant(Some(1)))
);
}