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}