diff --git a/bindings/python/python/pypaimon_rust/data.pyi b/bindings/python/python/pypaimon_rust/data.pyi new file mode 100644 index 000000000..c46aa2bbf --- /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 89f690a61..310cf78a5 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 000000000..f59ae3511 --- /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, +) -> PyResult> { + let column = make_array(ArrayData::from_pyarrow_bound(column)?); + let output = py + .detach(|| { + let column = column + .as_any() + .downcast_ref::() + .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 000000000..41025c98c --- /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 637d6ee29..0f65e4a62 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 000000000..9534d86a4 --- /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 { + 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::() + .ok_or_else(|| Error::DataInvalid { + message: format!("Variant {expected_name} field must be Binary"), + source: None, + }) +} + +fn append_null_row(builder: &mut FixedSizeListBuilder, field_count: usize) { + for _ in 0..field_count { + builder.values().append_null(); + } + builder.append(false); +} + +fn data_invalid(message: impl Into) -> Result { + 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::(); + 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::(); + 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::(); + + 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::(); + assert_eq!(values.value(0), 2.0); + } +} diff --git a/crates/paimon/src/variant.rs b/crates/paimon/src/variant.rs index 2199e6213..0fb8b9a22 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 { } fn get_metadata_key(metadata: &[u8], id: usize) -> Result { + 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 { } 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 { diff --git a/crates/paimon/src/variant/numeric.rs b/crates/paimon/src/variant/numeric.rs new file mode 100644 index 000000000..34ab0beaa --- /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, + output_width: usize, + last_layout: Option, +} + +struct ObjectLayoutProjection { + field_count: usize, + id_size: usize, + ids: Vec, + selected: Vec<(usize, usize)>, +} + +struct ProjectedField { + name: String, + outputs: Vec, +} + +impl VariantFloat32Projection { + pub(crate) fn new(metadata: &[u8], fields: &[String]) -> Result { + validate_metadata(metadata)?; + let mut requested = HashMap::<&str, Vec>::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, + output: &mut [Option], + ) -> 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> { + 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)]); + } +}