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 c9cf6c82 [python] Add batch numeric extraction for Variant fields 
(#997)
c9cf6c82 is described below

commit c9cf6c82e2f74221220ff32c298582a7cd0c69ad
Author: XiaoHongbo <[email protected]>
AuthorDate: Wed Sep 30 20:46:55 2026 +0800

    [python] Add batch numeric extraction for Variant fields (#997)
---
 bindings/python/python/pypaimon_rust/data.pyi |  26 +++
 bindings/python/src/lib.rs                    |   2 +
 bindings/python/src/variant.rs                |  55 +++++
 bindings/python/tests/test_variant.py         |  65 ++++++
 crates/paimon/src/arrow/mod.rs                |   2 +
 crates/paimon/src/arrow/variant.rs            | 284 ++++++++++++++++++++++++++
 crates/paimon/src/variant.rs                  |  21 +-
 crates/paimon/src/variant/numeric.rs          | 246 ++++++++++++++++++++++
 8 files changed, 693 insertions(+), 8 deletions(-)

diff --git a/bindings/python/python/pypaimon_rust/data.pyi 
b/bindings/python/python/pypaimon_rust/data.pyi
new file mode 100644
index 00000000..c46aa2bb
--- /dev/null
+++ b/bindings/python/python/pypaimon_rust/data.pyi
@@ -0,0 +1,26 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from typing import Sequence
+
+import pyarrow
+
+def variant_get_numeric_fields(
+    column: pyarrow.Array, fields: Sequence[str]
+) -> pyarrow.FixedSizeListArray:
+    """Extract literal top-level numeric fields as float32 in the requested 
order."""
+    ...
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index 89f690a6..310cf78a 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -26,6 +26,7 @@ mod schema;
 mod table;
 mod udf;
 mod update_assignment;
+mod variant;
 mod write;
 // ---- #285: observability ----
 mod partition;
@@ -35,5 +36,6 @@ mod tag;
 #[pymodule]
 fn pypaimon_rust(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
     context::register_module(py, m)?;
+    variant::register_module(py, m)?;
     Ok(())
 }
diff --git a/bindings/python/src/variant.rs b/bindings/python/src/variant.rs
new file mode 100644
index 00000000..f59ae351
--- /dev/null
+++ b/bindings/python/src/variant.rs
@@ -0,0 +1,55 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use arrow::array::{make_array, Array, ArrayData, StructArray};
+use arrow::pyarrow::{FromPyArrow, ToPyArrow};
+use pyo3::prelude::*;
+
+use crate::error::to_py_err;
+
+/// Extract literal top-level numeric Variant fields into a row-major array.
+#[pyfunction]
+fn variant_get_numeric_fields(
+    py: Python<'_>,
+    column: &Bound<'_, PyAny>,
+    fields: Vec<String>,
+) -> PyResult<Py<PyAny>> {
+    let column = make_array(ArrayData::from_pyarrow_bound(column)?);
+    let output = py
+        .detach(|| {
+            let column = column
+                .as_any()
+                .downcast_ref::<StructArray>()
+                .ok_or_else(|| paimon::Error::DataInvalid {
+                    message: "Expected a PyArrow Variant 
StructArray".to_string(),
+                    source: None,
+                })?;
+            paimon::arrow::variant_get_numeric_fields(column, &fields)
+        })
+        .map_err(to_py_err)?;
+    Ok(output.to_data().to_pyarrow(py)?.unbind())
+}
+
+pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> 
PyResult<()> {
+    let this = PyModule::new(py, "data")?;
+    this.add_function(wrap_pyfunction!(variant_get_numeric_fields, &this)?)?;
+    m.add_submodule(&this)?;
+    py.import("sys")?
+        .getattr("modules")?
+        .set_item("pypaimon_rust.data", this)?;
+    Ok(())
+}
diff --git a/bindings/python/tests/test_variant.py 
b/bindings/python/tests/test_variant.py
new file mode 100644
index 00000000..41025c98
--- /dev/null
+++ b/bindings/python/tests/test_variant.py
@@ -0,0 +1,65 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pyarrow as pa
+import pytest
+
+from pypaimon_rust.data import variant_get_numeric_fields
+
+
+def test_variant_get_numeric_fields_returns_ordered_fixed_size_lists():
+    # Java-compatible encoding for {"age": 27, "city": "Beijing"}.
+    value = bytes(
+        [
+            0x02,
+            0x02,
+            0x00,
+            0x01,
+            0x00,
+            0x02,
+            0x0A,
+            0x0C,
+            0x1B,
+            0x1D,
+            ord("B"),
+            ord("e"),
+            ord("i"),
+            ord("j"),
+            ord("i"),
+            ord("n"),
+            ord("g"),
+        ]
+    )
+    metadata = bytes([0x01, 0x02, 0x00, 0x03, 0x07]) + b"agecity"
+    column = pa.StructArray.from_arrays(
+        [pa.array([value, b""], pa.binary()), pa.array([metadata, b""], 
pa.binary())],
+        fields=[
+            pa.field("value", pa.binary(), nullable=False),
+            pa.field("metadata", pa.binary(), nullable=False),
+        ],
+        mask=pa.array([False, True]),
+    )
+
+    output = variant_get_numeric_fields(column, ["missing", "age"])
+
+    assert output.type == pa.list_(pa.field("item", pa.float32()), 2)
+    assert output.to_pylist() == [[None, 27.0], None]
+
+    with pytest.raises(NotImplementedError, match="city"):
+        variant_get_numeric_fields(column, ["city"])
+    with pytest.raises(ValueError, match="must not be empty"):
+        variant_get_numeric_fields(column, [])
diff --git a/crates/paimon/src/arrow/mod.rs b/crates/paimon/src/arrow/mod.rs
index 637d6ee2..0f65e4a6 100644
--- a/crates/paimon/src/arrow/mod.rs
+++ b/crates/paimon/src/arrow/mod.rs
@@ -24,9 +24,11 @@ pub(crate) mod residual;
 mod row_filter;
 pub(crate) mod schema_evolution;
 pub(crate) mod shredding;
+mod variant;
 
 pub use read_budget::ReadBudget;
 pub use row_filter::{RowFilter, RowFilterContext, RowFilterFactory};
+pub use variant::variant_get_numeric_fields;
 
 use crate::spec::{
     ArrayType, BigIntType, BooleanType, DataField, DataType as PaimonDataType, 
DateType,
diff --git a/crates/paimon/src/arrow/variant.rs 
b/crates/paimon/src/arrow/variant.rs
new file mode 100644
index 00000000..9534d86a
--- /dev/null
+++ b/crates/paimon/src/arrow/variant.rs
@@ -0,0 +1,284 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::sync::Arc;
+
+use arrow_array::builder::{FixedSizeListBuilder, Float32Builder};
+use arrow_array::{Array, BinaryArray, FixedSizeListArray, StructArray};
+use arrow_schema::{DataType, Field};
+
+use crate::variant::VariantFloat32Projection;
+use crate::{Error, Result};
+
+/// Extracts literal top-level numeric fields from an Arrow Variant column.
+///
+/// The result is row-major and preserves `fields` order. Missing fields,
+/// non-object roots, and Variant nulls become child nulls; SQL-null input rows
+/// remain parent nulls. Numeric values are converted to `f32`, which may lose
+/// precision.
+pub fn variant_get_numeric_fields(
+    column: &StructArray,
+    fields: &[String],
+) -> Result<FixedSizeListArray> {
+    if fields.is_empty() {
+        return data_invalid("Variant numeric field list must not be empty");
+    }
+
+    let width = i32::try_from(fields.len()).map_err(|_| 
Error::ResourceExhausted {
+        message: "Variant numeric field count exceeds Arrow 
limits".to_string(),
+    })?;
+    let value_capacity =
+        column
+            .len()
+            .checked_mul(fields.len())
+            .ok_or_else(|| Error::ResourceExhausted {
+                message: "Variant numeric output exceeds addressable 
memory".to_string(),
+            })?;
+
+    let value_column = variant_binary_child(column, 0, "value")?;
+    let metadata_column = variant_binary_child(column, 1, "metadata")?;
+    let mut builder = FixedSizeListBuilder::with_capacity(
+        Float32Builder::with_capacity(value_capacity),
+        width,
+        column.len(),
+    )
+    .with_field(Arc::new(Field::new("item", DataType::Float32, true)));
+    let mut projection_metadata = None;
+    let mut projection = None;
+    let mut offsets = Vec::new();
+    let mut extracted = vec![None; fields.len()];
+
+    for row in 0..column.len() {
+        if column.is_null(row) {
+            append_null_row(&mut builder, fields.len());
+            continue;
+        }
+        if value_column.is_null(row) || metadata_column.is_null(row) {
+            return data_invalid(format!(
+                "Variant row {row} has a null value or metadata child"
+            ));
+        }
+
+        let metadata = metadata_column.value(row);
+        if projection_metadata != Some(metadata) {
+            projection = Some(VariantFloat32Projection::new(metadata, 
fields)?);
+            projection_metadata = Some(metadata);
+        }
+        projection.as_mut().unwrap().extract_float32(
+            value_column.value(row),
+            metadata,
+            &mut offsets,
+            &mut extracted,
+        )?;
+        for value in &extracted {
+            builder.values().append_option(*value);
+        }
+        builder.append(true);
+    }
+
+    Ok(builder.finish())
+}
+
+fn variant_binary_child<'a>(
+    column: &'a StructArray,
+    index: usize,
+    expected_name: &str,
+) -> Result<&'a BinaryArray> {
+    let Some(field) = column.fields().get(index) else {
+        return data_invalid("Expected Variant struct fields value and 
metadata");
+    };
+    if column.num_columns() != 2
+        || field.name() != expected_name
+        || field.data_type() != &DataType::Binary
+    {
+        return data_invalid("Expected Variant struct fields value and 
metadata");
+    }
+    column
+        .column(index)
+        .as_any()
+        .downcast_ref::<BinaryArray>()
+        .ok_or_else(|| Error::DataInvalid {
+            message: format!("Variant {expected_name} field must be Binary"),
+            source: None,
+        })
+}
+
+fn append_null_row(builder: &mut FixedSizeListBuilder<Float32Builder>, 
field_count: usize) {
+    for _ in 0..field_count {
+        builder.values().append_null();
+    }
+    builder.append(false);
+}
+
+fn data_invalid<T>(message: impl Into<String>) -> Result<T> {
+    Err(Error::DataInvalid {
+        message: message.into(),
+        source: None,
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::variant::GenericVariant;
+    use arrow_array::builder::BinaryBuilder;
+    use arrow_array::cast::AsArray;
+    use arrow_buffer::{BooleanBuffer, NullBuffer};
+
+    fn variant_column(rows: &[Option<&str>]) -> StructArray {
+        let mut values = BinaryBuilder::new();
+        let mut metadata = BinaryBuilder::new();
+        let mut validity = Vec::with_capacity(rows.len());
+        for row in rows {
+            match row {
+                Some(json) => {
+                    let variant = GenericVariant::parse_json(json).unwrap();
+                    values.append_value(variant.value());
+                    metadata.append_value(variant.metadata());
+                    validity.push(true);
+                }
+                None => {
+                    values.append_value([]);
+                    metadata.append_value([]);
+                    validity.push(false);
+                }
+            }
+        }
+        StructArray::new(
+            match super::super::variant_arrow_type() {
+                DataType::Struct(fields) => fields,
+                _ => unreachable!(),
+            },
+            vec![Arc::new(values.finish()), Arc::new(metadata.finish())],
+            Some(NullBuffer::new(BooleanBuffer::from(validity))),
+        )
+    }
+
+    #[test]
+    fn extracts_numeric_fields_in_requested_order() {
+        let input = variant_column(&[
+            Some(r#"{"i":7,"f":1.25e0,"d":12.50,"missing":null}"#),
+            Some(r#"{"i":-2,"f":-0.0,"d":0}"#),
+            None,
+        ]);
+        let fields = vec![
+            "d".to_string(),
+            "i".to_string(),
+            "f".to_string(),
+            "missing".to_string(),
+            "absent".to_string(),
+        ];
+
+        let output = variant_get_numeric_fields(&input, &fields).unwrap();
+        assert_eq!(output.len(), 3);
+        assert!(output.is_valid(0));
+        assert!(output.is_valid(1));
+        assert!(output.is_null(2));
+        let values = output
+            .values()
+            .as_primitive::<arrow_array::types::Float32Type>();
+        assert_eq!(values.value(0), 12.5);
+        assert_eq!(values.value(1), 7.0);
+        assert_eq!(values.value(2), 1.25);
+        assert!(values.is_null(3));
+        assert!(values.is_null(4));
+        assert_eq!(values.value(5), 0.0);
+        assert_eq!(values.value(6), -2.0);
+        assert_eq!(values.value(7), 0.0);
+        assert!(values.is_null(8));
+        assert!(values.is_null(9));
+    }
+
+    #[test]
+    fn treats_field_names_as_literals() {
+        let input = variant_column(&[Some(r#"{"a.b":3,"a":{"b":9}}"#)]);
+        let output = variant_get_numeric_fields(&input, 
&["a.b".to_string()]).unwrap();
+        let values = output
+            .values()
+            .as_primitive::<arrow_array::types::Float32Type>();
+        assert_eq!(values.value(0), 3.0);
+    }
+
+    #[test]
+    fn rejects_non_numeric_fields() {
+        let input = variant_column(&[Some(r#"{"value":"3"}"#)]);
+        let err = variant_get_numeric_fields(&input, 
&["value".to_string()]).unwrap_err();
+        assert!(matches!(err, Error::Unsupported { .. }));
+    }
+
+    #[test]
+    fn rejects_empty_field_list() {
+        let input = variant_column(&[Some(r#"{"value":3}"#)]);
+        let err = variant_get_numeric_fields(&input, &[]).unwrap_err();
+        assert!(matches!(err, Error::DataInvalid { .. }));
+    }
+
+    #[test]
+    fn returns_null_fields_for_non_object_roots() {
+        let input = variant_column(&[
+            Some(r#"{"value":3}"#),
+            Some("null"),
+            Some("3"),
+            Some("[1,2]"),
+            None,
+        ]);
+        let output = variant_get_numeric_fields(&input, 
&["value".to_string()]).unwrap();
+        let values = output
+            .values()
+            .as_primitive::<arrow_array::types::Float32Type>();
+
+        assert_eq!(values.value(0), 3.0);
+        assert!(values.is_null(1));
+        assert!(values.is_null(2));
+        assert!(values.is_null(3));
+        assert!(output.is_valid(0));
+        assert!(output.is_valid(1));
+        assert!(output.is_valid(2));
+        assert!(output.is_valid(3));
+        assert!(output.is_null(4));
+    }
+
+    #[test]
+    fn rejects_malformed_payloads() {
+        let mut values = BinaryBuilder::new();
+        values.append_value([0x02]);
+        let mut metadata = BinaryBuilder::new();
+        metadata.append_value([0x01, 0x00, 0x00]);
+        let input = StructArray::new(
+            match super::super::variant_arrow_type() {
+                DataType::Struct(fields) => fields,
+                _ => unreachable!(),
+            },
+            vec![Arc::new(values.finish()), Arc::new(metadata.finish())],
+            None,
+        );
+
+        let err = variant_get_numeric_fields(&input, 
&["value".to_string()]).unwrap_err();
+        assert!(matches!(err, Error::DataInvalid { .. }));
+    }
+
+    #[test]
+    fn respects_sliced_array_offsets() {
+        let input = variant_column(&[Some(r#"{"v":1}"#), Some(r#"{"v":2}"#)]);
+        let sliced = input.slice(1, 1);
+        let output = variant_get_numeric_fields(&sliced, 
&["v".to_string()]).unwrap();
+        let values = output
+            .values()
+            .as_primitive::<arrow_array::types::Float32Type>();
+        assert_eq!(values.value(0), 2.0);
+    }
+}
diff --git a/crates/paimon/src/variant.rs b/crates/paimon/src/variant.rs
index 2199e621..0fb8b9a2 100644
--- a/crates/paimon/src/variant.rs
+++ b/crates/paimon/src/variant.rs
@@ -27,6 +27,9 @@ use crate::spec::{
 use crate::{Error, Result};
 use base64::{engine::general_purpose, Engine as _};
 
+mod numeric;
+pub(crate) use numeric::VariantFloat32Projection;
+
 const BASIC_TYPE_BITS: u8 = 2;
 const BASIC_TYPE_MASK: u8 = 0x3;
 const TYPE_INFO_MASK: u8 = 0x3f;
@@ -228,7 +231,7 @@ impl<'a> VariantRef<'a> {
                     layout.id_start + layout.id_size * i,
                     layout.id_size,
                 )?;
-                if key == get_metadata_key(self.metadata, id)? {
+                if key == get_metadata_key_ref(self.metadata, id)? {
                     let offset = read_unsigned(
                         self.value,
                         layout.offset_start + layout.offset_size * i,
@@ -252,7 +255,7 @@ impl<'a> VariantRef<'a> {
                     layout.id_start + layout.id_size * mid,
                     layout.id_size,
                 )?;
-                match java_string_cmp(&get_metadata_key(self.metadata, id)?, 
key) {
+                match java_string_cmp(get_metadata_key_ref(self.metadata, 
id)?, key) {
                     std::cmp::Ordering::Less => low = mid + 1,
                     std::cmp::Ordering::Greater => high = mid,
                     std::cmp::Ordering::Equal => {
@@ -951,6 +954,10 @@ fn metadata_offset_size(metadata: &[u8]) -> Result<usize> {
 }
 
 fn get_metadata_key(metadata: &[u8], id: usize) -> Result<String> {
+    Ok(get_metadata_key_ref(metadata, id)?.to_string())
+}
+
+fn get_metadata_key_ref(metadata: &[u8], id: usize) -> Result<&str> {
     let offset_size = metadata_offset_size(metadata)?;
     let dict_size = read_unsigned(metadata, 1, offset_size)?;
     if id >= dict_size {
@@ -964,12 +971,10 @@ fn get_metadata_key(metadata: &[u8], id: usize) -> 
Result<String> {
     }
     check_range(metadata, string_start + offset, next_offset - offset)?;
     let bytes = &metadata[string_start + offset..string_start + next_offset];
-    std::str::from_utf8(bytes)
-        .map(|v| v.to_string())
-        .map_err(|e| Error::DataInvalid {
-            message: "Malformed Variant metadata UTF-8".to_string(),
-            source: Some(Box::new(e)),
-        })
+    std::str::from_utf8(bytes).map_err(|e| Error::DataInvalid {
+        message: "Malformed Variant metadata UTF-8".to_string(),
+        source: Some(Box::new(e)),
+    })
 }
 
 fn get_boolean(value: &[u8], pos: usize) -> Result<bool> {
diff --git a/crates/paimon/src/variant/numeric.rs 
b/crates/paimon/src/variant/numeric.rs
new file mode 100644
index 00000000..34ab0bea
--- /dev/null
+++ b/crates/paimon/src/variant/numeric.rs
@@ -0,0 +1,246 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use super::*;
+
+pub(crate) struct VariantFloat32Projection {
+    fields: HashMap<usize, ProjectedField>,
+    output_width: usize,
+    last_layout: Option<ObjectLayoutProjection>,
+}
+
+struct ObjectLayoutProjection {
+    field_count: usize,
+    id_size: usize,
+    ids: Vec<u8>,
+    selected: Vec<(usize, usize)>,
+}
+
+struct ProjectedField {
+    name: String,
+    outputs: Vec<usize>,
+}
+
+impl VariantFloat32Projection {
+    pub(crate) fn new(metadata: &[u8], fields: &[String]) -> Result<Self> {
+        validate_metadata(metadata)?;
+        let mut requested = HashMap::<&str, Vec<usize>>::new();
+        for (index, field) in fields.iter().enumerate() {
+            requested.entry(field).or_default().push(index);
+        }
+
+        let offset_size = metadata_offset_size(metadata)?;
+        let dict_size = read_unsigned(metadata, 1, offset_size)?;
+        let mut projected = HashMap::with_capacity(fields.len());
+        for id in 0..dict_size {
+            let name = get_metadata_key_ref(metadata, id)?;
+            if let Some(outputs) = requested.get(name) {
+                projected.insert(
+                    id,
+                    ProjectedField {
+                        name: name.to_string(),
+                        outputs: outputs.clone(),
+                    },
+                );
+            }
+        }
+        Ok(Self {
+            fields: projected,
+            output_width: fields.len(),
+            last_layout: None,
+        })
+    }
+
+    pub(crate) fn extract_float32(
+        &mut self,
+        value: &[u8],
+        metadata: &[u8],
+        offsets: &mut Vec<usize>,
+        output: &mut [Option<f32>],
+    ) -> Result<()> {
+        if output.len() != self.output_width {
+            return data_invalid("Invalid Variant numeric output width");
+        }
+        output.fill(None);
+        if value_kind(value, 0)? != VariantKind::Object {
+            validate_payload(value, metadata)?;
+            return Ok(());
+        }
+
+        let layout = object_layout(value, 0)?;
+        offsets.clear();
+        offsets.reserve(layout.size + 1);
+        for index in 0..=layout.size {
+            offsets.push(read_unsigned(
+                value,
+                layout.offset_start + layout.offset_size * index,
+                layout.offset_size,
+            )?);
+        }
+        let data_size = offsets[layout.size];
+        if layout.data_start.checked_add(data_size) != Some(value.len()) {
+            return data_invalid("Malformed Variant root size");
+        }
+
+        let monotonic =
+            offsets.first() == Some(&0) && offsets.windows(2).all(|pair| 
pair[0] < pair[1]);
+        let sorted_offsets = if monotonic {
+            None
+        } else {
+            let mut sorted = offsets.clone();
+            sorted.sort_unstable();
+            if sorted.first() != Some(&0)
+                || sorted.last() != Some(&data_size)
+                || sorted.windows(2).any(|pair| pair[0] == pair[1])
+            {
+                return data_invalid("Malformed Variant object offsets");
+            }
+            Some(sorted)
+        };
+
+        self.prepare_layout(value, metadata, &layout)?;
+        for (index, id) in &self.last_layout.as_ref().unwrap().selected {
+            let projected = self.fields.get(id).unwrap();
+            let start = offsets[*index];
+            let end = match &sorted_offsets {
+                None => offsets[*index + 1],
+                Some(sorted) => {
+                    let next = sorted.partition_point(|offset| *offset <= 
start);
+                    *sorted.get(next).ok_or_else(|| Error::DataInvalid {
+                        message: "Malformed Variant object 
offsets".to_string(),
+                        source: None,
+                    })?
+                }
+            };
+            let child_pos =
+                layout
+                    .data_start
+                    .checked_add(start)
+                    .ok_or_else(|| Error::DataInvalid {
+                        message: "Malformed Variant object 
offsets".to_string(),
+                        source: None,
+                    })?;
+            let child_size = validate_value(value, metadata, child_pos, 1)?;
+            if child_size != end - start {
+                return data_invalid("Malformed Variant child size");
+            }
+            let child = VariantRef::new_at(value, metadata, child_pos)?;
+            let numeric = numeric_to_float32(child, &projected.name)?;
+            for output_index in &projected.outputs {
+                output[*output_index] = numeric;
+            }
+        }
+        Ok(())
+    }
+
+    fn prepare_layout(
+        &mut self,
+        value: &[u8],
+        metadata: &[u8],
+        layout: &ObjectLayout,
+    ) -> Result<()> {
+        let ids = &value[layout.id_start..layout.offset_start];
+        if self.last_layout.as_ref().is_some_and(|cached| {
+            cached.field_count == layout.size
+                && cached.id_size == layout.id_size
+                && cached.ids == ids
+        }) {
+            return Ok(());
+        }
+
+        let mut previous_key = None;
+        let mut selected = Vec::with_capacity(self.fields.len());
+        for index in 0..layout.size {
+            let id = read_unsigned(
+                value,
+                layout.id_start + layout.id_size * index,
+                layout.id_size,
+            )?;
+            let key = get_metadata_key_ref(metadata, id)?;
+            if previous_key
+                .is_some_and(|previous| java_string_cmp(previous, key) != 
std::cmp::Ordering::Less)
+            {
+                return data_invalid("Malformed Variant object key order");
+            }
+            previous_key = Some(key);
+            if self.fields.contains_key(&id) {
+                selected.push((index, id));
+            }
+        }
+        self.last_layout = Some(ObjectLayoutProjection {
+            field_count: layout.size,
+            id_size: layout.id_size,
+            ids: ids.to_vec(),
+            selected,
+        });
+        Ok(())
+    }
+}
+
+fn numeric_to_float32(value: VariantRef<'_>, field: &str) -> 
Result<Option<f32>> {
+    Ok(match value.kind()? {
+        VariantKind::Null => None,
+        VariantKind::Long => Some(value.get_long()? as f32),
+        VariantKind::Float => Some(value.get_float()?),
+        VariantKind::Double => Some(value.get_double()? as f32),
+        VariantKind::Decimal => {
+            let decimal = value.get_decimal()?;
+            Some((decimal.unscaled as f64 / 10f64.powi(decimal.scale as i32)) 
as f32)
+        }
+        kind => {
+            return Err(Error::Unsupported {
+                message: format!("Variant field '{field}' has non-numeric type 
{kind:?}"),
+            });
+        }
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn handles_non_monotonic_object_offsets() {
+        let variant = GenericVariant::parse_json(r#"{"a":1,"b":2}"#).unwrap();
+        let mut value = variant.value().to_vec();
+        let layout = object_layout(&value, 0).unwrap();
+        let first = read_unsigned(&value, layout.offset_start, 
layout.offset_size).unwrap();
+        let second = read_unsigned(
+            &value,
+            layout.offset_start + layout.offset_size,
+            layout.offset_size,
+        )
+        .unwrap();
+        write_le_at(&mut value, layout.offset_start, second, 
layout.offset_size);
+        write_le_at(
+            &mut value,
+            layout.offset_start + layout.offset_size,
+            first,
+            layout.offset_size,
+        );
+        validate_payload(&value, variant.metadata()).unwrap();
+
+        let fields = vec!["a".to_string(), "b".to_string(), "a".to_string()];
+        let mut projection = VariantFloat32Projection::new(variant.metadata(), 
&fields).unwrap();
+        let mut offsets = Vec::new();
+        let mut output = vec![None; fields.len()];
+        projection
+            .extract_float32(&value, variant.metadata(), &mut offsets, &mut 
output)
+            .unwrap();
+        assert_eq!(output, vec![Some(2.0), Some(1.0), Some(2.0)]);
+    }
+}

Reply via email to