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

github-merge-queue[bot] 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 eecf5f3870 fix(arrow-array): reset the lone offset of an empty FFI 
byte array (#11661)
eecf5f3870 is described below

commit eecf5f3870d7e8e13cb5ba58ddf1c811defa631e
Author: Chang Yong Lik <[email protected]>
AuthorDate: Sun Oct 25 16:16:27 2026 +0000

    fix(arrow-array): reset the lone offset of an empty FFI byte array (#11661)
    
    # Which issue does this PR close?
    
    Closes #11585.
    
    # Rationale for this change
    
    `StringArray::slice(n, 0)` keeps the offset it was sliced at as the
    array's single offset. When such an array is exported and imported
    again, `buffer_len` sizes the values buffer of an empty array at offset
    0 as 0, because that lone offset can be any value from the producer (see
    `test_empty_string_with_non_zero_offset`). The offset itself was kept,
    so the imported `ArrayData` failed `validate_full` with `First offset 2
    of Utf8 is larger than values length 0`.
    
    # What changes are included in this PR?
    
    In `ImportedArrowArray::consume`, an empty
    `Utf8`/`Binary`/`LargeUtf8`/`LargeBinary` array at offset 0 gets a
    zeroed offsets buffer, which matches the empty values buffer. Sizing the
    values buffer from the lone offset instead would read past the buffer
    for producers like the one in `test_empty_string_with_non_zero_offset`
    (offset 123 over an empty buffer).
    
    # Are these changes tested?
    
    Yes. `test_empty_slice_of_bytes` round-trips `slice(1, 0)` of each of
    the four types through `to_ffi`/`from_ffi`, then checks `validate_full`
    and equality with the exported data. It fails without the fix with the
    error above. `cargo test -p arrow-array --features ffi` and `--features
    ffi,force_validate` pass, and `cargo clippy -p arrow-array --all-targets
    --all-features -- -D warnings` is clean.
    
    # Are there any user-facing changes?
    
    No API changes. Empty sliced byte arrays imported through the C Data
    Interface now pass validation.
    
    AI use: Claude Code (AI) helped trace the import path and wrote the
    first draft of the fix, the test and this description; I reviewed and
    ran everything.
---
 arrow-array/src/ffi.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 78 insertions(+), 4 deletions(-)

diff --git a/arrow-array/src/ffi.rs b/arrow-array/src/ffi.rs
index 1ee6ee1a9c..d96c940547 100644
--- a/arrow-array/src/ffi.rs
+++ b/arrow-array/src/ffi.rs
@@ -293,7 +293,19 @@ impl ImportedArrowArray<'_> {
         };
 
         let data_layout = layout(&self.data_type);
-        let buffers = self.buffers(data_layout.can_contain_null_mask, 
data_layout.variadic)?;
+        let mut buffers = self.buffers(data_layout.can_contain_null_mask, 
data_layout.variadic)?;
+
+        // The values buffer of an empty array at offset 0 is imported as 
empty (see
+        // `buffer_len`), so its lone offset, which can be any value, is reset 
to 0.
+        if len == 0
+            && offset == 0
+            && matches!(
+                self.data_type,
+                DataType::Utf8 | DataType::Binary | DataType::LargeUtf8 | 
DataType::LargeBinary
+            )
+        {
+            buffers[0] = 
MutableBuffer::from_len_zeroed(buffers[0].len()).into();
+        }
 
         let null_bit_buffer = if data_layout.can_contain_null_mask {
             self.null_bit_buffer()
@@ -1402,10 +1414,11 @@ mod tests_from_ffi {
     use crate::{
         ArrayRef, GenericByteViewArray, ListArray,
         array::{
-            Array, BooleanArray, DictionaryArray, FixedSizeBinaryArray, 
FixedSizeListArray,
-            Int32Array, Int64Array, StringArray, StructArray, UInt32Array, 
UInt64Array,
+            Array, BinaryArray, BooleanArray, DictionaryArray, 
FixedSizeBinaryArray,
+            FixedSizeListArray, Int32Array, Int64Array, LargeBinaryArray, 
LargeStringArray,
+            StringArray, StructArray, UInt32Array, UInt64Array,
         },
-        ffi::{FFI_ArrowArray, FFI_ArrowSchema, from_ffi},
+        ffi::{FFI_ArrowArray, FFI_ArrowSchema, from_ffi, to_ffi},
         make_array,
     };
 
@@ -1741,6 +1754,67 @@ mod tests_from_ffi {
         Ok(())
     }
 
+    /// An empty slice keeps the offset it was sliced at as its lone offset, 
which
+    /// must not make the imported array invalid.
+    ///
+    /// <https://github.com/apache/arrow-rs/issues/11585>
+    #[test]
+    fn test_empty_slice_of_bytes() -> Result<()> {
+        let arrays: [ArrayRef; 4] = [
+            Arc::new(StringArray::from(vec!["ab", "c"])),
+            Arc::new(BinaryArray::from(vec![b"ab".as_ref(), b"c"])),
+            Arc::new(LargeStringArray::from(vec!["ab", "c"])),
+            Arc::new(LargeBinaryArray::from(vec![b"ab".as_ref(), b"c"])),
+        ];
+        for array in arrays {
+            let data = array.slice(1, 0).to_data();
+            let (array, schema) = to_ffi(&data)?;
+            let imported = unsafe { from_ffi(array, &schema) }?;
+
+            imported.validate_full()?;
+            assert_eq!(imported, data);
+        }
+
+        Ok(())
+    }
+
+    /// Unlike bytes, the values of lists and maps are a child array imported 
with its
+    /// own length, so the lone offset of an empty slice stays within it.
+    #[test]
+    fn test_empty_slice_of_lists() -> Result<()> {
+        use crate::builder::{
+            Int32Builder, LargeListBuilder, ListBuilder, MapBuilder, 
StringBuilder,
+        };
+
+        let mut list = ListBuilder::new(Int32Builder::new());
+        list.append_value([Some(1), Some(2)]);
+        list.append_value([Some(3)]);
+        let mut large_list = LargeListBuilder::new(Int32Builder::new());
+        large_list.append_value([Some(1), Some(2)]);
+        large_list.append_value([Some(3)]);
+        let mut map = MapBuilder::new(None, StringBuilder::new(), 
Int32Builder::new());
+        for (k, v) in [("a", 1), ("b", 2)] {
+            map.keys().append_value(k);
+            map.values().append_value(v);
+            map.append(true).unwrap();
+        }
+        let arrays: [ArrayRef; 3] = [
+            Arc::new(list.finish()),
+            Arc::new(large_list.finish()),
+            Arc::new(map.finish()),
+        ];
+        for array in arrays {
+            let data = array.slice(1, 0).to_data();
+            let (array, schema) = to_ffi(&data)?;
+            let imported = unsafe { from_ffi(array, &schema) }?;
+
+            imported.validate_full()?;
+            assert_eq!(imported, data);
+        }
+
+        Ok(())
+    }
+
     fn roundtrip_string_array(array: StringArray) -> StringArray {
         let data = array.into_data();
 

Reply via email to