Skip to main content

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    /// The type of each column.
147    #[must_use]
148    pub fn types(&self) -> Vec<LogicalType> {
149        self.columns.iter().map(|column| column.logical_type().clone()).collect()
150    }
151
152    /// Validate every storage-backed value reachable from this chunk.
153    pub fn validate_external(&self) -> Result<()> {
154        self.columns.iter().try_for_each(Vector::validate_external)
155    }
156
157    /// The value at a row and a column, or null if either is past the end.
158    ///
159    /// The slow path, same as [`Vector::value_at`]. It is what a result set is read out with and
160    /// what a test asserts on.
161    #[must_use]
162    pub fn value_at(&self, row: usize, column: usize) -> Value {
163        match self.columns.get(column) {
164            Some(held) => held.value_at(row),
165            None => Value::Null,
166        }
167    }
168
169    /// The value at a row and column, preserving storage read and validation failures.
170    pub fn try_value_at(&self, row: usize, column: usize) -> Result<Value> {
171        match self.columns.get(column) {
172            Some(held) => held.try_value_at(row),
173            None => Ok(Value::Null),
174        }
175    }
176
177    /// One row, left to right.
178    pub fn row(&self, row: usize) -> impl Iterator<Item = Value> + '_ {
179        self.columns.iter().map(move |column| column.value_at(row))
180    }
181
182    /// The rows a selection kept, without moving any of the values.
183    ///
184    /// Every column becomes a dictionary vector whose codes are the selection, which is the form
185    /// `spec/07-execution.md` section 7.1 asks a filter to produce rather than compacting. It takes
186    /// the chunk by value because that is what makes it free: the payload is moved into the new
187    /// vector rather than copied, so a filter that keeps one row in a thousand still costs the
188    /// selection and nothing else.
189    ///
190    /// # Errors
191    ///
192    /// If the selection points past the end of the chunk.
193    pub fn select(self, selection: &Selection) -> Result<Self> {
194        if let Some(bad) = selection.iter().find(|&index| index >= self.rows) {
195            return Err(Error::internal(format!(
196                "a selection keeps row {bad} of a chunk that has {} rows",
197                self.rows
198            )));
199        }
200        let rows = selection.len();
201        let codes = selection.indices();
202        let mut columns = Vec::with_capacity(self.columns.len());
203        for column in self.columns {
204            columns.push(Vector::dictionary(codes.to_vec(), column)?);
205        }
206        Self::with_rows(columns, rows)
207    }
208
209    /// The rows a selection kept, copied, so that nothing downstream reads through an indirection.
210    ///
211    /// The copying counterpart to [`Self::select`], and the two exist because neither one is right
212    /// twice. Which one to call is measured rather than argued, and the measurement says something
213    /// other than what the argument does, so here is both.
214    ///
215    /// The argument is that selecting pays nothing now and one redirection on every later read of
216    /// every kept row, while compacting pays a copy now and nothing afterwards, so the deciding
217    /// variable is selectivity: keep a few rows and select, keep most of them and compact. The
218    /// measurement says the deciding variable is not selectivity at all, it is how many times the
219    /// rows are read again afterwards, and selectivity barely moves the line. On server3, over a
220    /// chunk of two integer columns, compacting loses to selecting at every selectivity from one
221    /// percent to a hundred when there is one later pass over the kept rows, and beats it at every
222    /// selectivity from one percent to a hundred when there are sixteen. With four later passes the
223    /// two are within a few percent of each other everywhere. Put a varchar column in the chunk and
224    /// compaction loses almost everywhere, because copying string bytes is most of what it costs and
225    /// the dictionary it avoids is most of what it saves.
226    ///
227    /// Which is why nothing in the streaming pipeline calls this yet. A filter today feeds an
228    /// aggregate or a projection and that is one pass or two, and end to end on two million rows
229    /// `SELECT sum(a), sum(b), count(*) FROM t WHERE a > ?` measures the same either way at one
230    /// percent selectivity and fifty percent slower compacting at fifty percent selectivity. The
231    /// operators that will want this are the ones that hold chunks rather than pass them on, the
232    /// hash join build side and the sort, because a chunk that is kept alive as a selection keeps
233    /// the whole chunk it was selected from alive with it, and that is a hundred to one on memory
234    /// rather than a few percent on time.
235    ///
236    /// Takes the chunk by value like [`Self::select`] does, even though the payload is copied rather
237    /// than moved, because a caller that still wanted the original after compacting it would be
238    /// holding both copies and should say so.
239    ///
240    /// # Errors
241    ///
242    /// If the selection points past the end of the chunk, or if a column has a type there is no
243    /// vector for, which today means `ARRAY` and `UNION`.
244    pub fn compact(self, selection: &Selection) -> Result<Self> {
245        if let Some(bad) = selection.iter().find(|&index| index >= self.rows) {
246            return Err(Error::internal(format!(
247                "a selection keeps row {bad} of a chunk that has {} rows",
248                self.rows
249            )));
250        }
251        let rows = selection.len();
252        let indices = selection.indices();
253        let mut columns = Vec::with_capacity(self.columns.len());
254        for column in &self.columns {
255            columns.push(column.gather(indices)?);
256        }
257        Self::with_rows(columns, rows)
258    }
259
260    /// The columns at the given positions, in that order.
261    ///
262    /// A position may appear twice, which is what `SELECT x, x FROM t` is, and the second one costs
263    /// a copy. Every other position is moved.
264    ///
265    /// # Errors
266    ///
267    /// If a position is past the end of the chunk.
268    pub fn project(self, positions: &[usize]) -> Result<Self> {
269        let width = self.columns.len();
270        if let Some(&bad) = positions.iter().find(|&&position| position >= width) {
271            return Err(Error::internal(format!(
272                "column {bad} of a chunk that has {width} columns"
273            )));
274        }
275        let rows = self.rows;
276        let mut sources: Vec<Option<Vector>> = self.columns.into_iter().map(Some).collect();
277        let mut columns = Vec::with_capacity(positions.len());
278        for (at, &position) in positions.iter().enumerate() {
279            let last_use = !positions[at + 1..].contains(&position);
280            let taken = if last_use { sources[position].take() } else { sources[position].clone() };
281            match taken {
282                Some(column) => columns.push(column),
283                // Only reachable if the last-use bookkeeping above is wrong, since a position is
284                // taken on its last appearance and cloned on every earlier one.
285                None => {
286                    return Err(Error::internal(format!("column {position} was taken twice")));
287                }
288            }
289        }
290        Self::with_rows(columns, rows)
291    }
292
293    /// The same rows with every column in flat form.
294    ///
295    /// Costs a copy per column that was not already flat. It is here for the result set at the top
296    /// of a query, where the dictionary vectors a filter left behind would otherwise be handed to a
297    /// caller who has to understand them.
298    ///
299    /// # Errors
300    ///
301    /// If a column has a type there is no vector for, which today means `ARRAY` and `UNION`. A `LIST`
302    /// and a `MAP` flatten to themselves and a `STRUCT` to a struct of flattened fields, since none of
303    /// the three has a data slice for a caller to read and there is nothing flatter to become.
304    pub fn flatten(&self) -> Result<Self> {
305        let mut columns = Vec::with_capacity(self.columns.len());
306        for column in &self.columns {
307            // flatten: this is the chunk wide version of the vector call and it exists so that the
308            // one caller at the top of a query can say it once instead of per column. Whether the
309            // copy is deserved is decided where this is called from, which today is one line in
310            // `rudb::database`, and that line says why.
311            columns.push(column.flatten()?);
312        }
313        Self::with_rows(columns, self.rows)
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use rudb_common::LogicalType;
320
321    use super::*;
322    use crate::vector::{Data, Form};
323
324    fn integers(values: &[i32]) -> Vector {
325        Vector::flat(LogicalType::Integer, Data::Int32(values.to_vec().into()))
326            .expect("integers are an i32 layout")
327    }
328
329    #[test]
330    fn a_chunk_takes_its_length_from_its_columns() {
331        let chunk = Chunk::new(vec![integers(&[1, 2, 3]), integers(&[4, 5, 6])])
332            .expect("two columns of three");
333        assert_eq!(chunk.len(), 3);
334        assert_eq!(chunk.width(), 2);
335        assert_eq!(chunk.value_at(2, 1), Value::Integer(6));
336    }
337
338    #[test]
339    fn a_ragged_chunk_is_caught() {
340        let error = Chunk::new(vec![integers(&[1, 2, 3]), integers(&[4])])
341            .expect_err("a chunk is not ragged");
342        assert!(error.message().contains("column 1"), "{error}");
343    }
344
345    /// `SELECT count(*) FROM t` scans no columns and the row count still has to survive, which is
346    /// the reason the length is a field rather than the first column's length.
347    #[test]
348    fn a_chunk_with_no_columns_can_still_have_rows() {
349        let chunk = Chunk::with_rows(Vec::new(), 900).expect("no columns and nine hundred rows");
350        assert_eq!(chunk.len(), 900);
351        assert_eq!(chunk.width(), 0);
352        assert!(!chunk.is_empty(), "nine hundred rows is not empty");
353    }
354
355    #[test]
356    fn a_chunk_longer_than_a_vector_is_caught() {
357        let error = Chunk::with_rows(Vec::new(), VECTOR_SIZE + 1).expect_err("too long");
358        assert!(error.message().contains("longer than"), "{error}");
359    }
360
361    #[test]
362    fn an_empty_chunk_keeps_its_types() {
363        let chunk = Chunk::empty(&[LogicalType::Integer, LogicalType::Varchar]);
364        assert_eq!(chunk.len(), 0);
365        assert_eq!(chunk.types(), vec![LogicalType::Integer, LogicalType::Varchar]);
366    }
367
368    #[test]
369    fn selecting_keeps_the_rows_it_selected_and_no_others() {
370        let chunk = Chunk::new(vec![integers(&[10, 20, 30, 40]), integers(&[1, 2, 3, 4])])
371            .expect("four rows");
372        let kept = Selection::from_predicate(4, |index| index % 2 == 1);
373        let chunk = chunk.select(&kept).expect("rows one and three exist");
374        assert_eq!(chunk.len(), 2);
375        assert_eq!(chunk.row(0).collect::<Vec<_>>(), vec![Value::Integer(20), Value::Integer(2)]);
376        assert_eq!(chunk.row(1).collect::<Vec<_>>(), vec![Value::Integer(40), Value::Integer(4)]);
377    }
378
379    /// The reason `select` takes the chunk by value. If it copied the payload then a filter would
380    /// cost the same as a compaction and the selection would be a pure loss.
381    #[test]
382    fn selecting_leaves_the_values_where_they_were() {
383        let chunk = Chunk::new(vec![integers(&[10, 20, 30, 40])]).expect("four rows");
384        let kept = Selection::from_predicate(4, |index| index == 0);
385        let chunk = chunk.select(&kept).expect("row zero exists");
386        assert_eq!(chunk.column(0).expect("one column").form(), Form::Dictionary);
387    }
388
389    #[test]
390    fn a_selection_past_the_end_is_caught() {
391        let chunk = Chunk::new(vec![integers(&[1, 2])]).expect("two rows");
392        let mut kept = Selection::empty();
393        kept.push(7);
394        let error = chunk.select(&kept).expect_err("row seven does not exist");
395        assert!(error.message().contains("row 7"), "{error}");
396    }
397
398    /// The two halves of section 7.1's decision have to answer the same question the same way, or
399    /// the threshold between them is a place where a query changes its answer.
400    #[test]
401    fn compacting_keeps_the_same_rows_selecting_does_and_leaves_no_indirection() {
402        let chunk = Chunk::new(vec![integers(&[10, 20, 30, 40]), integers(&[1, 2, 3, 4])])
403            .expect("four rows");
404        let kept = Selection::from_predicate(4, |index| index % 2 == 1);
405        let selected = chunk.clone().select(&kept).expect("rows one and three exist");
406        let compacted = chunk.compact(&kept).expect("rows one and three exist");
407        assert_eq!(compacted.len(), selected.len());
408        for row in 0..compacted.len() {
409            assert_eq!(
410                compacted.row(row).collect::<Vec<_>>(),
411                selected.row(row).collect::<Vec<_>>()
412            );
413        }
414        assert_eq!(compacted.column(0).expect("one column").form(), Form::Flat);
415    }
416
417    #[test]
418    fn a_selection_past_the_end_is_caught_by_compacting_too() {
419        let chunk = Chunk::new(vec![integers(&[1, 2])]).expect("two rows");
420        let mut kept = Selection::empty();
421        kept.push(7);
422        let error = chunk.compact(&kept).expect_err("row seven does not exist");
423        assert!(error.message().contains("row 7"), "{error}");
424    }
425
426    #[test]
427    fn projecting_reorders_and_can_repeat_a_column() {
428        let chunk = Chunk::new(vec![integers(&[1, 2]), integers(&[3, 4])]).expect("two by two");
429        let chunk = chunk.project(&[1, 0, 1]).expect("both columns exist");
430        assert_eq!(chunk.width(), 3);
431        assert_eq!(
432            chunk.row(0).collect::<Vec<_>>(),
433            vec![Value::Integer(3), Value::Integer(1), Value::Integer(3)]
434        );
435    }
436
437    #[test]
438    fn projecting_a_column_that_is_not_there_is_caught() {
439        let chunk = Chunk::new(vec![integers(&[1, 2])]).expect("one column");
440        let error = chunk.project(&[0, 4]).expect_err("there is no column four");
441        assert!(error.message().contains("column 4"), "{error}");
442    }
443
444    #[test]
445    fn flattening_a_selected_chunk_gives_the_same_values() {
446        let chunk = Chunk::new(vec![integers(&[10, 20, 30])]).expect("three rows");
447        let kept = Selection::from_predicate(3, |index| index != 1);
448        let selected = chunk.select(&kept).expect("rows zero and two exist");
449        let flat = selected.flatten().expect("integers flatten");
450        assert_eq!(flat.column(0).expect("one column").form(), Form::Flat);
451        for row in 0..flat.len() {
452            assert_eq!(flat.value_at(row, 0), selected.value_at(row, 0), "row {row}");
453        }
454    }
455
456    #[test]
457    fn a_chunk_costs_what_its_columns_cost() {
458        let chunk = Chunk::new(vec![integers(&[1; 1000]), integers(&[2; 1000])])
459            .expect("two columns of a thousand");
460        let columns: usize = chunk.columns().iter().map(Vector::footprint).sum();
461        assert_eq!(chunk.footprint(), size_of::<Chunk>() + columns);
462        assert!(chunk.footprint() >= 8000, "two thousand i32: {}", chunk.footprint());
463    }
464}