This is an automated email from the ASF dual-hosted git repository.

Jefffrey 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 4da9d64a59 [Variant] Unshredded `VariantArray` producers annotate the 
`value` field as nullable (spec requires `required`) (#10320)
4da9d64a59 is described below

commit 4da9d64a59c765700486c404f76a0f23cdb394f0
Author: Kosta Tarasov <[email protected]>
AuthorDate: Fri Aug 7 21:32:59 2026 -0400

    [Variant] Unshredded `VariantArray` producers annotate the `value` field as 
nullable (spec requires `required`) (#10320)
    
    # Which issue does this PR close?
    
    <!--
    We generally require a GitHub issue to be filed for all bug fixes and
    enhancements and this helps us generate change logs for our releases.
    You can link an issue to this PR using the GitHub syntax.
    -->
    
    - Closes #10315.
    
    - let's wait till #10318 ships
    
    # Rationale for this change
    
    - Parquet spec required unshredded `VariantArray` to be non-nullable.
    Other readers might reject our impl because `unshred_variant` and
    `variant_get as_type None` return nullable unshredded Variant.
    <!--
    Why are you proposing this change? If this is already explained clearly
    in the issue then this section is not needed.
    Explaining clearly why changes are proposed helps reviewers understand
    your changes and offer better suggestions for fixes.
    -->
    
    # What changes are included in this PR?
    
    - builds on #10318
    - Add changes to `unshred_variant` and `variant_get` to return
    non-nullable unshredded Variant
    <!--
    There is no need to duplicate the description in the issue here but it
    is sometimes worth providing a summary of the individual changes in this
    PR.
    -->
    
    # Are these changes tested?
    
    - yes, added new unit tests and fixed some old ones
    <!--
    We typically require tests for all PRs in order to:
    1. Prevent the code from being accidentally broken by subsequent changes
    2. Serve as another way to document the expected behavior of the code
    
    If tests are not included in your PR, please explain why (for example,
    are they covered by existing tests)?
    
    If this PR claims a performance improvement, please include evidence
    such as benchmark results.
    -->
    
    # Are there any user-facing changes?
    
    - Yes, the nullability now conforms the spec
    <!--
    If there are user-facing changes then we may require documentation to be
    updated before approving the PR.
    
    If there are any breaking changes to public APIs, please call them out.
    -->
    
    ---------
    
    Co-authored-by: Jeffrey Vo <[email protected]>
---
 parquet-variant-compute/src/unshred_variant.rs  | 282 ++++++++++++++++++++++--
 parquet-variant-compute/src/variant_array.rs    |  29 ++-
 parquet-variant-compute/src/variant_get.rs      |  21 +-
 parquet-variant-compute/src/variant_to_arrow.rs |   5 +-
 4 files changed, 314 insertions(+), 23 deletions(-)

diff --git a/parquet-variant-compute/src/unshred_variant.rs 
b/parquet-variant-compute/src/unshred_variant.rs
index f9e8464296..18cc8a8d75 100644
--- a/parquet-variant-compute/src/unshred_variant.rs
+++ b/parquet-variant-compute/src/unshred_variant.rs
@@ -25,6 +25,7 @@ use arrow::array::{
     LargeBinaryArray, LargeStringArray, ListLikeArray, PrimitiveArray, 
StringArray,
     StringViewArray, StructArray,
 };
+use arrow::buffer::NullBuffer;
 use arrow::datatypes::{
     ArrowPrimitiveType, DataType, Date32Type, Decimal32Type, Decimal64Type, 
Decimal128Type,
     DecimalType, Float32Type, Float64Type, Int8Type, Int16Type, Int32Type, 
Int64Type,
@@ -35,8 +36,8 @@ use arrow::temporal_conversions::time64us_to_time;
 use chrono::{DateTime, Utc};
 use indexmap::IndexMap;
 use parquet_variant::{
-    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal4, 
VariantDecimal8,
-    VariantDecimal16, VariantDecimalType, VariantMetadata,
+    ListBuilder, ObjectBuilder, ObjectFieldBuilder, Variant, 
VariantBuilderExt, VariantDecimal4,
+    VariantDecimal8, VariantDecimal16, VariantDecimalType, VariantMetadata,
 };
 use std::marker::PhantomData;
 use std::sync::Arc;
@@ -58,18 +59,33 @@ use uuid::Uuid;
 /// - If the shredded data contains spec violations (e.g., field name 
conflicts)
 /// - If unsupported data types are encountered in typed_value columns
 pub fn unshred_variant(array: &VariantArray) -> Result<VariantArray> {
-    // Check if already unshredded (optimization for common case)
-    if array.typed_value_column().is_none() {
-        return Ok(array.clone());
+    let nulls = array.nulls();
+    let metadata = array.metadata_column();
+    let value_col = array.value_column();
+    let typed_value_col = array.typed_value_column();
+
+    // Already unshredded: no data movement needed, but the output must 
annotate `value` as
+    // non-nullable per the spec. Inputs whose value-nulls are not all masked 
by the parent null
+    // buffer (spec-invalid "missing" rows) cannot be re-annotated and fall 
through to the row
+    // loop below, whose top-level sink materializes `Variant::Null` for such 
rows.
+    if typed_value_col.is_none() {
+        if value_field_is_non_nullable(array) {
+            return Ok(array.clone());
+        }
+        if value_nulls_are_masked(value_col, nulls) {
+            return Ok(VariantArray::from_parts_unshredded(
+                metadata.clone(),
+                value_col.clone(),
+                nulls.cloned(),
+            ));
+        }
     }
 
     // NOTE: None/None at top-level is technically invalid, but the shredding 
spec requires us to
     // emit `Variant::Null` when a required value is missing.
-    let nulls = array.nulls();
     let mut row_builder = UnshredVariantRowBuilder::try_new_opt(array.inner())?
         .unwrap_or_else(UnshredVariantRowBuilder::null);
 
-    let metadata = array.metadata_column();
     let mut value_builder = VariantValueArrayBuilder::new(array.len());
     for i in 0..array.len() {
         if array.is_null(i) {
@@ -81,20 +97,67 @@ pub fn unshred_variant(array: &VariantArray) -> 
Result<VariantArray> {
                 )
             })?;
             let metadata = VariantMetadata::try_new(metadata_bytes)?;
-            let mut value_builder = value_builder.builder_ext(&metadata);
-            row_builder.append_row(&mut value_builder, &metadata, i)?;
+            let mut row_sink = 
TopLevelRowSink(value_builder.builder_ext(&metadata));
+            row_builder.append_row(&mut row_sink, &metadata, i)?;
         }
     }
 
     let value = value_builder.build()?;
-    Ok(VariantArray::from_parts(
+    Ok(VariantArray::from_parts_unshredded(
         metadata.clone(),
         Arc::new(value),
-        None,
         nulls.cloned(),
     ))
 }
 
+fn value_field_is_non_nullable(array: &VariantArray) -> bool {
+    array
+        .inner()
+        .field_by_name("value")
+        .is_some_and(|field| !field.is_nullable())
+}
+
+/// Returns true if every null in `value` is masked by a parent null, i.e. the 
column may be
+/// annotated non-nullable.
+fn value_nulls_are_masked(value: &ArrayRef, parent_nulls: Option<&NullBuffer>) 
-> bool {
+    value.null_count() == 0
+        || parent_nulls
+            .zip(value.nulls())
+            .is_some_and(|(parent, value_nulls)| parent.contains(value_nulls))
+}
+
+/// Wraps the sink that every top-level row is appended into. The row builders 
signal a missing
+/// value (value and typed_value both NULL) by calling `append_null`, and this 
wrapper gives that
+/// signal its top-level meaning: `Variant::Null`, because the non-nullable 
output `value` column
+/// cannot hold a physical NULL. Array-level NULL rows are appended before the 
sink is built, so
+/// they never reach it. Nested builders created via 
`try_new_object`/`try_new_list` are returned
+/// unwrapped, so nested missing values keep their own semantics, e.g. 
[`ObjectFieldBuilder`]
+/// omits the field.
+struct TopLevelRowSink<B>(B);
+
+impl<B: VariantBuilderExt> VariantBuilderExt for TopLevelRowSink<B> {
+    type State<'a>
+        = B::State<'a>
+    where
+        Self: 'a;
+
+    fn append_null(&mut self) {
+        self.0.append_value(Variant::Null);
+    }
+
+    fn append_value<'m, 'v>(&mut self, value: impl Into<Variant<'m, 'v>>) {
+        self.0.append_value(value);
+    }
+
+    fn try_new_list(&mut self) -> Result<ListBuilder<'_, Self::State<'_>>> {
+        self.0.try_new_list()
+    }
+
+    fn try_new_object(&mut self) -> Result<ObjectBuilder<'_, Self::State<'_>>> 
{
+        self.0.try_new_object()
+    }
+}
+
 /// Row builder for converting shredded VariantArray rows back to unshredded 
form
 enum UnshredVariantRowBuilder<'a> {
     PrimitiveInt8(UnshredPrimitiveRowBuilder<'a, PrimitiveArray<Int8Type>>),
@@ -696,16 +759,23 @@ impl<'a, L: ListLikeArray> ListUnshredVariantBuilder<'a, 
L> {
 
 #[cfg(test)]
 mod tests {
-    use crate::VariantArray;
+    use crate::{VariantArray, VariantArrayBuilder, shred_variant};
     use arrow::array::{
-        ArrayRef, BinaryArray, BinaryViewArray, LargeBinaryArray, 
LargeStringArray, StringViewArray,
+        Array, ArrayRef, AsArray, BinaryArray, BinaryViewArray, Int64Array, 
LargeBinaryArray,
+        LargeStringArray, StringViewArray,
     };
-    use parquet_variant::Variant;
+    use arrow::datatypes::{DataType, Field, Fields, TimeUnit};
+    use parquet_variant::{EMPTY_VARIANT_METADATA_BYTES, Variant, 
VariantBuilder, VariantDecimal8};
     use std::sync::Arc;
 
+    /// Returns the nullability annotation of the `value` field
+    fn value_field_is_nullable(array: &VariantArray) -> bool {
+        array.inner().field_by_name("value").unwrap().is_nullable()
+    }
+
     #[test]
     fn test_unshred_utf8view_typed_value() {
-        let metadata_bytes: &[u8] = &[0x01, 0x00, 0x00];
+        let metadata_bytes = EMPTY_VARIANT_METADATA_BYTES;
         let metadata: ArrayRef =
             Arc::new(BinaryViewArray::from_iter_values(vec![metadata_bytes; 
3]));
 
@@ -727,7 +797,7 @@ mod tests {
 
     #[test]
     fn test_unshred_largeutf8_typed_value() {
-        let metadata_bytes: &[u8] = &[0x01, 0x00, 0x00];
+        let metadata_bytes = EMPTY_VARIANT_METADATA_BYTES;
         let metadata: ArrayRef =
             Arc::new(BinaryViewArray::from_iter_values(vec![metadata_bytes; 
3]));
 
@@ -749,7 +819,7 @@ mod tests {
 
     #[test]
     fn test_unshred_binary_typed_value() {
-        let metadata_bytes: &[u8] = &[0x01, 0x00, 0x00];
+        let metadata_bytes = EMPTY_VARIANT_METADATA_BYTES;
         let metadata: ArrayRef =
             Arc::new(BinaryViewArray::from_iter_values(vec![metadata_bytes; 
3]));
 
@@ -771,7 +841,7 @@ mod tests {
 
     #[test]
     fn test_unshred_largebinary_typed_value() {
-        let metadata_bytes: &[u8] = &[0x01, 0x00, 0x00];
+        let metadata_bytes = EMPTY_VARIANT_METADATA_BYTES;
         let metadata: ArrayRef =
             Arc::new(BinaryViewArray::from_iter_values(vec![metadata_bytes; 
3]));
 
@@ -791,6 +861,182 @@ mod tests {
         assert_eq!(result.value(2), Variant::from(&b"\xde\xad\xbe\xef"[..]));
     }
 
+    #[test]
+    fn test_shred_unshred_round_trip_annotates_value_non_nullable() {
+        let mut builder = VariantArrayBuilder::new(1);
+        builder.append_variant(Variant::from(42i64));
+        let original = builder.build();
+        assert!(!value_field_is_nullable(&original));
+
+        let shredded = shred_variant(&original, &DataType::Int64).unwrap();
+        assert!(value_field_is_nullable(&shredded)); // legal: typed_value 
present
+
+        let unshredded = crate::unshred_variant(&shredded).unwrap();
+        assert!(!value_field_is_nullable(&unshredded));
+        assert_eq!(unshredded, original);
+    }
+
+    #[test]
+    fn test_unshred_with_nulls_annotates_value_non_nullable() {
+        // a null row plus an unshreddable row, so the shredded input 
exercises parent
+        // nulls and both value/typed_value columns
+        let mut builder = VariantArrayBuilder::new(3);
+        builder.append_variant(Variant::from(1i64));
+        builder.append_null();
+        builder.append_variant(Variant::from("s"));
+        let original = builder.build();
+
+        let shredded = shred_variant(&original, &DataType::Int64).unwrap();
+        let unshredded = crate::unshred_variant(&shredded).unwrap();
+
+        assert!(!value_field_is_nullable(&unshredded));
+        assert_eq!(unshredded.len(), 3);
+        assert_eq!(unshredded.value(0), Variant::from(1i64));
+        assert!(unshredded.is_null(1));
+        assert_eq!(unshredded.value(2), Variant::from("s"));
+    }
+
+    #[test]
+    fn test_unshred_already_unshredded_reannotates_nullable_value() {
+        let mut builder = VariantArrayBuilder::new(2);
+        builder.append_variant(Variant::from(42i64));
+        builder.append_null();
+        let original = builder.build();
+
+        // same data, but with the out-of-spec nullable `value` annotation
+        let nullable_input = VariantArray::from_parts(
+            original.metadata_column().clone(),
+            original.value_column().clone(),
+            None,
+            original.nulls().cloned(),
+        );
+        assert!(value_field_is_nullable(&nullable_input));
+
+        let unshredded = crate::unshred_variant(&nullable_input).unwrap();
+        assert!(!value_field_is_nullable(&unshredded));
+        assert_eq!(unshredded, original);
+    }
+
+    #[test]
+    fn test_unshred_missing_top_level_value_becomes_variant_null() {
+        let metadata_bytes = EMPTY_VARIANT_METADATA_BYTES;
+        let metadata: ArrayRef =
+            Arc::new(BinaryViewArray::from_iter_values(vec![metadata_bytes; 
3]));
+
+        // row 1 is null in both value and typed_value with a valid parent 
row: spec-invalid
+        // "missing" value, tolerated as Variant::Null (like 
`VariantArray::try_value`)
+        let typed_value: ArrayRef = Arc::new(Int64Array::from(vec![Some(1), 
None, Some(3)]));
+        let variant_array = VariantArray::perfectly_shredded(metadata, 
typed_value, None);
+
+        let result = crate::unshred_variant(&variant_array).unwrap();
+
+        assert!(!value_field_is_nullable(&result));
+        assert_eq!(result.inner().null_count(), 0);
+        assert_eq!(result.value(0), Variant::from(1i64));
+        assert_eq!(result.value(1), Variant::Null);
+        assert_eq!(result.value(2), Variant::from(3i64));
+    }
+
+    /// Shreds `original` to `as_type`, then drops the parent null buffer so 
the parent-null row
+    /// becomes a spec-invalid "missing" row (value and typed_value both NULL 
with a valid
+    /// parent), and asserts unshredding turns exactly that row into 
`Variant::Null`.
+    fn assert_missing_row_unshreds_to_variant_null(original: &VariantArray, 
as_type: &DataType) {
+        let shredded = shred_variant(original, as_type).unwrap();
+        // Row 0 must actually shred, so that its round trip below exercises 
the typed
+        // reconstruction path of this shape's row builder, not the value 
fallback.
+        assert!(shredded.typed_value_column().unwrap().is_valid(0));
+
+        // The parent-null row carries no metadata bytes, so give every row 
the metadata of row 0.
+        let metadata_bytes = 
shredded.metadata_column().as_binary_view().value(0);
+        let metadata: ArrayRef = 
Arc::new(BinaryViewArray::from_iter_values(std::iter::repeat_n(
+            metadata_bytes,
+            original.len(),
+        )));
+
+        let input = VariantArray::from_parts(
+            metadata,
+            shredded.value_column().clone(),
+            shredded.typed_value_column().cloned(),
+            None,
+        );
+
+        let result = crate::unshred_variant(&input).unwrap();
+        assert!(!value_field_is_nullable(&result));
+        assert_eq!(result.inner().null_count(), 0);
+        assert_eq!(result.value(0), original.value(0));
+        assert_eq!(result.value(1), Variant::Null);
+    }
+
+    /// Missing rows must become `Variant::Null` through every row-builder 
shape, since each
+    /// shape has its own expansion of `handle_unshredded_case`.
+    #[test]
+    fn test_unshred_missing_row_for_decimal_timestamp_object_list() {
+        let mut builder = VariantArrayBuilder::new(2);
+        builder.append_variant(Variant::from(VariantDecimal8::try_new(1234, 
2).unwrap()));
+        builder.append_null();
+        assert_missing_row_unshreds_to_variant_null(&builder.build(), 
&DataType::Decimal64(18, 2));
+
+        let mut builder = VariantArrayBuilder::new(2);
+        builder.append_variant(Variant::from(
+            chrono::DateTime::from_timestamp(1, 0).unwrap(),
+        ));
+        builder.append_null();
+        assert_missing_row_unshreds_to_variant_null(
+            &builder.build(),
+            &DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
+        );
+
+        let mut variant_builder = VariantBuilder::new();
+        let mut object_builder = variant_builder.new_object();
+        object_builder.insert("a", 1i64);
+        object_builder.finish();
+        let (object_metadata, object_value) = variant_builder.finish();
+        let mut builder = VariantArrayBuilder::new(2);
+        builder.append_variant(Variant::try_new(&object_metadata, 
&object_value).unwrap());
+        builder.append_null();
+        assert_missing_row_unshreds_to_variant_null(
+            &builder.build(),
+            &DataType::Struct(Fields::from(vec![Field::new("a", 
DataType::Int64, true)])),
+        );
+
+        let mut variant_builder = VariantBuilder::new();
+        let mut list_builder = variant_builder.new_list();
+        list_builder.append_value(1i64);
+        list_builder.append_value(2i64);
+        list_builder.finish();
+        let (list_metadata, list_value) = variant_builder.finish();
+        let mut builder = VariantArrayBuilder::new(2);
+        builder.append_variant(Variant::try_new(&list_metadata, 
&list_value).unwrap());
+        builder.append_null();
+        assert_missing_row_unshreds_to_variant_null(
+            &builder.build(),
+            &DataType::List(Arc::new(Field::new_list_field(DataType::Int64, 
true))),
+        );
+    }
+
+    #[test]
+    fn test_unshred_value_only_with_unmasked_nulls_materializes_variant_null() 
{
+        let mut builder = VariantArrayBuilder::new(1);
+        builder.append_variant(Variant::from(42i64));
+        let single = builder.build();
+        let metadata_bytes = 
single.metadata_column().as_binary_view().value(0);
+        let value_bytes = single.value_column().as_binary_view().value(0);
+
+        // unshredded input whose `value` null is not masked by a parent null: 
cannot be
+        // re-annotated in place, so unshredding must materialize 
Variant::Null bytes
+        let metadata: ArrayRef =
+            Arc::new(BinaryViewArray::from_iter_values(vec![metadata_bytes; 
2]));
+        let value: ArrayRef = 
Arc::new(BinaryViewArray::from(vec![Some(value_bytes), None]));
+        let input = VariantArray::from_parts(metadata, value, None, None);
+
+        let result = crate::unshred_variant(&input).unwrap();
+
+        assert!(!value_field_is_nullable(&result));
+        assert_eq!(result.inner().null_count(), 0);
+        assert_eq!(result.value(0), Variant::from(42i64));
+        assert_eq!(result.value(1), Variant::Null);
+    }
+
     #[test]
     fn test_unshred_returns_err_on_malformed_metadata() {
         // empty metadata bytes fail VariantMetadata's header parse. before 
this fix the
diff --git a/parquet-variant-compute/src/variant_array.rs 
b/parquet-variant-compute/src/variant_array.rs
index f8bce42b44..7af504572e 100644
--- a/parquet-variant-compute/src/variant_array.rs
+++ b/parquet-variant-compute/src/variant_array.rs
@@ -338,15 +338,42 @@ impl VariantArray {
         })
     }
 
+    /// Note: annotates `value` as nullable, which the spec only permits for 
shredded
+    /// variants. It is also needed by `variant_get`'s unshredded 
intermediates, whose
+    /// `value` column can contain unmasked nulls. Unshredded producers should 
use
+    /// [`Self::from_parts_unshredded`] instead.
     pub(crate) fn from_parts(
         metadata: ArrayRef,
         value: ArrayRef,
         typed_value: Option<ArrayRef>,
         nulls: Option<NullBuffer>,
+    ) -> Self {
+        Self::from_parts_with_nullable_value(metadata, value, typed_value, 
nulls, true)
+    }
+
+    /// Construct an unshredded `VariantArray`, annotating `value` as 
non-nullable as the
+    /// spec requires when there is no `typed_value` column.
+    ///
+    /// # Panics
+    /// If `value` contains nulls not masked by `nulls`.
+    pub(crate) fn from_parts_unshredded(
+        metadata: ArrayRef,
+        value: ArrayRef,
+        nulls: Option<NullBuffer>,
+    ) -> Self {
+        Self::from_parts_with_nullable_value(metadata, value, None, nulls, 
false)
+    }
+
+    fn from_parts_with_nullable_value(
+        metadata: ArrayRef,
+        value: ArrayRef,
+        typed_value: Option<ArrayRef>,
+        nulls: Option<NullBuffer>,
+        value_nullable: bool,
     ) -> Self {
         let mut builder = StructArrayBuilder::new()
             .with_field("metadata", metadata.clone(), false)
-            .with_field("value", value.clone(), true);
+            .with_field("value", value.clone(), value_nullable);
         if let Some(typed_value) = typed_value.clone() {
             builder = builder.with_field_ref(typed_value_field(&typed_value), 
typed_value);
         }
diff --git a/parquet-variant-compute/src/variant_get.rs 
b/parquet-variant-compute/src/variant_get.rs
index 8e5fc75256..d062d0ff08 100644
--- a/parquet-variant-compute/src/variant_get.rs
+++ b/parquet-variant-compute/src/variant_get.rs
@@ -292,10 +292,9 @@ fn shredded_get_path(
                     // Propagating metadata is not necessary for an all-NULL 
array, but is cheaper than constructing
                     // a new empty metadata array. (n * 3 bytes vs Arc bump)
                     let metadata = input.metadata_column().clone();
-                    let arr = VariantArray::from_parts(
+                    let arr = VariantArray::from_parts_unshredded(
                         metadata,
                         all_null_value_column(num_rows),
-                        None,
                         all_nulls,
                     );
                     return Ok(ArrayRef::from(arr));
@@ -2447,6 +2446,22 @@ mod test {
         );
     }
 
+    #[test]
+    fn test_variant_get_missing_path_as_variant_annotates_value_non_nullable() 
{
+        let (unshredded, shredded) = create_variant_get_as_variant_test_data();
+        let variant_field = 
VariantArray::try_new(&unshredded).unwrap().field("result");
+
+        // indexing into a struct typed_value can never match: all-null 
variant output
+        let options = 
GetOptions::new_with_path(VariantPath::try_from("field_name[0]").unwrap())
+            .with_as_type(Some(FieldRef::from(variant_field)));
+        let result = variant_get(&shredded, options).unwrap();
+        let result_variant = VariantArray::try_new(&result).unwrap();
+
+        assert_eq!(result_variant.inner().null_count(), result_variant.len());
+        let value_field = 
result_variant.inner().field_by_name("value").unwrap();
+        assert!(!value_field.is_nullable());
+    }
+
     fn create_variant_get_as_variant_test_data() -> (ArrayRef, ArrayRef) {
         let input_json: ArrayRef = Arc::new(StringArray::from(vec![
             Some(r#"{"field_name": {"k": 100000}}"#),
@@ -2478,6 +2493,8 @@ mod test {
 
         assert!(result_variant.typed_value_column().is_none());
         assert!(result_variant.value_column().null_count() < 
result_variant.len());
+        let value_field = 
result_variant.inner().field_by_name("value").unwrap();
+        assert!(!value_field.is_nullable());
 
         let expected_json: ArrayRef = Arc::new(StringArray::from(vec![
             Some(r#"{"k":100000}"#),
diff --git a/parquet-variant-compute/src/variant_to_arrow.rs 
b/parquet-variant-compute/src/variant_to_arrow.rs
index 37f1f27c3e..a6f32288eb 100644
--- a/parquet-variant-compute/src/variant_to_arrow.rs
+++ b/parquet-variant-compute/src/variant_to_arrow.rs
@@ -1463,10 +1463,11 @@ impl VariantToBinaryVariantArrowRowBuilder {
     }
 
     fn finish(mut self) -> Result<ArrayRef> {
-        let variant_array = VariantArray::from_parts(
+        // value-nulls are appended only alongside parent nulls, so the 
non-nullable
+        // `value` annotation is always valid here
+        let variant_array = VariantArray::from_parts_unshredded(
             self.metadata,
             Arc::new(self.builder.build()?),
-            None, // no typed_value column
             self.nulls.finish(),
         );
 

Reply via email to