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

Jefffrey 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 e6dbdbdeed Support Utf8View and BinaryView in substring (#10672)
e6dbdbdeed is described below

commit e6dbdbdeedd99121fa7166f1e6775bf956b55836
Author: Stefan Wang <[email protected]>
AuthorDate: Fri Aug 14 00:11:42 2026 -0700

    Support Utf8View and BinaryView in substring (#10672)
    
    # Which issue does this PR close?
    
    - Closes #10671.
    
    # Rationale for this change
    
    `substring()` takes `Utf8`, `LargeUtf8`, `Binary`, `LargeBinary`,
    `FixedSizeBinary` and `Dictionary`, but rejects the view types, so a
    caller holding a `StringViewArray` has to cast to `Utf8` first and pay
    for the copy. The other string kernels in this crate already accept
    them: `length`, `bit_length`, `like` and `concat_elements` all have
    `Utf8View` arms.
    
    # What changes are included in this PR?
    
    `Utf8View` and `BinaryView` arms. Views carry their own offsets rather
    than sharing one value buffer, so the byte range is computed per element
    and the result is built through the view builder. The existing
    `Dictionary` arm recurses, so a dictionary of views now works too.
    
    The utf-8 boundary check applies to `Utf8View` the same way it does to
    `Utf8`. The offset quoted in that error is per element, because there is
    no shared buffer to index into.
    
    # Are these changes tested?
    
    Yes. The view result is compared against the `Utf8` and `Binary` result
    for the same inputs, so the two paths cannot drift: empty strings,
    nulls, values longer than the twelve-byte inline limit, zero length,
    lengths past the end, and starts past either end in both directions.
    Plus the boundary error and a dictionary of views.
    
    The cross-check earned its keep — it caught the error offsets differing
    between the two paths, which is what prompted the note above.
    
    <details><summary>Tests</summary>
    
    ```
    $ cargo test -p arrow-string
    186 passed; 0 failed
    ```
    
    Removing the two new match arms fails exactly the four new tests, so
    they cannot pass vacuously:
    
    ```
    test substring::tests::string_view_matches_utf8 ... FAILED
    test substring::tests::binary_view_matches_binary ... FAILED
    test substring::tests::string_view_rejects_an_invalid_char_boundary ... 
FAILED
    test substring::tests::dictionary_of_string_view ... FAILED
    ```
    
    </details>
    
    ---------
    
    Signed-off-by: 1fanwang <[email protected]>
---
 arrow-string/src/substring.rs | 197 +++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 195 insertions(+), 2 deletions(-)

diff --git a/arrow-string/src/substring.rs b/arrow-string/src/substring.rs
index c8762499ca..66aece9d05 100644
--- a/arrow-string/src/substring.rs
+++ b/arrow-string/src/substring.rs
@@ -17,9 +17,10 @@
 
 //! Defines kernel to extract a substring of an Array
 //! Supported array types:
-//! [GenericStringArray], [GenericBinaryArray], [FixedSizeBinaryArray], 
[DictionaryArray]
+//! [GenericStringArray], [GenericBinaryArray], [GenericByteViewArray],
+//! [FixedSizeBinaryArray], [DictionaryArray]
 
-use arrow_array::builder::BufferBuilder;
+use arrow_array::builder::{BinaryViewBuilder, BufferBuilder, 
StringViewBuilder};
 use arrow_array::cast::AsArray;
 use arrow_array::types::*;
 use arrow_array::*;
@@ -102,6 +103,8 @@ pub fn substring(
             start as i32,
             length.map(|e| e as i32),
         ),
+        DataType::Utf8View => string_view_substring(array.as_string_view(), 
start, length),
+        DataType::BinaryView => binary_view_substring(array.as_binary_view(), 
start, length),
         _ => Err(ArrowError::ComputeError(format!(
             "substring does not support type {:?}",
             array.data_type()
@@ -247,6 +250,72 @@ fn utf8_bounds(val: &str, start: i64, length: 
Option<usize>) -> (usize, usize) {
     (start_offset, end_offset)
 }
 
+/// Byte range of one element, following the same rules as [`byte_substring`].
+fn view_substring_range(
+    original_length: usize,
+    start: i64,
+    substring_length: Option<u64>,
+) -> (usize, usize) {
+    let original_length = original_length as i64;
+    let new_start = match start.cmp(&0) {
+        Ordering::Greater => start.min(original_length),
+        Ordering::Equal => 0,
+        Ordering::Less => (original_length + start).max(0),
+    };
+    let new_end = match substring_length {
+        Some(length) => new_start.saturating_add(length as 
i64).min(original_length),
+        None => original_length,
+    };
+    (new_start as usize, new_end as usize)
+}
+
+fn string_view_substring(
+    array: &StringViewArray,
+    start: i64,
+    length: Option<u64>,
+) -> Result<ArrayRef, ArrowError> {
+    let mut builder = StringViewBuilder::with_capacity(array.len());
+
+    for idx in 0..array.len() {
+        if array.is_null(idx) {
+            builder.append_null();
+            continue;
+        }
+        let value = array.value(idx);
+        let (new_start, new_end) = view_substring_range(value.len(), start, 
length);
+        for offset in [new_start, new_end] {
+            if !value.is_char_boundary(offset) {
+                return Err(ArrowError::ComputeError(format!(
+                    "The offset {offset} is at an invalid utf-8 boundary."
+                )));
+            }
+        }
+        builder.append_value(&value[new_start..new_end]);
+    }
+
+    Ok(Arc::new(builder.finish()))
+}
+
+fn binary_view_substring(
+    array: &BinaryViewArray,
+    start: i64,
+    length: Option<u64>,
+) -> Result<ArrayRef, ArrowError> {
+    let mut builder = BinaryViewBuilder::with_capacity(array.len());
+
+    for idx in 0..array.len() {
+        if array.is_null(idx) {
+            builder.append_null();
+            continue;
+        }
+        let value = array.value(idx);
+        let (new_start, new_end) = view_substring_range(value.len(), start, 
length);
+        builder.append_value(&value[new_start..new_end]);
+    }
+
+    Ok(Arc::new(builder.finish()))
+}
+
 fn byte_substring<T: ByteArrayType>(
     array: &GenericByteArray<T>,
     start: T::Offset,
@@ -1035,4 +1104,128 @@ mod tests {
         let expected = BinaryArray::from(vec![Some(expected_bytes)]);
         assert_eq!(expected, *actual);
     }
+
+    #[test]
+    fn string_view_matches_utf8() {
+        let values = vec![
+            Some("hello world"),
+            Some(""),
+            None,
+            Some("a"),
+            Some("this one is definitely longer than twelve bytes"),
+        ];
+        let utf8 = StringArray::from(values.clone());
+        let view = StringViewArray::from(values);
+
+        for (start, length) in [
+            (0, None),
+            (0, Some(0)),
+            (0, Some(5)),
+            (0, Some(1000)),
+            (1, Some(3)),
+            (5, None),
+            (100, Some(2)),
+            (100, None),
+            (-3, None),
+            (-3, Some(2)),
+            (-100, Some(4)),
+            (-100, None),
+        ] {
+            let expected = substring(&utf8, start, length).unwrap();
+            let expected = expected.as_string::<i32>();
+            let actual = substring(&view, start, length).unwrap();
+            let actual = actual.as_string_view();
+            assert_eq!(
+                expected.iter().collect::<Vec<_>>(),
+                actual.iter().collect::<Vec<_>>(),
+                "start={start} length={length:?}"
+            );
+        }
+    }
+
+    #[test]
+    fn binary_view_matches_binary() {
+        let values: Vec<Option<&[u8]>> = vec![
+            Some(b"hello world"),
+            Some(b""),
+            None,
+            Some(b"abc"),
+            Some(b"this one is definitely longer than twelve bytes"),
+        ];
+        let binary = BinaryArray::from(values.clone());
+        let view = BinaryViewArray::from(values);
+
+        for (start, length) in [
+            (0, None),
+            (0, Some(5)),
+            (2, Some(3)),
+            (-3, None),
+            (100, Some(2)),
+        ] {
+            let expected = substring(&binary, start, length).unwrap();
+            let expected = expected.as_binary::<i32>();
+            let actual = substring(&view, start, length).unwrap();
+            let actual = actual.as_binary_view();
+            assert_eq!(
+                expected.iter().collect::<Vec<_>>(),
+                actual.iter().collect::<Vec<_>>(),
+                "start={start} length={length:?}"
+            );
+        }
+    }
+
+    #[test]
+    fn string_view_slices_on_a_multi_byte_boundary() {
+        // "é" and "ö" are two bytes, the CJK chars three, so 0/3/6 are the 
offsets that land
+        // on a char boundary in both values.
+        let values = vec![Some("héllo wörld"), Some("日本語"), None];
+        let utf8 = StringArray::from(values.clone());
+        let view = StringViewArray::from(values);
+
+        for (start, length) in [
+            (0, Some(3)),
+            (3, Some(3)),
+            (0, Some(6)),
+            (3, None),
+            (-3, None),
+            (-6, None),
+        ] {
+            let expected = substring(&utf8, start, length).unwrap();
+            let expected = expected.as_string::<i32>();
+            let actual = substring(&view, start, length).unwrap();
+            let actual = actual.as_string_view();
+            assert_eq!(
+                expected.iter().collect::<Vec<_>>(),
+                actual.iter().collect::<Vec<_>>(),
+                "start={start} length={length:?}"
+            );
+        }
+
+        let actual = substring(&view, 0, Some(3)).unwrap();
+        let actual = actual.as_string_view();
+        assert_eq!(actual.value(0), "hé");
+        assert_eq!(actual.value(1), "日");
+    }
+
+    #[test]
+    fn string_view_rejects_an_invalid_char_boundary() {
+        let view = StringViewArray::from(vec![Some("E=mc²")]);
+        let err = substring(&view, 0, Some(5)).unwrap_err().to_string();
+        assert!(err.contains("invalid utf-8 boundary"), "{err}");
+    }
+
+    #[test]
+    fn dictionary_of_string_view() {
+        let view = StringViewArray::from(vec![Some("hello world"), 
Some("bye")]);
+        let dict = DictionaryArray::new(Int32Array::from(vec![0, 1, 0]), 
Arc::new(view));
+
+        let actual = substring(&dict, 0, Some(3)).unwrap();
+        let actual = actual.as_any_dictionary();
+
+        let values = actual.values().as_string_view();
+        assert_eq!(
+            values.iter().collect::<Vec<_>>(),
+            vec![Some("hel"), Some("bye")]
+        );
+    }
 }

Reply via email to