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}