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

alamb 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 f5f09eaa71 Finish implementing Variant::Object and Variant::List 
(#7666)
f5f09eaa71 is described below

commit f5f09eaa710dd887bf6e488df605e13c23071a88
Author: Ryan Johnson <[email protected]>
AuthorDate: Tue Jun 17 04:16:02 2025 -0700

    Finish implementing Variant::Object and Variant::List (#7666)
    
    # Which issue does this PR close?
    
    - Closes https://github.com/apache/arrow-rs/issues/7665
    
    # Rationale for this change
    
    Continuing the ongoing variant implementation effort.
    
    # What changes are included in this PR?
    
    As per title -- implement fairly complete support for variant objects
    and arrays. Also add some unit tests.
    
    Note: This PR renames `VariantArray` as `VariantList` to align with
    parquet and arrow terminology, and to not conflict with the
    `VariantArray` we will eventually need to define for holding an arrow
    array of variant-typed data.
    
    # Are there any user-facing changes?
    
    Those variant subtypes should now be usable.
    
    ---------
    
    Co-authored-by: Andrew Lamb <[email protected]>
---
 parquet-variant/src/utils.rs             |  42 ++
 parquet-variant/src/variant.rs           | 635 ++++++++++++++++++++++++++++---
 parquet-variant/tests/variant_interop.rs | 145 +++++--
 3 files changed, 719 insertions(+), 103 deletions(-)

diff --git a/parquet-variant/src/utils.rs b/parquet-variant/src/utils.rs
index 128221b4dd..0eca30e408 100644
--- a/parquet-variant/src/utils.rs
+++ b/parquet-variant/src/utils.rs
@@ -57,3 +57,45 @@ pub(crate) fn string_from_slice(slice: &[u8], range: 
Range<usize>) -> Result<&st
     str::from_utf8(slice_from_slice(slice, range)?)
         .map_err(|_| ArrowError::InvalidArgumentError("invalid UTF-8 
string".to_string()))
 }
+
+/// Performs a binary search on a slice using a fallible key extraction 
function.
+///
+/// This is similar to the standard library's `binary_search_by`, but allows 
the key
+/// extraction function to fail. If key extraction fails during the search, 
that error
+/// is propagated immediately.
+///
+/// # Arguments
+/// * `slice` - The slice to search in
+/// * `target` - The target value to search for
+/// * `key_extractor` - A function that extracts a comparable key from slice 
elements.
+///   This function can fail and return an error.
+///
+/// # Returns
+/// * `Ok(Ok(index))` - Element found at the given index
+/// * `Ok(Err(index))` - Element not found, but would be inserted at the given 
index
+/// * `Err(e)` - Key extraction failed with error `e`
+pub(crate) fn try_binary_search_by<T, K, E, F>(
+    slice: &[T],
+    target: &K,
+    mut key_extractor: F,
+) -> Result<Result<usize, usize>, E>
+where
+    K: Ord,
+    F: FnMut(&T) -> Result<K, E>,
+{
+    let mut left = 0;
+    let mut right = slice.len();
+
+    while left < right {
+        let mid = (left + right) / 2;
+        let key = key_extractor(&slice[mid])?;
+
+        match key.cmp(target) {
+            std::cmp::Ordering::Equal => return Ok(Ok(mid)),
+            std::cmp::Ordering::Greater => right = mid,
+            std::cmp::Ordering::Less => left = mid + 1,
+        }
+    }
+
+    Ok(Err(left))
+}
diff --git a/parquet-variant/src/variant.rs b/parquet-variant/src/variant.rs
index 0968f5609e..3ebc193678 100644
--- a/parquet-variant/src/variant.rs
+++ b/parquet-variant/src/variant.rs
@@ -17,7 +17,10 @@
 use crate::decoder::{
     self, get_basic_type, get_primitive_type, VariantBasicType, 
VariantPrimitiveType,
 };
-use crate::utils::{array_from_slice, first_byte_from_slice, slice_from_slice, 
string_from_slice};
+use crate::utils::{
+    array_from_slice, first_byte_from_slice, slice_from_slice, 
string_from_slice,
+    try_binary_search_by,
+};
 use arrow_schema::ArrowError;
 use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
 use std::{num::TryFromIntError, ops::Range};
@@ -89,7 +92,7 @@ impl OffsetSizeBytes {
 }
 
 #[derive(Clone, Debug, Copy, PartialEq)]
-pub struct VariantMetadataHeader {
+pub(crate) struct VariantMetadataHeader {
     version: u8,
     is_sorted: bool,
     /// Note: This is `offset_size_minus_one` + 1
@@ -113,7 +116,7 @@ impl VariantMetadataHeader {
     /// - sorted_strings is a 1-bit value indicating whether dictionary 
strings are sorted and unique.
     /// - offset_size_minus_one is a 2-bit value providing the number of bytes 
per dictionary size and offset field.
     /// - The actual number of bytes, offset_size, is offset_size_minus_one + 1
-    pub fn try_new(bytes: &[u8]) -> Result<Self, ArrowError> {
+    pub(crate) fn try_new(bytes: &[u8]) -> Result<Self, ArrowError> {
         let header = first_byte_from_slice(bytes)?;
 
         let version = header & 0x0F; // First four bits
@@ -267,7 +270,8 @@ impl<'m> VariantMetadata<'m> {
         Ok(result)
     }
 
-    pub fn header(&self) -> VariantMetadataHeader {
+    #[allow(unused)]
+    pub(crate) fn header(&self) -> VariantMetadataHeader {
         self.header
     }
 
@@ -301,65 +305,219 @@ impl<'m> VariantMetadata<'m> {
     }
 }
 
-#[derive(Clone, Copy, Debug, PartialEq)]
-pub struct VariantObject<'m, 'v> {
-    pub metadata: VariantMetadata<'m>,
-    pub value_metadata: u8,
-    pub value_data: &'v [u8],
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) struct VariantObjectHeader {
+    field_offset_size: OffsetSizeBytes,
+    field_id_size: OffsetSizeBytes,
+    num_elements: usize,
+    field_ids_start_byte: usize,
+    field_offsets_start_byte: usize,
+    values_start_byte: usize,
 }
-impl<'m, 'v> VariantObject<'m, 'v> {
-    pub fn fields(&self) -> Result<impl Iterator<Item = (&'m str, Variant<'m, 
'v>)>, ArrowError> {
-        todo!();
-        #[allow(unreachable_code)] // Just to infer the return type
-        Ok(vec![].into_iter())
+
+impl VariantObjectHeader {
+    pub(crate) fn try_new(value: &[u8]) -> Result<Self, ArrowError> {
+        // Parse the header byte to get object parameters
+        let header = first_byte_from_slice(value)?;
+        let value_header = header >> 2;
+
+        let field_offset_size_minus_one = value_header & 0x03; // Last 2 bits
+        let field_id_size_minus_one = (value_header >> 2) & 0x03; // Next 2 
bits
+        let is_large = value_header & 0x10; // 5th bit
+
+        let field_offset_size = 
OffsetSizeBytes::try_new(field_offset_size_minus_one)?;
+        let field_id_size = OffsetSizeBytes::try_new(field_id_size_minus_one)?;
+
+        // Determine num_elements size based on is_large flag
+        let num_elements_size = if is_large != 0 {
+            OffsetSizeBytes::Four
+        } else {
+            OffsetSizeBytes::One
+        };
+
+        // Parse num_elements
+        let num_elements = num_elements_size.unpack_usize(value, 1, 0)?;
+
+        // Calculate byte offsets for different sections
+        let field_ids_start_byte = 1 + num_elements_size as usize;
+        let field_offsets_start_byte = field_ids_start_byte + num_elements * 
field_id_size as usize;
+        let values_start_byte =
+            field_offsets_start_byte + (num_elements + 1) * field_offset_size 
as usize;
+
+        // Verify that the last field offset array entry is inside the value 
slice
+        let last_field_offset_byte =
+            field_offsets_start_byte + (num_elements + 1) * field_offset_size 
as usize;
+        if last_field_offset_byte > value.len() {
+            return Err(ArrowError::InvalidArgumentError(format!(
+                "Last field offset array entry at offset {} with length {} is 
outside the value slice of length {}",
+                last_field_offset_byte,
+                field_offset_size as usize,
+                value.len()
+            )));
+        }
+
+        // Verify that the value of the last field offset array entry fits 
inside the value slice
+        let last_field_offset =
+            field_offset_size.unpack_usize(value, field_offsets_start_byte, 
num_elements)?;
+        if values_start_byte + last_field_offset > value.len() {
+            return Err(ArrowError::InvalidArgumentError(format!(
+                "Last field offset value {} at offset {} is outside the value 
slice of length {}",
+                last_field_offset,
+                values_start_byte,
+                value.len()
+            )));
+        }
+        Ok(Self {
+            field_offset_size,
+            field_id_size,
+            num_elements,
+            field_ids_start_byte,
+            field_offsets_start_byte,
+            values_start_byte,
+        })
     }
-    pub fn field(&self, _name: &'m str) -> Result<Variant<'m, 'v>, ArrowError> 
{
-        todo!()
+
+    /// Returns the number of key-value pairs in this object
+    pub(crate) fn num_elements(&self) -> usize {
+        self.num_elements
     }
 }
 
-#[derive(Clone, Copy, Debug, PartialEq)]
-pub struct VariantArray<'m, 'v> {
+#[derive(Clone, Debug, PartialEq)]
+pub struct VariantObject<'m, 'v> {
     pub metadata: VariantMetadata<'m>,
-    pub value_metadata: u8,
-    pub value_data: &'v [u8],
+    pub value: &'v [u8],
+    header: VariantObjectHeader,
 }
 
-impl<'m, 'v> VariantArray<'m, 'v> {
-    /// Return the length of this array
+impl<'m, 'v> VariantObject<'m, 'v> {
+    pub fn try_new(metadata: VariantMetadata<'m>, value: &'v [u8]) -> 
Result<Self, ArrowError> {
+        Ok(Self {
+            metadata,
+            value,
+            header: VariantObjectHeader::try_new(value)?,
+        })
+    }
+
+    /// Returns the number of key-value pairs in this object
     pub fn len(&self) -> usize {
-        todo!()
+        self.header.num_elements()
     }
 
-    /// Is the array of zero length
+    /// Returns true if the object contains no key-value pairs
     pub fn is_empty(&self) -> bool {
         self.len() == 0
     }
 
-    pub fn values(&self) -> Result<impl Iterator<Item = Variant<'m, 'v>>, 
ArrowError> {
-        todo!();
-        #[allow(unreachable_code)] // Just to infer the return type
-        Ok(vec![].into_iter())
+    pub fn fields(&self) -> Result<impl Iterator<Item = (&'m str, Variant<'m, 
'v>)>, ArrowError> {
+        let field_list = self.parse_field_list()?;
+        Ok(field_list.into_iter())
+    }
+
+    pub fn field(&self, name: &str) -> Result<Option<Variant<'m, 'v>>, 
ArrowError> {
+        // Binary search through the field IDs of this object to find the 
requested field name.
+        //
+        // NOTE: This does not require a sorted metadata dictionary, because 
the variant spec
+        // requires object field ids to be lexically sorted by their 
corresponding string values,
+        // and probing the dictionary for a field id is always O(1) work.
+        let (field_ids, field_offsets) = self.parse_field_arrays()?;
+        let search_result = try_binary_search_by(&field_ids, &name, 
|&field_id| {
+            self.metadata.get_field_by(field_id)
+        })?;
+
+        let Ok(index) = search_result else {
+            return Ok(None);
+        };
+        let start_offset = field_offsets[index];
+        let end_offset = field_offsets[index + 1];
+        let value_bytes = slice_from_slice(
+            self.value,
+            self.header.values_start_byte + start_offset
+                ..self.header.values_start_byte + end_offset,
+        )?;
+        let variant = Variant::try_new_with_metadata(self.metadata, 
value_bytes)?;
+        Ok(Some(variant))
+    }
+
+    /// Parse field IDs and field offsets arrays using the cached header
+    fn parse_field_arrays(&self) -> Result<(Vec<usize>, Vec<usize>), 
ArrowError> {
+        // Parse field IDs
+        let field_ids = (0..self.header.num_elements)
+            .map(|i| {
+                self.header.field_id_size.unpack_usize(
+                    self.value,
+                    self.header.field_ids_start_byte,
+                    i,
+                )
+            })
+            .collect::<Result<Vec<_>, _>>()?;
+        debug_assert_eq!(field_ids.len(), self.header.num_elements);
+
+        // Parse field offsets (num_elements + 1 entries)
+        let field_offsets = (0..=self.header.num_elements)
+            .map(|i| {
+                self.header.field_offset_size.unpack_usize(
+                    self.value,
+                    self.header.field_offsets_start_byte,
+                    i,
+                )
+            })
+            .collect::<Result<Vec<_>, _>>()?;
+        debug_assert_eq!(field_offsets.len(), self.header.num_elements + 1);
+
+        Ok((field_ids, field_offsets))
+    }
+
+    /// Parse all fields into a vector for iteration
+    fn parse_field_list(&self) -> Result<Vec<(&'m str, Variant<'m, 'v>)>, 
ArrowError> {
+        let (field_ids, field_offsets) = self.parse_field_arrays()?;
+
+        let mut fields = Vec::with_capacity(self.header.num_elements);
+
+        for i in 0..self.header.num_elements {
+            let field_id = field_ids[i];
+            let field_name = self.metadata.get_field_by(field_id)?;
+
+            let start_offset = field_offsets[i];
+            let value_bytes =
+                slice_from_slice(self.value, self.header.values_start_byte + 
start_offset..)?;
+            let variant = Variant::try_new_with_metadata(self.metadata, 
value_bytes)?;
+
+            fields.push((field_name, variant));
+        }
+
+        Ok(fields)
     }
+}
 
-    pub fn get(&self, index: usize) -> Result<Variant<'m, 'v>, ArrowError> {
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) struct VariantListHeader {
+    offset_size: OffsetSizeBytes,
+    is_large: bool,
+    num_elements: usize,
+    first_offset_byte: usize,
+    first_value_byte: usize,
+}
+
+impl VariantListHeader {
+    pub(crate) fn try_new(value: &[u8]) -> Result<Self, ArrowError> {
         // The 6 first bits to the left are the value_header and the 2 bits
         // to the right are the basic type, so we shift to get only the 
value_header
-        let value_header = self.value_metadata >> 2;
+        let value_header = first_byte_from_slice(value)? >> 2;
         let is_large = (value_header & 0x04) != 0; // 3rd bit from the right
         let field_offset_size_minus_one = value_header & 0x03; // Last two bits
         let offset_size = 
OffsetSizeBytes::try_new(field_offset_size_minus_one)?;
+
         // The size of the num_elements entry in the array value_data is 4 
bytes if
         // is_large is true, otherwise 1 byte.
         let num_elements_size = match is_large {
             true => OffsetSizeBytes::Four,
             false => OffsetSizeBytes::One,
         };
-        // Read the num_elements
-        // The size of the num_elements entry in the array value_data is 4 
bytes if
-        // is_large is true, otherwise 1 byte.
-        let num_elements = num_elements_size.unpack_usize(self.value_data, 0, 
0)?;
-        let first_offset_byte = num_elements_size as usize;
+
+        // Skip the header byte to read the num_elements
+        let num_elements = num_elements_size.unpack_usize(value, 1, 0)?;
+        let first_offset_byte = 1 + num_elements_size as usize;
 
         let overflow =
             || ArrowError::InvalidArgumentError("Variant value_byte_length 
overflow".into());
@@ -377,30 +535,138 @@ impl<'m, 'v> VariantArray<'m, 'v> {
             .checked_add(value_bytes)
             .ok_or_else(overflow)?;
 
-        // Skip num_elements bytes to read the offsets
-        let start_field_offset_from_first_value_byte =
-            offset_size.unpack_usize(self.value_data, first_offset_byte, 
index)?;
-        let end_field_offset_from_first_value_byte =
-            offset_size.unpack_usize(self.value_data, first_offset_byte, index 
+ 1)?;
+        // Verify that the last offset array entry is inside the value slice
+        let last_offset_byte = first_offset_byte + n_offsets * offset_size as 
usize;
+        if last_offset_byte > value.len() {
+            return Err(ArrowError::InvalidArgumentError(format!(
+                "Last offset array entry at offset {} with length {} is 
outside the value slice of length {}",
+                last_offset_byte,
+                offset_size as usize,
+                value.len()
+            )));
+        }
+
+        // Verify that the value of the last offset array entry fits inside 
the value slice
+        let last_offset = offset_size.unpack_usize(value, first_offset_byte, 
num_elements)?;
+        if first_value_byte + last_offset > value.len() {
+            return Err(ArrowError::InvalidArgumentError(format!(
+                "Last offset value {} at offset {} is outside the value slice 
of length {}",
+                last_offset,
+                first_value_byte,
+                value.len()
+            )));
+        }
+
+        Ok(Self {
+            offset_size,
+            is_large,
+            num_elements,
+            first_offset_byte,
+            first_value_byte,
+        })
+    }
+
+    /// Returns the number of elements in this list
+    pub(crate) fn num_elements(&self) -> usize {
+        self.num_elements
+    }
+
+    /// Returns the offset size in bytes
+    #[allow(unused)]
+    pub(crate) fn offset_size(&self) -> usize {
+        self.offset_size as _
+    }
+
+    /// Returns whether this is a large list
+    #[allow(unused)]
+    pub(crate) fn is_large(&self) -> bool {
+        self.is_large
+    }
+
+    /// Returns the byte offset where the offset array starts
+    pub(crate) fn first_offset_byte(&self) -> usize {
+        self.first_offset_byte
+    }
+
+    /// Returns the byte offset where the values start
+    pub(crate) fn first_value_byte(&self) -> usize {
+        self.first_value_byte
+    }
+}
+
+/// Represents a variant array.
+///
+/// NOTE: The "list" naming differs from the variant spec -- which calls it 
"array" -- in order to be
+/// consistent with parquet and arrow type naming. Otherwise, the name would 
conflict with the
+/// `VariantArray : Array` we must eventually define for variant-typed arrow 
arrays.
+#[derive(Clone, Debug, PartialEq)]
+pub struct VariantList<'m, 'v> {
+    pub metadata: VariantMetadata<'m>,
+    pub value: &'v [u8],
+    header: VariantListHeader,
+}
+
+impl<'m, 'v> VariantList<'m, 'v> {
+    pub fn try_new(metadata: VariantMetadata<'m>, value: &'v [u8]) -> 
Result<Self, ArrowError> {
+        Ok(Self {
+            metadata,
+            value,
+            header: VariantListHeader::try_new(value)?,
+        })
+    }
+
+    /// Return the length of this array
+    pub fn len(&self) -> usize {
+        self.header.num_elements()
+    }
+
+    /// Is the array of zero length
+    pub fn is_empty(&self) -> bool {
+        self.len() == 0
+    }
+
+    pub fn values(&self) -> Result<impl Iterator<Item = Variant<'m, 'v>>, 
ArrowError> {
+        let len = self.len();
+        let values = (0..len)
+            .map(move |i| self.get(i))
+            .collect::<Result<Vec<_>, _>>()?;
+        Ok(values.into_iter())
+    }
+
+    pub fn get(&self, index: usize) -> Result<Variant<'m, 'v>, ArrowError> {
+        if index >= self.header.num_elements() {
+            return Err(ArrowError::InvalidArgumentError(format!(
+                "Index {} out of bounds for list of length {}",
+                index,
+                self.header.num_elements()
+            )));
+        }
+
+        // Skip header and num_elements bytes to read the offsets
+        let start_field_offset_from_first_value_byte = 
self.header.offset_size.unpack_usize(
+            self.value,
+            self.header.first_offset_byte(),
+            index,
+        )?;
+        let end_field_offset_from_first_value_byte = 
self.header.offset_size.unpack_usize(
+            self.value,
+            self.header.first_offset_byte(),
+            index + 1,
+        )?;
 
         // Read the value bytes from the offsets
         let variant_value_bytes = slice_from_slice(
-            self.value_data,
-            first_value_byte + start_field_offset_from_first_value_byte
-                ..first_value_byte + end_field_offset_from_first_value_byte,
+            self.value,
+            self.header.first_value_byte() + 
start_field_offset_from_first_value_byte
+                ..self.header.first_value_byte() + 
end_field_offset_from_first_value_byte,
         )?;
         let variant = Variant::try_new_with_metadata(self.metadata, 
variant_value_bytes)?;
         Ok(variant)
     }
 }
 
-// impl<'m, 'v> Index<usize> for VariantArray<'m, 'v> {
-//     type Output = Variant<'m, 'v>;
-//
-// }
-
 /// Variant value. May contain references to metadata and value
-#[derive(Clone, Debug, Copy, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
 pub enum Variant<'m, 'v> {
     // TODO: Add types for the rest of the primitive types, once API is agreed 
upon
     Null,
@@ -426,7 +692,7 @@ pub enum Variant<'m, 'v> {
 
     // need both metadata & value
     Object(VariantObject<'m, 'v>),
-    Array(VariantArray<'m, 'v>),
+    List(VariantList<'m, 'v>),
 }
 
 impl<'m, 'v> Variant<'m, 'v> {
@@ -510,16 +776,8 @@ impl<'m, 'v> Variant<'m, 'v> {
             VariantBasicType::ShortString => {
                 
Variant::ShortString(decoder::decode_short_string(value_metadata, value_data)?)
             }
-            VariantBasicType::Object => Variant::Object(VariantObject {
-                metadata,
-                value_metadata,
-                value_data,
-            }),
-            VariantBasicType::Array => Variant::Array(VariantArray {
-                metadata,
-                value_metadata,
-                value_data,
-            }),
+            VariantBasicType::Object => 
Variant::Object(VariantObject::try_new(metadata, value)?),
+            VariantBasicType::Array => 
Variant::List(VariantList::try_new(metadata, value)?),
         };
         Ok(new_self)
     }
@@ -1028,7 +1286,7 @@ impl<'m, 'v> Variant<'m, 'v> {
     pub fn metadata(&self) -> Option<&'m VariantMetadata> {
         match self {
             Variant::Object(VariantObject { metadata, .. })
-            | Variant::Array(VariantArray { metadata, .. }) => Some(metadata),
+            | Variant::List(VariantList { metadata, .. }) => Some(metadata),
             _ => None,
         }
     }
@@ -1293,7 +1551,7 @@ mod tests {
     }
 
     /// Too short buffer test (missing one required offset).
-    /// Should error with “metadata shorter than dictionary_size implies”.
+    /// Should error with "metadata shorter than dictionary_size implies".
     #[test]
     fn try_new_missing_last_value() {
         let bytes = &[
@@ -1361,4 +1619,257 @@ mod tests {
             "unexpected error: {err:?}"
         );
     }
+
+    #[test]
+    fn test_variant_object_simple() {
+        // Create metadata with field names: "age", "name", "active" (sorted)
+        // Header: version=1, sorted=1, offset_size=1 (offset_size_minus_one=0)
+        // So header byte = 00_0_1_0001 = 0x10
+        let metadata_bytes = vec![
+            0b0001_0001,
+            3, // dictionary size
+            0, // "active"
+            6, // "age"
+            9, // "name"
+            13,
+            b'a',
+            b'c',
+            b't',
+            b'i',
+            b'v',
+            b'e',
+            b'a',
+            b'g',
+            b'e',
+            b'n',
+            b'a',
+            b'm',
+            b'e',
+        ];
+        let metadata = VariantMetadata::try_new(&metadata_bytes).unwrap();
+
+        // Create object value data for: {"active": true, "age": 42, "name": 
"hello"}
+        // Field IDs in sorted order: [0, 1, 2] (active, age, name)
+        // Header: basic_type=2, field_offset_size_minus_one=0, 
field_id_size_minus_one=0, is_large=0
+        // value_header = 0000_00_00 = 0x00
+        // So header byte = (0x00 << 2) | 2 = 0x02
+        let object_value = vec![
+            0x02, // header: basic_type=2, value_header=0x00
+            3,    // num_elements = 3
+            // Field IDs (1 byte each): active=0, age=1, name=2
+            0, 1, 2,
+            // Field offsets (1 byte each): 4 offsets total
+            0, // offset to first value (boolean true)
+            1, // offset to second value (int8)
+            3, // offset to third value (short string)
+            9, // end offset
+            // Values:
+            0x04, // boolean true: primitive_header=1, basic_type=0 -> (1 << 
2) | 0 = 0x04
+            0x0C,
+            42, // int8: primitive_header=3, basic_type=0 -> (3 << 2) | 0 = 
0x0C, then value 42
+            0x15, b'h', b'e', b'l', b'l',
+            b'o', // short string: length=5, basic_type=1 -> (5 << 2) | 1 = 
0x15
+        ];
+
+        let variant_obj = VariantObject::try_new(metadata, 
&object_value).unwrap();
+
+        // Test basic properties
+        assert_eq!(variant_obj.len(), 3);
+        assert!(!variant_obj.is_empty());
+
+        // Test field access
+        let active_field = variant_obj.field("active").unwrap();
+        assert!(active_field.is_some());
+        assert_eq!(active_field.unwrap().as_boolean(), Some(true));
+
+        let age_field = variant_obj.field("age").unwrap();
+        assert!(age_field.is_some());
+        assert_eq!(age_field.unwrap().as_int8(), Some(42));
+
+        let name_field = variant_obj.field("name").unwrap();
+        assert!(name_field.is_some());
+        assert_eq!(name_field.unwrap().as_string(), Some("hello"));
+
+        // Test non-existent field
+        let missing_field = variant_obj.field("missing").unwrap();
+        assert!(missing_field.is_none());
+
+        // Test fields iterator
+        let fields: Vec<_> = variant_obj.fields().unwrap().collect();
+        assert_eq!(fields.len(), 3);
+
+        // Fields should be in sorted order: active, age, name
+        assert_eq!(fields[0].0, "active");
+        assert_eq!(fields[0].1.as_boolean(), Some(true));
+
+        assert_eq!(fields[1].0, "age");
+        assert_eq!(fields[1].1.as_int8(), Some(42));
+
+        assert_eq!(fields[2].0, "name");
+        assert_eq!(fields[2].1.as_string(), Some("hello"));
+    }
+
+    #[test]
+    fn test_variant_object_empty() {
+        // Create metadata with no fields
+        let metadata_bytes = vec![
+            0x11, // header: version=1, sorted=0, offset_size_minus_one=0
+            0,    // dictionary_size = 0
+            0,    // offset[0] = 0 (end of dictionary)
+        ];
+        let metadata = VariantMetadata::try_new(&metadata_bytes).unwrap();
+
+        // Create empty object value data: {}
+        let object_value = vec![
+            0x02, // header: basic_type=2, value_header=0x00
+            0,    // num_elements = 0
+            0,    // single offset pointing to end
+                  // No field IDs, no values
+        ];
+
+        let variant_obj = VariantObject::try_new(metadata, 
&object_value).unwrap();
+
+        // Test basic properties
+        assert_eq!(variant_obj.len(), 0);
+        assert!(variant_obj.is_empty());
+
+        // Test field access on empty object
+        let missing_field = variant_obj.field("anything").unwrap();
+        assert!(missing_field.is_none());
+
+        // Test fields iterator on empty object
+        let fields: Vec<_> = variant_obj.fields().unwrap().collect();
+        assert_eq!(fields.len(), 0);
+    }
+
+    #[test]
+    fn test_variant_list_simple() {
+        // Create simple metadata (empty dictionary for this test)
+        let metadata_bytes = vec![
+            0x01, // header: version=1, sorted=0, offset_size_minus_one=0
+            0,    // dictionary_size = 0
+            0,    // offset[0] = 0 (end of dictionary)
+        ];
+        let metadata = VariantMetadata::try_new(&metadata_bytes).unwrap();
+
+        // Create list value data for: [42, true, "hi"]
+        // Header: basic_type=3 (array), field_offset_size_minus_one=0, 
is_large=0
+        // value_header = 0000_0_0_00 = 0x00
+        // So header byte = (0x00 << 2) | 3 = 0x03
+        let list_value = vec![
+            0x03, // header: basic_type=3, value_header=0x00
+            3,    // num_elements = 3
+            // Offsets (1 byte each): 4 offsets total
+            0, // offset to first value (int8)
+            2, // offset to second value (boolean true)
+            3, // offset to third value (short string)
+            6, // end offset
+            // Values:
+            0x0C,
+            42,   // int8: primitive_header=3, basic_type=0 -> (3 << 2) | 0 = 
0x0C, then value 42
+            0x04, // boolean true: primitive_header=1, basic_type=0 -> (1 << 
2) | 0 = 0x04
+            0x09, b'h', b'i', // short string: length=2, basic_type=1 -> (2 << 
2) | 1 = 0x09
+        ];
+
+        let variant_list = VariantList::try_new(metadata, 
&list_value).unwrap();
+
+        // Test basic properties
+        assert_eq!(variant_list.len(), 3);
+        assert!(!variant_list.is_empty());
+
+        // Test individual element access
+        let elem0 = variant_list.get(0).unwrap();
+        assert_eq!(elem0.as_int8(), Some(42));
+
+        let elem1 = variant_list.get(1).unwrap();
+        assert_eq!(elem1.as_boolean(), Some(true));
+
+        let elem2 = variant_list.get(2).unwrap();
+        assert_eq!(elem2.as_string(), Some("hi"));
+
+        // Test out of bounds access
+        let out_of_bounds = variant_list.get(3);
+        assert!(out_of_bounds.is_err());
+        assert!(matches!(
+            out_of_bounds.unwrap_err(),
+            ArrowError::InvalidArgumentError(ref msg) if msg.contains("out of 
bounds")
+        ));
+
+        // Test values iterator
+        let values: Vec<_> = variant_list.values().unwrap().collect();
+        assert_eq!(values.len(), 3);
+        assert_eq!(values[0].as_int8(), Some(42));
+        assert_eq!(values[1].as_boolean(), Some(true));
+        assert_eq!(values[2].as_string(), Some("hi"));
+    }
+
+    #[test]
+    fn test_variant_list_empty() {
+        // Create simple metadata (empty dictionary)
+        let metadata_bytes = vec![
+            0x01, // header: version=1, sorted=0, offset_size_minus_one=0
+            0,    // dictionary_size = 0
+            0,    // offset[0] = 0 (end of dictionary)
+        ];
+        let metadata = VariantMetadata::try_new(&metadata_bytes).unwrap();
+
+        // Create empty list value data: []
+        let list_value = vec![
+            0x03, // header: basic_type=3, value_header=0x00
+            0,    // num_elements = 0
+            0,    // single offset pointing to end
+                  // No values
+        ];
+
+        let variant_list = VariantList::try_new(metadata, 
&list_value).unwrap();
+
+        // Test basic properties
+        assert_eq!(variant_list.len(), 0);
+        assert!(variant_list.is_empty());
+
+        // Test out of bounds access on empty list
+        let out_of_bounds = variant_list.get(0);
+        assert!(out_of_bounds.is_err());
+
+        // Test values iterator on empty list
+        let values: Vec<_> = variant_list.values().unwrap().collect();
+        assert_eq!(values.len(), 0);
+    }
+
+    #[test]
+    fn test_variant_list_large() {
+        // Create simple metadata (empty dictionary)
+        let metadata_bytes = vec![
+            0x01, // header: version=1, sorted=0, offset_size_minus_one=0
+            0,    // dictionary_size = 0
+            0,    // offset[0] = 0 (end of dictionary)
+        ];
+        let metadata = VariantMetadata::try_new(&metadata_bytes).unwrap();
+
+        // Create large list value data with 2-byte offsets: [null, false]
+        // Header: is_large=1, field_offset_size_minus_one=1, basic_type=3 
(array)
+        let list_bytes = vec![
+            0x17, // header = 000_1_01_11 = 0x17
+            2, 0, 0, 0, // num_elements = 2 (4 bytes because is_large=1)
+            // Offsets (2 bytes each): 3 offsets total
+            0x00, 0x00, 0x01, 0x00, // first value (null)
+            0x02, 0x00, // second value (boolean false)
+            // Values:
+            0x00, // null: primitive_header=0, basic_type=0 -> (0 << 2) | 0 = 
0x00
+            0x08, // boolean false: primitive_header=2, basic_type=0 -> (2 << 
2) | 0 = 0x08
+        ];
+
+        let variant_list = VariantList::try_new(metadata, 
&list_bytes).unwrap();
+
+        // Test basic properties
+        assert_eq!(variant_list.len(), 2);
+        assert!(!variant_list.is_empty());
+
+        // Test individual element access
+        let elem0 = variant_list.get(0).unwrap();
+        assert_eq!(elem0.as_null(), Some(()));
+
+        let elem1 = variant_list.get(1).unwrap();
+        assert_eq!(elem1.as_boolean(), Some(false));
+    }
 }
diff --git a/parquet-variant/tests/variant_interop.rs 
b/parquet-variant/tests/variant_interop.rs
index a627cc8947..7c165967c9 100644
--- a/parquet-variant/tests/variant_interop.rs
+++ b/parquet-variant/tests/variant_interop.rs
@@ -23,9 +23,8 @@
 use std::fs;
 use std::path::{Path, PathBuf};
 
-use arrow_schema::ArrowError;
 use chrono::NaiveDate;
-use parquet_variant::{Variant, VariantMetadata};
+use parquet_variant::Variant;
 
 fn cases_dir() -> PathBuf {
     Path::new(env!("CARGO_MANIFEST_DIR"))
@@ -34,11 +33,24 @@ fn cases_dir() -> PathBuf {
         .join("variant")
 }
 
-fn load_case(name: &str) -> Result<(Vec<u8>, Vec<u8>), ArrowError> {
-    let root = cases_dir();
-    let meta = fs::read(root.join(format!("{name}.metadata")))?;
-    let val = fs::read(root.join(format!("{name}.value")))?;
-    Ok((meta, val))
+struct Case {
+    metadata: Vec<u8>,
+    value: Vec<u8>,
+}
+
+impl Case {
+    /// Load the case with the given name from the parquet testing repository.
+    fn load(name: &str) -> Self {
+        let root = cases_dir();
+        let metadata = 
fs::read(root.join(format!("{name}.metadata"))).unwrap();
+        let value = fs::read(root.join(format!("{name}.value"))).unwrap();
+        Self { metadata, value }
+    }
+
+    /// Return the Variant for this case.
+    fn variant(&self) -> Variant<'_, '_> {
+        Variant::try_new(&self.metadata, &self.value).expect("Failed to parse 
variant")
+    }
 }
 
 /// Return a list of the values from the parquet testing repository:
@@ -67,47 +79,98 @@ fn get_primitive_cases() -> Vec<(&'static str, 
Variant<'static, 'static>)> {
         ("short_string", Variant::ShortString("Less than 64 bytes (❤\u{fe0f} 
with utf8)")),
     ]
 }
-
-fn get_non_primitive_cases() -> Vec<&'static str> {
-    vec!["object_primitive", "array_primitive"]
-}
-
 #[test]
-fn variant_primitive() -> Result<(), ArrowError> {
+fn variant_primitive() {
     let cases = get_primitive_cases();
     for (case, want) in cases {
-        let (metadata, value) = load_case(case)?;
-        let got = Variant::try_new(&metadata, &value)?;
+        let case = Case::load(case);
+        let got = case.variant();
         assert_eq!(got, want);
     }
-    Ok(())
 }
-
 #[test]
-fn variant_non_primitive() -> Result<(), ArrowError> {
-    let cases = get_non_primitive_cases();
-    for case in cases {
-        let (metadata, value) = load_case(case)?;
-        let variant_metadata = VariantMetadata::try_new(&metadata)?;
-        let variant = Variant::try_new(&metadata, &value)?;
-        match case {
-            "object_primitive" => {
-                assert!(matches!(variant, Variant::Object(_)));
-                assert_eq!(variant_metadata.dictionary_size(), 7);
-                let dict_val = variant_metadata.get_field_by(0)?;
-                assert_eq!(dict_val, "int_field");
-            }
-            "array_primitive" => match variant {
-                Variant::Array(arr) => {
-                    let v = arr.get(0)?;
-                    assert!(matches!(v, Variant::Int8(2)));
-                    let v = arr.get(1)?;
-                    assert!(matches!(v, Variant::Int8(1)));
-                }
-                _ => panic!("expected an array"),
+fn variant_object_empty() {
+    let case = Case::load("object_empty");
+    let Variant::Object(variant_object) = case.variant() else {
+        panic!("expected an object");
+    };
+    assert_eq!(variant_object.len(), 0);
+    assert!(variant_object.is_empty());
+}
+#[test]
+fn variant_object_primitive() {
+    // the data is defined in
+    // 
https://github.com/apache/parquet-testing/blob/84d525a8731cec345852fb4ea2e7c581fbf2ef29/variant/data_dictionary.json#L46-L53
+    //
+    // ```json
+    // " "object_primitive": {
+    //         "boolean_false_field": false,
+    //         "boolean_true_field": true,
+    //         "double_field": 1.23456789,
+    //         "int_field": 1,
+    //         "null_field": null,
+    //         "string_field": "Apache Parquet",
+    //         "timestamp_field": "2025-04-16T12:34:56.78"
+    //     },
+    // ```
+    let case = Case::load("object_primitive");
+    let Variant::Object(variant_object) = case.variant() else {
+        panic!("expected an object");
+    };
+    let expected_fields = vec![
+        ("boolean_false_field", Variant::BooleanFalse),
+        ("boolean_true_field", Variant::BooleanTrue),
+        // spark wrote this as a decimal4 (not a double)
+        (
+            "double_field",
+            Variant::Decimal4 {
+                integer: 123456789,
+                scale: 8,
             },
-            _ => unreachable!(),
-        }
+        ),
+        ("int_field", Variant::Int8(1)),
+        ("null_field", Variant::Null),
+        ("string_field", Variant::ShortString("Apache Parquet")),
+        (
+            // apparently spark wrote this as a string (not a timestamp)
+            "timestamp_field",
+            Variant::ShortString("2025-04-16T12:34:56.78"),
+        ),
+    ];
+    let actual_fields: Vec<_> = variant_object.fields().unwrap().collect();
+    assert_eq!(actual_fields, expected_fields);
+}
+#[test]
+fn variant_array_primitive() {
+    // The data is defined in
+    // 
https://github.com/apache/parquet-testing/blob/84d525a8731cec345852fb4ea2e7c581fbf2ef29/variant/data_dictionary.json#L24-L29
+    //
+    // ```json
+    // "array_primitive": [
+    //    2,
+    //    1,
+    //    5,
+    //    9
+    // ],
+    // ```
+    let case = Case::load("array_primitive");
+    let Variant::List(list) = case.variant() else {
+        panic!("expected an array");
+    };
+    let expected = vec![
+        Variant::Int8(2),
+        Variant::Int8(1),
+        Variant::Int8(5),
+        Variant::Int8(9),
+    ];
+    let actual: Vec<_> = list.values().unwrap().collect();
+    assert_eq!(actual, expected);
+
+    // Call `get` for each individual element
+    for (i, expected_value) in expected.iter().enumerate() {
+        let got = list.get(i).unwrap();
+        assert_eq!(&got, expected_value);
     }
-    Ok(())
 }
+
+// TODO: Add tests for object_nested and array_nested


Reply via email to