Skip to main content

datafusion_functions/string/
common.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Common utilities for implementing string functions
19
20use std::sync::Arc;
21
22use crate::strings::{
23    GenericStringArrayBuilder, STRING_VIEW_INIT_BLOCK_SIZE, STRING_VIEW_MAX_BLOCK_SIZE,
24    StringViewArrayBuilder, StringWriter, append_view,
25};
26use arrow::array::{
27    Array, ArrayRef, AsArray, GenericStringArray, NullBufferBuilder, OffsetSizeTrait,
28    StringViewArray, new_null_array,
29};
30use arrow::buffer::{Buffer, NullBuffer, OffsetBuffer, ScalarBuffer};
31use arrow::datatypes::DataType;
32use datafusion_common::Result;
33use datafusion_common::cast::{as_generic_string_array, as_string_view_array};
34use datafusion_common::{ScalarValue, exec_err};
35use datafusion_expr::ColumnarValue;
36
37/// Trait for trim operations, allowing compile-time dispatch instead of runtime matching.
38///
39/// Each implementation performs its specific trim operation and returns
40/// (trimmed_str, start_offset) where start_offset is the byte offset
41/// from the beginning of the input string where the trimmed result starts.
42pub(crate) trait Trimmer {
43    fn trim<'a>(input: &'a str, pattern: &[char]) -> (&'a str, u32);
44
45    /// Optimized trim for a single ASCII byte.
46    /// Uses byte-level scanning instead of char-level iteration.
47    fn trim_ascii_char(input: &str, byte: u8) -> (&str, u32);
48}
49
50/// Returns the number of leading bytes matching `byte`
51#[inline]
52fn leading_bytes(bytes: &[u8], byte: u8) -> usize {
53    bytes.iter().take_while(|&&b| b == byte).count()
54}
55
56/// Returns the number of trailing bytes matching `byte`
57#[inline]
58fn trailing_bytes(bytes: &[u8], byte: u8) -> usize {
59    bytes.iter().rev().take_while(|&&b| b == byte).count()
60}
61
62/// Left trim - removes leading characters
63pub(crate) struct TrimLeft;
64
65impl Trimmer for TrimLeft {
66    #[inline]
67    fn trim<'a>(input: &'a str, pattern: &[char]) -> (&'a str, u32) {
68        if pattern.len() == 1 && pattern[0].is_ascii() {
69            return Self::trim_ascii_char(input, pattern[0] as u8);
70        }
71        let trimmed = input.trim_start_matches(pattern);
72        let offset = (input.len() - trimmed.len()) as u32;
73        (trimmed, offset)
74    }
75
76    #[inline]
77    fn trim_ascii_char(input: &str, byte: u8) -> (&str, u32) {
78        let start = leading_bytes(input.as_bytes(), byte);
79        (&input[start..], start as u32)
80    }
81}
82
83/// Right trim - removes trailing characters
84pub(crate) struct TrimRight;
85
86impl Trimmer for TrimRight {
87    #[inline]
88    fn trim<'a>(input: &'a str, pattern: &[char]) -> (&'a str, u32) {
89        if pattern.len() == 1 && pattern[0].is_ascii() {
90            return Self::trim_ascii_char(input, pattern[0] as u8);
91        }
92        let trimmed = input.trim_end_matches(pattern);
93        (trimmed, 0)
94    }
95
96    #[inline]
97    fn trim_ascii_char(input: &str, byte: u8) -> (&str, u32) {
98        let bytes = input.as_bytes();
99        let end = bytes.len() - trailing_bytes(bytes, byte);
100        (&input[..end], 0)
101    }
102}
103
104/// Both trim - removes both leading and trailing characters
105pub(crate) struct TrimBoth;
106
107impl Trimmer for TrimBoth {
108    #[inline]
109    fn trim<'a>(input: &'a str, pattern: &[char]) -> (&'a str, u32) {
110        if pattern.len() == 1 && pattern[0].is_ascii() {
111            return Self::trim_ascii_char(input, pattern[0] as u8);
112        }
113        let left_trimmed = input.trim_start_matches(pattern);
114        let offset = (input.len() - left_trimmed.len()) as u32;
115        let trimmed = left_trimmed.trim_end_matches(pattern);
116        (trimmed, offset)
117    }
118
119    #[inline]
120    fn trim_ascii_char(input: &str, byte: u8) -> (&str, u32) {
121        let bytes = input.as_bytes();
122        let start = leading_bytes(bytes, byte);
123        let end = bytes.len() - trailing_bytes(&bytes[start..], byte);
124        (&input[start..end], start as u32)
125    }
126}
127
128pub(crate) fn general_trim<T: OffsetSizeTrait, Tr: Trimmer>(
129    args: &[ArrayRef],
130    use_string_view: bool,
131) -> Result<ArrayRef> {
132    if use_string_view {
133        string_view_trim::<Tr>(args)
134    } else {
135        string_trim::<T, Tr>(args)
136    }
137}
138
139/// Applies the trim function to the given string view array(s)
140/// and returns a new string view array with the trimmed values.
141///
142/// Pre-computes the pattern characters once for scalar patterns to avoid
143/// repeated allocations per row.
144fn string_view_trim<Tr: Trimmer>(args: &[ArrayRef]) -> Result<ArrayRef> {
145    let string_view_array = as_string_view_array(&args[0])?;
146    let mut views_buf = Vec::with_capacity(string_view_array.len());
147    let mut null_builder = NullBufferBuilder::new(string_view_array.len());
148
149    match args.len() {
150        1 => {
151            // Trim spaces by default
152            for (src_str_opt, raw_view) in string_view_array
153                .iter()
154                .zip(string_view_array.views().iter())
155            {
156                if let Some(src_str) = src_str_opt {
157                    let (trimmed, offset) = Tr::trim_ascii_char(src_str, b' ');
158                    append_view(&mut views_buf, raw_view, trimmed, offset);
159                    null_builder.append_non_null();
160                } else {
161                    null_builder.append_null();
162                    views_buf.push(0);
163                }
164            }
165        }
166        2 => {
167            let characters_array = as_string_view_array(&args[1])?;
168
169            if characters_array.len() == 1 {
170                // Scalar pattern - pre-compute pattern chars once
171                if characters_array.is_null(0) {
172                    return Ok(new_null_array(
173                        &DataType::Utf8View,
174                        string_view_array.len(),
175                    ));
176                }
177
178                let pattern: Vec<char> = characters_array.value(0).chars().collect();
179                for (src_str_opt, raw_view) in string_view_array
180                    .iter()
181                    .zip(string_view_array.views().iter())
182                {
183                    trim_and_append_view::<Tr>(
184                        src_str_opt,
185                        &pattern,
186                        &mut views_buf,
187                        &mut null_builder,
188                        raw_view,
189                    );
190                }
191            } else {
192                // Per-row pattern - must compute pattern chars for each row
193                let mut pattern: Vec<char> = Vec::new();
194                for ((src_str_opt, raw_view), characters_opt) in string_view_array
195                    .iter()
196                    .zip(string_view_array.views().iter())
197                    .zip(characters_array.iter())
198                {
199                    if let (Some(src_str), Some(characters)) =
200                        (src_str_opt, characters_opt)
201                    {
202                        pattern.clear();
203                        pattern.extend(characters.chars());
204                        let (trimmed, offset) = Tr::trim(src_str, &pattern);
205                        append_view(&mut views_buf, raw_view, trimmed, offset);
206                        null_builder.append_non_null();
207                    } else {
208                        null_builder.append_null();
209                        views_buf.push(0);
210                    }
211                }
212            }
213        }
214        other => {
215            return exec_err!(
216                "Function TRIM was called with {other} arguments. It requires at least 1 and at most 2."
217            );
218        }
219    }
220
221    let views_buf = ScalarBuffer::from(views_buf);
222    let nulls_buf = null_builder.finish();
223
224    // Safety:
225    // (1) The blocks of the given views are all provided
226    // (2) Each of the range `view.offset+start..end` of view in views_buf is within
227    // the bounds of each of the blocks
228    unsafe {
229        let array = StringViewArray::new_unchecked(
230            views_buf,
231            string_view_array.data_buffers().to_vec(),
232            nulls_buf,
233        );
234        Ok(Arc::new(array) as ArrayRef)
235    }
236}
237
238/// Trims the given string and appends the trimmed string to the views buffer
239/// and the null buffer.
240///
241/// Arguments
242/// - `src_str_opt`: The original string value (represented by the view)
243/// - `pattern`: Pre-computed character pattern to trim
244/// - `views_buf`: The buffer to append the updated views to
245/// - `null_builder`: The buffer to append the null values to
246/// - `original_view`: The original view value (that contains src_str_opt)
247#[inline]
248fn trim_and_append_view<Tr: Trimmer>(
249    src_str_opt: Option<&str>,
250    pattern: &[char],
251    views_buf: &mut Vec<u128>,
252    null_builder: &mut NullBufferBuilder,
253    original_view: &u128,
254) {
255    if let Some(src_str) = src_str_opt {
256        let (trimmed, offset) = Tr::trim(src_str, pattern);
257        append_view(views_buf, original_view, trimmed, offset);
258        null_builder.append_non_null();
259    } else {
260        null_builder.append_null();
261        views_buf.push(0);
262    }
263}
264
265/// Builds the trimmed output array by writing the trimmed slices straight into
266/// the value buffer, rather than collecting through a string builder.
267///
268/// Every trimmed value is a substring of its input, so the byte range the input
269/// spans bounds the output's. Reserving that much up front means one allocation
270/// and no growth during the copy, and it also guarantees the running offset stays
271/// within `T` (the input array's own offsets already fit).
272///
273/// `nulls` becomes the output null buffer; null rows contribute no bytes.
274/// `trim_row` is called only for non-null rows, with the row index and its value,
275/// and must return a subslice of the value it is given.
276fn build_trimmed<T: OffsetSizeTrait, F>(
277    string_array: &GenericStringArray<T>,
278    nulls: Option<NullBuffer>,
279    mut trim_row: F,
280) -> ArrayRef
281where
282    F: for<'a> FnMut(usize, &'a str) -> &'a str,
283{
284    let len = string_array.len();
285    let input_offsets = string_array.value_offsets();
286    let start = input_offsets.first().unwrap().as_usize();
287    let end = input_offsets.last().unwrap().as_usize();
288
289    let mut values: Vec<u8> = Vec::with_capacity(end - start);
290    let mut offsets: Vec<T> = Vec::with_capacity(len + 1);
291    offsets.push(T::usize_as(0));
292
293    match &nulls {
294        // Keeping the null check out of the all-valid path leaves it branch-free.
295        None => {
296            for i in 0..len {
297                // SAFETY: `i` is in bounds.
298                let s = unsafe { string_array.value_unchecked(i) };
299                values.extend_from_slice(trim_row(i, s).as_bytes());
300                offsets.push(T::usize_as(values.len()));
301            }
302        }
303        Some(validity) => {
304            for i in 0..len {
305                if validity.is_valid(i) {
306                    // SAFETY: `i` is in bounds.
307                    let s = unsafe { string_array.value_unchecked(i) };
308                    values.extend_from_slice(trim_row(i, s).as_bytes());
309                }
310                offsets.push(T::usize_as(values.len()));
311            }
312        }
313    }
314
315    let offsets = OffsetBuffer::new(ScalarBuffer::from(offsets));
316    // SAFETY: trimming splits `s` on char boundaries, so the value buffer is a
317    // concatenation of valid UTF-8; the offsets are monotonic and end at its length.
318    let array = unsafe {
319        GenericStringArray::<T>::new_unchecked(offsets, Buffer::from_vec(values), nulls)
320    };
321    Arc::new(array)
322}
323
324/// Applies the trim function to the given string array(s)
325/// and returns a new string array with the trimmed values.
326///
327/// Pre-computes the pattern characters once for scalar patterns to avoid
328/// repeated allocations per row.
329fn string_trim<T: OffsetSizeTrait, Tr: Trimmer>(args: &[ArrayRef]) -> Result<ArrayRef> {
330    let string_array = as_generic_string_array::<T>(&args[0])?;
331
332    match args.len() {
333        1 => {
334            // Trim spaces by default
335            Ok(build_trimmed(
336                string_array,
337                string_array.nulls().cloned(),
338                |_, s| Tr::trim_ascii_char(s, b' ').0,
339            ))
340        }
341        2 => {
342            let characters_array = as_generic_string_array::<T>(&args[1])?;
343
344            if characters_array.len() == 1 {
345                // Scalar pattern - pre-compute pattern chars once
346                if characters_array.is_null(0) {
347                    return Ok(new_null_array(
348                        string_array.data_type(),
349                        string_array.len(),
350                    ));
351                }
352
353                let pattern: Vec<char> = characters_array.value(0).chars().collect();
354                return Ok(build_trimmed(
355                    string_array,
356                    string_array.nulls().cloned(),
357                    |_, s| Tr::trim(s, &pattern).0,
358                ));
359            }
360
361            // Indexing `characters_array` per row below requires the two arguments
362            // to line up.
363            if characters_array.len() != string_array.len() {
364                return exec_err!(
365                    "Function TRIM was called with mismatched argument lengths"
366                );
367            }
368
369            // A row is null if either argument is null.
370            let nulls = NullBuffer::union(string_array.nulls(), characters_array.nulls());
371
372            // Per-row pattern - must compute pattern chars for each row
373            let mut pattern: Vec<char> = Vec::new();
374            Ok(build_trimmed(string_array, nulls, |i, s| {
375                pattern.clear();
376                pattern.extend(characters_array.value(i).chars());
377                Tr::trim(s, &pattern).0
378            }))
379        }
380        other => {
381            exec_err!(
382                "Function TRIM was called with {other} arguments. It requires at least 1 and at most 2."
383            )
384        }
385    }
386}
387
388pub(crate) fn to_lower(args: &[ColumnarValue], name: &str) -> Result<ColumnarValue> {
389    case_conversion(args, true, name)
390}
391
392pub(crate) fn to_upper(args: &[ColumnarValue], name: &str) -> Result<ColumnarValue> {
393    case_conversion(args, false, name)
394}
395
396#[inline]
397fn unicode_case(s: &str, lower: bool) -> String {
398    if lower {
399        s.to_lowercase()
400    } else {
401        s.to_uppercase()
402    }
403}
404
405/// Writes the case-converted form of `s` directly into `w`.
406///
407/// Uppercasing is a context-free character mapping, so each character is
408/// mapped and streamed straight into the output buffer, avoiding the
409/// intermediate `String` that `str::to_uppercase` allocates per row.
410///
411/// Lowercasing is *not* context-free — `str::to_lowercase` applies the
412/// special Greek final-sigma rule (Σ becomes ς at the end of a word but σ
413/// elsewhere), which a per-character mapping cannot reproduce — so it keeps
414/// using `str::to_lowercase`.
415#[inline]
416fn write_unicode_case(w: &mut impl StringWriter, s: &str, lower: bool) {
417    if lower {
418        w.write_str(&s.to_lowercase());
419    } else {
420        for c in s.chars() {
421            for upper in c.to_uppercase() {
422                w.write_char(upper);
423            }
424        }
425    }
426}
427
428fn case_conversion(
429    args: &[ColumnarValue],
430    lower: bool,
431    name: &str,
432) -> Result<ColumnarValue> {
433    match &args[0] {
434        ColumnarValue::Array(array) => Ok(ColumnarValue::Array(
435            case_conversion_columnar_array(array, lower, name)?,
436        )),
437        ColumnarValue::Scalar(scalar) => Ok(ColumnarValue::Scalar(
438            case_conversion_scalar(scalar, lower, name)?,
439        )),
440    }
441}
442
443fn case_conversion_scalar(
444    scalar: &ScalarValue,
445    lower: bool,
446    name: &str,
447) -> Result<ScalarValue> {
448    match scalar {
449        ScalarValue::Utf8(a) => {
450            let result = a.as_ref().map(|x| unicode_case(x, lower));
451            Ok(ScalarValue::Utf8(result))
452        }
453        ScalarValue::LargeUtf8(a) => {
454            let result = a.as_ref().map(|x| unicode_case(x, lower));
455            Ok(ScalarValue::LargeUtf8(result))
456        }
457        ScalarValue::Utf8View(a) => {
458            let result = a.as_ref().map(|x| unicode_case(x, lower));
459            Ok(ScalarValue::Utf8View(result))
460        }
461        ScalarValue::Dictionary(key_type, value) => {
462            let converted = case_conversion_scalar(value.as_ref(), lower, name)?;
463            Ok(ScalarValue::Dictionary(
464                key_type.clone(),
465                Box::new(converted),
466            ))
467        }
468        other => exec_err!("Unsupported data type {other:?} for function {name}"),
469    }
470}
471
472fn case_conversion_columnar_array(
473    array: &ArrayRef,
474    lower: bool,
475    name: &str,
476) -> Result<ArrayRef> {
477    match array.data_type() {
478        DataType::Utf8 => case_conversion_array::<i32>(array, lower),
479        DataType::LargeUtf8 => case_conversion_array::<i64>(array, lower),
480        DataType::Utf8View => case_conversion_utf8view(array, lower),
481        DataType::Dictionary(_, _) => case_conversion_dictionary(array, lower, name),
482        other => exec_err!("Unsupported data type {other:?} for function {name}"),
483    }
484}
485
486fn case_conversion_utf8view(array: &ArrayRef, lower: bool) -> Result<ArrayRef> {
487    let string_array = as_string_view_array(array)?;
488    if string_array.is_ascii() {
489        return Ok(Arc::new(case_conversion_utf8view_ascii(
490            string_array,
491            lower,
492        )));
493    }
494    let item_len = string_array.len();
495    // Null-preserving: reuse the input null buffer as the output null buffer.
496    let nulls = string_array.nulls().cloned();
497    let mut builder = StringViewArrayBuilder::with_capacity(item_len);
498
499    if let Some(ref n) = nulls {
500        for i in 0..item_len {
501            if n.is_null(i) {
502                builder.try_append_placeholder()?;
503            } else {
504                // SAFETY: `n.is_null(i)` was false in the branch above.
505                let s = unsafe { string_array.value_unchecked(i) };
506                builder.try_append_value(&unicode_case(s, lower))?;
507            }
508        }
509    } else {
510        for i in 0..item_len {
511            // SAFETY: no null buffer means every index is valid.
512            let s = unsafe { string_array.value_unchecked(i) };
513            builder.try_append_value(&unicode_case(s, lower))?;
514        }
515    }
516
517    Ok(Arc::new(builder.finish(nulls)?))
518}
519
520fn case_conversion_dictionary(
521    array: &ArrayRef,
522    lower: bool,
523    name: &str,
524) -> Result<ArrayRef> {
525    let dictionary = array.as_any_dictionary();
526    let converted = case_conversion_columnar_array(dictionary.values(), lower, name)?;
527    Ok(dictionary.with_values(converted))
528}
529
530fn case_conversion_array<O: OffsetSizeTrait>(
531    array: &ArrayRef,
532    lower: bool,
533) -> Result<ArrayRef> {
534    const PRE_ALLOC_BYTES: usize = 8;
535
536    let string_array = as_generic_string_array::<O>(array)?;
537    if string_array.is_ascii() {
538        return case_conversion_ascii_array::<O>(string_array, lower);
539    }
540
541    // Values contain non-ASCII.
542    let item_len = string_array.len();
543    let offsets = string_array.value_offsets();
544    let start = offsets.first().unwrap().as_usize();
545    let end = offsets.last().unwrap().as_usize();
546    let capacity = (end - start) + PRE_ALLOC_BYTES;
547    // Null-preserving: reuse the input null buffer as the output null buffer.
548    let nulls = string_array.nulls().cloned();
549    let mut builder = GenericStringArrayBuilder::<O>::with_capacity(item_len, capacity);
550
551    if let Some(ref n) = nulls {
552        for i in 0..item_len {
553            if n.is_null(i) {
554                builder.try_append_placeholder()?;
555            } else {
556                // SAFETY: `n.is_null(i)` was false in the branch above.
557                let s = unsafe { string_array.value_unchecked(i) };
558                builder.try_append_with(|w| write_unicode_case(w, s, lower))?;
559            }
560        }
561    } else {
562        for i in 0..item_len {
563            // SAFETY: no null buffer means every index is valid.
564            let s = unsafe { string_array.value_unchecked(i) };
565            builder.try_append_with(|w| write_unicode_case(w, s, lower))?;
566        }
567    }
568    Ok(Arc::new(builder.finish(nulls)?))
569}
570
571/// Fast path for case conversion on an all-ASCII `StringViewArray`.
572fn case_conversion_utf8view_ascii(
573    array: &StringViewArray,
574    lower: bool,
575) -> StringViewArray {
576    // Specialize per conversion so the byte call inlines in the hot loops below.
577    if lower {
578        case_conversion_utf8view_ascii_inner(array, u8::to_ascii_lowercase)
579    } else {
580        case_conversion_utf8view_ascii_inner(array, u8::to_ascii_uppercase)
581    }
582}
583
584/// Walks the views once and produces a new `StringViewArray` with
585/// case-converted bytes. Inline strings (<= 12 bytes) are converted in-place;
586/// long strings copy-and-convert into output buffers and have their view fields
587/// rewritten to address the new bytes. ASCII case conversion preserves is byte
588/// length, so no row migrates between the inline and long layouts.
589fn case_conversion_utf8view_ascii_inner<F: Fn(&u8) -> u8>(
590    array: &StringViewArray,
591    convert: F,
592) -> StringViewArray {
593    let item_len = array.len();
594    let views = array.views();
595    let data_buffers = array.data_buffers();
596    let nulls = array.nulls();
597
598    let mut new_views: Vec<u128> = Vec::with_capacity(item_len);
599    // Long values are packed into `in_progress`; when full it is sealed into
600    // `completed` and a new, larger block is started — same block-doubling
601    // scheme as Arrow's `GenericByteViewBuilder`.
602    let mut in_progress: Vec<u8> = Vec::new();
603    let mut completed: Vec<Buffer> = Vec::new();
604    let mut block_size: u32 = STRING_VIEW_INIT_BLOCK_SIZE;
605
606    for i in 0..item_len {
607        if nulls.is_some_and(|n| n.is_null(i)) {
608            // Zero view = empty, no buffer reference; the null buffer is what
609            // marks the row null, so the view's value is irrelevant.
610            new_views.push(0);
611            continue;
612        }
613        let view = views[i];
614        // Length is the low 32 bits; `as u32` discards the rest of the view.
615        let len = view as u32 as usize;
616        if len == 0 {
617            new_views.push(0);
618            continue;
619        }
620        let mut bytes = view.to_le_bytes();
621        if len <= 12 {
622            // Inline: value is in bytes[4..4+len], no buffer reference. Convert
623            // in place; nothing else in the view needs to change.
624            for b in &mut bytes[4..4 + len] {
625                *b = convert(b);
626            }
627            new_views.push(u128::from_le_bytes(bytes));
628        } else {
629            // Long: input view points into shared `data_buffers` we can't
630            // mutate, so copy-convert into our own buffer and rewrite the
631            // view's prefix/buffer_index/offset (length is preserved).
632
633            // Ensure the current block has room; otherwise flush and grow.
634            let required_cap = in_progress.len() + len;
635            if in_progress.capacity() < required_cap {
636                if !in_progress.is_empty() {
637                    completed.push(Buffer::from_vec(std::mem::take(&mut in_progress)));
638                }
639                if block_size < STRING_VIEW_MAX_BLOCK_SIZE {
640                    block_size = block_size.saturating_mul(2);
641                }
642                let to_reserve = len.max(block_size as usize);
643                #[expect(
644                    clippy::disallowed_methods,
645                    reason = "StringView's block size bounds growth, so reserve cannot overflow capacity arithmetically. This hot loop intentionally avoids the extra `try_reserve` checks. It remains subject to allocator failure/OOM, which must be managed externally."
646                )]
647                in_progress.reserve(to_reserve);
648            }
649
650            // The in-progress block will be sealed at index `completed.len()`,
651            // and our value starts at the current write position within it.
652            let buffer_index: u32 = i32::try_from(completed.len())
653                .expect("buffer count exceeds i32::MAX")
654                as u32;
655            let new_offset: u32 =
656                i32::try_from(in_progress.len()).expect("offset exceeds i32::MAX") as u32;
657
658            // Source location from the input view: bytes 8..12 are buffer
659            // index, bytes 12..16 are the offset within it.
660            let src_buffer_index =
661                u32::from_le_bytes(bytes[8..12].try_into().unwrap()) as usize;
662            let src_offset =
663                u32::from_le_bytes(bytes[12..16].try_into().unwrap()) as usize;
664            let src =
665                &data_buffers[src_buffer_index].as_slice()[src_offset..src_offset + len];
666
667            let prefix_start = in_progress.len();
668            in_progress.extend(src.iter().map(&convert));
669
670            // Rewrite the three long-view fields; bytes[0..4] (length) is
671            // left untouched. The prefix is read back from the bytes we just
672            // wrote so the converted value has a single source of truth.
673            let prefix: [u8; 4] = in_progress[prefix_start..prefix_start + 4]
674                .try_into()
675                .unwrap();
676            bytes[4..8].copy_from_slice(&prefix);
677            bytes[8..12].copy_from_slice(&buffer_index.to_le_bytes());
678            bytes[12..16].copy_from_slice(&new_offset.to_le_bytes());
679            new_views.push(u128::from_le_bytes(bytes));
680        }
681    }
682
683    if !in_progress.is_empty() {
684        completed.push(Buffer::from_vec(in_progress));
685    }
686
687    // SAFETY: each long view's buffer_index addresses a buffer we wrote, and
688    // its offset addresses bytes within that buffer; prefixes were copied from
689    // those same bytes; inline views were rewritten from valid inline bytes;
690    // null/empty rows are zero views with no buffer reference; row count is
691    // unchanged.
692    unsafe {
693        StringViewArray::new_unchecked(
694            ScalarBuffer::from(new_views),
695            completed,
696            array.nulls().cloned(),
697        )
698    }
699}
700
701/// Fast path for case conversion on an all-ASCII string array. ASCII case
702/// conversion is byte-length-preserving, so we can convert the entire addressed
703/// byte range in one pass over the value buffer and reuse the offsets and nulls
704/// buffers — rebasing the offsets when the input is a sliced array.
705fn case_conversion_ascii_array<O: OffsetSizeTrait>(
706    string_array: &GenericStringArray<O>,
707    lower: bool,
708) -> Result<ArrayRef> {
709    let value_offsets = string_array.value_offsets();
710    let start = value_offsets.first().unwrap().as_usize();
711    let end = value_offsets.last().unwrap().as_usize();
712    let relevant = &string_array.value_data()[start..end];
713
714    let converted: Vec<u8> = if lower {
715        relevant.iter().map(u8::to_ascii_lowercase).collect()
716    } else {
717        relevant.iter().map(u8::to_ascii_uppercase).collect()
718    };
719    let values = Buffer::from_vec(converted);
720
721    // Shift offsets from `start`-based to 0-based so they index into `values`.
722    let offsets = string_array
723        .offsets()
724        .clone()
725        .subtract(string_array.offsets()[0]);
726
727    let nulls = string_array.nulls().cloned();
728    // SAFETY: offsets are monotonic and in-bounds for `values`; nulls
729    // (if any) match the slice length.
730    Ok(Arc::new(unsafe {
731        GenericStringArray::<O>::new_unchecked(offsets, values, nulls)
732    }))
733}