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 2e035fd4d9 feat: add `OffsetBuffer::subtract` to allow to shift 
offsets by value (#10120)
2e035fd4d9 is described below

commit 2e035fd4d9e030473ef757e37ff9a5eae7524f40
Author: Raz Luvaton <[email protected]>
AuthorDate: Tue Jun 23 17:13:36 2026 +0300

    feat: add `OffsetBuffer::subtract` to allow to shift offsets by value 
(#10120)
    
    # Which issue does this PR close?
    
    N/A
    
    # Rationale for this change
    
    When dealing with sliced variable size arrays it is common to subtract
    the offset to shift them to 0, so this is a helper for that
    
    # What changes are included in this PR?
    
    added `OffsetBuffer::subtract` method
    
    # Are these changes tested?
    yes
    
    # Are there any user-facing changes?
    new function
---
 arrow-array/src/array/list_array.rs |   2 +-
 arrow-buffer/src/buffer/offset.rs   | 216 ++++++++++++++++++++++++++++++++++++
 2 files changed, 217 insertions(+), 1 deletion(-)

diff --git a/arrow-array/src/array/list_array.rs 
b/arrow-array/src/array/list_array.rs
index 24f7774f2b..f74ff7d3f3 100644
--- a/arrow-array/src/array/list_array.rs
+++ b/arrow-array/src/array/list_array.rs
@@ -38,7 +38,7 @@ use std::sync::Arc;
 /// [`StringArray`]: crate::array::StringArray
 /// [`LargeStringArray`]: crate::array::LargeStringArray
 pub trait OffsetSizeTrait:
-    ArrowNativeType + std::ops::AddAssign + Integer + num_traits::CheckedAdd
+    ArrowNativeType + std::ops::AddAssign + Integer + num_traits::CheckedAdd + 
num_traits::CheckedSub
 {
     /// True for 64 bit offset size and false for 32 bit offset size
     const IS_LARGE: bool;
diff --git a/arrow-buffer/src/buffer/offset.rs 
b/arrow-buffer/src/buffer/offset.rs
index d3a4b3cffb..86ce100c23 100644
--- a/arrow-buffer/src/buffer/offset.rs
+++ b/arrow-buffer/src/buffer/offset.rs
@@ -326,6 +326,69 @@ impl<O: ArrowNativeType> OffsetBuffer<O> {
 
         end_offset_of_last_valid_value != last_offset
     }
+
+    /// Subtract `rhs` from all offsets
+    /// This will try to reuse the existing allocation as much as possible
+    ///
+    /// Panics: this will panic if `rhs` > the first offset or if `rhs` will 
lead to overflow (when `rhs` is negative)
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// # use arrow_buffer::OffsetBuffer;
+    /// let offsets = OffsetBuffer::<i32>::from_lengths(vec![4, 1, 5, 6]);
+    /// assert_eq!(offsets.as_ref(), &[0, 4, 5, 10, 16]);
+    ///
+    /// let sliced_offsets = offsets.slice(1, 2);
+    /// assert_eq!(sliced_offsets.as_ref(), &[4, 5, 10]);
+    ///
+    /// let shifted_offsets = sliced_offsets.subtract(4);
+    /// assert_eq!(shifted_offsets.as_ref(), &[0, 1, 6]);
+    /// ```
+    ///
+    pub fn subtract(self, rhs: O) -> Self
+    where
+        O: std::ops::Sub<Output = O> + std::cmp::PartialOrd + 
num_traits::CheckedSub,
+    {
+        if rhs == O::usize_as(0) {
+            return self;
+        }
+
+        let len = self.len();
+
+        // Offset buffer is guaranteed to be non-empty
+        assert!(
+            self[0] >= rhs,
+            "shifted offsets will become negative which is not allowed"
+        );
+
+        // If negative, make sure that this will not create an overflow
+        if rhs < O::usize_as(0) {
+            self[len - 1].checked_sub(&rhs).expect("must not overflow");
+        }
+
+        // try and reuse buffer
+        let shifted_offsets: Vec<O> = match 
self.into_inner().into_inner().into_vec() {
+            // If we can reuse the buffer, update in place
+            Ok(mut v) => {
+                for offset in v.iter_mut() {
+                    *offset = *offset - rhs;
+                }
+                v
+            }
+            // otherwise, buffer is shared so we need a copy
+            Err(buffer) => {
+                let offsets = ScalarBuffer::<O>::from(buffer);
+                offsets.iter().map(|offset| *offset - rhs).collect()
+            }
+        };
+        let shifted_buffer = ScalarBuffer::from(shifted_offsets);
+        // Safety: offsets are valid as they are coming from a valid
+        // offset buffer and we checked overflow above, and we
+        // subtracted the same value from all offsets, thus keeping the
+        // same properties as the input buffer
+        unsafe { Self::new_unchecked(shifted_buffer) }
+    }
 }
 
 impl<T: ArrowNativeType> Deref for OffsetBuffer<T> {
@@ -810,4 +873,157 @@ mod tests {
         let nulls = NullBuffer::new_valid(5); // expects 3
         offsets.has_non_empty_nulls(Some(&nulls));
     }
+
+    #[test]
+    #[should_panic(expected = "shifted offsets will become negative which is 
not allowed")]
+    fn 
should_panic_for_subtract_by_value_that_will_cause_offsets_to_be_less_than_zero()
 {
+        // self[0] = 0, rhs = 1 -> 0 >= 1 is false -> assert fires
+        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, 3, 
6]));
+        offsets.subtract(1);
+    }
+
+    #[test]
+    fn 
subtract_by_value_that_will_cause_offsets_to_be_less_than_zero_for_outside_the_slice()
 {
+        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, 1, 
4, 7]));
+        let sliced = offsets.slice(1, 2); // [1, 4, 7]
+        drop(offsets);
+        assert_eq!(sliced.as_ref(), &[1, 4, 7]);
+
+        let result = sliced.subtract(1);
+        assert_eq!(result.as_ref(), &[0, 3, 6]);
+        assert_eq!(result.len(), 3);
+    }
+
+    #[test]
+    #[should_panic(expected = "must not overflow")]
+    fn should_panic_subtract_by_value_that_will_cause_offsets_to_overflow() {
+        // rhs = -1 (negative). self[0] = 0 >= -1 passes.
+        // last offset i32::MAX - (-1) = i32::MAX + 1 -> checked_sub returns 
None -> expect fires
+        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, 5, 
i32::MAX]));
+        offsets.subtract(-1);
+    }
+
+    #[test]
+    fn 
subtract_by_value_that_will_cause_offsets_to_overflow_outside_the_slice() {
+        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, 3, 
6, i32::MAX]));
+        let sliced = offsets.slice(0, 2); // [0, 3, 6]
+        assert_eq!(sliced.as_ref(), &[0, 3, 6]);
+
+        let result = sliced.subtract(-1);
+        assert_eq!(result.as_ref(), &[1, 4, 7]);
+        assert_eq!(result.len(), 3);
+    }
+
+    #[test]
+    fn 
when_shift_is_0_subtract_should_reuse_the_buffer_even_when_it_is_shared() {
+        // subtract(0) hits the early `return self` before any into_mutable,
+        // so the returned buffer is the exact same allocation even while 
shared.
+        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, 3, 
6]));
+        let shared = offsets.clone(); // refcount now 2 -> shared
+        let result = offsets.subtract(0);
+        assert!(
+            result.ptr_eq(&shared),
+            "subtract(0) must return the same underlying buffer, even when 
shared"
+        );
+    }
+
+    #[test]
+    fn should_reuse_the_underline_data_when_the_buffer_is_not_shared() {
+        // Unique ownership, offset 0 -> into_mutable succeeds -> mutate in 
place,
+        // and MutableBuffer -> Buffer keeps the same allocation.
+        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![2, 5, 
8]));
+        let ptr_before = offsets.as_ptr();
+        let result = offsets.subtract(2);
+        assert_eq!(
+            ptr_before,
+            result.as_ptr(),
+            "a non-shared buffer should be mutated in place, reusing the 
allocation"
+        );
+        assert_eq!(result.as_ref(), &[0, 3, 6]);
+    }
+
+    #[test]
+    fn should_create_a_new_buffer_when_the_buffer_is_shared() {
+        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![2, 5, 
8]));
+        let shared = offsets.clone();
+        let ptr_before = offsets.as_ptr();
+        let result = offsets.subtract(2);
+        assert_ne!(
+            ptr_before,
+            result.as_ptr(),
+            "a shared buffer must not be mutated in place; a new allocation is 
created"
+        );
+        assert_eq!(result.as_ref(), &[0, 3, 6]);
+        // The shared view is untouched.
+        assert_eq!(shared.as_ref(), &[2, 5, 8]);
+    }
+
+    #[test]
+    fn when_shift_is_negative_it_should_shift_offsets_in_the_right_direction() 
{
+        // rhs = -2 -> offset - (-2) = offset + 2, so all offsets move up.
+        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, 3, 
6]));
+        let result = offsets.subtract(-2);
+        assert_eq!(result.as_ref(), &[2, 5, 8]);
+    }
+
+    // Replace this test with test that assert a reuse after PR #10118 is 
merged
+    #[test]
+    fn for_sliced_unshared_buffer_shift_should_not_reuse_buffer() {
+        // Underlying [0, 3, 6, 9, 12]; slice -> view [3, 6, 9].
+        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![1, 3, 
6, 9, 12]));
+        let sliced = offsets.slice(1, 2); // [3, 6, 9]
+        drop(offsets); // uniquely owned
+        assert_eq!(sliced.as_ref(), &[3, 6, 9]);
+
+        let ptr_before = sliced.as_ptr();
+        let result = sliced.subtract(1);
+
+        assert_ne!(
+            ptr_before,
+            result.as_ptr(),
+            "should not be reused until #10118 is merged"
+        );
+
+        assert_eq!(result.as_ref(), &[2, 5, 8]);
+    }
+
+    #[test]
+    fn for_sliced_but_start_at_0_unshared_buffer_shift_should_reuse_buffer() {
+        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![1, 3, 
6, 9, 12]));
+        let sliced = offsets.slice(0, 2);
+        drop(offsets); // uniquely owned
+        assert_eq!(sliced.as_ref(), &[1, 3, 6]);
+
+        let ptr_before = sliced.as_ptr();
+        let result = sliced.subtract(1);
+
+        assert_eq!(ptr_before, result.as_ptr(), "should be reused");
+
+        assert_eq!(result.as_ref(), &[0, 2, 5]);
+    }
+
+    #[test]
+    fn 
for_sliced_shared_buffer_shifted_buffer_should_only_include_the_sliced_data() {
+        // Underlying: [0, 3, 6, 9, 12]; slice(1, 2) -> view [3, 6, 9].
+        // `offsets` stays alive, so the sliced buffer is shared -> Err branch.
+        // The Err branch copies `len` (= 3) elements from the *sliced* 
typed_data,
+        // so the result contains only the sliced data, shifted.
+        let offsets = OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, 3, 
6, 9, 12]));
+        let sliced = offsets.slice(1, 2);
+        assert_eq!(sliced.as_ref(), &[3, 6, 9]);
+
+        let result = sliced.subtract(3);
+
+        assert_eq!(
+            result.as_ref(),
+            &[0, 3, 6],
+            "shifted result should contain only the sliced data"
+        );
+        assert_eq!(result.len(), 3);
+
+        // Assert that the underlying buffer of the result is not sliced to 
make sure it does not include the data outside the slice range from the 
original buffer
+        let underlying_buffer = result.inner().inner();
+        assert_eq!(underlying_buffer.ptr_offset(), 0);
+        assert_eq!(underlying_buffer.len(), 3 * std::mem::size_of::<i32>());
+    }
 }

Reply via email to