Skip to main content

rudb_vector/
string.rs

1//! The string representation.
2//!
3//! `spec/07-execution.md` section 7.1: a string is a 16 byte structure, 4 bytes of length, 4 bytes
4//! of prefix, and 8 bytes that are either the rest of a short string or a way to find a long one.
5//! Strings of 12 bytes or fewer live entirely inside the structure. The prefix means most
6//! comparisons and most equality tests answer without dereferencing anything, which on the string
7//! heavy queries in ClickBench is the difference between a cache hit and a cache miss per row.
8//!
9//! **Where this differs from the specification, and why.** The document says the last 8 bytes are
10//! a pointer, which is what DuckDB and Umbra do. Here they are a block index and an offset, which
11//! is what Arrow's `StringView` does. The sizes are identical, the prefix trick is identical, and
12//! the prefix trick is the part that makes it fast. The difference is one predictable load against
13//! one pointer chase on the slow path only, and in exchange the whole representation is safe code
14//! with no pinning machinery, which does not exist until the buffer manager arrives at M2. This is
15//! the kind of decision that gets remeasured rather than argued about, and it is tracked as an
16//! issue so that M3 measures it instead of inheriting it.
17
18use rudb_common::{Error, Result};
19
20use crate::buffer::Buffer;
21
22/// The longest string that fits entirely inside a view.
23pub const INLINE_LIMIT: usize = 12;
24
25/// A 16 byte handle on a string.
26///
27/// The layout is a `u32` length and 12 bytes of payload. For a string of 12 bytes or fewer the
28/// payload is the string, zero padded. For a longer one the first 4 bytes are the prefix and the
29/// last 8 are the offset into the column's arena.
30///
31/// Arrow spends 4 of those 8 bytes on a buffer index and 4 on an offset within the buffer, because
32/// an Arrow array is a list of buffers. This column is one arena, so there is no buffer to name and
33/// the whole 8 bytes are the offset, which reads as one load rather than two and takes the reachable
34/// size of a column from 4 GiB to more than anything will ever put in one.
35///
36/// A view on its own cannot produce a long string, only a short one. That is deliberate: the arena
37/// lives in the [`StringColumn`] and the borrow checker is what stops a view from outliving it,
38/// rather than a rule somebody has to remember.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub struct StringView {
41    length: u32,
42    payload: [u8; 12],
43}
44
45impl StringView {
46    /// The view on the empty string.
47    ///
48    /// What a copy loop writes for a position that resolved to nowhere, for the same reason a fixed
49    /// width copy writes a zero there. The views are a parallel array to a validity mask, so a row
50    /// that got skipped rather than filled would put every row after it at the wrong index.
51    #[must_use]
52    pub const fn empty() -> Self {
53        Self { length: 0, payload: [0; 12] }
54    }
55
56    /// A view on a string that fits inline.
57    ///
58    /// # Panics
59    ///
60    /// If the string is longer than [`INLINE_LIMIT`]. Callers that do not know the length go
61    /// through [`StringColumn::push`], which decides.
62    #[must_use]
63    pub fn inline(text: &str) -> Self {
64        assert!(text.len() <= INLINE_LIMIT, "a string of {} bytes is not inline", text.len());
65        let mut payload = [0u8; 12];
66        payload[..text.len()].copy_from_slice(text.as_bytes());
67        Self { length: text.len() as u32, payload }
68    }
69
70    /// A view on a string that lives in the arena.
71    fn indirect(text: &str, offset: u64) -> Self {
72        let mut payload = [0u8; 12];
73        payload[..4].copy_from_slice(&text.as_bytes()[..4]);
74        payload[4..].copy_from_slice(&offset.to_le_bytes());
75        Self { length: text.len() as u32, payload }
76    }
77
78    /// A view on bytes, whatever they are, wherever they turn out to live.
79    ///
80    /// The one constructor that takes bytes rather than a `&str`, and the two callers want it for
81    /// different reasons. A copy between two columns has bytes that were validated on the way into
82    /// the first one and validating again would be work for nothing. A `BLOB` has bytes that were
83    /// never text and are not going to become it. `offset` is where they are in the destination
84    /// arena and is ignored for a string short enough to sit in the view.
85    ///
86    /// It is public because the string view form of a vector is built from views a caller made, and
87    /// a scan laying chunks over a page of strings is exactly the caller that has bytes and an
88    /// offset into somebody else's arena rather than a column to push into.
89    #[must_use]
90    pub fn over(bytes: &[u8], offset: u64) -> Self {
91        let mut payload = [0u8; 12];
92        if bytes.len() <= INLINE_LIMIT {
93            payload[..bytes.len()].copy_from_slice(bytes);
94        } else {
95            payload[..4].copy_from_slice(&bytes[..4]);
96            payload[4..].copy_from_slice(&offset.to_le_bytes());
97        }
98        Self { length: bytes.len() as u32, payload }
99    }
100
101    /// The length in bytes.
102    #[must_use]
103    pub fn len(&self) -> usize {
104        self.length as usize
105    }
106
107    /// Whether the string is empty.
108    #[must_use]
109    pub fn is_empty(&self) -> bool {
110        self.length == 0
111    }
112
113    /// Whether the whole string is in the view.
114    #[must_use]
115    pub fn is_inline(&self) -> bool {
116        self.len() <= INLINE_LIMIT
117    }
118
119    /// The first four bytes, zero padded.
120    ///
121    /// This is the whole point of the representation. Two strings with different prefixes are
122    /// different, and two strings with the same prefix are usually equal, so a filter on a string
123    /// column resolves without touching the payload on almost every row.
124    #[must_use]
125    pub fn prefix(&self) -> [u8; 4] {
126        [self.payload[0], self.payload[1], self.payload[2], self.payload[3]]
127    }
128
129    /// The bytes, when the whole string is in the view.
130    ///
131    /// A comparison wants bytes rather than a `&str`, because SQL's string order is byte order and
132    /// because [`Self::as_inline_str`] pays for a UTF-8 validation that a comparison has no use
133    /// for. On a filter against a varchar column that validation is the whole cost of the row.
134    #[must_use]
135    pub fn inline_bytes(&self) -> Option<&[u8]> {
136        if self.is_inline() { Some(&self.payload[..self.len()]) } else { None }
137    }
138
139    /// The string, when it is short enough to be in the view.
140    #[must_use]
141    pub fn as_inline_str(&self) -> Option<&str> {
142        if !self.is_inline() {
143            return None;
144        }
145        // `None` rather than a panic for a view that holds a blob, since the payload is whatever
146        // was written and only a column of text can promise that is a string.
147        std::str::from_utf8(&self.payload[..self.len()]).ok()
148    }
149
150    /// The bytes, given the arena the long strings of this column live in.
151    ///
152    /// A short string is in the view and the arena is not read at all, which is why this takes the
153    /// arena rather than requiring one that has the string in it.
154    ///
155    /// This exists because a view and the bytes it points at do not have to be held by the same
156    /// object. [`StringColumn`] owns both, and the string view form of a vector holds the views
157    /// itself and shares the arena with every other cut of the same page, so a cut of a varchar
158    /// column is the views and nothing else. Both of them resolve a row the same way, and this is
159    /// where that one way is written.
160    #[must_use]
161    pub fn bytes_in<'a>(&'a self, arena: &'a [u8]) -> Option<&'a [u8]> {
162        if let Some(inline) = self.inline_bytes() {
163            return Some(inline);
164        }
165        arena.get(self.offset()..self.offset() + self.len())
166    }
167
168    fn offset(&self) -> usize {
169        u64::from_le_bytes([
170            self.payload[4],
171            self.payload[5],
172            self.payload[6],
173            self.payload[7],
174            self.payload[8],
175            self.payload[9],
176            self.payload[10],
177            self.payload[11],
178        ]) as usize
179    }
180
181    /// Whether these two views are definitely different, answered from the view alone.
182    ///
183    /// A `false` here means the payloads have to be compared. A `true` means they do not, which on
184    /// a filter against a selective literal is almost every row.
185    #[must_use]
186    pub fn definitely_differs(&self, other: &Self) -> bool {
187        self.length != other.length || self.prefix() != other.prefix()
188    }
189}
190
191/// A column of strings: the views, and the one arena the long ones live in.
192///
193/// The arena is append only, so an offset recorded in a view stays correct for the life of the
194/// column even though the arena's address does not. That is the property a `Vec<u8>` has and a raw
195/// pointer into it does not, and it is the reason a view holds an offset.
196///
197/// This was a `Vec<Vec<u8>>` of fixed size blocks, which meant reading one long string was two
198/// dependent loads, the outer vector's element to find the block's data pointer and then the bytes.
199/// One arena makes it one, from a base the compiler can keep in a register across a row loop, and it
200/// deletes the case where a string longer than a block needed a block of its own. On server3, over a
201/// chunk of 1024 strings, comparing a column against a literal went from 14.9 nanoseconds a row to
202/// 13.2 at 40 bytes a string and from 14.2 to 12.9 at 120, gathering half the rows from 29.5 to 25.3
203/// and from 36.9 to 29.1, and building the column from 12.0 to 8.9 at 40 bytes.
204///
205/// # The one number that got worse, and what it actually is
206///
207/// Building a column whose payload passes 128 KiB, which at 1024 rows means strings averaging more
208/// than 128 bytes, went the other way: 14.6 nanoseconds a row to 41.0. That is not the copy and it
209/// is not the doubling, it is glibc. An allocation that size comes from `mmap` rather than the heap,
210/// so it is handed back to the kernel when the column is dropped and the next chunk faults every
211/// page of it in again, while sixteen KiB blocks come back off a free list already faulted. Run the
212/// same benchmark with `MALLOC_MMAP_THRESHOLD_` raised and the arena builds that column in 9.6
213/// nanoseconds a row against the blocks' 16.2, so the design is not what is slow there.
214///
215/// The fix is that a chunk's payload should come from a pool the engine owns rather than from
216/// `malloc` per chunk, which is the buffer manager at layer three and is where this belongs.
217/// [`Self::reserve_bytes`] is the part that is available now, and it recovers a quarter of it.
218///
219/// # Equality is about the strings and not about the arena
220///
221/// [`Self::over`] means two columns holding exactly the same strings can hold completely different
222/// arenas, because one of them was built by copying the strings in and the other was built over a
223/// page that already had them somewhere in it with other strings in between. Derived equality would
224/// call those two columns different, and every test in the workspace that compares two vectors would
225/// then be asserting on how a column was built rather than on what is in it. So equality is the
226/// strings, position by position, which is the only definition that survives the seam.
227#[derive(Debug, Clone, Default, Eq)]
228pub struct StringColumn {
229    views: Vec<StringView>,
230    arena: Buffer<u8>,
231}
232
233impl StringColumn {
234    /// How many bytes of memory this column is holding.
235    ///
236    /// The views and the arena. A short string lives inside its view and costs nothing beyond it,
237    /// which is the whole reason the representation exists, so a column of short strings costs
238    /// sixteen bytes a string and a column of long ones costs sixteen plus the bytes themselves.
239    #[must_use]
240    pub fn footprint(&self) -> usize {
241        self.views.capacity() * size_of::<StringView>() + self.arena.footprint()
242    }
243
244    /// An empty column.
245    #[must_use]
246    pub fn new() -> Self {
247        Self::default()
248    }
249
250    /// An empty column with room for `capacity` strings.
251    #[must_use]
252    pub fn with_capacity(capacity: usize) -> Self {
253        Self { views: Vec::with_capacity(capacity), arena: Buffer::new() }
254    }
255
256    /// A column with no strings in it yet, over an arena that already holds bytes.
257    ///
258    /// The seam `spec/engine/03-data-plane.md` section 3.5 asks for. Without it the only way in is
259    /// [`Self::push`], which copies, so a scan reading a Parquet page of strings copies every byte of
260    /// the page into an arena and the query then reads the copy. With it the page is the arena: the
261    /// scan hands the bytes over once, records where each string starts with
262    /// [`Self::push_in_place`], and nothing is copied but the views.
263    ///
264    /// It is useful today, because a reader that already has the page in a `Vec<u8>` can move it in
265    /// rather than copy out of it. It matters at layer three, when the [`Buffer`] is the pinned page
266    /// itself and the move is not even that.
267    ///
268    /// Appending with [`Self::push`] afterwards still works and still appends to the arena. That is
269    /// the case to keep away from once a real page is in here, because writing through a borrowed
270    /// buffer copies it, which is [`Buffer::to_mut`] and is the whole page.
271    #[must_use]
272    pub fn over(arena: Buffer<u8>) -> Self {
273        Self { views: Vec::new(), arena }
274    }
275
276    /// This column with its arena held as a page, so that a copy of it does not copy the bytes.
277    ///
278    /// The views are still copied, because they are a `Vec` and a run of them is what a cut of the
279    /// column is. Sixteen bytes a row rather than every byte of every string, which is the same
280    /// split the [`StringView`](crate::vector::Form::StringView) form already makes for the same
281    /// reason.
282    #[must_use]
283    pub fn into_page(self) -> Self {
284        Self { views: self.views, arena: self.arena.into_page() }
285    }
286
287    /// A column from views that already point into `arena`.
288    ///
289    /// The way back in from [`Self::into_parts`], for the caller that took a column apart to hold
290    /// the payload once and the views many times and now wants a column again. Nothing here checks
291    /// that a view points inside the arena, for the same reason [`Self::bytes`] answers `None`
292    /// rather than panicking when one does not: a view that points nowhere reads as no bytes, which
293    /// is the empty string, and that is a wrong answer rather than an unsound one.
294    #[must_use]
295    pub fn from_parts(views: Vec<StringView>, arena: Buffer<u8>) -> Self {
296        Self { views, arena }
297    }
298
299    /// The values at `at`, over this column's arena rather than over a copy of the bytes.
300    ///
301    /// What a cut, a gather and a flatten of a column whose payload is a page all want. A view says
302    /// where its bytes are, so putting the views in a different order or keeping only some of them
303    /// leaves every one of them pointing at the same bytes it pointed at before, and the answer is
304    /// the same column of strings the copying version builds. Sixteen bytes a row move and the
305    /// payload does not, which is the split [`Self::into_page`] exists to make and is what the
306    /// [`StringView`](crate::vector::Form::StringView) form of a vector already makes for itself.
307    ///
308    /// `None` when the arena is this column's own rather than a page, because then there is no
309    /// sharing to be had: cloning an owned arena copies every byte of it, including the bytes of
310    /// every value the caller did not ask for, and the copying version is both smaller and faster.
311    /// A producer that means its payload to be read many times says so with [`Self::into_page`].
312    ///
313    /// A position this column does not have comes back as the empty string, which is what the
314    /// copying version writes for a position that resolved to nowhere.
315    #[must_use]
316    pub fn viewing(&self, at: impl Iterator<Item = usize>) -> Option<Self> {
317        if !self.arena.is_shared() {
318            return None;
319        }
320        let views = at
321            .map(|index| self.views.get(index).copied().unwrap_or_else(StringView::empty))
322            .collect();
323        Some(Self { views, arena: self.arena.clone() })
324    }
325
326    /// How many strings are in the column.
327    #[must_use]
328    pub fn len(&self) -> usize {
329        self.views.len()
330    }
331
332    /// Whether the column has no strings in it.
333    #[must_use]
334    pub fn is_empty(&self) -> bool {
335        self.views.is_empty()
336    }
337
338    /// The views, for a kernel that wants to compare prefixes without reading any payload.
339    #[must_use]
340    pub fn views(&self) -> &[StringView] {
341        &self.views
342    }
343
344    /// Appends a string and returns its index.
345    pub fn push(&mut self, text: &str) -> usize {
346        let view = if text.len() <= INLINE_LIMIT {
347            StringView::inline(text)
348        } else {
349            let offset = self.arena.len() as u64;
350            self.arena.extend_from_slice(text.as_bytes());
351            StringView::indirect(text, offset)
352        };
353        self.views.push(view);
354        self.views.len() - 1
355    }
356
357    /// Appends the string at `index` of another column, and returns its index here.
358    ///
359    /// This is what a gather and a slice over a string column want, and it is worth having next to
360    /// [`Self::push`] because that one takes a `&str` and the only way to get one out of a column
361    /// is [`Self::get`], which validates UTF-8. Validating there is a waste on this path twice
362    /// over: the bytes were validated on the way into the source column, and a copy cannot make
363    /// valid bytes invalid. Reading a ClickBench partition spent eight percent of its cycles on
364    /// that second validation.
365    ///
366    /// A position past the end of the source appends the empty string, which is what the copy loop
367    /// wants for a row that resolved to nowhere.
368    pub fn push_from(&mut self, source: &Self, index: usize) -> usize {
369        self.push_bytes(source.bytes(index).unwrap_or(b""))
370    }
371
372    /// Appends bytes that are not required to be text, and returns their index.
373    ///
374    /// What a `BLOB` is stored through. The column is the same column either way, because a string
375    /// here is already a length and some bytes and text is the reading rather than the storage, so
376    /// a blob costs nothing extra and shares every kernel that works on views. What it does not
377    /// share is [`Self::get`], which answers `None` for bytes that are not a string, so a caller
378    /// holding blobs reads them with [`Self::bytes`].
379    pub fn push_bytes(&mut self, bytes: &[u8]) -> usize {
380        let offset = self.arena.len() as u64;
381        if bytes.len() > INLINE_LIMIT {
382            self.arena.extend_from_slice(bytes);
383        }
384        self.views.push(StringView::over(bytes, offset));
385        self.views.len() - 1
386    }
387
388    /// Records a string that is already in the arena, and returns its index.
389    ///
390    /// The half of the seam that does the work. [`Self::over`] puts the page in, this says where in
391    /// it a string is, and between them a column of long strings is built without the payload being
392    /// touched at all.
393    ///
394    /// A string short enough to sit inside a view is copied into the view, which is at most twelve
395    /// bytes and is what makes it readable without going near the arena at all. Everything longer
396    /// keeps its bytes where they are and the view records the offset.
397    ///
398    /// # Errors
399    ///
400    /// If the range is not inside the arena, or if the bytes are not valid UTF-8. The validation is
401    /// the one cost this seam does not remove, and it is here rather than skipped because
402    /// [`Self::get`] hands back a `&str` and a column that cannot produce one for a string it claims
403    /// to hold is a wrong answer rather than a slow one. Skipping it is not an option a DuckDB
404    /// compatible reader has either: DuckDB reads a Parquet byte array that is not UTF-8 and throws
405    /// `Invalid Input Error`, so a reader that let it through would disagree about which files are
406    /// readable at all.
407    pub fn push_in_place(&mut self, offset: usize, len: usize) -> Result<usize> {
408        let end = offset.checked_add(len).ok_or_else(|| {
409            Error::internal(format!(
410                "a string at {offset} of {len} bytes runs off the end of memory"
411            ))
412        })?;
413        let bytes = self.arena.get(offset..end).ok_or_else(|| {
414            Error::internal(format!(
415                "a string at {offset} of {len} bytes is not inside a {} byte arena",
416                self.arena.len()
417            ))
418        })?;
419        // One pass, which is what `rudb_common::utf8::valid` is for. This used to run `is_ascii`
420        // and then `str::from_utf8` over whatever the first one did not settle, and on a column of
421        // URLs that is nearly every string twice: the ASCII walk stops at the Cyrillic in the query
422        // string and the real validator then starts again from the front with its own prologue in
423        // front of it. A scan profile put the second of those at two hundred instructions a URL.
424        if !rudb_common::utf8::valid(bytes) {
425            return Err(Error::internal(format!("the bytes at {offset} are not valid UTF-8")));
426        }
427        self.views.push(StringView::over(bytes, offset as u64));
428        Ok(self.views.len() - 1)
429    }
430
431    /// The bytes the long strings live in.
432    ///
433    /// For a column over a page this is the page, including whatever of it no view points at. The
434    /// offsets in the views are offsets into exactly this, which is what makes them meaningful to a
435    /// reader that put the page here in the first place.
436    #[must_use]
437    pub fn arena(&self) -> &[u8] {
438        &self.arena
439    }
440
441    /// The views and the arena, taken out of the column rather than borrowed from it.
442    ///
443    /// What the string view form of a vector is built from. It takes `self` because the point of
444    /// that form is that the arena moves into an `Arc` and is never copied again, and a method that
445    /// borrowed would have to clone every byte of the arena to hand one over.
446    #[must_use]
447    pub fn into_parts(self) -> (Vec<StringView>, Buffer<u8>) {
448        (self.views, self.arena)
449    }
450
451    /// The bytes at `index`, or `None` past the end.
452    ///
453    /// This is what a comparison, a hash and an equality check all actually want, and it is worth
454    /// having separately from [`Self::get`] because that one validates UTF-8 and they do not need
455    /// it. Everything in a column arrived through [`Self::push`], which takes a `&str`, so the
456    /// bytes are valid either way and the validation is a scan of the payload that changes no
457    /// answer. On a varchar filter it was measured at most of the per row cost.
458    #[must_use]
459    pub fn bytes(&self, index: usize) -> Option<&[u8]> {
460        self.views.get(index)?.bytes_in(&self.arena)
461    }
462
463    /// The string at `index`, or `None` past the end.
464    #[must_use]
465    pub fn get(&self, index: usize) -> Option<&str> {
466        // Written from a `&str` into a block that is append only, so the bytes are the same bytes.
467        std::str::from_utf8(self.bytes(index)?).ok()
468    }
469
470    /// Every string in order.
471    pub fn iter(&self) -> impl Iterator<Item = &str> {
472        (0..self.len()).filter_map(|index| self.get(index))
473    }
474
475    /// Total bytes of payload held in the arena, which is what the memory accounting wants.
476    ///
477    /// For a column over a page it is the page and not the part of it any view points at, which is
478    /// the right answer for accounting, because the page is what is resident.
479    #[must_use]
480    pub fn heap_bytes(&self) -> usize {
481        self.arena.len()
482    }
483
484    /// Room for `bytes` of payload, taken in one allocation rather than as the strings arrive.
485    ///
486    /// A builder that knows the total byte count, which a scan reading a page and a gather copying a
487    /// column both do, saves the doubling entirely. Nothing is wrong without it, which is why it is
488    /// a hint and not a constructor argument.
489    ///
490    /// Not for a column built by [`Self::over`] on a page it shares, because reserving writes and a
491    /// write through a shared buffer copies the whole page out first. Such a column is not appended
492    /// to anyway: its strings are already in its arena and [`Self::push_in_place`] records where.
493    pub fn reserve_bytes(&mut self, bytes: usize) {
494        self.arena.reserve(bytes);
495    }
496
497    /// Room for `count` more strings, taken in one allocation rather than as they arrive.
498    ///
499    /// The views and not the payload, which is the half [`Self::reserve_bytes`] does not cover and
500    /// is the only half that matters to a column built by [`Self::over`], whose payload is already
501    /// there. A Parquet page of a hundred thousand strings is one and three quarter megabytes of
502    /// views, and growing that from nothing is twenty allocations and a copy of everything written
503    /// so far each time.
504    pub fn reserve_views(&mut self, count: usize) {
505        self.views.reserve(count);
506    }
507}
508
509/// Two columns are equal when they hold the same strings in the same order, whatever their arenas
510/// look like.
511///
512/// See the note on [`StringColumn`]. Comparing the views is not enough on its own either, because
513/// two views of the same long string at different offsets in different arenas are different views,
514/// so the comparison is length, then view by view with the payload read for the ones that are not
515/// inline. The prefix inside the view is what makes that cheap: a pair that differs in the first
516/// four bytes or in the length is settled without either arena being touched.
517impl PartialEq for StringColumn {
518    fn eq(&self, other: &Self) -> bool {
519        self.views.len() == other.views.len()
520            && (0..self.views.len()).all(|index| {
521                let mine = self.views[index];
522                let theirs = other.views[index];
523                if mine.definitely_differs(&theirs) {
524                    return false;
525                }
526                if mine.is_inline() {
527                    return mine == theirs;
528                }
529                self.bytes(index) == other.bytes(index)
530            })
531    }
532}
533
534impl<'a> Extend<&'a str> for StringColumn {
535    fn extend<T: IntoIterator<Item = &'a str>>(&mut self, iter: T) {
536        for text in iter {
537            self.push(text);
538        }
539    }
540}
541
542impl<'a> FromIterator<&'a str> for StringColumn {
543    fn from_iter<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
544        let mut column = Self::new();
545        column.extend(iter);
546        column
547    }
548}
549
550#[cfg(test)]
551mod tests {
552    use super::{INLINE_LIMIT, StringColumn, StringView};
553    use crate::buffer::Buffer;
554
555    /// The seam, used the way layer three will use it. The page arrives whole, each string is
556    /// recorded where it already is, and the arena at the end is the page byte for byte, including
557    /// the header this page has in front of the strings and the bytes between them that belong to
558    /// nothing. A column that had copied would have an arena the size of the strings instead.
559    #[test]
560    fn a_column_over_a_page_records_the_strings_without_moving_them() {
561        let page =
562            b"HEADER..a string well past the inline limit!!a second one past the limit".to_vec();
563        let mut column = StringColumn::over(Buffer::from_vec(page.clone()));
564        assert_eq!(column.push_in_place(8, 37).expect("inside the page"), 0);
565        assert_eq!(column.push_in_place(45, 27).expect("inside the page"), 1);
566        assert_eq!(column.get(0), Some("a string well past the inline limit!!"));
567        assert_eq!(column.get(1), Some("a second one past the limit"));
568        assert_eq!(column.arena(), page.as_slice());
569        assert_eq!(column.heap_bytes(), page.len());
570        assert_eq!(column.len(), 2);
571    }
572
573    /// Copying between two columns, which is what a gather and a slice over a string column are.
574    /// A column built over a page has an arena full of bytes no view points at, and the copy has to
575    /// take the strings rather than the arena, so the destination holds the strings and nothing
576    /// else. The last case is the row that resolved to nowhere, which is an empty string here and a
577    /// null in the validity mask beside it.
578    #[test]
579    fn copying_from_another_column_takes_the_strings_and_not_the_page_they_were_in() {
580        let page = b"HEADER..a string well past the inline limit!!short".to_vec();
581        let mut source = StringColumn::over(Buffer::from_vec(page.clone()));
582        source.push_in_place(8, 37).expect("inside the page");
583        source.push_in_place(45, 5).expect("inside the page");
584
585        let mut out = StringColumn::new();
586        assert_eq!(out.push_from(&source, 1), 0);
587        assert_eq!(out.push_from(&source, 0), 1);
588        assert_eq!(out.push_from(&source, 9), 2, "a position that is not there");
589
590        assert_eq!(out.get(0), Some("short"));
591        assert_eq!(out.get(1), Some("a string well past the inline limit!!"));
592        assert_eq!(out.get(2), Some(""));
593        assert!(out.views()[0].is_inline(), "a short string stays in its view");
594        assert!(!out.views()[1].is_inline());
595        assert_eq!(out.views()[1].prefix(), *b"a st", "the prefix is the string's own");
596        assert_eq!(
597            out.arena(),
598            b"a string well past the inline limit!!",
599            "the arena is the long strings and not the page"
600        );
601    }
602
603    /// Bytes that are not text, which is what a `BLOB` holds. Both sides of the inline limit,
604    /// because a short one lives in its view and a long one lives in the arena and the byte that is
605    /// not a character has to survive either way. Reading them back as text is `None` and reading
606    /// them back as bytes is what went in.
607    #[test]
608    fn a_column_holds_bytes_that_are_not_a_string() {
609        let long = b"\xff\xfe and a good deal more than twelve bytes of it";
610        let mut column = StringColumn::new();
611        assert_eq!(column.push_bytes(b"a\xffb"), 0);
612        assert_eq!(column.push_bytes(long), 1);
613        assert_eq!(column.push_bytes(b""), 2);
614
615        assert_eq!(column.bytes(0), Some(b"a\xffb".as_slice()));
616        assert_eq!(column.bytes(1), Some(long.as_slice()));
617        assert_eq!(column.bytes(2), Some(b"".as_slice()));
618        assert_eq!(column.get(0), None, "a stray 0xff is not a character");
619        assert_eq!(column.get(1), None);
620        assert!(column.views()[0].is_inline());
621        assert!(!column.views()[1].is_inline());
622        assert_eq!(column.arena(), long, "only the long one needed the arena");
623    }
624
625    /// A copy of a copy, because the second one reads its bytes out of an arena the first one wrote
626    /// rather than out of a page, and an offset written in one and read in the other is the way
627    /// this goes wrong.
628    #[test]
629    fn copying_from_a_column_that_was_itself_copied_reads_the_same_strings() {
630        let mut first = StringColumn::new();
631        for text in ["a string well past the inline limit", "short", "another long one past it"] {
632            first.push(text);
633        }
634        let mut second = StringColumn::new();
635        for index in (0..first.len()).rev() {
636            second.push_from(&first, index);
637        }
638        let mut third = StringColumn::new();
639        for index in 0..second.len() {
640            third.push_from(&second, index);
641        }
642        assert_eq!(
643            third.iter().collect::<Vec<_>>(),
644            ["another long one past it", "short", "a string well past the inline limit"]
645        );
646    }
647
648    /// A string short enough to live inside its view is copied into the view, which is twelve bytes
649    /// and is what lets it be read without the arena. The page is still the arena and is still
650    /// untouched, so a page of short strings costs the views and nothing else.
651    #[test]
652    fn a_short_string_in_a_page_is_copied_into_its_view() {
653        let mut column = StringColumn::over(Buffer::from_vec(b"one.two".to_vec()));
654        column.push_in_place(0, 3).expect("inside the page");
655        column.push_in_place(4, 3).expect("inside the page");
656        assert!(column.views()[0].is_inline());
657        assert_eq!(column.get(0), Some("one"));
658        assert_eq!(column.get(1), Some("two"));
659        assert_eq!(column.arena(), b"one.two");
660    }
661
662    /// The two ways a caller can be wrong about a page, both of them answered before anything is
663    /// recorded rather than at the point somebody reads the string back and finds nothing there.
664    #[test]
665    fn a_range_outside_the_page_or_bytes_that_are_not_text_are_refused() {
666        let mut column = StringColumn::over(Buffer::from_vec(vec![0xff, 0xfe, 0xfd]));
667        assert!(column.push_in_place(2, 4).is_err());
668        assert!(column.push_in_place(usize::MAX, 1).is_err());
669        assert!(column.push_in_place(0, 3).is_err());
670        assert_eq!(column.len(), 0);
671
672        // The ASCII check in front of the validator answers whole words at a time, so the bad byte
673        // is put past the first word and past the inline limit as well, where a check that only
674        // looked at the head or only at the payload in the view would miss it.
675        let mut page = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec();
676        page.push(0x80);
677        let len = page.len();
678        let mut column = StringColumn::over(Buffer::from_vec(page));
679        assert!(column.push_in_place(0, len).is_err());
680        assert!(column.push_in_place(0, len - 1).is_ok());
681
682        // Text that is not ASCII and is valid goes through, which is the other half of the check:
683        // the fast path decides nothing on its own, it only decides who has to look.
684        let page = "søk på nettet".as_bytes().to_vec();
685        let len = page.len();
686        let mut column = StringColumn::over(Buffer::from_vec(page));
687        column.push_in_place(0, len).expect("valid text that is not ASCII");
688        assert_eq!(column.get(0), Some("søk på nettet"));
689    }
690
691    /// What the seam does to equality. The same two strings, one column built by copying them in
692    /// and one built over a page that has them in the other order with a gap in the middle, and the
693    /// two arenas have nothing in common. Equality is the strings, so the columns are equal.
694    #[test]
695    fn the_same_strings_over_different_arenas_are_the_same_column() {
696        let copied: StringColumn =
697            ["the first string past the limit", "the second string past the limit"]
698                .into_iter()
699                .collect();
700        let page =
701            b"gap!the second string past the limit....the first string past the limit".to_vec();
702        let mut over = StringColumn::over(Buffer::from_vec(page));
703        over.push_in_place(40, 31).expect("inside the page");
704        over.push_in_place(4, 32).expect("inside the page");
705        assert_ne!(copied.arena(), over.arena());
706        assert_eq!(copied, over);
707
708        let mut different: StringColumn = copied.clone();
709        different.push("a third one past the inline limit");
710        assert_ne!(copied, different);
711    }
712
713    #[test]
714    fn a_view_is_sixteen_bytes_and_stays_sixteen_bytes() {
715        // The number the whole design is built around. A vector of 1024 strings is 16 KiB of
716        // views, which is the budget spec/07-execution.md section 7.1 spends on purpose.
717        assert_eq!(size_of::<StringView>(), 16);
718        assert_eq!(align_of::<StringView>(), 4);
719    }
720
721    #[test]
722    fn twelve_bytes_is_inline_and_thirteen_is_not() {
723        let mut column = StringColumn::new();
724        column.push("123456789012");
725        column.push("1234567890123");
726        assert!(column.views()[0].is_inline());
727        assert!(!column.views()[1].is_inline());
728        assert_eq!(column.get(0), Some("123456789012"));
729        assert_eq!(column.get(1), Some("1234567890123"));
730        assert_eq!(INLINE_LIMIT, 12);
731    }
732
733    #[test]
734    fn a_prefix_answers_the_comparison_without_reading_the_payload() {
735        let mut column = StringColumn::new();
736        column.push("https://example.com/a");
737        column.push("https://example.com/b");
738        column.push("mailto:someone@example.com");
739        let views = column.views();
740        // Same prefix, same length: the payloads have to be read. This is the case the prefix
741        // cannot help with, and on a URL column it is the common case, which is why the
742        // dictionary work at M3 matters more than this does.
743        assert!(!views[0].definitely_differs(&views[1]));
744        // Different prefix: answered from the view.
745        assert!(views[0].definitely_differs(&views[2]));
746    }
747
748    /// A string of any size goes in whole, with the short ones on either side of it still reading
749    /// back. The old layout had a size at which a string stopped fitting a block and got one of its
750    /// own, and one arena has no such size, so the case worth keeping is the one that used to be
751    /// special rather than the branch that used to handle it.
752    #[test]
753    fn a_string_far_larger_than_any_block_would_have_been_goes_in_whole() {
754        let long = "x".repeat(40 * 1024);
755        let mut column = StringColumn::new();
756        column.push("short");
757        column.push(&long);
758        column.push("also short");
759        assert_eq!(column.get(1), Some(long.as_str()));
760        assert_eq!(column.get(2), Some("also short"));
761        assert_eq!(column.heap_bytes(), long.len());
762    }
763
764    /// The property the whole arena rests on. Two thousand strings is tens of reallocations, and
765    /// every one of them moves the bytes to a new address while the offsets recorded in the views
766    /// before it stay exactly as they were. A view holding a pointer would be reading freed memory
767    /// by the end of this test.
768    #[test]
769    fn the_arena_moving_underneath_does_not_move_what_the_views_point_at() {
770        let mut column = StringColumn::new();
771        let strings: Vec<String> =
772            (0..2000).map(|i| format!("value number {i} padded out")).collect();
773        for text in &strings {
774            column.push(text);
775        }
776        for (index, text) in strings.iter().enumerate() {
777            assert_eq!(column.get(index), Some(text.as_str()), "at {index}");
778        }
779        assert_eq!(column.len(), 2000);
780        assert_eq!(column.iter().count(), 2000);
781    }
782
783    #[test]
784    fn reserving_bytes_changes_nothing_but_where_the_allocation_happens() {
785        let mut column = StringColumn::with_capacity(3);
786        column.reserve_bytes(128);
787        for text in ["a string past the limit", "another one past it", "short"] {
788            column.push(text);
789        }
790        assert_eq!(column.get(0), Some("a string past the limit"));
791        assert_eq!(column.get(1), Some("another one past it"));
792        assert_eq!(column.get(2), Some("short"));
793        assert_eq!(column.heap_bytes(), 42);
794    }
795
796    #[test]
797    fn the_empty_string_is_inline_and_reads_back_empty() {
798        let mut column = StringColumn::new();
799        column.push("");
800        assert_eq!(column.get(0), Some(""));
801        assert!(column.views()[0].is_empty());
802        assert_eq!(column.heap_bytes(), 0);
803    }
804
805    #[test]
806    fn multibyte_text_survives_the_inline_boundary() {
807        // The boundary is bytes and not characters, so a four byte emoji is what decides whether
808        // a three character string is inline.
809        let mut column = StringColumn::new();
810        column.push("héllo wörld");
811        column.push("🦀🦀🦀🦀");
812        assert_eq!(column.get(0), Some("héllo wörld"));
813        assert_eq!(column.get(1), Some("🦀🦀🦀🦀"));
814        assert!(!column.views()[1].is_inline());
815    }
816
817    #[test]
818    fn reading_past_the_end_is_none_rather_than_a_panic() {
819        let column: StringColumn = ["a", "b"].into_iter().collect();
820        assert_eq!(column.get(2), None);
821        assert_eq!(column.len(), 2);
822    }
823
824    /// The bytes and the string have to be the same string on both sides of the inline boundary
825    /// and on multibyte text, because the comparison kernels read the bytes and everything else
826    /// reads the string, and a disagreement between them would be a filter that matched a row the
827    /// projection then printed differently.
828    #[test]
829    fn the_bytes_and_the_string_are_the_same_string() {
830        let long = "x".repeat(9000);
831        let words = ["", "a", "twelve bytes", "thirteen bytes", "π is two bytes", &long];
832        let column: StringColumn = words.into_iter().collect();
833        for (index, text) in words.iter().enumerate() {
834            assert_eq!(column.bytes(index), Some(text.as_bytes()), "at {index}");
835            assert_eq!(column.get(index), Some(*text), "at {index}");
836        }
837        assert_eq!(column.bytes(words.len()), None);
838    }
839}