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 std::cmp::Ordering;
19use std::collections::HashMap;
20
21use rudb_common::{Error, Result};
22
23use crate::buffer::Buffer;
24
25/// The longest string that fits entirely inside a view.
26pub const INLINE_LIMIT: usize = 12;
27
28/// A 16 byte handle on a string.
29///
30/// The layout is a `u32` length and 12 bytes of payload. For a string of 12 bytes or fewer the
31/// payload is the string, zero padded. For a longer one the first 4 bytes are the prefix and the
32/// last 8 are the offset into the column's arena.
33///
34/// Arrow spends 4 of those 8 bytes on a buffer index and 4 on an offset within the buffer, because
35/// an Arrow array is a list of buffers. This column is one arena, so there is no buffer to name and
36/// the whole 8 bytes are the offset, which reads as one load rather than two and takes the reachable
37/// size of a column from 4 GiB to more than anything will ever put in one.
38///
39/// A view on its own cannot produce a long string, only a short one. That is deliberate: the arena
40/// lives in the [`StringColumn`] and the borrow checker is what stops a view from outliving it,
41/// rather than a rule somebody has to remember.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub struct StringView {
44    length: u32,
45    payload: [u8; 12],
46}
47
48impl StringView {
49    /// The view on the empty string.
50    ///
51    /// What a copy loop writes for a position that resolved to nowhere, for the same reason a fixed
52    /// width copy writes a zero there. The views are a parallel array to a validity mask, so a row
53    /// that got skipped rather than filled would put every row after it at the wrong index.
54    #[must_use]
55    pub const fn empty() -> Self {
56        Self { length: 0, payload: [0; 12] }
57    }
58
59    /// A view on a string that fits inline.
60    ///
61    /// # Panics
62    ///
63    /// If the string is longer than [`INLINE_LIMIT`]. Callers that do not know the length go
64    /// through [`StringColumn::push`], which decides.
65    #[must_use]
66    pub fn inline(text: &str) -> Self {
67        assert!(text.len() <= INLINE_LIMIT, "a string of {} bytes is not inline", text.len());
68        let mut payload = [0u8; 12];
69        payload[..text.len()].copy_from_slice(text.as_bytes());
70        Self { length: text.len() as u32, payload }
71    }
72
73    /// A view on a string that lives in the arena.
74    fn indirect(text: &str, offset: u64) -> Self {
75        let mut payload = [0u8; 12];
76        payload[..4].copy_from_slice(&text.as_bytes()[..4]);
77        payload[4..].copy_from_slice(&offset.to_le_bytes());
78        Self { length: text.len() as u32, payload }
79    }
80
81    /// A view on bytes, whatever they are, wherever they turn out to live.
82    ///
83    /// The one constructor that takes bytes rather than a `&str`, and the two callers want it for
84    /// different reasons. A copy between two columns has bytes that were validated on the way into
85    /// the first one and validating again would be work for nothing. A `BLOB` has bytes that were
86    /// never text and are not going to become it. `offset` is where they are in the destination
87    /// arena and is ignored for a string short enough to sit in the view.
88    ///
89    /// It is public because the string view form of a vector is built from views a caller made, and
90    /// a scan laying chunks over a page of strings is exactly the caller that has bytes and an
91    /// offset into somebody else's arena rather than a column to push into.
92    #[must_use]
93    pub fn over(bytes: &[u8], offset: u64) -> Self {
94        let mut payload = [0u8; 12];
95        if bytes.len() <= INLINE_LIMIT {
96            payload[..bytes.len()].copy_from_slice(bytes);
97        } else {
98            payload[..4].copy_from_slice(&bytes[..4]);
99            payload[4..].copy_from_slice(&offset.to_le_bytes());
100        }
101        Self { length: bytes.len() as u32, payload }
102    }
103
104    /// The length in bytes.
105    #[must_use]
106    #[inline]
107    pub fn len(&self) -> usize {
108        self.length as usize
109    }
110
111    /// Whether the string is empty.
112    #[must_use]
113    pub fn is_empty(&self) -> bool {
114        self.length == 0
115    }
116
117    /// Whether the whole string is in the view.
118    #[must_use]
119    #[inline]
120    pub fn is_inline(&self) -> bool {
121        self.len() <= INLINE_LIMIT
122    }
123
124    /// The same string after the arena it points into was laid `by` bytes further along.
125    pub(crate) fn shifted(self, by: u64) -> Self {
126        if self.is_inline() {
127            return self;
128        }
129        let mut shifted = self;
130        shifted.payload[4..].copy_from_slice(&(self.offset() as u64 + by).to_le_bytes());
131        shifted
132    }
133
134    /// The first four bytes, zero padded.
135    ///
136    /// This is the whole point of the representation. Two strings with different prefixes are
137    /// different, and two strings with the same prefix are usually equal, so a filter on a string
138    /// column resolves without touching the payload on almost every row.
139    #[must_use]
140    pub fn prefix(&self) -> [u8; 4] {
141        [self.payload[0], self.payload[1], self.payload[2], self.payload[3]]
142    }
143
144    /// The bytes, when the whole string is in the view.
145    ///
146    /// A comparison wants bytes rather than a `&str`, because SQL's string order is byte order and
147    /// because [`Self::as_inline_str`] pays for a UTF-8 validation that a comparison has no use
148    /// for. On a filter against a varchar column that validation is the whole cost of the row.
149    #[must_use]
150    #[inline]
151    pub fn inline_bytes(&self) -> Option<&[u8]> {
152        if self.is_inline() { Some(&self.payload[..self.len()]) } else { None }
153    }
154
155    /// The string, when it is short enough to be in the view.
156    #[must_use]
157    pub fn as_inline_str(&self) -> Option<&str> {
158        if !self.is_inline() {
159            return None;
160        }
161        // `None` rather than a panic for a view that holds a blob, since the payload is whatever
162        // was written and only a column of text can promise that is a string.
163        std::str::from_utf8(&self.payload[..self.len()]).ok()
164    }
165
166    /// The bytes, given the arena the long strings of this column live in.
167    ///
168    /// A short string is in the view and the arena is not read at all, which is why this takes the
169    /// arena rather than requiring one that has the string in it.
170    ///
171    /// This exists because a view and the bytes it points at do not have to be held by the same
172    /// object. [`StringColumn`] owns both, and the string view form of a vector holds the views
173    /// itself and shares the arena with every other cut of the same page, so a cut of a varchar
174    /// column is the views and nothing else. Both of them resolve a row the same way, and this is
175    /// where that one way is written.
176    #[must_use]
177    #[inline]
178    pub fn bytes_in<'a>(&'a self, arena: &'a [u8]) -> Option<&'a [u8]> {
179        if let Some(inline) = self.inline_bytes() {
180            return Some(inline);
181        }
182        arena.get(self.offset()..self.offset() + self.len())
183    }
184
185    #[inline]
186    fn offset(&self) -> usize {
187        u64::from_le_bytes([
188            self.payload[4],
189            self.payload[5],
190            self.payload[6],
191            self.payload[7],
192            self.payload[8],
193            self.payload[9],
194            self.payload[10],
195            self.payload[11],
196        ]) as usize
197    }
198
199    /// The byte order of two strings when the views alone decide it, and `None` when the bytes in
200    /// the arena have to be read.
201    ///
202    /// Two inline strings always decide: the payloads are zero padded, and padding keeps byte order
203    /// because a string that runs out at a byte where the other has a zero still sorts first, which
204    /// the length settles when the padded payloads are the same. The same argument makes two
205    /// different prefixes decide for any pair. What is left is two strings with the same four bytes
206    /// in front and at least one of them long, and that is the one case that needs the arena.
207    #[must_use]
208    #[inline]
209    pub fn known_order(&self, other: &Self) -> Option<Ordering> {
210        if self.is_inline() && other.is_inline() {
211            let order = padded(&self.payload).cmp(&padded(&other.payload));
212            return Some(order.then(self.length.cmp(&other.length)));
213        }
214        let (left, right) = (u32::from_be_bytes(self.prefix()), u32::from_be_bytes(other.prefix()));
215        if left == right { None } else { Some(left.cmp(&right)) }
216    }
217
218    /// Whether these two views are definitely different, answered from the view alone.
219    ///
220    /// A `false` here means the payloads have to be compared. A `true` means they do not, which on
221    /// a filter against a selective literal is almost every row.
222    #[must_use]
223    pub fn definitely_differs(&self, other: &Self) -> bool {
224        self.length != other.length || self.prefix() != other.prefix()
225    }
226}
227
228/// A payload as one number whose order is the byte order of the twelve bytes.
229#[inline]
230fn padded(payload: &[u8; 12]) -> u128 {
231    let mut wide = [0u8; 16];
232    wide[..12].copy_from_slice(payload);
233    u128::from_be_bytes(wide)
234}
235
236/// A column of strings: the views, and the one arena the long ones live in.
237///
238/// The arena is append only, so an offset recorded in a view stays correct for the life of the
239/// column even though the arena's address does not. That is the property a `Vec<u8>` has and a raw
240/// pointer into it does not, and it is the reason a view holds an offset.
241///
242/// This was a `Vec<Vec<u8>>` of fixed size blocks, which meant reading one long string was two
243/// dependent loads, the outer vector's element to find the block's data pointer and then the bytes.
244/// One arena makes it one, from a base the compiler can keep in a register across a row loop, and it
245/// deletes the case where a string longer than a block needed a block of its own. On server3, over a
246/// chunk of 1024 strings, comparing a column against a literal went from 14.9 nanoseconds a row to
247/// 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
248/// and from 36.9 to 29.1, and building the column from 12.0 to 8.9 at 40 bytes.
249///
250/// # The one number that got worse, and what it actually is
251///
252/// Building a column whose payload passes 128 KiB, which at 1024 rows means strings averaging more
253/// than 128 bytes, went the other way: 14.6 nanoseconds a row to 41.0. That is not the copy and it
254/// is not the doubling, it is glibc. An allocation that size comes from `mmap` rather than the heap,
255/// so it is handed back to the kernel when the column is dropped and the next chunk faults every
256/// page of it in again, while sixteen KiB blocks come back off a free list already faulted. Run the
257/// same benchmark with `MALLOC_MMAP_THRESHOLD_` raised and the arena builds that column in 9.6
258/// nanoseconds a row against the blocks' 16.2, so the design is not what is slow there.
259///
260/// The fix is that a chunk's payload should come from a pool the engine owns rather than from
261/// `malloc` per chunk, which is the buffer manager at layer three and is where this belongs.
262/// [`Self::reserve_bytes`] is the part that is available now, and it recovers a quarter of it.
263///
264/// # Equality is about the strings and not about the arena
265///
266/// [`Self::over`] means two columns holding exactly the same strings can hold completely different
267/// arenas, because one of them was built by copying the strings in and the other was built over a
268/// page that already had them somewhere in it with other strings in between. Derived equality would
269/// call those two columns different, and every test in the workspace that compares two vectors would
270/// then be asserting on how a column was built rather than on what is in it. So equality is the
271/// strings, position by position, which is the only definition that survives the seam.
272#[derive(Debug, Clone, Default, Eq)]
273pub struct StringColumn {
274    views: Buffer<StringView>,
275    arena: Buffer<u8>,
276}
277
278impl StringColumn {
279    /// How many bytes of memory this column is holding.
280    ///
281    /// The views and the arena. A short string lives inside its view and costs nothing beyond it,
282    /// which is the whole reason the representation exists, so a column of short strings costs
283    /// sixteen bytes a string and a column of long ones costs sixteen plus the bytes themselves.
284    #[must_use]
285    pub fn footprint(&self) -> usize {
286        self.views.footprint() + self.arena.footprint()
287    }
288
289    /// An empty column.
290    #[must_use]
291    pub fn new() -> Self {
292        Self::default()
293    }
294
295    /// An empty column with room for `capacity` strings.
296    #[must_use]
297    pub fn with_capacity(capacity: usize) -> Self {
298        Self { views: Buffer::with_capacity(capacity), arena: Buffer::new() }
299    }
300
301    /// A column with no strings in it yet, over an arena that already holds bytes.
302    ///
303    /// The seam `spec/engine/03-data-plane.md` section 3.5 asks for. Without it the only way in is
304    /// [`Self::push`], which copies, so a scan reading a Parquet page of strings copies every byte of
305    /// the page into an arena and the query then reads the copy. With it the page is the arena: the
306    /// scan hands the bytes over once, records where each string starts with
307    /// [`Self::push_in_place`], and nothing is copied but the views.
308    ///
309    /// It is useful today, because a reader that already has the page in a `Vec<u8>` can move it in
310    /// rather than copy out of it. It matters at layer three, when the [`Buffer`] is the pinned page
311    /// itself and the move is not even that.
312    ///
313    /// Appending with [`Self::push`] afterwards still works and still appends to the arena. That is
314    /// the case to keep away from once a real page is in here, because writing through a borrowed
315    /// buffer copies it, which is [`Buffer::to_mut`] and is the whole page.
316    #[must_use]
317    pub fn over(arena: Buffer<u8>) -> Self {
318        Self { views: Buffer::new(), arena }
319    }
320
321    /// This column with its arena held as a page, so that a copy of it does not copy the bytes.
322    ///
323    /// The views are still copied, because they are a `Vec` and a run of them is what a cut of the
324    /// column is. Sixteen bytes a row rather than every byte of every string, which is the same
325    /// split the [`StringView`](crate::vector::Form::StringView) form already makes for the same
326    /// reason.
327    #[must_use]
328    pub fn into_page(self) -> Self {
329        Self { views: self.views.into_page(), arena: self.arena.into_page() }
330    }
331
332    /// A column from views that already point into `arena`.
333    ///
334    /// The way back in from [`Self::into_parts`], for the caller that took a column apart to hold
335    /// the payload once and the views many times and now wants a column again. Nothing here checks
336    /// that a view points inside the arena, for the same reason [`Self::bytes`] answers `None`
337    /// rather than panicking when one does not: a view that points nowhere reads as no bytes, which
338    /// is the empty string, and that is a wrong answer rather than an unsound one.
339    #[must_use]
340    pub fn from_parts(views: Vec<StringView>, arena: Buffer<u8>) -> Self {
341        Self { views: Buffer::from_vec(views), arena }
342    }
343
344    /// The values at `at`, over this column's arena rather than over a copy of the bytes.
345    ///
346    /// What a cut, a gather and a flatten of a column whose payload is a page all want. A view says
347    /// where its bytes are, so putting the views in a different order or keeping only some of them
348    /// leaves every one of them pointing at the same bytes it pointed at before, and the answer is
349    /// the same column of strings the copying version builds. Sixteen bytes a row move and the
350    /// payload does not, which is the split [`Self::into_page`] exists to make and is what the
351    /// [`StringView`](crate::vector::Form::StringView) form of a vector already makes for itself.
352    ///
353    /// `None` when the arena is this column's own rather than a page, because then there is no
354    /// sharing to be had: cloning an owned arena copies every byte of it, including the bytes of
355    /// every value the caller did not ask for, and the copying version is both smaller and faster.
356    /// A producer that means its payload to be read many times says so with [`Self::into_page`].
357    ///
358    /// A position this column does not have comes back as the empty string, which is what the
359    /// copying version writes for a position that resolved to nowhere.
360    #[must_use]
361    pub fn viewing(&self, at: impl Iterator<Item = usize>) -> Option<Self> {
362        if !self.arena.is_shared() {
363            return None;
364        }
365        let views = at
366            .map(|index| self.views.get(index).copied().unwrap_or_else(StringView::empty))
367            .collect();
368        Some(Self { views, arena: self.arena.clone() })
369    }
370
371    /// The strings from `from` to `to`, over this column's arena and its views.
372    ///
373    /// The cut [`Self::viewing`] makes for a run of rows rather than a set of them, and cheaper,
374    /// because a run of views is a window too. When the views are a page as well as the arena the
375    /// cut moves nothing at all, which is what a scan and a sorted load hand on: every chunk of a
376    /// column is a cut of it, and every one of those cuts used to copy sixteen bytes a row.
377    ///
378    /// `None` when the arena is this column's own, for the reason [`Self::viewing`] gives, and when
379    /// the run goes past the end, which is for the caller's padding path.
380    #[must_use]
381    pub fn window(&self, from: usize, to: usize) -> Option<Self> {
382        if !self.arena.is_shared() || from > to || to > self.views.len() {
383            return None;
384        }
385        Some(Self { views: self.views.slice(from, to - from), arena: self.arena.clone() })
386    }
387
388    /// Whether the views and the arena are both pages, so that a copy of the column copies neither.
389    #[must_use]
390    pub fn is_paged(&self) -> bool {
391        self.views.is_shared() && self.arena.is_shared()
392    }
393
394    /// This column and `next` as one, when both are windows of the same views over the same arena
395    /// and `next` starts where this one ends. See [`Buffer::joined`].
396    #[must_use]
397    pub fn joined(&self, next: &Self) -> Option<Self> {
398        if !self.arena.same_window(&next.arena) {
399            return None;
400        }
401        Some(Self { views: self.views.joined(&next.views)?, arena: self.arena.clone() })
402    }
403
404    /// How many strings are in the column.
405    #[must_use]
406    pub fn len(&self) -> usize {
407        self.views.len()
408    }
409
410    /// Whether the column has no strings in it.
411    #[must_use]
412    pub fn is_empty(&self) -> bool {
413        self.views.is_empty()
414    }
415
416    /// The views, for a kernel that wants to compare prefixes without reading any payload.
417    #[must_use]
418    pub fn views(&self) -> &[StringView] {
419        &self.views
420    }
421
422    /// Appends a string and returns its index.
423    pub fn push(&mut self, text: &str) -> usize {
424        let view = if text.len() <= INLINE_LIMIT {
425            StringView::inline(text)
426        } else {
427            let offset = self.arena.len() as u64;
428            self.arena.extend_from_slice(text.as_bytes());
429            StringView::indirect(text, offset)
430        };
431        self.views.push(view);
432        self.views.len() - 1
433    }
434
435    /// Appends the string at `index` of another column, and returns its index here.
436    ///
437    /// This is what a gather and a slice over a string column want, and it is worth having next to
438    /// [`Self::push`] because that one takes a `&str` and the only way to get one out of a column
439    /// is [`Self::get`], which validates UTF-8. Validating there is a waste on this path twice
440    /// over: the bytes were validated on the way into the source column, and a copy cannot make
441    /// valid bytes invalid. Reading a ClickBench partition spent eight percent of its cycles on
442    /// that second validation.
443    ///
444    /// A position past the end of the source appends the empty string, which is what the copy loop
445    /// wants for a row that resolved to nowhere.
446    pub fn push_from(&mut self, source: &Self, index: usize) -> usize {
447        self.push_bytes(source.bytes(index).unwrap_or(b""))
448    }
449
450    /// Appends every string of `source`, in order, copying its arena whole when `arenas` says
451    /// that pays.
452    ///
453    /// A scan cuts a page of strings into chunk sized columns that all hold the page as their
454    /// arena, so one cut of SF1 `lineitem`'s comments points at 210KB of a 3.75MB arena. Copying
455    /// that arena for each cut would copy it eighteen times, and copying a string at a time is what
456    /// laying the 6 million comments end to end spent 300ms on. So the arena is copied once, the
457    /// first time a cut of it arrives, and every cut of it moves its views along by where it
458    /// landed. A Parquet page also holds a four byte length before each string and the short strings
459    /// the views carry themselves, which on the comments is one byte in six that no view points
460    /// at. An arena with more than one byte in five like that is copied a string at a time instead,
461    /// so that a filtered cut of a page does not carry the rest of the page along for as long as
462    /// the result lives.
463    pub(crate) fn push_column(&mut self, source: &Self, arenas: &mut Arenas) {
464        self.views.reserve(source.views.len());
465        let key = Arenas::key(source);
466        // An arena nobody counted is still worth one copy when it is mostly read, and a column built
467        // to be laid and then dropped is entirely read, so this is the usual answer for one of those.
468        // What it does not get is a line in `placed`, because the address it would be filed under is
469        // about to go back to the allocator. See the note on [`Arenas`].
470        let (live, share) = match arenas.counted(source) {
471            Some(live) => (live, true),
472            None => (live_bytes(source), false),
473        };
474        let base = match arenas.placed.get(&key) {
475            Some(&base) => Some(base),
476            None if Arenas::mostly_read(source.arena.len(), live) => {
477                let base = self.arena.len() as u64;
478                self.arena.extend_from_slice(source.arena());
479                if share {
480                    arenas.placed.insert(key, base);
481                }
482                Some(base)
483            }
484            None => None,
485        };
486        if let Some(base) = base {
487            self.views.to_mut().extend(source.views.iter().map(|view| view.shifted(base)));
488            return;
489        }
490        self.arena.reserve(live_bytes(source));
491        for index in 0..source.len() {
492            self.push_from(source, index);
493        }
494    }
495
496    /// Appends bytes that are not required to be text, and returns their index.
497    ///
498    /// What a `BLOB` is stored through. The column is the same column either way, because a string
499    /// here is already a length and some bytes and text is the reading rather than the storage, so
500    /// a blob costs nothing extra and shares every kernel that works on views. What it does not
501    /// share is [`Self::get`], which answers `None` for bytes that are not a string, so a caller
502    /// holding blobs reads them with [`Self::bytes`].
503    pub fn push_bytes(&mut self, bytes: &[u8]) -> usize {
504        let offset = self.arena.len() as u64;
505        if bytes.len() > INLINE_LIMIT {
506            self.arena.extend_from_slice(bytes);
507        }
508        self.views.push(StringView::over(bytes, offset));
509        self.views.len() - 1
510    }
511
512    /// Records a string that is already in the arena, and returns its index.
513    ///
514    /// The half of the seam that does the work. [`Self::over`] puts the page in, this says where in
515    /// it a string is, and between them a column of long strings is built without the payload being
516    /// touched at all.
517    ///
518    /// A string short enough to sit inside a view is copied into the view, which is at most twelve
519    /// bytes and is what makes it readable without going near the arena at all. Everything longer
520    /// keeps its bytes where they are and the view records the offset.
521    ///
522    /// # Errors
523    ///
524    /// If the range is not inside the arena, or if the bytes are not valid UTF-8. The validation is
525    /// the one cost this seam does not remove, and it is here rather than skipped because
526    /// [`Self::get`] hands back a `&str` and a column that cannot produce one for a string it claims
527    /// to hold is a wrong answer rather than a slow one. Skipping it is not an option a DuckDB
528    /// compatible reader has either: DuckDB reads a Parquet byte array that is not UTF-8 and throws
529    /// `Invalid Input Error`, so a reader that let it through would disagree about which files are
530    /// readable at all.
531    pub fn push_in_place(&mut self, offset: usize, len: usize) -> Result<usize> {
532        let end = offset.checked_add(len).ok_or_else(|| {
533            Error::internal(format!(
534                "a string at {offset} of {len} bytes runs off the end of memory"
535            ))
536        })?;
537        let bytes = self.arena.get(offset..end).ok_or_else(|| {
538            Error::internal(format!(
539                "a string at {offset} of {len} bytes is not inside a {} byte arena",
540                self.arena.len()
541            ))
542        })?;
543        // One pass, which is what `rudb_common::utf8::valid` is for. This used to run `is_ascii`
544        // and then `str::from_utf8` over whatever the first one did not settle, and on a column of
545        // URLs that is nearly every string twice: the ASCII walk stops at the Cyrillic in the query
546        // string and the real validator then starts again from the front with its own prologue in
547        // front of it. A scan profile put the second of those at two hundred instructions a URL.
548        if !rudb_common::utf8::valid(bytes) {
549            return Err(Error::internal(format!("the bytes at {offset} are not valid UTF-8")));
550        }
551        self.views.push(StringView::over(bytes, offset as u64));
552        Ok(self.views.len() - 1)
553    }
554
555    /// Records every string of a page whose strings sit end to end in the arena, and checks them
556    /// for text once rather than one at a time.
557    ///
558    /// `ends` are where each string stops, the first starting at `start`. Text cut at places that
559    /// each fall at the start of a character is text in every piece, so one pass over the whole run
560    /// and a look at the byte after each cut answers what [`Self::push_in_place`] answers per string.
561    /// On the order comments of TPC-H q13 the check per string was a twelfth of the query, most of it
562    /// the setup of a call for forty odd bytes.
563    ///
564    /// # Errors
565    ///
566    /// If an end is before the one ahead of it or past the arena, or if the bytes are not valid
567    /// UTF-8, in which case nothing has been recorded.
568    pub fn push_run_in_place(&mut self, start: usize, ends: &[usize]) -> Result<()> {
569        let last = ends.last().copied().unwrap_or(start);
570        let run = self.arena.get(start..last).ok_or_else(|| {
571            Error::internal(format!(
572                "strings from {start} to {last} are not inside a {} byte arena",
573                self.arena.len()
574            ))
575        })?;
576        let mut from = start;
577        for &end in ends {
578            if end < from {
579                return Err(Error::internal(format!("a string ends at {end} before {from}")));
580            }
581            from = end;
582        }
583        // A byte of the form 10xxxxxx continues a character, so a cut before one splits it.
584        let cut = |at: usize| run.get(at - start).is_some_and(|&byte| byte & 0xC0 == 0x80);
585        if !rudb_common::utf8::valid(run) || ends.iter().any(|&end| cut(end)) {
586            return Err(Error::internal(format!("the bytes from {start} are not valid UTF-8")));
587        }
588        self.views.reserve(ends.len());
589        let mut from = start;
590        for &end in ends {
591            self.views.push(StringView::over(&self.arena[from..end], from as u64));
592            from = end;
593        }
594        Ok(())
595    }
596
597    /// The same seam for a column whose bytes were never claimed to be text.
598    ///
599    /// What a `BLOB` or a `BIT` page is read through. [`Self::push_in_place`] validates because the
600    /// caller is promising a `&str` later and a column that cannot produce one is a wrong answer.
601    /// A blob promises nothing of the sort: its whole point is that the bytes are bytes, so the
602    /// validation there is not a check that has been skipped, it is a check about a claim nobody
603    /// made. [`Self::get`] answers `None` for a row put in this way and [`Self::bytes`] answers it,
604    /// which is the same split [`Self::push_bytes`] already has.
605    ///
606    /// # Errors
607    ///
608    /// If the range is not inside the arena.
609    pub fn push_bytes_in_place(&mut self, offset: usize, len: usize) -> Result<usize> {
610        let end = offset.checked_add(len).ok_or_else(|| {
611            Error::internal(format!(
612                "a value at {offset} of {len} bytes runs off the end of memory"
613            ))
614        })?;
615        let bytes = self.arena.get(offset..end).ok_or_else(|| {
616            Error::internal(format!(
617                "a value at {offset} of {len} bytes is not inside a {} byte arena",
618                self.arena.len()
619            ))
620        })?;
621        self.views.push(StringView::over(bytes, offset as u64));
622        Ok(self.views.len() - 1)
623    }
624
625    /// The bytes the long strings live in.
626    ///
627    /// For a column over a page this is the page, including whatever of it no view points at. The
628    /// offsets in the views are offsets into exactly this, which is what makes them meaningful to a
629    /// reader that put the page here in the first place.
630    #[must_use]
631    pub fn arena(&self) -> &[u8] {
632        &self.arena
633    }
634
635    /// Whether this column's own views read nearly all of its arena.
636    ///
637    /// The question [`Arenas`] asks of every arena it is about to lay, asked of one column on its own.
638    /// It is the difference between a column that was built to hold exactly these strings, where a
639    /// copy of the arena is a copy of the answer, and a cut of somebody else's page, where it drags
640    /// the rest of the page along. See the note on [`Arenas`] for what depends on that.
641    pub(crate) fn mostly_read(&self) -> bool {
642        Arenas::mostly_read(self.arena.len(), live_bytes(self))
643    }
644
645    /// The views and the arena, taken out of the column rather than borrowed from it.
646    ///
647    /// What the string view form of a vector is built from. It takes `self` because the point of
648    /// that form is that the arena moves into an `Arc` and is never copied again, and a method that
649    /// borrowed would have to clone every byte of the arena to hand one over.
650    #[must_use]
651    pub fn into_parts(self) -> (Vec<StringView>, Buffer<u8>) {
652        (self.views.into_vec(), self.arena)
653    }
654
655    /// The bytes at `index`, or `None` past the end.
656    ///
657    /// This is what a comparison, a hash and an equality check all actually want, and it is worth
658    /// having separately from [`Self::get`] because that one validates UTF-8 and they do not need
659    /// it. Everything in a column arrived through [`Self::push`], which takes a `&str`, so the
660    /// bytes are valid either way and the validation is a scan of the payload that changes no
661    /// answer. On a varchar filter it was measured at most of the per row cost.
662    #[must_use]
663    #[inline]
664    pub fn bytes(&self, index: usize) -> Option<&[u8]> {
665        self.views.get(index)?.bytes_in(&self.arena)
666    }
667
668    /// The string at `index`, or `None` past the end.
669    #[must_use]
670    pub fn get(&self, index: usize) -> Option<&str> {
671        // Written from a `&str` into a block that is append only, so the bytes are the same bytes.
672        std::str::from_utf8(self.bytes(index)?).ok()
673    }
674
675    /// Every string in order.
676    pub fn iter(&self) -> impl Iterator<Item = &str> {
677        (0..self.len()).filter_map(|index| self.get(index))
678    }
679
680    /// Total bytes of payload held in the arena, which is what the memory accounting wants.
681    ///
682    /// For a column over a page it is the page and not the part of it any view points at, which is
683    /// the right answer for accounting, because the page is what is resident.
684    #[must_use]
685    pub fn heap_bytes(&self) -> usize {
686        self.arena.len()
687    }
688
689    /// Room for `bytes` of payload, taken in one allocation rather than as the strings arrive.
690    ///
691    /// A builder that knows the total byte count, which a scan reading a page and a gather copying a
692    /// column both do, saves the doubling entirely. Nothing is wrong without it, which is why it is
693    /// a hint and not a constructor argument.
694    ///
695    /// Not for a column built by [`Self::over`] on a page it shares, because reserving writes and a
696    /// write through a shared buffer copies the whole page out first. Such a column is not appended
697    /// to anyway: its strings are already in its arena and [`Self::push_in_place`] records where.
698    pub fn reserve_bytes(&mut self, bytes: usize) {
699        self.arena.reserve(bytes);
700    }
701
702    /// Room for `count` more strings, taken in one allocation rather than as they arrive.
703    ///
704    /// The views and not the payload, which is the half [`Self::reserve_bytes`] does not cover and
705    /// is the only half that matters to a column built by [`Self::over`], whose payload is already
706    /// there. A Parquet page of a hundred thousand strings is one and three quarter megabytes of
707    /// views, and growing that from nothing is twenty allocations and a copy of everything written
708    /// so far each time.
709    pub fn reserve_views(&mut self, count: usize) {
710        self.views.reserve(count);
711    }
712}
713
714/// The arenas a run of string columns share, for laying the columns end to end.
715///
716/// Counted over every column before any of them is laid, because whether an arena is worth
717/// copying whole depends on how much of it all the columns cut from it read, and the first cut
718/// alone reads a sliver. An arena is known by where its bytes are and how many there are.
719///
720/// An address only tells two arenas apart while both of them are alive, so the one thing this must
721/// never do is remember an address that is about to be freed. Only a counted arena is recorded:
722/// counting happens over the columns the caller is holding for the whole of the lay, and two live
723/// allocations cannot sit at the same address, so a key in `placed` always means the arena it was
724/// taken from.
725///
726/// A column built on the way past is the one that is not recorded. Flattening a dictionary, or a run
727/// of views, builds a column that is laid and then dropped before the next one is built, and the
728/// allocator is free to hand the same bytes back for it. Recording one of those meant the next
729/// column to land on the address was given a base worked out for somebody else's bytes, and its
730/// views were shifted by it without its own arena ever being copied in. What came back was strings
731/// of the right length read from the wrong place, so a group key came out as the tail of one value
732/// followed by the head of the next. That is #1413, which took TPC-H q16 at SF1 about half the time
733/// it ran.
734///
735/// Not recorded is not the same as not copied. Such a column is still laid in one copy of its arena
736/// when it is mostly read, which it always is, since a column that was just built holds exactly the
737/// bytes its views point at. Only the sharing goes, and there was never anything to share: each of
738/// those columns has an arena of its own and the next one is a different arena that happens to be at
739/// the same address. Laying them a string at a time instead is what cost 300ms on the six million
740/// SF1 `lineitem` comments, which is the whole reason the copy is here.
741#[derive(Debug, Default)]
742pub(crate) struct Arenas {
743    live: HashMap<(usize, usize), usize>,
744    placed: HashMap<(usize, usize), u64>,
745}
746
747impl Arenas {
748    /// Records the bytes `column` reads out of its arena.
749    pub(crate) fn count(&mut self, column: &StringColumn) {
750        *self.live.entry(Self::key(column)).or_default() += live_bytes(column);
751    }
752
753    /// The bytes laying every counted column takes: an arena that is mostly read is copied whole
754    /// and any other one a string at a time.
755    pub(crate) fn bytes(&self) -> usize {
756        self.live
757            .iter()
758            .map(|(&(_, len), &live)| if Self::mostly_read(len, live) { len } else { live })
759            .sum()
760    }
761
762    pub(crate) fn mostly_read(arena: usize, live: usize) -> bool {
763        arena <= live.saturating_add(live / 4)
764    }
765
766    fn key(column: &StringColumn) -> (usize, usize) {
767        (column.arena.as_ptr() as usize, column.arena.len())
768    }
769
770    /// The bytes of `column`'s arena read by every column counted, for an arena that was counted.
771    ///
772    /// `None` says nobody counted this arena, which is the answer that keeps its address out of
773    /// `placed`. Answering with `column`'s own live bytes instead, which is what this used to do,
774    /// made a column built on the way past look like an arena that is entirely read, so every one of
775    /// them was copied whole and recorded. See the note on the type.
776    fn counted(&self, column: &StringColumn) -> Option<usize> {
777        self.live.get(&Self::key(column)).copied()
778    }
779}
780
781/// The bytes of a column's arena its views point at, counting a byte twice if two views do.
782fn live_bytes(column: &StringColumn) -> usize {
783    column.views.iter().filter(|view| !view.is_inline()).map(StringView::len).sum()
784}
785
786/// Two columns are equal when they hold the same strings in the same order, whatever their arenas
787/// look like.
788///
789/// See the note on [`StringColumn`]. Comparing the views is not enough on its own either, because
790/// two views of the same long string at different offsets in different arenas are different views,
791/// so the comparison is length, then view by view with the payload read for the ones that are not
792/// inline. The prefix inside the view is what makes that cheap: a pair that differs in the first
793/// four bytes or in the length is settled without either arena being touched.
794impl PartialEq for StringColumn {
795    fn eq(&self, other: &Self) -> bool {
796        self.views.len() == other.views.len()
797            && (0..self.views.len()).all(|index| {
798                let mine = self.views[index];
799                let theirs = other.views[index];
800                if mine.definitely_differs(&theirs) {
801                    return false;
802                }
803                if mine.is_inline() {
804                    return mine == theirs;
805                }
806                self.bytes(index) == other.bytes(index)
807            })
808    }
809}
810
811impl<'a> Extend<&'a str> for StringColumn {
812    fn extend<T: IntoIterator<Item = &'a str>>(&mut self, iter: T) {
813        for text in iter {
814            self.push(text);
815        }
816    }
817}
818
819impl<'a> FromIterator<&'a str> for StringColumn {
820    fn from_iter<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
821        let mut column = Self::new();
822        column.extend(iter);
823        column
824    }
825}
826
827#[cfg(test)]
828mod tests {
829    use std::sync::Arc;
830
831    use super::{Arenas, INLINE_LIMIT, StringColumn, StringView};
832    use crate::buffer::Buffer;
833
834    #[test]
835    fn an_order_the_views_decide_is_the_byte_order_and_only_a_shared_prefix_is_left_open() {
836        // Zero bytes, strings that are a prefix of each other, and both sides of the inline limit,
837        // which is where padding could have put two strings in the wrong order.
838        let strings: [&[u8]; 14] = [
839            b"",
840            b"\0",
841            b"a",
842            b"a\0",
843            b"a\0\0",
844            b"ab",
845            b"abc",
846            b"abcd",
847            b"abcd\0",
848            b"abcdefghijkl",
849            b"abcdefghijkl\0",
850            b"abcdefghijklm",
851            b"abce",
852            b"b the long one past twelve bytes",
853        ];
854        let mut column = StringColumn::new();
855        for bytes in strings {
856            column.push_bytes(bytes);
857        }
858        let views = column.views();
859        for (left, a) in strings.iter().enumerate() {
860            for (right, b) in strings.iter().enumerate() {
861                let known = views[left].known_order(&views[right]);
862                let (a_long, b_long) = (a.len() > INLINE_LIMIT, b.len() > INLINE_LIMIT);
863                if let Some(order) = known {
864                    assert_eq!(order, a.cmp(b), "{a:?} against {b:?}");
865                } else {
866                    assert!(a_long || b_long, "{a:?} against {b:?} are both inline");
867                    assert_eq!(a.get(..4), b.get(..4), "{a:?} against {b:?}");
868                }
869            }
870        }
871    }
872
873    /// The seam, used the way layer three will use it. The page arrives whole, each string is
874    /// recorded where it already is, and the arena at the end is the page byte for byte, including
875    /// the header this page has in front of the strings and the bytes between them that belong to
876    /// nothing. A column that had copied would have an arena the size of the strings instead.
877    #[test]
878    fn cuts_of_one_page_lay_the_page_once_and_a_sparse_cut_lays_its_strings() {
879        let strings =
880            ["the first string past the inline limit", "short", "a second string past the limit"];
881        let mut bytes = Vec::new();
882        let mut at = Vec::new();
883        for text in strings {
884            at.push((bytes.len(), text.len()));
885            bytes.extend_from_slice(text.as_bytes());
886        }
887        let page = Arc::new(bytes);
888        let cut = |rows: &[usize]| {
889            let mut column = StringColumn::over(Buffer::from_arc(Arc::clone(&page)));
890            for &row in rows {
891                column.push_in_place(at[row].0, at[row].1).expect("inside the page");
892            }
893            column
894        };
895        let (first, second) = (cut(&[0, 1]), cut(&[2]));
896        let mut arenas = Arenas::default();
897        arenas.count(&first);
898        arenas.count(&second);
899        assert_eq!(arenas.bytes(), page.len(), "what the lay below takes, reserved up front");
900        let mut laid = StringColumn::from_iter(["a string already there, past the limit"]);
901        let before = laid.arena().len();
902        laid.push_column(&first, &mut arenas);
903        laid.push_column(&second, &mut arenas);
904        assert_eq!(laid.arena().len(), before + page.len(), "the page is laid once");
905        let expected =
906            ["a string already there, past the limit", strings[0], strings[1], strings[2]];
907        assert_eq!(laid.iter().collect::<Vec<_>>(), expected);
908
909        let mut sparse = StringColumn::new();
910        let mut alone = Arenas::default();
911        alone.count(&second);
912        assert_eq!(alone.bytes(), strings[2].len(), "a sliver reserves only its own bytes");
913        sparse.push_column(&second, &mut alone);
914        assert_eq!(sparse.arena(), strings[2].as_bytes(), "a sliver of a page is copied alone");
915        assert_eq!(sparse.get(0), Some(strings[2]));
916    }
917
918    /// An arena nobody counted is laid a string at a time and its address is not written down.
919    ///
920    /// The address of a column that was built to be laid and then dropped says nothing about which
921    /// bytes are there once it has been, so remembering it hands the next column to land on it a
922    /// base belonging to somebody else. #1413.
923    #[test]
924    fn an_arena_that_nobody_counted_is_not_remembered_by_its_address() {
925        let text = "a string built on the way past, well over the inline limit";
926        let built = StringColumn::from_iter([text]);
927        let mut laid = StringColumn::new();
928        let mut arenas = Arenas::default();
929        laid.push_column(&built, &mut arenas);
930        assert!(arenas.placed.is_empty(), "an uncounted arena was recorded by its address");
931        assert_eq!(laid.get(0), Some(text));
932    }
933
934    /// Not being recorded does not mean being laid a string at a time.
935    ///
936    /// Two views over the same bytes is what tells the two apart: one copy of the arena lays those
937    /// bytes once and a string at a time lays them twice. The column here is one nobody counted, so
938    /// it is the case #1413 made suspicious, and it still gets its one copy.
939    #[test]
940    fn an_arena_that_nobody_counted_is_still_laid_in_one_copy() {
941        let text = "a string two views point at, well over the inline limit";
942        let page = Arc::new(text.as_bytes().to_vec());
943        let mut twice = StringColumn::over(Buffer::from_arc(Arc::clone(&page)));
944        twice.push_in_place(0, text.len()).expect("inside the page");
945        twice.push_in_place(0, text.len()).expect("inside the page");
946        let mut laid = StringColumn::new();
947        let mut arenas = Arenas::default();
948        laid.push_column(&twice, &mut arenas);
949        assert!(arenas.placed.is_empty(), "an uncounted arena was recorded by its address");
950        assert_eq!(laid.arena().len(), text.len(), "the arena was laid once and not once a view");
951        assert_eq!(laid.get(0), Some(text));
952        assert_eq!(laid.get(1), Some(text));
953    }
954
955    /// And an arena that was counted still is, so the lay of a page is still one copy of the page.
956    ///
957    /// The other half of the rule above. Without this the fix for #1413 would read as though the
958    /// whole point of [`Arenas`] had been switched off.
959    #[test]
960    fn an_arena_that_was_counted_is_still_copied_whole() {
961        let text = "a string on a page the caller holds, well over the inline limit";
962        let page = StringColumn::from_iter([text]);
963        let mut laid = StringColumn::new();
964        let mut arenas = Arenas::default();
965        arenas.count(&page);
966        laid.push_column(&page, &mut arenas);
967        assert_eq!(arenas.placed.len(), 1, "a counted arena is copied whole and written down");
968        assert_eq!(laid.get(0), Some(text));
969    }
970
971    #[test]
972    fn a_column_over_a_page_records_the_strings_without_moving_them() {
973        let page =
974            b"HEADER..a string well past the inline limit!!a second one past the limit".to_vec();
975        let mut column = StringColumn::over(Buffer::from_vec(page.clone()));
976        assert_eq!(column.push_in_place(8, 37).expect("inside the page"), 0);
977        assert_eq!(column.push_in_place(45, 27).expect("inside the page"), 1);
978        assert_eq!(column.get(0), Some("a string well past the inline limit!!"));
979        assert_eq!(column.get(1), Some("a second one past the limit"));
980        assert_eq!(column.arena(), page.as_slice());
981        assert_eq!(column.heap_bytes(), page.len());
982        assert_eq!(column.len(), 2);
983    }
984
985    /// A page checked for text once answers what a check per string answers: it takes the strings
986    /// of good text, and refuses bytes that are not text and a cut through the middle of a
987    /// character, which would leave both halves not text though the whole run is.
988    #[test]
989    fn a_run_of_strings_is_checked_for_text_once_and_as_strictly() {
990        let page = "ab\u{e9}t\u{e9} and a string well past the inline limit".as_bytes().to_vec();
991        let mut column = StringColumn::over(Buffer::from_vec(page.clone()));
992        column.push_run_in_place(0, &[2, 2, 7, page.len()]).expect("text");
993        assert_eq!(column.get(0), Some("ab"));
994        assert_eq!(column.get(1), Some(""));
995        assert_eq!(column.get(2), Some("\u{e9}t\u{e9}"));
996        assert_eq!(column.get(3), Some(" and a string well past the inline limit"));
997
998        let mut split = StringColumn::over(Buffer::from_vec(page.clone()));
999        assert!(split.push_run_in_place(0, &[3, page.len()]).is_err(), "a cut inside a character");
1000        assert_eq!(split.len(), 0);
1001        let mut bad = StringColumn::over(Buffer::from_vec(vec![b'a', 0xff, b'b']));
1002        assert!(bad.push_run_in_place(0, &[1, 3]).is_err(), "bytes that are not text");
1003        let mut back = StringColumn::over(Buffer::from_vec(page.clone()));
1004        assert!(back.push_run_in_place(0, &[5, 4, page.len()]).is_err(), "an end before its start");
1005        let mut past = StringColumn::over(Buffer::from_vec(page));
1006        assert!(past.push_run_in_place(0, &[4, 400]).is_err(), "an end past the page");
1007    }
1008
1009    /// Copying between two columns, which is what a gather and a slice over a string column are.
1010    /// A column built over a page has an arena full of bytes no view points at, and the copy has to
1011    /// take the strings rather than the arena, so the destination holds the strings and nothing
1012    /// else. The last case is the row that resolved to nowhere, which is an empty string here and a
1013    /// null in the validity mask beside it.
1014    #[test]
1015    fn copying_from_another_column_takes_the_strings_and_not_the_page_they_were_in() {
1016        let page = b"HEADER..a string well past the inline limit!!short".to_vec();
1017        let mut source = StringColumn::over(Buffer::from_vec(page.clone()));
1018        source.push_in_place(8, 37).expect("inside the page");
1019        source.push_in_place(45, 5).expect("inside the page");
1020
1021        let mut out = StringColumn::new();
1022        assert_eq!(out.push_from(&source, 1), 0);
1023        assert_eq!(out.push_from(&source, 0), 1);
1024        assert_eq!(out.push_from(&source, 9), 2, "a position that is not there");
1025
1026        assert_eq!(out.get(0), Some("short"));
1027        assert_eq!(out.get(1), Some("a string well past the inline limit!!"));
1028        assert_eq!(out.get(2), Some(""));
1029        assert!(out.views()[0].is_inline(), "a short string stays in its view");
1030        assert!(!out.views()[1].is_inline());
1031        assert_eq!(out.views()[1].prefix(), *b"a st", "the prefix is the string's own");
1032        assert_eq!(
1033            out.arena(),
1034            b"a string well past the inline limit!!",
1035            "the arena is the long strings and not the page"
1036        );
1037    }
1038
1039    /// Bytes that are not text, which is what a `BLOB` holds. Both sides of the inline limit,
1040    /// because a short one lives in its view and a long one lives in the arena and the byte that is
1041    /// not a character has to survive either way. Reading them back as text is `None` and reading
1042    /// them back as bytes is what went in.
1043    #[test]
1044    fn a_column_holds_bytes_that_are_not_a_string() {
1045        let long = b"\xff\xfe and a good deal more than twelve bytes of it";
1046        let mut column = StringColumn::new();
1047        assert_eq!(column.push_bytes(b"a\xffb"), 0);
1048        assert_eq!(column.push_bytes(long), 1);
1049        assert_eq!(column.push_bytes(b""), 2);
1050
1051        assert_eq!(column.bytes(0), Some(b"a\xffb".as_slice()));
1052        assert_eq!(column.bytes(1), Some(long.as_slice()));
1053        assert_eq!(column.bytes(2), Some(b"".as_slice()));
1054        assert_eq!(column.get(0), None, "a stray 0xff is not a character");
1055        assert_eq!(column.get(1), None);
1056        assert!(column.views()[0].is_inline());
1057        assert!(!column.views()[1].is_inline());
1058        assert_eq!(column.arena(), long, "only the long one needed the arena");
1059    }
1060
1061    /// A copy of a copy, because the second one reads its bytes out of an arena the first one wrote
1062    /// rather than out of a page, and an offset written in one and read in the other is the way
1063    /// this goes wrong.
1064    #[test]
1065    fn copying_from_a_column_that_was_itself_copied_reads_the_same_strings() {
1066        let mut first = StringColumn::new();
1067        for text in ["a string well past the inline limit", "short", "another long one past it"] {
1068            first.push(text);
1069        }
1070        let mut second = StringColumn::new();
1071        for index in (0..first.len()).rev() {
1072            second.push_from(&first, index);
1073        }
1074        let mut third = StringColumn::new();
1075        for index in 0..second.len() {
1076            third.push_from(&second, index);
1077        }
1078        assert_eq!(
1079            third.iter().collect::<Vec<_>>(),
1080            ["another long one past it", "short", "a string well past the inline limit"]
1081        );
1082    }
1083
1084    /// A string short enough to live inside its view is copied into the view, which is twelve bytes
1085    /// and is what lets it be read without the arena. The page is still the arena and is still
1086    /// untouched, so a page of short strings costs the views and nothing else.
1087    #[test]
1088    fn a_short_string_in_a_page_is_copied_into_its_view() {
1089        let mut column = StringColumn::over(Buffer::from_vec(b"one.two".to_vec()));
1090        column.push_in_place(0, 3).expect("inside the page");
1091        column.push_in_place(4, 3).expect("inside the page");
1092        assert!(column.views()[0].is_inline());
1093        assert_eq!(column.get(0), Some("one"));
1094        assert_eq!(column.get(1), Some("two"));
1095        assert_eq!(column.arena(), b"one.two");
1096    }
1097
1098    /// The two ways a caller can be wrong about a page, both of them answered before anything is
1099    /// recorded rather than at the point somebody reads the string back and finds nothing there.
1100    #[test]
1101    fn a_range_outside_the_page_or_bytes_that_are_not_text_are_refused() {
1102        let mut column = StringColumn::over(Buffer::from_vec(vec![0xff, 0xfe, 0xfd]));
1103        assert!(column.push_in_place(2, 4).is_err());
1104        assert!(column.push_in_place(usize::MAX, 1).is_err());
1105        assert!(column.push_in_place(0, 3).is_err());
1106        assert_eq!(column.len(), 0);
1107
1108        // The ASCII check in front of the validator answers whole words at a time, so the bad byte
1109        // is put past the first word and past the inline limit as well, where a check that only
1110        // looked at the head or only at the payload in the view would miss it.
1111        let mut page = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec();
1112        page.push(0x80);
1113        let len = page.len();
1114        let mut column = StringColumn::over(Buffer::from_vec(page));
1115        assert!(column.push_in_place(0, len).is_err());
1116        assert!(column.push_in_place(0, len - 1).is_ok());
1117
1118        // Text that is not ASCII and is valid goes through, which is the other half of the check:
1119        // the fast path decides nothing on its own, it only decides who has to look.
1120        let page = "søk på nettet".as_bytes().to_vec();
1121        let len = page.len();
1122        let mut column = StringColumn::over(Buffer::from_vec(page));
1123        column.push_in_place(0, len).expect("valid text that is not ASCII");
1124        assert_eq!(column.get(0), Some("søk på nettet"));
1125    }
1126
1127    /// What the seam does to equality. The same two strings, one column built by copying them in
1128    /// and one built over a page that has them in the other order with a gap in the middle, and the
1129    /// two arenas have nothing in common. Equality is the strings, so the columns are equal.
1130    #[test]
1131    fn the_same_strings_over_different_arenas_are_the_same_column() {
1132        let copied: StringColumn =
1133            ["the first string past the limit", "the second string past the limit"]
1134                .into_iter()
1135                .collect();
1136        let page =
1137            b"gap!the second string past the limit....the first string past the limit".to_vec();
1138        let mut over = StringColumn::over(Buffer::from_vec(page));
1139        over.push_in_place(40, 31).expect("inside the page");
1140        over.push_in_place(4, 32).expect("inside the page");
1141        assert_ne!(copied.arena(), over.arena());
1142        assert_eq!(copied, over);
1143
1144        let mut different: StringColumn = copied.clone();
1145        different.push("a third one past the inline limit");
1146        assert_ne!(copied, different);
1147    }
1148
1149    #[test]
1150    fn a_view_is_sixteen_bytes_and_stays_sixteen_bytes() {
1151        // The number the whole design is built around. A vector of 1024 strings is 16 KiB of
1152        // views, which is the budget spec/07-execution.md section 7.1 spends on purpose.
1153        assert_eq!(size_of::<StringView>(), 16);
1154        assert_eq!(align_of::<StringView>(), 4);
1155    }
1156
1157    #[test]
1158    fn twelve_bytes_is_inline_and_thirteen_is_not() {
1159        let mut column = StringColumn::new();
1160        column.push("123456789012");
1161        column.push("1234567890123");
1162        assert!(column.views()[0].is_inline());
1163        assert!(!column.views()[1].is_inline());
1164        assert_eq!(column.get(0), Some("123456789012"));
1165        assert_eq!(column.get(1), Some("1234567890123"));
1166        assert_eq!(INLINE_LIMIT, 12);
1167    }
1168
1169    #[test]
1170    fn a_prefix_answers_the_comparison_without_reading_the_payload() {
1171        let mut column = StringColumn::new();
1172        column.push("https://example.com/a");
1173        column.push("https://example.com/b");
1174        column.push("mailto:someone@example.com");
1175        let views = column.views();
1176        // Same prefix, same length: the payloads have to be read. This is the case the prefix
1177        // cannot help with, and on a URL column it is the common case, which is why the
1178        // dictionary work at M3 matters more than this does.
1179        assert!(!views[0].definitely_differs(&views[1]));
1180        // Different prefix: answered from the view.
1181        assert!(views[0].definitely_differs(&views[2]));
1182    }
1183
1184    /// A string of any size goes in whole, with the short ones on either side of it still reading
1185    /// back. The old layout had a size at which a string stopped fitting a block and got one of its
1186    /// own, and one arena has no such size, so the case worth keeping is the one that used to be
1187    /// special rather than the branch that used to handle it.
1188    #[test]
1189    fn a_string_far_larger_than_any_block_would_have_been_goes_in_whole() {
1190        let long = "x".repeat(40 * 1024);
1191        let mut column = StringColumn::new();
1192        column.push("short");
1193        column.push(&long);
1194        column.push("also short");
1195        assert_eq!(column.get(1), Some(long.as_str()));
1196        assert_eq!(column.get(2), Some("also short"));
1197        assert_eq!(column.heap_bytes(), long.len());
1198    }
1199
1200    /// The property the whole arena rests on. Two thousand strings is tens of reallocations, and
1201    /// every one of them moves the bytes to a new address while the offsets recorded in the views
1202    /// before it stay exactly as they were. A view holding a pointer would be reading freed memory
1203    /// by the end of this test.
1204    #[test]
1205    fn the_arena_moving_underneath_does_not_move_what_the_views_point_at() {
1206        let mut column = StringColumn::new();
1207        let strings: Vec<String> =
1208            (0..2000).map(|i| format!("value number {i} padded out")).collect();
1209        for text in &strings {
1210            column.push(text);
1211        }
1212        for (index, text) in strings.iter().enumerate() {
1213            assert_eq!(column.get(index), Some(text.as_str()), "at {index}");
1214        }
1215        assert_eq!(column.len(), 2000);
1216        assert_eq!(column.iter().count(), 2000);
1217    }
1218
1219    #[test]
1220    fn reserving_bytes_changes_nothing_but_where_the_allocation_happens() {
1221        let mut column = StringColumn::with_capacity(3);
1222        column.reserve_bytes(128);
1223        for text in ["a string past the limit", "another one past it", "short"] {
1224            column.push(text);
1225        }
1226        assert_eq!(column.get(0), Some("a string past the limit"));
1227        assert_eq!(column.get(1), Some("another one past it"));
1228        assert_eq!(column.get(2), Some("short"));
1229        assert_eq!(column.heap_bytes(), 42);
1230    }
1231
1232    #[test]
1233    fn the_empty_string_is_inline_and_reads_back_empty() {
1234        let mut column = StringColumn::new();
1235        column.push("");
1236        assert_eq!(column.get(0), Some(""));
1237        assert!(column.views()[0].is_empty());
1238        assert_eq!(column.heap_bytes(), 0);
1239    }
1240
1241    #[test]
1242    fn multibyte_text_survives_the_inline_boundary() {
1243        // The boundary is bytes and not characters, so a four byte emoji is what decides whether
1244        // a three character string is inline.
1245        let mut column = StringColumn::new();
1246        column.push("héllo wörld");
1247        column.push("🦀🦀🦀🦀");
1248        assert_eq!(column.get(0), Some("héllo wörld"));
1249        assert_eq!(column.get(1), Some("🦀🦀🦀🦀"));
1250        assert!(!column.views()[1].is_inline());
1251    }
1252
1253    #[test]
1254    fn reading_past_the_end_is_none_rather_than_a_panic() {
1255        let column: StringColumn = ["a", "b"].into_iter().collect();
1256        assert_eq!(column.get(2), None);
1257        assert_eq!(column.len(), 2);
1258    }
1259
1260    /// The bytes and the string have to be the same string on both sides of the inline boundary
1261    /// and on multibyte text, because the comparison kernels read the bytes and everything else
1262    /// reads the string, and a disagreement between them would be a filter that matched a row the
1263    /// projection then printed differently.
1264    #[test]
1265    fn the_bytes_and_the_string_are_the_same_string() {
1266        let long = "x".repeat(9000);
1267        let words = ["", "a", "twelve bytes", "thirteen bytes", "π is two bytes", &long];
1268        let column: StringColumn = words.into_iter().collect();
1269        for (index, text) in words.iter().enumerate() {
1270            assert_eq!(column.bytes(index), Some(text.as_bytes()), "at {index}");
1271            assert_eq!(column.get(index), Some(*text), "at {index}");
1272        }
1273        assert_eq!(column.bytes(words.len()), None);
1274    }
1275}