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