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

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new d8205a0f [core] Extract shredded Variant fields from typed columns 
without row-wise decoding (#1069)
d8205a0f is described below

commit d8205a0fc04ee5b609bf81edb05aafa263c13052
Author: Dapeng Sun(孙大鹏) <[email protected]>
AuthorDate: Thu Oct 8 16:11:48 2026 +0800

    [core] Extract shredded Variant fields from typed columns without row-wise 
decoding (#1069)
---
 .../datafusion/tests/variant_pushdown.rs           |  66 ++++
 crates/paimon/src/arrow/format/parquet.rs          | 143 ++++++-
 crates/paimon/src/arrow/shredding/variant.rs       | 433 ++++++++++++++++++++-
 3 files changed, 618 insertions(+), 24 deletions(-)

diff --git a/crates/integrations/datafusion/tests/variant_pushdown.rs 
b/crates/integrations/datafusion/tests/variant_pushdown.rs
index bb8b50e7..004430c9 100644
--- a/crates/integrations/datafusion/tests/variant_pushdown.rs
+++ b/crates/integrations/datafusion/tests/variant_pushdown.rs
@@ -267,6 +267,72 @@ async fn 
full_variant_projection_prevents_extraction_pushdown() {
     assert_eq!(ages.values(), &[27, 32]);
 }
 
+#[tokio::test]
+async fn residual_only_extraction_after_add_column_reads_values() {
+    let (_tmp, sql_context) = common::setup_sql_context().await;
+    common::exec(
+        &sql_context,
+        r#"
+        CREATE TABLE paimon.test_db.residual_t (
+            id INT,
+            payload VARIANT
+        ) WITH (
+            'file.format' = 'parquet',
+            'data-evolution.enabled' = 'true',
+            'row-tracking.enabled' = 'true',
+            'variant.shreddingSchema' =
+                
'{"type":"ROW","fields":[{"name":"payload","type":{"type":"ROW","fields":[{"name":"age","type":"INT"}]}}]}'
+        )
+        "#,
+    )
+    .await;
+    common::exec(
+        &sql_context,
+        r#"
+        INSERT INTO paimon.test_db.residual_t (id, payload)
+        SELECT 1, parse_json('{"age":27,"rest":"A"}')
+        UNION ALL
+        SELECT 2, parse_json('{"age":32,"rest":"B"}')
+        "#,
+    )
+    .await;
+    common::exec(
+        &sql_context,
+        "ALTER TABLE paimon.test_db.residual_t ADD COLUMN extra INT",
+    )
+    .await;
+
+    let batches = sql_context
+        .sql(
+            r#"
+            SELECT id, try_variant_get(payload, '$.rest', 'string') AS rest
+            FROM paimon.test_db.residual_t
+            ORDER BY id
+            "#,
+        )
+        .await
+        .unwrap()
+        .collect()
+        .await
+        .unwrap();
+    let rest = batches
+        .iter()
+        .flat_map(|batch| {
+            let column = batch.column_by_name("rest").unwrap();
+            let column = datafusion::arrow::compute::cast(
+                column,
+                &datafusion::arrow::datatypes::DataType::Utf8,
+            )
+            .unwrap();
+            let strings = 
column.as_any().downcast_ref::<StringArray>().unwrap();
+            (0..strings.len())
+                .map(|row| strings.value(row).to_string())
+                .collect::<Vec<_>>()
+        })
+        .collect::<Vec<_>>();
+    assert_eq!(rest, vec!["A".to_string(), "B".to_string()]);
+}
+
 #[tokio::test]
 async fn data_evolution_row_id_survives_variant_extraction_pushdown() {
     let (_tmp, sql_context) = 
setup_data_evolution_shredded_variant_table_with_rows().await;
diff --git a/crates/paimon/src/arrow/format/parquet.rs 
b/crates/paimon/src/arrow/format/parquet.rs
index 605699d1..48132c27 100644
--- a/crates/paimon/src/arrow/format/parquet.rs
+++ b/crates/paimon/src/arrow/format/parquet.rs
@@ -664,7 +664,18 @@ fn rebuilt_reader_options(variant_schema: 
&Option<arrow_schema::SchemaRef>) -> A
     }
 }
 
-/// File schema with `metadata: Dictionary(Int32, Binary)` for plain Variant 
columns read for extraction.
+/// Index of the Binary `metadata` child of a plain or shredded Variant struct.
+fn variant_metadata_child(children: &arrow_schema::Fields) -> Option<usize> {
+    let shredded = children.iter().any(|child| child.name() == "typed_value");
+    if !shredded && !crate::arrow::is_variant_arrow_fields(children) {
+        return None;
+    }
+    children.iter().position(|child| {
+        child.name() == "metadata" && child.data_type() == 
&arrow_schema::DataType::Binary
+    })
+}
+
+/// File schema with `metadata: Dictionary(Int32, Binary)` for Variant columns 
read for extraction.
 fn dictionary_variant_metadata_schema(
     schema: &arrow_schema::SchemaRef,
     read_fields: &[DataField],
@@ -679,21 +690,21 @@ fn dictionary_variant_metadata_schema(
                     && 
crate::spec::is_variant_extraction_row_type(read.data_type())
             });
             match field.data_type() {
-                arrow_schema::DataType::Struct(children)
-                    if extraction && 
crate::arrow::is_variant_arrow_fields(children) =>
-                {
+                arrow_schema::DataType::Struct(children) if extraction => {
+                    let Some(metadata) = variant_metadata_child(children) else 
{
+                        return Arc::clone(field);
+                    };
                     changed = true;
                     let mut children = children
                         .iter()
                         .map(|child| child.as_ref().clone())
                         .collect::<Vec<_>>();
-                    children[1] =
-                        children[1]
-                            .clone()
-                            .with_data_type(arrow_schema::DataType::Dictionary(
-                                Box::new(arrow_schema::DataType::Int32),
-                                Box::new(arrow_schema::DataType::Binary),
-                            ));
+                    children[metadata] = 
children[metadata].clone().with_data_type(
+                        arrow_schema::DataType::Dictionary(
+                            Box::new(arrow_schema::DataType::Int32),
+                            Box::new(arrow_schema::DataType::Binary),
+                        ),
+                    );
                     Arc::new(
                         field
                             .as_ref()
@@ -7561,4 +7572,114 @@ mod tests {
                 .unwrap();
         assert_eq!(actual.as_ref(), expected.as_ref());
     }
+
+    #[tokio::test]
+    async fn shredded_variant_extraction_reads_metadata_as_dictionary() {
+        let fields = vec![DataField::new(
+            0,
+            "v".to_string(),
+            DataType::Variant(VariantType::new()),
+        )];
+        let options = HashMap::from([(
+            "variant.shreddingSchema".to_string(),
+            
r#"{"type":"ROW","fields":[{"name":"v","type":{"type":"ROW","fields":[{"name":"x","type":"DOUBLE"},{"name":"y","type":"BIGINT"}]}}]}"#.to_string(),
+        )]);
+        let rows = [
+            r#"{"x":1.5e0,"y":2,"z":"rest"}"#,
+            r#"{"y":-1,"x":4e0}"#,
+            r#"{"x":"text","y":6}"#,
+        ];
+        let variants = rows
+            .iter()
+            .map(|json| GenericVariant::parse_json(json).unwrap())
+            .collect::<Vec<_>>();
+        let ArrowDataType::Struct(variant_fields) = variant_arrow_type() else {
+            unreachable!()
+        };
+        let column = StructArray::new(
+            variant_fields,
+            vec![
+                Arc::new(BinaryArray::from_iter_values(
+                    variants.iter().map(|v| v.value()),
+                )),
+                Arc::new(BinaryArray::from_iter_values(
+                    variants.iter().map(|v| v.metadata()),
+                )),
+            ],
+            None,
+        );
+        let schema = build_target_arrow_schema(&fields).unwrap();
+        let batch = RecordBatch::try_new(schema.clone(), 
vec![Arc::new(column.clone())]).unwrap();
+        let file_io = FileIOBuilder::new("memory").build().unwrap();
+        let path = format!(
+            "memory:/shredded_dictionary_{}.parquet",
+            uuid::Uuid::new_v4()
+        );
+        let mut writer = create_format_writer(
+            &file_io.new_output(&path).unwrap(),
+            schema,
+            "zstd",
+            1,
+            None,
+            Some(&fields),
+            Some(&options),
+        )
+        .await
+        .unwrap();
+        writer.write(&batch).await.unwrap();
+        writer.close().await.unwrap();
+        let data = file_io.new_input(&path).unwrap().read().await.unwrap();
+
+        let float = || DataType::Float(crate::spec::FloatType::new());
+        for paths in [vec!["$.y", "$.x"], vec!["$.y"]] {
+            let row_type = crate::spec::variant_extraction_row(
+                true,
+                paths
+                    .iter()
+                    .map(|path| (float(), path.to_string(), false, 
"UTC".to_string()))
+                    .collect::<Vec<_>>(),
+            )
+            .unwrap();
+            let read_fields = vec![DataField::new(
+                0,
+                "v".to_string(),
+                DataType::Row(row_type.clone()),
+            )];
+            let batches = ParquetFormatReader::default()
+                .read_batch_stream(
+                    Box::new(TrackingFileRead::new(data.clone())),
+                    data.len() as u64,
+                    &read_fields,
+                    None,
+                    None,
+                    None,
+                )
+                .await
+                .unwrap()
+                .try_collect::<Vec<_>>()
+                .await
+                .unwrap();
+            let read = batches[0]
+                .column_by_name("v")
+                .unwrap()
+                .as_any()
+                .downcast_ref::<StructArray>()
+                .unwrap();
+            assert!(read.column_by_name("typed_value").is_some());
+            assert!(matches!(
+                read.column_by_name("metadata").unwrap().data_type(),
+                ArrowDataType::Dictionary(key, value)
+                    if key.as_ref() == &ArrowDataType::Int32 && value.as_ref() 
== &ArrowDataType::Binary
+            ));
+            let expected = 
crate::arrow::shredding::variant::assemble_variant_extraction_array(
+                &column, &row_type,
+            )
+            .unwrap();
+            let actual = 
crate::arrow::shredding::variant::assemble_variant_extraction_array(
+                read, &row_type,
+            )
+            .unwrap();
+            assert_eq!(actual.as_ref(), expected.as_ref());
+        }
+    }
 }
diff --git a/crates/paimon/src/arrow/shredding/variant.rs 
b/crates/paimon/src/arrow/shredding/variant.rs
index 4244d998..519510f4 100644
--- a/crates/paimon/src/arrow/shredding/variant.rs
+++ b/crates/paimon/src/arrow/shredding/variant.rs
@@ -866,7 +866,9 @@ fn assemble_array_to_logical(
         }
         DataType::Row(row_type)
             if is_variant_extraction_row(row_type)
-                && (is_variant_storage_array(array) || 
is_plain_variant_array(array)) =>
+                && (is_variant_storage_array(array)
+                    || is_plain_variant_array(array)
+                    || is_dictionary_variant_storage_array(array)) =>
         {
             Ok(Some(assemble_variant_extraction_array(array, row_type)?))
         }
@@ -904,6 +906,17 @@ pub(crate) fn assemble_variant_extraction_array(
     if let Some(projected) = assemble_plain_variant_projection(input, fields, 
&metadata)? {
         return Ok(projected);
     }
+    if let Some(projected) = assemble_typed_shredded_projection(input, fields, 
&metadata)? {
+        return Ok(projected);
+    }
+    assemble_row_wise_variant_extraction(input, fields, &metadata)
+}
+
+fn assemble_row_wise_variant_extraction(
+    input: &StructArray,
+    fields: &[DataField],
+    metadata: &[crate::spec::VariantFieldMetadata],
+) -> Result<ArrayRef> {
     let materialized = materialize_variant_metadata(input)?;
     let input = &materialized;
 
@@ -1066,6 +1079,146 @@ fn assemble_plain_variant_projection(
     )))
 }
 
+/// Column-wise extraction of numeric fields held in typed shredded leaves; 
`None` means some
+/// path needs the row-wise path (residual values, arrays, nested results or 
other casts).
+fn assemble_typed_shredded_projection(
+    input: &StructArray,
+    fields: &[DataField],
+    metadata: &[crate::spec::VariantFieldMetadata],
+) -> Result<Option<ArrayRef>> {
+    if fields.is_empty() || !is_shredded_variant_array(input) {
+        return Ok(None);
+    }
+    let mut arrow_fields = Vec::with_capacity(fields.len());
+    let mut columns = Vec::with_capacity(fields.len());
+    for (field, field_metadata) in fields.iter().zip(metadata) {
+        let Some(leaf) = typed_shredded_leaf(input, field_metadata.path()) 
else {
+            return Ok(None);
+        };
+        let Some(column) = cast_typed_leaf(leaf.as_ref(), field.data_type(), 
input.nulls()) else {
+            return Ok(None);
+        };
+        arrow_fields.push(ArrowField::new(
+            field.name(),
+            paimon_type_to_arrow(field.data_type())?,
+            field.data_type().is_nullable(),
+        ));
+        columns.push(column);
+    }
+    let validities = (0..input.len()).map(|row| input.is_valid(row)).collect();
+    let array = StructArray::try_new(arrow_fields.into(), columns, 
Some(null_buffer(validities)))
+        .map_err(|e| Error::UnexpectedError {
+        message: format!("Failed to build Variant extraction StructArray: 
{e}"),
+        source: Some(Box::new(e)),
+    })?;
+    Ok(Some(Arc::new(array)))
+}
+
+/// The typed leaf of an object-key path, if no valid row needs the residual 
Variant value.
+fn typed_shredded_leaf(input: &StructArray, path: &str) -> Option<ArrayRef> {
+    let segments = crate::variant::parse_path(path).ok()?;
+    if segments.is_empty() {
+        return None;
+    }
+    let valid = input.nulls();
+    let mut node = input;
+    for segment in &segments {
+        let crate::variant::PathSegment::Key(key) = segment else {
+            return None;
+        };
+        let object = node
+            .column_by_name(VARIANT_TYPED_VALUE_FIELD_NAME)?
+            .as_any()
+            .downcast_ref::<StructArray>()?;
+        if !valid_where(object, valid) {
+            return None;
+        }
+        node = object
+            .column_by_name(key)?
+            .as_any()
+            .downcast_ref::<StructArray>()?;
+        if !valid_where(node, valid) {
+            return None;
+        }
+    }
+    if node
+        .column_by_name(VARIANT_VALUE_FIELD_NAME)
+        .is_some_and(|value| any_valid_where(value.as_ref(), valid))
+    {
+        return None;
+    }
+    node.column_by_name(VARIANT_TYPED_VALUE_FIELD_NAME).cloned()
+}
+
+/// Whether `array` is non-null on every row that `valid` keeps.
+fn valid_where(array: &dyn Array, valid: Option<&NullBuffer>) -> bool {
+    let Some(nulls) = array.nulls() else {
+        return true;
+    };
+    match valid {
+        None => nulls.null_count() == 0,
+        Some(valid) => (valid.inner() & &!nulls.inner()).count_set_bits() == 0,
+    }
+}
+
+/// Whether `array` is non-null on any row that `valid` keeps.
+fn any_valid_where(array: &dyn Array, valid: Option<&NullBuffer>) -> bool {
+    if array.null_count() == array.len() {
+        return false;
+    }
+    match (array.nulls(), valid) {
+        (None, None) => true,
+        (None, Some(valid)) => valid.null_count() < valid.len(),
+        (Some(nulls), None) => nulls.null_count() < nulls.len(),
+        (Some(nulls), Some(valid)) => (valid.inner() & 
nulls.inner()).count_set_bits() > 0,
+    }
+}
+
+/// Mirrors `cast_variant_to_shredded_value` for numeric leaves, whose casts 
cannot fail.
+fn cast_typed_leaf(
+    leaf: &dyn Array,
+    data_type: &DataType,
+    valid: Option<&NullBuffer>,
+) -> Option<ArrayRef> {
+    use arrow_array::cast::AsArray;
+    use arrow_array::types::{Float32Type, Float64Type, Int16Type, Int32Type, 
Int64Type, Int8Type};
+    let nulls = NullBuffer::union(leaf.nulls(), valid);
+    macro_rules! map {
+        ($from:ty, $to:ty, $f:expr) => {{
+            let values = leaf
+                .as_primitive::<$from>()
+                .values()
+                .iter()
+                .map($f)
+                .collect();
+            Arc::new(arrow_array::PrimitiveArray::<$to>::new(values, nulls)) 
as ArrayRef
+        }};
+    }
+    Some(match (leaf.data_type(), data_type) {
+        (ArrowDataType::Float64, DataType::Float(_)) => {
+            map!(Float64Type, Float32Type, |v| *v as f32)
+        }
+        (ArrowDataType::Float32, DataType::Float(_)) => map!(Float32Type, 
Float32Type, |v| *v),
+        (ArrowDataType::Int64, DataType::Float(_)) => map!(Int64Type, 
Float32Type, |v| *v as f32),
+        (ArrowDataType::Int32, DataType::Float(_)) => map!(Int32Type, 
Float32Type, |v| *v as f32),
+        (ArrowDataType::Int16, DataType::Float(_)) => map!(Int16Type, 
Float32Type, |v| *v as f32),
+        (ArrowDataType::Int8, DataType::Float(_)) => map!(Int8Type, 
Float32Type, |v| *v as f32),
+        (ArrowDataType::Float64, DataType::Double(_)) => map!(Float64Type, 
Float64Type, |v| *v),
+        (ArrowDataType::Float32, DataType::Double(_)) => {
+            map!(Float32Type, Float64Type, |v| *v as f64)
+        }
+        (ArrowDataType::Int64, DataType::Double(_)) => map!(Int64Type, 
Float64Type, |v| *v as f64),
+        (ArrowDataType::Int32, DataType::Double(_)) => map!(Int32Type, 
Float64Type, |v| *v as f64),
+        (ArrowDataType::Int16, DataType::Double(_)) => map!(Int16Type, 
Float64Type, |v| *v as f64),
+        (ArrowDataType::Int8, DataType::Double(_)) => map!(Int8Type, 
Float64Type, |v| *v as f64),
+        (ArrowDataType::Int64, DataType::BigInt(_)) => map!(Int64Type, 
Int64Type, |v| *v),
+        (ArrowDataType::Int32, DataType::BigInt(_)) => map!(Int32Type, 
Int64Type, |v| *v as i64),
+        (ArrowDataType::Int16, DataType::BigInt(_)) => map!(Int16Type, 
Int64Type, |v| *v as i64),
+        (ArrowDataType::Int8, DataType::BigInt(_)) => map!(Int8Type, 
Int64Type, |v| *v as i64),
+        _ => return None,
+    })
+}
+
 fn variant_from_storage_row(input: &StructArray, row: usize) -> 
Result<Option<GenericVariant>> {
     if input.is_null(row) {
         return Ok(None);
@@ -1189,12 +1342,42 @@ fn has_dictionary_variant_metadata(fields: &Fields) -> 
bool {
             if key.as_ref() == &ArrowDataType::Int32 && value.as_ref() == 
&ArrowDataType::Binary)
 }
 
+fn is_dictionary_metadata(field: &ArrowField) -> bool {
+    field.name() == VARIANT_METADATA_FIELD_NAME
+        && matches!(field.data_type(), ArrowDataType::Dictionary(key, value)
+            if key.as_ref() == &ArrowDataType::Int32 && value.as_ref() == 
&ArrowDataType::Binary)
+}
+
+/// Variant storage, shredded or clipped to `metadata` and `value`, whose 
metadata the Parquet
+/// reader kept dictionary-encoded; recognized by child names, not by order.
+fn is_dictionary_variant_storage_array(array: &dyn Array) -> bool {
+    let ArrowDataType::Struct(fields) = array.data_type() else {
+        return false;
+    };
+    let named = |name: &str| fields.iter().find(|field| field.name() == name);
+    fields.iter().all(|field| {
+        [
+            VARIANT_METADATA_FIELD_NAME,
+            VARIANT_VALUE_FIELD_NAME,
+            VARIANT_TYPED_VALUE_FIELD_NAME,
+        ]
+        .contains(&field.name().as_str())
+    }) && named(VARIANT_METADATA_FIELD_NAME).is_some_and(|field| 
is_dictionary_metadata(field))
+        && (named(VARIANT_VALUE_FIELD_NAME)
+            .is_some_and(|field| field.data_type() == &ArrowDataType::Binary)
+            || named(VARIANT_TYPED_VALUE_FIELD_NAME).is_some())
+}
+
 /// Turns dictionary-encoded Variant metadata back into Binary for the generic 
row path.
 fn materialize_variant_metadata(input: &StructArray) -> Result<StructArray> {
-    if !has_dictionary_variant_metadata(input.fields()) {
+    let Some(index) = input
+        .fields()
+        .iter()
+        .position(|field| is_dictionary_metadata(field))
+    else {
         return Ok(input.clone());
-    }
-    let metadata = arrow_cast::cast(input.column(1), 
&ArrowDataType::Binary).map_err(|e| {
+    };
+    let metadata = arrow_cast::cast(input.column(index), 
&ArrowDataType::Binary).map_err(|e| {
         Error::DataInvalid {
             message: format!("Failed to decode dictionary Variant metadata: 
{e}"),
             source: Some(Box::new(e)),
@@ -1205,15 +1388,14 @@ fn materialize_variant_metadata(input: &StructArray) -> 
Result<StructArray> {
         .iter()
         .map(|f| f.as_ref().clone())
         .collect::<Vec<_>>();
-    fields[1] = fields[1].clone().with_data_type(ArrowDataType::Binary);
-    StructArray::try_new(
-        fields.into(),
-        vec![input.column(0).clone(), metadata],
-        input.nulls().cloned(),
-    )
-    .map_err(|e| Error::DataInvalid {
-        message: format!("Failed to rebuild Variant struct: {e}"),
-        source: Some(Box::new(e)),
+    fields[index] = 
fields[index].clone().with_data_type(ArrowDataType::Binary);
+    let mut columns = input.columns().to_vec();
+    columns[index] = metadata;
+    StructArray::try_new(fields.into(), columns, 
input.nulls().cloned()).map_err(|e| {
+        Error::DataInvalid {
+            message: format!("Failed to rebuild Variant struct: {e}"),
+            source: Some(Box::new(e)),
+        }
     })
 }
 
@@ -2220,6 +2402,231 @@ mod tests {
         assert_eq!(names.value(1), "Bob");
     }
 
+    fn shredded_for_test(rows: &[Option<&str>], fields_json: &str) -> 
StructArray {
+        let logical_fields = vec![DataField::new(
+            1,
+            "v".to_string(),
+            DataType::Variant(VariantType::new()),
+        )];
+        let options = HashMap::from([(
+            "variant.shreddingSchema".to_string(),
+            format!(
+                
r#"{{"type":"ROW","fields":[{{"name":"v","type":{{"type":"ROW","fields":{fields_json}}}}}]}}"#
+            ),
+        )]);
+        let physical_fields = 
configured_variant_shredding_fields(&logical_fields, &options)
+            .unwrap()
+            .expect("shredding fields");
+        let variants = rows
+            .iter()
+            .map(|row| 
GenericVariant::parse_json(row.unwrap_or("null")).unwrap())
+            .collect::<Vec<_>>();
+        let plain = variant_array_for_test(&variants);
+        let (fields, columns, _) = plain
+            .as_any()
+            .downcast_ref::<StructArray>()
+            .unwrap()
+            .clone()
+            .into_parts();
+        let validities = rows.iter().map(Option::is_some).collect();
+        let plain = StructArray::try_new(fields, columns, 
Some(null_buffer(validities))).unwrap();
+        let batch = RecordBatch::try_new(
+            build_target_arrow_schema(&logical_fields).unwrap(),
+            vec![Arc::new(plain)],
+        )
+        .unwrap();
+        let physical =
+            batch_to_shredded_physical(&batch, &logical_fields, 
&physical_fields).unwrap();
+        physical
+            .column(0)
+            .as_any()
+            .downcast_ref::<StructArray>()
+            .unwrap()
+            .clone()
+    }
+
+    fn with_dictionary_metadata(input: &StructArray) -> StructArray {
+        let index = input
+            .fields()
+            .iter()
+            .position(|field| field.name() == VARIANT_METADATA_FIELD_NAME)
+            .unwrap();
+        let dictionary = ArrowDataType::Dictionary(
+            Box::new(ArrowDataType::Int32),
+            Box::new(ArrowDataType::Binary),
+        );
+        let mut fields = input
+            .fields()
+            .iter()
+            .map(|field| field.as_ref().clone())
+            .collect::<Vec<_>>();
+        fields[index] = 
fields[index].clone().with_data_type(dictionary.clone());
+        let mut columns = input.columns().to_vec();
+        columns[index] = arrow_cast::cast(&columns[index], 
&dictionary).unwrap();
+        StructArray::try_new(fields.into(), columns, 
input.nulls().cloned()).unwrap()
+    }
+
+    fn extraction_for_test(
+        paths: &[(DataType, &str)],
+    ) -> (RowType, Vec<crate::spec::VariantFieldMetadata>) {
+        let row_type = variant_extraction_row(
+            true,
+            paths
+                .iter()
+                .map(|(data_type, path)| {
+                    (
+                        data_type.clone(),
+                        path.to_string(),
+                        false,
+                        "UTC".to_string(),
+                    )
+                })
+                .collect::<Vec<_>>(),
+        )
+        .unwrap();
+        let metadata = row_type
+            .fields()
+            .iter()
+            .map(|field| 
parse_variant_metadata(field.description().unwrap()).unwrap())
+            .collect();
+        (row_type, metadata)
+    }
+
+    #[test]
+    fn typed_shredded_extraction_matches_row_path() {
+        let input = shredded_for_test(
+            &[
+                Some(r#"{"a":1.5e0,"b":7,"n":{"x":3.5e0},"other":"x"}"#),
+                None,
+                Some(r#"{"b":9,"n":{"x":5e-1},"other":1}"#),
+                Some(r#"{"a":-1e300,"b":-9007199254740993,"n":{"x":1e-310}}"#),
+            ],
+            r#"[{"name":"a","type":"DOUBLE"},{"name":"b","type":"BIGINT"},
+                
{"name":"n","type":{"type":"ROW","fields":[{"name":"x","type":"DOUBLE"}]}}]"#,
+        );
+        let float = || DataType::Float(FloatType::new());
+        let double = || DataType::Double(crate::spec::DoubleType::new());
+        let bigint = || DataType::BigInt(crate::spec::BigIntType::new());
+        let (row_type, metadata) = extraction_for_test(&[
+            (float(), "$.a"),
+            (double(), "$['a']"),
+            (float(), "$.b"),
+            (double(), "$.b"),
+            (bigint(), "$.b"),
+            (float(), "$.n.x"),
+            (double(), "$.n.x"),
+        ]);
+        let expected =
+            assemble_row_wise_variant_extraction(&input, row_type.fields(), 
&metadata).unwrap();
+        let typed = assemble_typed_shredded_projection(&input, 
row_type.fields(), &metadata)
+            .unwrap()
+            .expect("typed leaves take the column-wise path");
+        assert_eq!(typed.as_ref(), expected.as_ref());
+        use arrow_array::cast::AsArray;
+        let typed = typed.as_any().downcast_ref::<StructArray>().unwrap();
+        assert!(typed.is_null(1));
+        assert!(typed.column(0).is_null(2));
+        assert_eq!(
+            typed
+                .column(2)
+                .as_primitive::<arrow_array::types::Float32Type>()
+                .value(3),
+            -9007199254740993_i64 as f32
+        );
+        let dictionary = with_dictionary_metadata(&input);
+        let actual = assemble_variant_extraction_array(&dictionary, 
&row_type).unwrap();
+        assert_eq!(actual.as_ref(), expected.as_ref());
+    }
+
+    #[test]
+    fn dictionary_storage_without_typed_value_is_assembled() {
+        let input = shredded_for_test(
+            &[
+                Some(r#"{"age":27,"rest":"A"}"#),
+                None,
+                Some(r#"{"age":32,"rest":"B"}"#),
+            ],
+            r#"[{"name":"age","type":"INT"}]"#,
+        );
+        // A residual-only extraction reads the shredded root clipped to 
metadata and value.
+        let keep =
+            |name: &str| name == VARIANT_METADATA_FIELD_NAME || name == 
VARIANT_VALUE_FIELD_NAME;
+        let (fields, columns, nulls) = input.clone().into_parts();
+        let (fields, columns): (Vec<_>, Vec<_>) = fields
+            .iter()
+            .zip(columns)
+            .filter(|(field, _)| keep(field.name()))
+            .map(|(field, column)| (field.as_ref().clone(), column))
+            .unzip();
+        let clipped = StructArray::try_new(fields.into(), columns, 
nulls).unwrap();
+        let dictionary = with_dictionary_metadata(&clipped);
+        assert!(is_dictionary_variant_storage_array(&dictionary));
+        let (row_type, metadata) =
+            
extraction_for_test(&[(DataType::VarChar(VarCharType::string_type()), 
"$.rest")]);
+        let read_fields = vec![DataField::new(
+            1,
+            "v".to_string(),
+            DataType::Row(row_type.clone()),
+        )];
+        let batch = RecordBatch::try_new(
+            Arc::new(arrow_schema::Schema::new(vec![ArrowField::new(
+                "v",
+                dictionary.data_type().clone(),
+                true,
+            )])),
+            vec![Arc::new(dictionary)],
+        )
+        .unwrap();
+        let assembled = assemble_shredded_variant_batch(batch, 
&read_fields).unwrap();
+        let expected =
+            assemble_row_wise_variant_extraction(&clipped, row_type.fields(), 
&metadata).unwrap();
+        assert_eq!(assembled.column(0).as_ref(), expected.as_ref());
+        let rest = expected.as_any().downcast_ref::<StructArray>().unwrap();
+        let rest = rest
+            .column(0)
+            .as_any()
+            .downcast_ref::<StringArray>()
+            .unwrap();
+        assert_eq!(rest.value(0), "A");
+        assert!(rest.is_null(1));
+        assert_eq!(rest.value(2), "B");
+    }
+
+    #[test]
+    fn typed_shredded_extraction_falls_back_for_residual_values() {
+        let input = shredded_for_test(
+            &[
+                Some(r#"{"a":"old","b":1,"n":{"x":1e0}}"#),
+                Some(r#"{"a":2.5e0,"b":2}"#),
+                Some(r#"[1,2]"#),
+                None,
+            ],
+            r#"[{"name":"a","type":"DOUBLE"},{"name":"b","type":"BIGINT"},
+                
{"name":"n","type":{"type":"ROW","fields":[{"name":"x","type":"DOUBLE"}]}}]"#,
+        );
+        let float = || DataType::Float(FloatType::new());
+        for paths in [
+            vec![(float(), "$.a")],
+            vec![(float(), "$.b")],
+            vec![(float(), "$.n.x")],
+            vec![(float(), "$.missing")],
+            vec![(DataType::VarChar(VarCharType::string_type()), "$.b")],
+        ] {
+            let (row_type, metadata) = extraction_for_test(&paths);
+            assert!(
+                assemble_typed_shredded_projection(&input, row_type.fields(), 
&metadata)
+                    .unwrap()
+                    .is_none()
+            );
+            let expected =
+                assemble_row_wise_variant_extraction(&input, 
row_type.fields(), &metadata).unwrap();
+            let actual =
+                
assemble_variant_extraction_array(&with_dictionary_metadata(&input), &row_type)
+                    .unwrap();
+            assert_eq!(actual.as_ref(), expected.as_ref());
+        }
+    }
+
     #[test]
     fn inferred_schema_transforms_and_reassembles_variant_batch() {
         let logical_fields = vec![

Reply via email to