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