This is an automated email from the ASF dual-hosted git repository.
jayzhan211 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 9500647b24 fix(variant): accept Dictionary and RunEndEncoded metadata
in VariantArray (#10810)
9500647b24 is described below
commit 9500647b24ae6c9d271daa9c7d0487deb8849a16
Author: Peter Lee <[email protected]>
AuthorDate: Fri Aug 28 06:40:58 2026 +0800
fix(variant): accept Dictionary and RunEndEncoded metadata in VariantArray
(#10810)
# Which issue does this PR close?
- Closes #10802.
# Rationale for this change
The canonical `arrow.parquet.variant` extension permits Dictionary and
RunEndEncoded metadata, but `VariantArray` only accepted unencoded
binary arrays. Downstream users therefore had to decode valid metadata
eagerly before using Variant compute kernels.
# What changes are included in this PR?
- Accept Dictionary and RunEndEncoded metadata with Binary, LargeBinary,
or BinaryView logical values.
- Resolve encoded values and logical nulls in the shared metadata byte
accessor.
- Route `variant_to_json` through `VariantArray` so it shares the same
validation and row access.
- Add coverage for Dictionary/REE metadata, nulls, slices,
`variant_get`, and `variant_to_json`.
# Are these changes tested?
- `cargo test -p parquet-variant-compute --lib` — 364 passed
- `cargo test -p parquet-variant-compute --doc` — 13 passed
- `cargo clippy -p parquet-variant-compute --all-targets -- -D warnings`
- `cargo fmt --all -- --check`
- `git diff --check`
# Are there any user-facing changes?
Yes. `VariantArray` and its compute kernels now accept canonical
Dictionary- and RunEndEncoded metadata. There are no public API
signature changes or breaking changes.
# AI-assisted development
Codex assisted with tracing callers, implementing the focused change,
and drafting tests. I reviewed the resulting code and verified it with
the checks above.
---------
Co-authored-by: Claude Fable 5 <[email protected]>
---
parquet-variant-compute/src/to_json.rs | 62 +++---------
parquet-variant-compute/src/variant_array.rs | 143 ++++++++++++++++++++++++---
2 files changed, 138 insertions(+), 67 deletions(-)
diff --git a/parquet-variant-compute/src/to_json.rs
b/parquet-variant-compute/src/to_json.rs
index efe50c2a09..0a173fb5e9 100644
--- a/parquet-variant-compute/src/to_json.rs
+++ b/parquet-variant-compute/src/to_json.rs
@@ -15,73 +15,33 @@
// specific language governing permissions and limitations
// under the License.
-//! Module for transforming a batch of Variants represented as
-//! STRUCT<metadata: BINARY, value: BINARY> into a batch of JSON strings.
+//! Module for transforming a batch of Variants into JSON strings.
-use arrow::array::{Array, ArrayRef, BinaryArray, BooleanBufferBuilder,
StringArray, StructArray};
+use crate::VariantArray;
+use arrow::array::{ArrayRef, BooleanBufferBuilder, StringArray};
use arrow::buffer::{Buffer, NullBuffer, OffsetBuffer, ScalarBuffer};
-use arrow::datatypes::DataType;
use arrow_schema::ArrowError;
-use parquet_variant::Variant;
use parquet_variant_json::VariantToJson;
-/// Transform a batch of Variant represented as STRUCT<metadata: BINARY,
value: BINARY> to a batch
-/// of JSON strings where nulls are preserved. The JSON strings in the input
must be valid.
+/// Transform a [`VariantArray`] to a batch of JSON strings where nulls are
preserved.
pub fn variant_to_json(input: &ArrayRef) -> Result<StringArray, ArrowError> {
- let struct_array = input
- .as_any()
- .downcast_ref::<StructArray>()
- .ok_or_else(|| ArrowError::CastError("Expected StructArray as
input".into()))?;
-
- // Validate field types
- let data_type = struct_array.data_type();
- match data_type {
- DataType::Struct(inner_fields) => {
- if inner_fields.len() != 2
- || inner_fields[0].data_type() != &DataType::Binary
- || inner_fields[1].data_type() != &DataType::Binary
- {
- return Err(ArrowError::CastError(
- "Expected struct with two binary fields".into(),
- ));
- }
- }
- _ => {
- return Err(ArrowError::CastError(
- "Expected StructArray with known fields".into(),
- ));
- }
- }
-
- let metadata_array = struct_array
- .column(0)
- .as_any()
- .downcast_ref::<BinaryArray>()
- .ok_or_else(|| ArrowError::CastError("Expected BinaryArray for
'metadata'".into()))?;
-
- let value_array = struct_array
- .column(1)
- .as_any()
- .downcast_ref::<BinaryArray>()
- .ok_or_else(|| ArrowError::CastError("Expected BinaryArray for
'value'".into()))?;
+ let array = VariantArray::try_new(input)?;
// Zero-copy builder
// The size per JSON string is assumed to be 128 bytes. If this holds
true, resizing could be
// minimized for performance.
- let mut json_buffer: Vec<u8> = Vec::with_capacity(struct_array.len() *
128);
- let mut offsets: Vec<i32> = Vec::with_capacity(struct_array.len() + 1);
- let mut validity = BooleanBufferBuilder::new(struct_array.len());
+ let mut json_buffer: Vec<u8> = Vec::with_capacity(array.len() * 128);
+ let mut offsets: Vec<i32> = Vec::with_capacity(array.len() + 1);
+ let mut validity = BooleanBufferBuilder::new(array.len());
let mut current_offset: i32 = 0;
offsets.push(current_offset);
- for i in 0..struct_array.len() {
- if struct_array.is_null(i) {
+ for i in 0..array.len() {
+ if array.is_null(i) {
validity.append(false);
offsets.push(current_offset);
} else {
- let metadata = metadata_array.value(i);
- let value = value_array.value(i);
- let variant = Variant::new(metadata, value);
+ let variant = array.try_value(i)?;
let start_len = json_buffer.len();
variant.to_json(&mut json_buffer)?;
let written = (json_buffer.len() - start_len) as i32;
diff --git a/parquet-variant-compute/src/variant_array.rs
b/parquet-variant-compute/src/variant_array.rs
index b8a0894719..4dd3b1aab0 100644
--- a/parquet-variant-compute/src/variant_array.rs
+++ b/parquet-variant-compute/src/variant_array.rs
@@ -22,7 +22,10 @@ use crate::type_conversion::{
generic_conversion_single_value,
generic_conversion_single_value_with_result,
primitive_conversion_single_value,
};
-use arrow::array::{Array, ArrayRef, AsArray, StructArray, new_null_array};
+use arrow::array::{
+ Array, ArrayRef, AsArray, StructArray, downcast_dictionary_array,
downcast_run_array,
+ new_null_array,
+};
use arrow::buffer::NullBuffer;
use arrow::compute::cast;
use arrow::datatypes::{
@@ -41,12 +44,30 @@ use parquet_variant::{
use std::borrow::Cow;
use std::sync::Arc;
-/// Returns the raw bytes at the given index from a binary-like array, return
`None` if the array isn't binary-like.
+/// Returns the logical bytes at the given index from a binary-like array,
resolving dictionary
+/// and run-end encodings. Returns `None` for nulls or if the logical values
aren't binary-like.
pub(crate) fn binary_array_value(array: &dyn Array, index: usize) ->
Option<&[u8]> {
+ if array.is_null(index) {
+ return None;
+ }
match array.data_type() {
DataType::Binary => Some(array.as_binary::<i32>().value(index)),
DataType::LargeBinary => Some(array.as_binary::<i64>().value(index)),
DataType::BinaryView => Some(array.as_binary_view().value(index)),
+ DataType::Dictionary(..) => downcast_dictionary_array! {
+ array => {
+ let index = array.key(index)?;
+ binary_array_value(array.values().as_ref(), index)
+ },
+ _ => unreachable!(),
+ },
+ DataType::RunEndEncoded(..) => downcast_run_array! {
+ array => {
+ let index = array.get_physical_index(index);
+ binary_array_value(array.values().as_ref(), index)
+ },
+ _ => unreachable!(),
+ },
_ => None,
}
}
@@ -83,6 +104,25 @@ pub(crate) fn validate_binary_array(array: &dyn Array,
field_name: &str) -> Resu
}
}
+/// Validates that a metadata array has binary-like logical values.
+fn validate_metadata_array(array: &dyn Array) -> Result<()> {
+ let is_binary = |data_type: &DataType| {
+ matches!(
+ data_type,
+ DataType::Binary | DataType::LargeBinary | DataType::BinaryView
+ )
+ };
+ match array.data_type() {
+ data_type if is_binary(data_type) => Ok(()),
+ DataType::Dictionary(_, values) if is_binary(values) => Ok(()),
+ DataType::RunEndEncoded(_, values) if is_binary(values.data_type()) =>
Ok(()),
+ _ => Err(ArrowError::InvalidArgumentError(format!(
+ "VariantArray 'metadata' field must be Binary, LargeBinary,
BinaryView, or a Dictionary or RunEndEncoded array of one of those types, got
{}",
+ array.data_type()
+ ))),
+ }
+}
+
/// Arrow Variant [`ExtensionType`].
///
/// Represents the canonical Arrow Extension Type for storing variants.
@@ -263,7 +303,7 @@ pub struct VariantArray {
/// Reference to the underlying StructArray
inner: StructArray,
- /// The metadata column of this variant (Binary, LargeBinary, or
BinaryView)
+ /// The metadata column of this variant (Binary, LargeBinary, or
BinaryView), possibly encoded
metadata: ArrayRef,
/// how is this variant array shredded?
@@ -285,7 +325,7 @@ impl VariantArray {
/// # Requirements of the `StructArray`
///
/// 1. A required field named `metadata` which is binary, large_binary, or
- /// binary_view
+ /// binary_view, optionally dictionary or run-end-encoded
///
/// 2. A required field named `value` that is binary, large_binary, or
/// binary_view
@@ -293,10 +333,6 @@ impl VariantArray {
/// 3. An optional field named `typed_value` which can be any primitive
type
/// or be a list, large_list, list_view or struct
///
- /// NOTE: It is also permissible for the metadata field to be
- /// Dictionary-Encoded, preferably (but not required) with an index type of
- /// int8.
- ///
pub fn try_new(inner: &dyn Array) -> Result<Self> {
// Canonicalize shredded typed_value fields (e.g. decimal narrowing)
let inner = canonicalize_shredded_types(inner)?;
@@ -309,13 +345,13 @@ impl VariantArray {
// Note the specification allows for any order so we must search by
name
- // Ensure the StructArray has a metadata field that is a binary type
+ // Ensure the StructArray has a metadata field with binary-like
logical values
let Some(metadata_col) = inner.column_by_name("metadata") else {
return Err(ArrowError::InvalidArgumentError(
"Invalid VariantArray: StructArray must contain a 'metadata'
field".to_string(),
));
};
- validate_binary_array(metadata_col.as_ref(), "metadata")?;
+ validate_metadata_array(metadata_col.as_ref())?;
let shredding_state = ShreddingState::try_from(inner)?;
@@ -1187,7 +1223,7 @@ fn typed_value_to_variant(typed_value: &ArrayRef, index:
usize) -> Result<Varian
/// Canonicalize shredded typed_value fields (e.g. decimal narrowing) and
/// verify that all data types in the struct are legal for a variant array.
fn canonicalize_shredded_types(array: &dyn Array) -> Result<ArrayRef> {
- let new_type = canonicalize_and_verify_data_type(array.data_type())?;
+ let new_type = canonicalize_and_verify_data_type_impl(array.data_type(),
true)?;
if let Cow::Borrowed(_) = new_type
&& let Some(array) = array.as_struct_opt()
{
@@ -1200,6 +1236,13 @@ fn canonicalize_shredded_types(array: &dyn Array) ->
Result<ArrayRef> {
/// appear in a (possibly shredded) variant array. It also narrows decimal
types to the smallest
/// valid precision (e.g. Decimal128 -> Decimal32 when the precision fits).
fn canonicalize_and_verify_data_type(data_type: &DataType) -> Result<Cow<'_,
DataType>> {
+ canonicalize_and_verify_data_type_impl(data_type, false)
+}
+
+fn canonicalize_and_verify_data_type_impl(
+ data_type: &DataType,
+ skip_top_level_metadata: bool,
+) -> Result<Cow<'_, DataType>> {
use DataType::*;
// helper macros
@@ -1279,6 +1322,9 @@ fn canonicalize_and_verify_data_type(data_type:
&DataType) -> Result<Cow<'_, Dat
// of the data type. Even if some fields change, the others are
shallow arc clones.
let mut new_fields = std::collections::HashMap::new();
for (i, field) in fields.iter().enumerate() {
+ if skip_top_level_metadata && field.name() == "metadata" {
+ continue;
+ }
if let Cow::Owned(new_field) =
canonicalize_and_verify_field(field)? {
new_fields.insert(i, new_field);
}
@@ -1346,14 +1392,15 @@ impl ShreddedVariantFieldArray {
#[cfg(test)]
mod test {
- use crate::VariantArrayBuilder;
+ use crate::{GetOptions, VariantArrayBuilder, json_to_variant, variant_get,
variant_to_json};
use std::str::FromStr;
use super::*;
use arrow::array::{
- BinaryArray, BinaryViewArray, Decimal32Array, Decimal64Array,
Decimal128Array,
- FixedSizeBinaryArray, Int32Array, Int64Array, LargeBinaryArray,
LargeListArray,
- LargeListViewArray, ListArray, ListViewArray, Time64MicrosecondArray,
+ BinaryArray, BinaryDictionaryBuilder, BinaryRunBuilder,
BinaryViewArray, Decimal32Array,
+ Decimal64Array, Decimal128Array, FixedSizeBinaryArray, Int8Array,
Int32Array, Int64Array,
+ LargeBinaryArray, LargeListArray, LargeListViewArray, ListArray,
ListViewArray,
+ StringArray, Time64MicrosecondArray,
};
use arrow::buffer::{OffsetBuffer, ScalarBuffer};
use arrow_schema::{Field, Fields};
@@ -1429,10 +1476,74 @@ mod test {
let err = VariantArray::try_new(&array);
assert_eq!(
err.unwrap_err().to_string(),
- "Invalid argument error: VariantArray 'metadata' field must be
Binary, LargeBinary, or BinaryView, got Int32"
+ "Invalid argument error: VariantArray 'metadata' field must be
Binary, LargeBinary, BinaryView, or a Dictionary or RunEndEncoded array of one
of those types, got Int32"
);
}
+ #[test]
+ fn encoded_metadata_supports_nulls_slices_and_variant_get() {
+ let json: ArrayRef = Arc::new(StringArray::from(vec![
+ Some(r#"{"a":0}"#),
+ Some(r#"{"a":1}"#),
+ None,
+ Some(r#"{"b":3}"#),
+ Some(r#"{"b":4}"#),
+ ]));
+ let baseline = json_to_variant(&json).unwrap();
+ let metadata = baseline.metadata_column().as_binary_view();
+ let metadata_a = metadata.value(0);
+ let metadata_b = metadata.value(3);
+
+ let logical_metadata = [
+ Some(metadata_a),
+ Some(metadata_a),
+ None,
+ Some(metadata_b),
+ Some(metadata_b),
+ ];
+
+ let mut dictionary = BinaryDictionaryBuilder::<Int8Type>::new();
+ dictionary.extend(logical_metadata);
+ let dictionary: ArrayRef = Arc::new(dictionary.finish());
+
+ let mut ree = BinaryRunBuilder::<Int16Type>::new();
+ ree.extend(logical_metadata);
+ let run_end_encoded: ArrayRef = Arc::new(ree.finish());
+
+ for metadata in [dictionary, run_end_encoded] {
+ assert_eq!(binary_array_value(metadata.as_ref(), 2), None);
+ let fields = Fields::from(vec![
+ Field::new("metadata", metadata.data_type().clone(), false),
+ Field::new("value",
baseline.value_column().data_type().clone(), false),
+ ]);
+ let input = StructArray::try_new(
+ fields,
+ vec![metadata, baseline.value_column().clone()],
+ baseline.nulls().cloned(),
+ )
+ .unwrap()
+ .slice(1, 3);
+
+ let variant = VariantArray::try_new(&input).unwrap();
+ assert_eq!(variant.value(0), baseline.value(1));
+ assert!(variant.is_null(1));
+ assert_eq!(variant.value(2), baseline.value(3));
+
+ let input: ArrayRef = Arc::new(input);
+ let options = GetOptions::new_with_path("b".try_into().unwrap())
+ .with_as_type(Some(Arc::new(Field::new("b", DataType::Int8,
true))));
+ let result = variant_get(&input, options).unwrap();
+ assert_eq!(
+ result.as_primitive::<Int8Type>(),
+ &Int8Array::from(vec![None, None, Some(3)])
+ );
+ assert_eq!(
+ variant_to_json(&input).unwrap(),
+ StringArray::from(vec![Some(r#"{"a":1}"#), None,
Some(r#"{"b":3}"#)])
+ );
+ }
+ }
+
#[test]
fn invalid_value_field_type() {
let fields = Fields::from(vec![