Skip to content

[Variant] Unshredded VariantArray producers annotate the value field as nullable (spec requires required) - #10320

Open
sdf-jkl wants to merge 7 commits into
apache:mainfrom
sdf-jkl:unshredded-value-non-nullable-10315
Open

[Variant] Unshredded VariantArray producers annotate the value field as nullable (spec requires required)#10320
sdf-jkl wants to merge 7 commits into
apache:mainfrom
sdf-jkl:unshredded-value-non-nullable-10315

Conversation

@sdf-jkl

@sdf-jkl sdf-jkl commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

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.

What changes are included in this PR?

Are these changes tested?

  • yes, added new unit tests and fixed some old ones

Are there any user-facing changes?

  • Yes, the nullability now conforms the spec

@github-actions github-actions Bot added parquet Changes to the parquet crate parquet-variant parquet-variant* crates labels Jul 11, 2026
@sdf-jkl sdf-jkl changed the title Unshredded value non nullable 10315 [Variant] Unshredded VariantArray producers annotate the value field as nullable (spec requires required) Jul 11, 2026
@sdf-jkl sdf-jkl changed the title [Variant] Unshredded VariantArray producers annotate the value field as nullable (spec requires required) [Variant] Unshredded VariantArray producers annotate the value field as nullable (spec requires required) Jul 11, 2026
@github-actions github-actions Bot removed the parquet Changes to the parquet crate label Jul 23, 2026
@sdf-jkl
sdf-jkl marked this pull request as ready for review July 23, 2026 23:18
@sdf-jkl

sdf-jkl commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@klion26 Please take a look 🙏

@klion26 klion26 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sdf-jkl Thanks for the pr. I dealt with some unexpected emergencies this week. I need some more time to take a look at it; I will response asap.

@klion26 klion26 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sdf-jkl I've left some comments, please take a look

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)?;
if value_col.is_null(i) && typed_value_col.is_none_or(|tv| tv.is_null(i)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic can be moved into macro handle_unshredded_case and ValueOnlyUnshredVariantBuilder::append_row

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, added here but with a twist. These methods also cover the nested fields, and a missing object field must stay absent instead of becoming Variant::Null (otherwise {"a": 3} would unshred as {"a": 3, "b": null} under a partially shredded schema). So the substitution lives in a sink wrapper (TopLevelRowSink) that only the top level uses.

///
/// # Panics
/// If `value` contains nulls not masked by `nulls`.
pub(crate) fn from_parts_unshredded(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to panic? The callers of this function, unshred_variant and shredded_get_path(in variant_get) both return Result

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The panic is not new logic in this function, it comes from StructArray::new inside StructArrayBuilder::build, which validates that a non-nullable field has no unmasked nulls. The doc comment just makes that existing behavior visible.

I kept it as a panic rather than a Result because it can only fire on an internal bug, never on bad input data.

}

#[test]
fn test_variant_get_missing_path_as_variant_annotates_value_non_nullable() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to add this test

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR changes two production call sites, not just unshred_variant. The missing-path branch of shredded_get_path used to build its all-NULL output with a nullable value field and now uses from_parts_unshredded. This test pins that second call site, without it only the unshred_variant paths would be covered.

/// 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 {
match value.logical_nulls() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Is there any reason that logical_nulls is better than nulls here?
  2. The match arms None and null_count() == 0 seem can be merged into ArrayRef::null_count() == 0?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied here

}

#[test]
fn test_unshred_already_unshredded_reannotates_nullable_value() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 Do we need to add some tests for decimal/timestamp/list/map? currently they'are all covered by the macro handle_unshredded_case but they're differnt enum of UnshredVariantRowBuilder
2 Do we need to add a test that returns null (not Variant::Null)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Thanks, added here
  2. That case is covered by test_unshred_with_nulls_annotates_value_non_nullable: row 1 is a top-level NULL row and the test asserts unshredded.is_null(1) on the output.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

parquet-variant parquet-variant* crates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Variant] Unshredded VariantArray producers annotate the value field as nullable (spec requires required)

2 participants