Skip to main content

volas_core/
strbuf.rs

1//! [`StrBuffer`]: the Arrow-native columnar string layout — one contiguous UTF-8
2//! [`Buffer<u8>`] plus an `n+1` [`Buffer<i64>`] of offsets (Arrow `LargeUtf8`).
3//!
4//! This replaces an `Arc<Vec<String>>` (one heap allocation per cell, pointer-
5//! chasing scans): a `StrBuffer` is a single allocation, cache-friendly to build
6//! and scan, and byte-compatible with Arrow for zero-copy interop. Cell access is
7//! a slice of the data buffer (`get` / `iter`). Missing cells are tracked by the
8//! column's separate `Validity`; an NA cell carries an empty (or placeholder) span
9//! here, exactly like the old `String::new()` placeholder.
10
11use std::fmt;
12
13use crate::buffer::Buffer;
14
15/// A contiguous UTF-8 string column buffer (Arrow `LargeUtf8`: i64 offsets).
16#[derive(Clone)]
17pub struct StrBuffer {
18    /// `len + 1` offsets into `data`; `offsets[0] == 0`, `offsets[len] == data.len()`.
19    offsets: Buffer<i64>,
20    /// Concatenated UTF-8 bytes of every cell.
21    data: Buffer<u8>,
22}
23
24impl StrBuffer {
25    /// Build from owned `String`s.
26    #[inline]
27    pub fn from_vec(v: Vec<String>) -> Self {
28        v.into_iter().collect()
29    }
30
31    /// Zero-copy from foreign Arrow `LargeUtf8` buffers (offsets + data + their guard).
32    #[inline]
33    pub fn from_buffers(offsets: Buffer<i64>, data: Buffer<u8>) -> Self {
34        StrBuffer { offsets, data }
35    }
36
37    /// The raw offset / data buffers (for a zero-copy Arrow export).
38    #[inline]
39    pub fn buffers(&self) -> (&Buffer<i64>, &Buffer<u8>) {
40        (&self.offsets, &self.data)
41    }
42
43    #[inline]
44    pub fn len(&self) -> usize {
45        self.offsets.len().saturating_sub(1)
46    }
47
48    #[inline]
49    pub fn is_empty(&self) -> bool {
50        self.len() == 0
51    }
52
53    /// Cell `i` as `&str` (a borrow into the data buffer). Bounds-checked.
54    #[inline]
55    pub fn get(&self, i: usize) -> &str {
56        let off = self.offsets.as_slice();
57        let (s, e) = (off[i] as usize, off[i + 1] as usize);
58        // SAFETY: the data is built from valid UTF-8 (`from_iter`) or imported from
59        // an Arrow Utf8 array (UTF-8 by spec), and offsets are on char boundaries.
60        unsafe { std::str::from_utf8_unchecked(&self.data.as_slice()[s..e]) }
61    }
62
63    /// Cell `i` as `&str`, **without** bounds checks — the zero-overhead accessor for
64    /// the internal hot kernels (compare / sort / scan), mirroring arrow-rs
65    /// `GenericStringArray::value_unchecked`.
66    ///
67    /// # Safety
68    /// `i < self.len()`. The offsets are a structural invariant — monotonic with
69    /// `offsets[len] == data.len()` — so `i < len` makes both offset reads and the
70    /// `[s..e]` data span in bounds; only the redundant checks are elided.
71    #[inline]
72    pub unsafe fn get_unchecked(&self, i: usize) -> &str {
73        let off = self.offsets.as_slice();
74        let (s, e) = (*off.get_unchecked(i) as usize, *off.get_unchecked(i + 1) as usize);
75        std::str::from_utf8_unchecked(self.data.as_slice().get_unchecked(s..e))
76    }
77
78    /// Iterate the cells as `&str`. The two backing slices are resolved **once** (a
79    /// single `Buffer` match each, hoisted out of the walk), then indexed without
80    /// bounds checks — so a sequential scan is one tight loop, matching a `&[String]`
81    /// walk with no per-element overhead.
82    #[inline]
83    pub fn iter(&self) -> impl Iterator<Item = &str> + '_ {
84        let off = self.offsets.as_slice();
85        let data = self.data.as_slice();
86        (0..self.len()).map(move |i| {
87            // SAFETY: `i < len` ⟹ `i + 1 <= len < off.len()`, and offsets are monotonic
88            // with `off[len] == data.len()`, so `off[i]..off[i + 1]` is in bounds of `data`.
89            unsafe {
90                let s = *off.get_unchecked(i) as usize;
91                let e = *off.get_unchecked(i + 1) as usize;
92                std::str::from_utf8_unchecked(data.get_unchecked(s..e))
93            }
94        })
95    }
96
97    /// Materialize owned `String`s (only where a `Vec<String>` is genuinely needed).
98    pub fn to_vec(&self) -> Vec<String> {
99        self.iter().map(String::from).collect()
100    }
101
102    /// A contiguous sub-range `[start, end)` as a fresh `StrBuffer` (re-packs the
103    /// bytes; `Buffer` borrowing of a sub-span is a future refinement).
104    pub fn slice(&self, start: usize, end: usize) -> StrBuffer {
105        (start..end).map(|i| self.get(i)).collect()
106    }
107
108    /// Append the cells of `items` **in place** — the offset and data buffers grow via
109    /// copy-on-write rather than being rebuilt, so a live row-by-row append stays
110    /// amortised `O(1)` per cell instead of `O(n)` (which made repeated append `O(n²)`).
111    /// A borrowed (or shared) buffer materialises once on the first append.
112    pub fn extend<S: AsRef<str>>(&mut self, items: impl IntoIterator<Item = S>) {
113        let data = self.data.make_mut();
114        let offsets = self.offsets.make_mut();
115        for s in items {
116            data.extend_from_slice(s.as_ref().as_bytes());
117            offsets.push(data.len() as i64);
118        }
119    }
120}
121
122/// Incremental builder (append / scatter paths).
123#[derive(Default)]
124pub struct StrBufferBuilder {
125    offsets: Vec<i64>,
126    data: Vec<u8>,
127}
128
129impl StrBufferBuilder {
130    pub fn with_capacity(n: usize) -> Self {
131        let mut offsets = Vec::with_capacity(n + 1);
132        offsets.push(0);
133        StrBufferBuilder { offsets, data: Vec::new() }
134    }
135    #[inline]
136    pub fn push(&mut self, s: &str) {
137        self.data.extend_from_slice(s.as_bytes());
138        self.offsets.push(self.data.len() as i64);
139    }
140    pub fn finish(mut self) -> StrBuffer {
141        if self.offsets.is_empty() {
142            self.offsets.push(0);
143        }
144        StrBuffer {
145            offsets: Buffer::from_vec(self.offsets),
146            data: Buffer::from_vec(self.data),
147        }
148    }
149}
150
151impl PartialEq for StrBuffer {
152    fn eq(&self, other: &Self) -> bool {
153        self.len() == other.len() && self.iter().zip(other.iter()).all(|(a, b)| a == b)
154    }
155}
156
157impl fmt::Debug for StrBuffer {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        f.debug_list().entries(self.iter()).finish()
160    }
161}
162
163impl<S: AsRef<str>> FromIterator<S> for StrBuffer {
164    /// The single construction path: concatenate the cells' UTF-8 into one `data`
165    /// buffer, recording the running byte boundary in `offsets` (`n + 1` entries).
166    fn from_iter<I: IntoIterator<Item = S>>(it: I) -> Self {
167        let mut offsets = vec![0i64];
168        let mut data = Vec::new();
169        for s in it {
170            data.extend_from_slice(s.as_ref().as_bytes());
171            offsets.push(data.len() as i64);
172        }
173        StrBuffer {
174            offsets: Buffer::from_vec(offsets),
175            data: Buffer::from_vec(data),
176        }
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn build_access_and_emptiness() {
186        let sb = StrBuffer::from_vec(vec!["a".into(), "".into(), "cd".into()]);
187        assert_eq!(sb.len(), 3);
188        assert!(!sb.is_empty());
189        assert_eq!(sb.get(2), "cd");
190        // SAFETY: 0 < len.
191        assert_eq!(unsafe { sb.get_unchecked(1) }, "");
192        assert_eq!(sb.iter().collect::<Vec<_>>(), ["a", "", "cd"]);
193        assert_eq!(sb.to_vec(), vec!["a".to_string(), "".into(), "cd".into()]);
194        assert!(StrBuffer::from_vec(vec![]).is_empty());
195    }
196
197    #[test]
198    fn buffers_round_trip_zero_copy() {
199        // The Arrow bridge path: take the raw offset/data buffers out and rebuild.
200        let sb = StrBuffer::from_vec(vec!["xy".into(), "z".into()]);
201        let (offsets, data) = sb.buffers();
202        let rebuilt = StrBuffer::from_buffers(offsets.clone(), data.clone());
203        assert_eq!(rebuilt, sb);
204        assert_eq!(rebuilt.slice(1, 2), StrBuffer::from_vec(vec!["z".into()]));
205    }
206
207    #[test]
208    fn builder_default_finishes_empty() {
209        // A `Default` builder that never pushes must still yield valid `[0]` offsets.
210        let empty = StrBufferBuilder::default().finish();
211        assert!(empty.is_empty());
212        let mut b = StrBufferBuilder::with_capacity(2);
213        b.push("p");
214        b.push("qr");
215        assert_eq!(b.finish(), StrBuffer::from_vec(vec!["p".into(), "qr".into()]));
216    }
217
218    #[test]
219    fn debug_lists_cells() {
220        let sb = StrBuffer::from_vec(vec!["a".into(), "b".into()]);
221        assert_eq!(format!("{sb:?}"), r#"["a", "b"]"#);
222    }
223}