rudb_vector/chunk.rs
1//! A batch of columns, which is the unit every operator passes to the next one.
2//!
3//! A chunk is some vectors of the same length plus that length. It is not a table and it is not a
4//! result set: it is at most [`VECTOR_SIZE`] rows, because the whole point of the number in
5//! `spec/04-architecture.md` section 4.3 is that a batch of this width stays in L1 while an
6//! operator works on it, and a type that can hold ten times that many rows is a type that lets an
7//! operator quietly stop being vectorized.
8//!
9//! The row count is stored rather than derived, which matters for the one case that looks like a
10//! mistake and is not. `SELECT count(*) FROM t` scans no columns, so the chunk the scan produces
11//! has no vectors in it and still has to say how many rows went past, and a chunk that derived its
12//! length from its first column would say zero.
13
14use rudb_common::{Error, LogicalType, Result, Value};
15
16use crate::selection::Selection;
17use crate::vector::{VECTOR_SIZE, Vector};
18
19/// A batch of columns of equal length.
20#[derive(Debug, Clone, PartialEq)]
21pub struct Chunk {
22 columns: Vec<Vector>,
23 rows: usize,
24}
25
26/// The scheduler's half of the data plane contract, imposed now rather than at layer eight.
27///
28/// `spec/engine/03-data-plane.md` section 3.10. A chunk is what one thread hands another, so a chunk
29/// is `Send`, and that is not free: it rules out an `Rc` anywhere in a vector, it rules out a borrow
30/// of thread local state, and it is what the pin handle in [`Buffer`](crate::Buffer) is protecting
31/// against a lifetime parameter.
32///
33/// It is a static assertion rather than a comment because the failure mode is quiet. Every one of
34/// those mistakes compiles perfectly well on its own and is only a problem the day a chunk is put in
35/// a queue, which is eight layers from here and far too late to be told. This way the build breaks
36/// on the commit that introduces it.
37const _: () = {
38 const fn assert_send<T: Send>() {}
39 assert_send::<Chunk>();
40};
41
42impl Chunk {
43 /// A chunk of `columns`, taking the row count from the first of them.
44 ///
45 /// # Errors
46 ///
47 /// If the columns are not all the same length, or if there are more rows than [`VECTOR_SIZE`].
48 pub fn new(columns: Vec<Vector>) -> Result<Self> {
49 let rows = columns.first().map_or(0, Vector::len);
50 Self::with_rows(columns, rows)
51 }
52
53 /// A chunk of `columns` that is `rows` long, for the case where there are no columns to take
54 /// the count from.
55 ///
56 /// # Errors
57 ///
58 /// If any column is not `rows` long, or if `rows` is more than [`VECTOR_SIZE`].
59 pub fn with_rows(columns: Vec<Vector>, rows: usize) -> Result<Self> {
60 if rows > VECTOR_SIZE {
61 return Err(Error::internal(format!(
62 "a chunk of {rows} rows is longer than the {VECTOR_SIZE} row vector"
63 )));
64 }
65 for (index, column) in columns.iter().enumerate() {
66 if column.len() != rows {
67 return Err(Error::internal(format!(
68 "column {index} of a chunk is {} rows and the chunk is {rows}",
69 column.len()
70 )));
71 }
72 }
73 Ok(Self { columns, rows })
74 }
75
76 /// A chunk of the given types with no rows in it.
77 ///
78 /// What a scan of an empty table returns and what an operator returns when it is done. The
79 /// types are kept, because a consumer asks a chunk what its columns are before it asks whether
80 /// there are any.
81 #[must_use]
82 pub fn empty(types: &[LogicalType]) -> Self {
83 let columns =
84 types.iter().map(|ty| Vector::constant(ty.clone(), Value::Null, 0)).collect::<Vec<_>>();
85 Self { columns, rows: 0 }
86 }
87
88 /// The columns.
89 #[must_use]
90 pub fn columns(&self) -> &[Vector] {
91 &self.columns
92 }
93
94 /// One column.
95 ///
96 /// # Errors
97 ///
98 /// If there is no column at `index`.
99 pub fn column(&self, index: usize) -> Result<&Vector> {
100 self.columns.get(index).ok_or_else(|| {
101 Error::internal(format!(
102 "column {index} of a chunk that has {} columns",
103 self.columns.len()
104 ))
105 })
106 }
107
108 /// The columns, given up.
109 #[must_use]
110 pub fn into_columns(self) -> Vec<Vector> {
111 self.columns
112 }
113
114 /// How many columns.
115 #[must_use]
116 pub fn width(&self) -> usize {
117 self.columns.len()
118 }
119
120 /// How many rows.
121 #[must_use]
122 pub fn len(&self) -> usize {
123 self.rows
124 }
125
126 /// Whether there are no rows.
127 #[must_use]
128 pub fn is_empty(&self) -> bool {
129 self.rows == 0
130 }
131
132 /// How many bytes of memory this chunk is holding.
133 ///
134 /// What the memory limit charges for a chunk somebody kept. A chunk handed from one operator to
135 /// the next and dropped is not charged at all, because charging it would count the same
136 /// megabyte once per level of the tree, and the levels of the tree are not where a query runs
137 /// out of memory.
138 ///
139 /// Every size in this workspace counts the thing itself as well as what it owns, so a column's
140 /// own bytes are already in its own number and are not added again here.
141 #[must_use]
142 pub fn footprint(&self) -> usize {
143 size_of::<Self>() + self.columns.iter().map(Vector::footprint).sum::<usize>()
144 }
145
146 /// This chunk with every column's payload held as a page, so that a copy of it is free.
147 ///
148 /// For a chunk that is going to be stored and handed out many times, which is what an in memory
149 /// table's chunks are. See [`Vector::into_pages`] for what it does to each form.
150 #[must_use]
151 pub fn into_pages(self) -> Self {
152 Self {
153 columns: self.columns.into_iter().map(Vector::into_pages).collect(),
154 rows: self.rows,
155 }
156 }
157
158 /// The type of each column.
159 #[must_use]
160 pub fn types(&self) -> Vec<LogicalType> {
161 self.columns.iter().map(|column| column.logical_type().clone()).collect()
162 }
163
164 /// Validate every storage-backed value reachable from this chunk.
165 pub fn validate_external(&self) -> Result<()> {
166 self.columns.iter().try_for_each(Vector::validate_external)
167 }
168
169 /// The value at a row and a column, or null if either is past the end.
170 ///
171 /// The slow path, same as [`Vector::value_at`]. It is what a result set is read out with and
172 /// what a test asserts on.
173 #[must_use]
174 pub fn value_at(&self, row: usize, column: usize) -> Value {
175 match self.columns.get(column) {
176 Some(held) => held.value_at(row),
177 None => Value::Null,
178 }
179 }
180
181 /// The value at a row and column, preserving storage read and validation failures.
182 pub fn try_value_at(&self, row: usize, column: usize) -> Result<Value> {
183 match self.columns.get(column) {
184 Some(held) => held.try_value_at(row),
185 None => Ok(Value::Null),
186 }
187 }
188
189 /// One row, left to right.
190 pub fn row(&self, row: usize) -> impl Iterator<Item = Value> + '_ {
191 self.columns.iter().map(move |column| column.value_at(row))
192 }
193
194 /// The rows a selection kept, without moving any of the values.
195 ///
196 /// Every column becomes a dictionary vector whose codes are the selection, which is the form
197 /// `spec/07-execution.md` section 7.1 asks a filter to produce rather than compacting. It takes
198 /// the chunk by value because that is what makes it free: the payload is moved into the new
199 /// vector rather than copied, so a filter that keeps one row in a thousand still costs the
200 /// selection and nothing else.
201 ///
202 /// A column that is already a stable dictionary is the one exception, and it composes the two
203 /// levels of codes instead of stacking them. Stacking is just as cheap here and it hides the
204 /// thing that matters: a stable dictionary is a promise that codes from separate chunks name the
205 /// same values, and the aggregate, the group key store and the string kernels all read that
206 /// promise off the outermost body. Wrapping it in a second dictionary breaks the promise, so a
207 /// `GROUP BY SearchPhrase` behind a `WHERE SearchPhrase <> ''` fell off the code path and hashed
208 /// strings instead, which measured at 30 ms of processor time against 4 ms for the same group by
209 /// with nothing in front of it. Composing costs one lookup per kept row and keeps the promise.
210 ///
211 /// # Errors
212 ///
213 /// If the selection points past the end of the chunk.
214 pub fn select(self, selection: &Selection) -> Result<Self> {
215 if let Some(bad) = selection.iter().find(|&index| index >= self.rows) {
216 return Err(Error::internal(format!(
217 "a selection keeps row {bad} of a chunk that has {} rows",
218 self.rows
219 )));
220 }
221 let rows = selection.len();
222 let codes = selection.indices();
223 let mut columns = Vec::with_capacity(self.columns.len());
224 for column in self.columns {
225 if column.stable_dictionary_parts().is_some() {
226 columns.push(column.gather(codes)?);
227 } else {
228 columns.push(Vector::dictionary(codes.to_vec(), column)?);
229 }
230 }
231 Self::with_rows(columns, rows)
232 }
233
234 /// The rows a selection kept, copied, so that nothing downstream reads through an indirection.
235 ///
236 /// The copying counterpart to [`Self::select`], and the two exist because neither one is right
237 /// twice. Which one to call is measured rather than argued, and the measurement says something
238 /// other than what the argument does, so here is both.
239 ///
240 /// The argument is that selecting pays nothing now and one redirection on every later read of
241 /// every kept row, while compacting pays a copy now and nothing afterwards, so the deciding
242 /// variable is selectivity: keep a few rows and select, keep most of them and compact. The
243 /// measurement says the deciding variable is not selectivity at all, it is how many times the
244 /// rows are read again afterwards, and selectivity barely moves the line. On server3, over a
245 /// chunk of two integer columns, compacting loses to selecting at every selectivity from one
246 /// percent to a hundred when there is one later pass over the kept rows, and beats it at every
247 /// selectivity from one percent to a hundred when there are sixteen. With four later passes the
248 /// two are within a few percent of each other everywhere. Put a varchar column in the chunk and
249 /// compaction loses almost everywhere, because copying string bytes is most of what it costs and
250 /// the dictionary it avoids is most of what it saves.
251 ///
252 /// Which is why nothing in the streaming pipeline calls this yet. A filter today feeds an
253 /// aggregate or a projection and that is one pass or two, and end to end on two million rows
254 /// `SELECT sum(a), sum(b), count(*) FROM t WHERE a > ?` measures the same either way at one
255 /// percent selectivity and fifty percent slower compacting at fifty percent selectivity. The
256 /// operators that will want this are the ones that hold chunks rather than pass them on, the
257 /// hash join build side and the sort, because a chunk that is kept alive as a selection keeps
258 /// the whole chunk it was selected from alive with it, and that is a hundred to one on memory
259 /// rather than a few percent on time.
260 ///
261 /// Takes the chunk by value like [`Self::select`] does, even though the payload is copied rather
262 /// than moved, because a caller that still wanted the original after compacting it would be
263 /// holding both copies and should say so.
264 ///
265 /// # Errors
266 ///
267 /// If the selection points past the end of the chunk, or if a column has a type there is no
268 /// vector for, which today means `ARRAY` and `UNION`.
269 pub fn compact(self, selection: &Selection) -> Result<Self> {
270 if let Some(bad) = selection.iter().find(|&index| index >= self.rows) {
271 return Err(Error::internal(format!(
272 "a selection keeps row {bad} of a chunk that has {} rows",
273 self.rows
274 )));
275 }
276 let rows = selection.len();
277 let indices = selection.indices();
278 let mut columns = Vec::with_capacity(self.columns.len());
279 for column in &self.columns {
280 columns.push(column.gather(indices)?);
281 }
282 Self::with_rows(columns, rows)
283 }
284
285 /// The columns at the given positions, in that order.
286 ///
287 /// A position may appear twice, which is what `SELECT x, x FROM t` is, and the second one costs
288 /// a copy. Every other position is moved.
289 ///
290 /// # Errors
291 ///
292 /// If a position is past the end of the chunk.
293 pub fn project(self, positions: &[usize]) -> Result<Self> {
294 let width = self.columns.len();
295 if let Some(&bad) = positions.iter().find(|&&position| position >= width) {
296 return Err(Error::internal(format!(
297 "column {bad} of a chunk that has {width} columns"
298 )));
299 }
300 let rows = self.rows;
301 let mut sources: Vec<Option<Vector>> = self.columns.into_iter().map(Some).collect();
302 let mut columns = Vec::with_capacity(positions.len());
303 for (at, &position) in positions.iter().enumerate() {
304 let last_use = !positions[at + 1..].contains(&position);
305 let taken = if last_use { sources[position].take() } else { sources[position].clone() };
306 match taken {
307 Some(column) => columns.push(column),
308 // Only reachable if the last-use bookkeeping above is wrong, since a position is
309 // taken on its last appearance and cloned on every earlier one.
310 None => {
311 return Err(Error::internal(format!("column {position} was taken twice")));
312 }
313 }
314 }
315 Self::with_rows(columns, rows)
316 }
317
318 /// The same rows with every column in flat form.
319 ///
320 /// Costs a copy per column that was not already flat. It is here for the result set at the top
321 /// of a query, where the dictionary vectors a filter left behind would otherwise be handed to a
322 /// caller who has to understand them.
323 ///
324 /// # Errors
325 ///
326 /// If a column has a type there is no vector for, which today means `ARRAY` and `UNION`. A `LIST`
327 /// and a `MAP` flatten to themselves and a `STRUCT` to a struct of flattened fields, since none of
328 /// the three has a data slice for a caller to read and there is nothing flatter to become.
329 pub fn flatten(&self) -> Result<Self> {
330 let mut columns = Vec::with_capacity(self.columns.len());
331 for column in &self.columns {
332 // flatten: this is the chunk wide version of the vector call and it exists so that the
333 // one caller at the top of a query can say it once instead of per column. Whether the
334 // copy is deserved is decided where this is called from, which today is one line in
335 // `rudb::database`, and that line says why.
336 columns.push(column.flatten()?);
337 }
338 Self::with_rows(columns, self.rows)
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use std::sync::Arc;
345
346 use rudb_common::LogicalType;
347
348 use super::*;
349 use crate::vector::{Data, Form};
350
351 fn integers(values: &[i32]) -> Vector {
352 Vector::flat(LogicalType::Integer, Data::Int32(values.to_vec().into()))
353 .expect("integers are an i32 layout")
354 }
355
356 #[test]
357 fn a_chunk_takes_its_length_from_its_columns() {
358 let chunk = Chunk::new(vec![integers(&[1, 2, 3]), integers(&[4, 5, 6])])
359 .expect("two columns of three");
360 assert_eq!(chunk.len(), 3);
361 assert_eq!(chunk.width(), 2);
362 assert_eq!(chunk.value_at(2, 1), Value::Integer(6));
363 }
364
365 #[test]
366 fn a_ragged_chunk_is_caught() {
367 let error = Chunk::new(vec![integers(&[1, 2, 3]), integers(&[4])])
368 .expect_err("a chunk is not ragged");
369 assert!(error.message().contains("column 1"), "{error}");
370 }
371
372 /// `SELECT count(*) FROM t` scans no columns and the row count still has to survive, which is
373 /// the reason the length is a field rather than the first column's length.
374 #[test]
375 fn a_chunk_with_no_columns_can_still_have_rows() {
376 let chunk = Chunk::with_rows(Vec::new(), 900).expect("no columns and nine hundred rows");
377 assert_eq!(chunk.len(), 900);
378 assert_eq!(chunk.width(), 0);
379 assert!(!chunk.is_empty(), "nine hundred rows is not empty");
380 }
381
382 #[test]
383 fn a_chunk_longer_than_a_vector_is_caught() {
384 let error = Chunk::with_rows(Vec::new(), VECTOR_SIZE + 1).expect_err("too long");
385 assert!(error.message().contains("longer than"), "{error}");
386 }
387
388 #[test]
389 fn an_empty_chunk_keeps_its_types() {
390 let chunk = Chunk::empty(&[LogicalType::Integer, LogicalType::Varchar]);
391 assert_eq!(chunk.len(), 0);
392 assert_eq!(chunk.types(), vec![LogicalType::Integer, LogicalType::Varchar]);
393 }
394
395 #[test]
396 fn selecting_keeps_the_rows_it_selected_and_no_others() {
397 let chunk = Chunk::new(vec![integers(&[10, 20, 30, 40]), integers(&[1, 2, 3, 4])])
398 .expect("four rows");
399 let kept = Selection::from_predicate(4, |index| index % 2 == 1);
400 let chunk = chunk.select(&kept).expect("rows one and three exist");
401 assert_eq!(chunk.len(), 2);
402 assert_eq!(chunk.row(0).collect::<Vec<_>>(), vec![Value::Integer(20), Value::Integer(2)]);
403 assert_eq!(chunk.row(1).collect::<Vec<_>>(), vec![Value::Integer(40), Value::Integer(4)]);
404 }
405
406 /// The reason `select` takes the chunk by value. If it copied the payload then a filter would
407 /// cost the same as a compaction and the selection would be a pure loss.
408 #[test]
409 fn selecting_leaves_the_values_where_they_were() {
410 let chunk = Chunk::new(vec![integers(&[10, 20, 30, 40])]).expect("four rows");
411 let kept = Selection::from_predicate(4, |index| index == 0);
412 let chunk = chunk.select(&kept).expect("row zero exists");
413 assert_eq!(chunk.column(0).expect("one column").form(), Form::Dictionary);
414 }
415
416 /// The promise a stable dictionary makes is about the outermost body, so a filter in front of a
417 /// group by has to compose the codes rather than stack a second dictionary on top of them.
418 #[test]
419 fn selecting_a_stable_dictionary_composes_the_codes_instead_of_stacking_them() {
420 let values = Arc::new(integers(&[10, 20, 30]));
421 let column = Vector::stable_dictionary(vec![2, 0, 1, 2], values).expect("three codes");
422 let chunk = Chunk::new(vec![column]).expect("four rows");
423 let kept = Selection::from_predicate(4, |index| index % 2 == 1);
424 let chunk = chunk.select(&kept).expect("rows one and three exist");
425 let column = chunk.column(0).expect("one column");
426 let (codes, values) = column.stable_dictionary_parts().expect("still a stable dictionary");
427 assert_eq!(codes, [0, 2]);
428 assert_eq!(values.len(), 3);
429 assert_eq!(column.value_at(0), Value::Integer(10));
430 assert_eq!(column.value_at(1), Value::Integer(30));
431 }
432
433 #[test]
434 fn a_selection_past_the_end_is_caught() {
435 let chunk = Chunk::new(vec![integers(&[1, 2])]).expect("two rows");
436 let mut kept = Selection::empty();
437 kept.push(7);
438 let error = chunk.select(&kept).expect_err("row seven does not exist");
439 assert!(error.message().contains("row 7"), "{error}");
440 }
441
442 /// The two halves of section 7.1's decision have to answer the same question the same way, or
443 /// the threshold between them is a place where a query changes its answer.
444 #[test]
445 fn compacting_keeps_the_same_rows_selecting_does_and_leaves_no_indirection() {
446 let chunk = Chunk::new(vec![integers(&[10, 20, 30, 40]), integers(&[1, 2, 3, 4])])
447 .expect("four rows");
448 let kept = Selection::from_predicate(4, |index| index % 2 == 1);
449 let selected = chunk.clone().select(&kept).expect("rows one and three exist");
450 let compacted = chunk.compact(&kept).expect("rows one and three exist");
451 assert_eq!(compacted.len(), selected.len());
452 for row in 0..compacted.len() {
453 assert_eq!(
454 compacted.row(row).collect::<Vec<_>>(),
455 selected.row(row).collect::<Vec<_>>()
456 );
457 }
458 assert_eq!(compacted.column(0).expect("one column").form(), Form::Flat);
459 }
460
461 #[test]
462 fn a_selection_past_the_end_is_caught_by_compacting_too() {
463 let chunk = Chunk::new(vec![integers(&[1, 2])]).expect("two rows");
464 let mut kept = Selection::empty();
465 kept.push(7);
466 let error = chunk.compact(&kept).expect_err("row seven does not exist");
467 assert!(error.message().contains("row 7"), "{error}");
468 }
469
470 #[test]
471 fn projecting_reorders_and_can_repeat_a_column() {
472 let chunk = Chunk::new(vec![integers(&[1, 2]), integers(&[3, 4])]).expect("two by two");
473 let chunk = chunk.project(&[1, 0, 1]).expect("both columns exist");
474 assert_eq!(chunk.width(), 3);
475 assert_eq!(
476 chunk.row(0).collect::<Vec<_>>(),
477 vec![Value::Integer(3), Value::Integer(1), Value::Integer(3)]
478 );
479 }
480
481 #[test]
482 fn projecting_a_column_that_is_not_there_is_caught() {
483 let chunk = Chunk::new(vec![integers(&[1, 2])]).expect("one column");
484 let error = chunk.project(&[0, 4]).expect_err("there is no column four");
485 assert!(error.message().contains("column 4"), "{error}");
486 }
487
488 #[test]
489 fn flattening_a_selected_chunk_gives_the_same_values() {
490 let chunk = Chunk::new(vec![integers(&[10, 20, 30])]).expect("three rows");
491 let kept = Selection::from_predicate(3, |index| index != 1);
492 let selected = chunk.select(&kept).expect("rows zero and two exist");
493 let flat = selected.flatten().expect("integers flatten");
494 assert_eq!(flat.column(0).expect("one column").form(), Form::Flat);
495 for row in 0..flat.len() {
496 assert_eq!(flat.value_at(row, 0), selected.value_at(row, 0), "row {row}");
497 }
498 }
499
500 #[test]
501 fn a_chunk_costs_what_its_columns_cost() {
502 let chunk = Chunk::new(vec![integers(&[1; 1000]), integers(&[2; 1000])])
503 .expect("two columns of a thousand");
504 let columns: usize = chunk.columns().iter().map(Vector::footprint).sum();
505 assert_eq!(chunk.footprint(), size_of::<Chunk>() + columns);
506 assert!(chunk.footprint() >= 8000, "two thousand i32: {}", chunk.footprint());
507 }
508}