sdf-jkl opened a new issue, #10315:
URL: https://github.com/apache/arrow-rs/issues/10315

   ### Describe the bug
   
   The Variant spec has two distinct requirements on the `value` column of a 
variant group:
   
   1. **Presence**: the group must contain a field named `value` (tracked by 
#10306, which covers omitting the column entirely).
   2. **Annotation**: per [VariantEncoding.md]:
   
      > The `value` field must be annotated as `required` for unshredded 
Variant values, or `optional` if parts of the value are shredded as typed 
Parquet columns.
   
   This issue tracks a violation of the second requirement: 
`VariantArray::from_parts` hardcodes the `value` field as nullable, so kernels 
that produce **unshredded** output through it annotate `value` as 
nullable/`optional` even though the data never contains an unmasked null there:
   
   | Producer | Output | `value` annotation | Spec |
   |---|---|---|---|
   | `VariantArrayBuilder::build` | unshredded | non-nullable | ✅ |
   | `json_to_variant` / `cast_to_variant` (via builder) | unshredded | 
non-nullable | ✅ |
   | `unshred_variant` (via `from_parts`) | unshredded | nullable | ❌ must be 
`required` |
   | `variant_get` with `as_type: None` 
(`VariantToBinaryVariantArrowRowBuilder::finish`, via `from_parts`) | 
unshredded | nullable | ❌ must be `required` |
   | `shred_variant` | shredded | nullable | ✅ (`optional` is legal when 
`typed_value` exists) |
   
   Notably, `VariantValueArrayBuilder::append_null` already documents the rule 
("It is only valid to call this method when building the `value` field of a 
shredded variant column...") — the callers respect it at the data level 
(value-nulls only under struct-null rows, i.e. masked), but `from_parts` then 
discards that guarantee at the schema level.
   
   Consequences:
   
   - Writing `unshred_variant` output to Parquet produces `optional group v 
(VARIANT) { required binary metadata; optional binary value; }`, which is out 
of spec for unshredded variants — the annotation-level sibling of #10306.
   - Arrow C++'s `VariantExtensionType::IsSupportedStorageType` explicitly 
rejects nullable-`value` storage for unshredded variants (`nullable_value` is 
one of the invalid cases in `cpp/src/parquet/arrow/variant_test.cc`), so C++ 
cannot attach the variant extension type to such data.
   - `VariantArray::eq` compares the inner `StructArray` including data types, 
so a `shred_variant` → `unshred_variant` round trip yields an array that is not 
equal to the `VariantArrayBuilder`-produced original purely because of the 
annotation.
   
   ### To Reproduce
   
   ```rust
   use arrow_schema::DataType;
   use parquet_variant::Variant;
   use parquet_variant_compute::{VariantArrayBuilder, shred_variant, 
unshred_variant};
   
   let mut builder = VariantArrayBuilder::new(1);
   builder.append_variant(Variant::from(42i64));
   let original = builder.build();
   
   // spec-correct: the builder annotates `value` as non-nullable
   let (idx, field) = original.inner().fields().find("value").unwrap();
   assert!(!field.is_nullable());
   
   // round trip through shredding
   let shredded = shred_variant(&original, &DataType::Int64).unwrap();
   let unshredded = unshred_variant(&shredded).unwrap();
   
   // out of spec: the output is unshredded but `value` is annotated nullable
   let (idx, field) = unshredded.inner().fields().find("value").unwrap();
   assert!(field.is_nullable());
   ```
   
   ### Expected behavior
   
   Unshredded `VariantArray`s produced by `unshred_variant` and `variant_get` 
(binary-variant output path) should annotate the `value` field as non-nullable, 
matching `VariantArrayBuilder` and the spec.
   
   Note a blanket fix in `from_parts` (e.g. `nullable = typed_value.is_none()`) 
is not correct: `variant_get`'s internal `NotShredded` intermediates are 
unshredded arrays whose `value` column legitimately contains unmasked nulls 
(rows whose data lived in `typed_value` before the path step, later surfaced as 
`Variant::Null` by `try_value`), and `StructArray::new` rejects unmasked nulls 
under a non-nullable field. Options:
   
   1. Caller-directed (preferred): the two affected producers build the `value` 
field non-nullable; their value-nulls are always masked by the parent null 
buffer, which Arrow permits.
   2. Data-driven in `from_parts`: annotate non-nullable when 
`typed_value.is_none()` and all value-nulls are covered by the parent null 
buffer.
   3. Normalize annotations at the Parquet write boundary only.
   
   ### Additional context
   
   - Presence-level sibling issue: #10306
   - Spec discussion around `value` requirements: apache/parquet-format#591
   - Arrow C++ variant storage validation (rejects nullable `value`): 
`cpp/src/arrow/extension/parquet_variant.cc` (`IsSupportedStorageType`); 
shredding support tracked in apache/arrow#45948
   
   [VariantEncoding.md]: 
https://github.com/apache/parquet-format/blob/master/VariantEncoding.md#variant-in-parquet
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to