Skip to main content

rudb_vector/
assemble.rs

1//! One vector built out of pieces that each answer a different set of its rows.
2//!
3//! The inverse of [`Vector::gather`]. A gather says where each output row reads from, so it wants
4//! one source and a position per row. An assembly says where each input row writes to, so it takes
5//! several sources and a position per row of each of them, and the rows no source claims come out
6//! null.
7//!
8//! `CASE` is the shape that wants this. Each arm is evaluated over the rows no earlier arm claimed,
9//! which is a correctness rule rather than a performance one, since `CASE WHEN x <> 0 THEN 1 / x
10//! ELSE 0 END` divides by zero on the rows the arm excludes if the arm is evaluated for them. So the
11//! arms produce several short answers that have to end up interleaved in the order the rows arrived
12//! in, and interleaving them is what this is. A join assembling a payload out of a matched side and
13//! an unmatched side wants the same thing.
14//!
15//! # Why it is not a `Vec<Value>`
16//!
17//! Because that is a heap allocation per string and a drop per string afterwards, on top of the
18//! walk through the enum that a `Value` is. On the ClickBench query that groups by a `CASE` over
19//! `Referer`, building the answer that way was about a quarter of the whole query: a quarter of the
20//! instructions were in `value_at`, the `Value` drop glue, `malloc` and `free`, for an answer whose
21//! bytes were already sitting in a string arena and only needed to be pointed at.
22//!
23//! What happens instead is that the pieces are laid end to end into one run of data and the
24//! interleave is then a single gather over that run, which is a typed loop per physical layout and
25//! is the same loop [`Vector::gather`] already goes down. A string's bytes are copied once, into one
26//! arena that was sized before any of them moved.
27//!
28//! # The shape of the interface
29//!
30//! A builder rather than a function taking a slice of pieces, because the caller producing the
31//! pieces is usually borrowing scratch space to produce each one and cannot hold two of them at
32//! once. [`Assembly::place`] reads a piece and is done with it, so the borrow ends between arms and
33//! nothing has to be cloned to keep it alive.
34
35use rudb_common::{Error, LogicalType, Result, Value};
36
37use crate::string::StringView;
38use crate::validity::Validity;
39use crate::vector::{Data, NOWHERE, Vector, copy_of, empty_data_for, layout_of};
40
41/// A vector being built out of pieces, each landing at the positions it is given.
42///
43/// Build one with [`Assembly::new`], call [`Assembly::place`] once per piece, and finish it with
44/// [`Assembly::finish`]. A row no piece claims is null.
45#[derive(Debug)]
46pub struct Assembly {
47    ty: LogicalType,
48    rows: usize,
49    /// The pieces laid end to end, for a type that has a flat layout to lay them in.
50    data: Data,
51    /// Where each output row reads from in `data`, or [`NOWHERE`] for a row no piece claimed.
52    at: Vec<usize>,
53    /// Whether each output row holds a value rather than a null.
54    live: Vec<bool>,
55    /// The fallback for the nested types, which have no run of data to lay anything end to end in.
56    ///
57    /// A `LIST`, a `STRUCT` and a `MAP` are a child vector and a run of entries rather than a run of
58    /// values, so laying two of them end to end is not an append to one buffer and the copy loop
59    /// this is built around has nothing to walk. They go through values, which is what they did
60    /// before this existed and is not a regression for them. Nothing on a ClickBench or TPC-H path
61    /// reaches it.
62    values: Option<Vec<Value>>,
63}
64
65impl Assembly {
66    /// An assembly of `rows` rows of `ty`, with every row null until a piece claims it.
67    ///
68    /// # Errors
69    ///
70    /// If the type is one there is no flat layout for yet, which today means `ARRAY` and `UNION`.
71    pub fn new(ty: LogicalType, rows: usize) -> Result<Self> {
72        let nested =
73            matches!(ty, LogicalType::List(_) | LogicalType::Struct(_) | LogicalType::Map(_, _));
74        let values = if nested { Some(vec![Value::Null; rows]) } else { None };
75        let data = if nested { Data::Empty } else { empty_data_for(&ty)? };
76        Ok(Self { ty, rows, data, at: vec![NOWHERE; rows], live: vec![false; rows], values })
77    }
78
79    /// How many rows the finished vector will have.
80    #[must_use]
81    pub fn rows(&self) -> usize {
82        self.rows
83    }
84
85    /// Writes row `n` of `piece` at output row `positions[n]`, for every row of `piece`.
86    ///
87    /// A row claimed twice takes the value the later call gave it, which is not a case `CASE`
88    /// produces, since its arms run over disjoint sets of rows, and is defined rather than left
89    /// open so that a caller that does it gets an answer instead of whichever of the two the copy
90    /// loop happened to reach.
91    ///
92    /// # Errors
93    ///
94    /// If `piece` has a different number of rows than there are positions, if a position is past the
95    /// end of the assembly, or if `piece` is not of a layout that can be laid after what is already
96    /// there.
97    pub fn place(&mut self, positions: &[u32], piece: &Vector) -> Result<()> {
98        if positions.len() != piece.len() {
99            return Err(Error::internal(format!(
100                "a piece of {} rows placed at {} positions",
101                piece.len(),
102                positions.len()
103            )));
104        }
105        for &row in positions {
106            if row as usize >= self.rows {
107                return Err(Error::internal(format!(
108                    "row {row} placed in an assembly of {} rows",
109                    self.rows
110                )));
111            }
112        }
113        if let Some(values) = &mut self.values {
114            // row at a time: the nested fallback named on the field above. A `LIST`, a `STRUCT` and
115            // a `MAP` are a child vector and a run of entries rather than a run of values, so there
116            // is no buffer to lay one after another and no typed copy to do the interleave with.
117            for (slot, &row) in positions.iter().enumerate() {
118                values[row as usize] = piece.value_at(slot);
119            }
120            return Ok(());
121        }
122        // flatten: the copy loop that does the interleave reads a run of data, and a piece can
123        // arrive constant, dictionary encoded or bit packed. Flattening is itself a typed loop per
124        // layout, so writing the piece out once here is what stops it being read a value at a time
125        // later, and a piece that is already flat is not copied at all.
126        let flat = piece.flatten()?;
127        let Some(from) = flat.data() else {
128            return Err(Error::internal("a flattened vector with no run of data in it"));
129        };
130        let start = self.data.len();
131        let appended = extend(&mut self.data, from)?;
132        for (slot, &row) in positions.iter().enumerate() {
133            let row = row as usize;
134            // A piece whose data is empty is the untyped null, so it claims its rows and they are
135            // null, which is what leaving them at `NOWHERE` says.
136            if slot < appended {
137                self.at[row] = start + slot;
138                self.live[row] = !piece.is_null_at(slot);
139            } else {
140                self.at[row] = NOWHERE;
141                self.live[row] = false;
142            }
143        }
144        Ok(())
145    }
146
147    /// The finished vector.
148    ///
149    /// # Errors
150    ///
151    /// If the run of data that came out of the pieces is not one the type can hold.
152    pub fn finish(self) -> Result<Vector> {
153        if let Some(values) = self.values {
154            return Vector::from_values(self.ty, &values);
155        }
156        // An untyped null has no run of data to gather out of, and a gather over one would give a
157        // vector of no values calling itself `rows` long.
158        if matches!(self.data, Data::Empty) {
159            return Ok(Vector::constant(self.ty, Value::Null, self.rows));
160        }
161        let gathered = copy_of(&self.data, &self.at);
162        Ok(Vector::flat(self.ty, gathered)?.with_validity(Validity::from_run(&self.live)))
163    }
164}
165
166/// Lays a run of data end to end after another, answering how many values it appended.
167///
168/// The typed loop per layout is the whole point: an append of a thousand `i64` is one `memcpy` and
169/// an append of a thousand strings is one arena growth and a thousand sixteen byte views, neither of
170/// which touches a `Value`.
171fn extend(into: &mut Data, from: &Data) -> Result<usize> {
172    macro_rules! extended {
173        ($(($variant:ident, $native:ty, $zero:expr)),+ $(,)?) => {
174            match (&mut *into, from) {
175                // Nothing to append, which is what an untyped null piece is. The caller reads the
176                // count and leaves those rows null rather than pointing them anywhere.
177                (_, Data::Empty) => Ok(0),
178                $((Data::$variant(out), Data::$variant(values)) => {
179                    out.extend_from_slice(values.as_slice());
180                    Ok(values.len())
181                })+
182                // The one layout where an append is a copy of bytes rather than a copy of fixed
183                // width slots. The arena is grown once for all of them, because a view carries its
184                // length so the total is known before any of the bytes move.
185                (Data::Varlen(out), Data::Varlen(values)) => {
186                    out.reserve_views(values.len());
187                    out.reserve_bytes(
188                        values
189                            .views()
190                            .iter()
191                            .filter(|view| !view.is_inline())
192                            .map(StringView::len)
193                            .sum(),
194                    );
195                    for index in 0..values.len() {
196                        out.push_from(values, index);
197                    }
198                    Ok(values.len())
199                }
200                (out, from) => Err(Error::internal(format!(
201                    "a run of {:?} values cannot be laid after a run of {:?} ones",
202                    layout_of(from),
203                    layout_of(out)
204                ))),
205            }
206        };
207    }
208    crate::for_each_layout!(fixed, extended)
209}
210
211/// Unit tests for the assembly.
212///
213/// That the engine actually goes through here rather than through the old path was checked rather
214/// than assumed, by gating a panic on [`Assembly::place`] and running the `rudb` suite with it
215/// armed. Three tests failed and no others: the one that is a `CASE` by name, the one that runs the
216/// catalog views the engine ships with, and the one over the native frequency synopsis. All three
217/// have a `CASE` in them and nothing else in the suite does.
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::Chunk;
222
223    /// Every row of a vector, as values, which is what an assembly is checked against.
224    fn values(vector: &Vector) -> Vec<Value> {
225        (0..vector.len()).map(|row| vector.value_at(row)).collect()
226    }
227
228    /// The answer the assembly has to reach, written the slow obvious way.
229    ///
230    /// A `Vec<Value>` filled by scattering and then handed to [`Vector::from_values`] is exactly
231    /// what `CASE` used to do, so this is the reference rather than a second opinion.
232    fn scattered(ty: &LogicalType, rows: usize, pieces: &[(Vec<u32>, Vector)]) -> Vector {
233        let mut answers = vec![Value::Null; rows];
234        for (positions, piece) in pieces {
235            for (slot, &row) in positions.iter().enumerate() {
236                answers[row as usize] = piece.value_at(slot);
237            }
238        }
239        Vector::from_values(ty.clone(), &answers).expect("the reference builds")
240    }
241
242    /// Builds an assembly out of pieces and checks it against the slow way of getting there.
243    fn agrees(ty: &LogicalType, rows: usize, pieces: &[(Vec<u32>, Vector)]) -> Vector {
244        let mut assembly = Assembly::new(ty.clone(), rows).expect("an assembly of this type");
245        for (positions, piece) in pieces {
246            assembly.place(positions, piece).expect("the piece is placed");
247        }
248        let built = assembly.finish().expect("the assembly finishes");
249        assert_eq!(built.len(), rows, "an assembly of {rows} rows");
250        assert_eq!(values(&built), values(&scattered(ty, rows, pieces)), "against the slow way");
251        built
252    }
253
254    #[test]
255    fn two_pieces_interleave_back_into_the_order_the_rows_came_in() {
256        let evens = Vector::from_values(LogicalType::BigInt, &[Value::BigInt(0), Value::BigInt(2)])
257            .expect("a vector");
258        let odds = Vector::from_values(LogicalType::BigInt, &[Value::BigInt(1), Value::BigInt(3)])
259            .expect("a vector");
260        let built = agrees(&LogicalType::BigInt, 4, &[(vec![0, 2], evens), (vec![1, 3], odds)]);
261        assert_eq!(
262            values(&built),
263            vec![Value::BigInt(0), Value::BigInt(1), Value::BigInt(2), Value::BigInt(3)]
264        );
265    }
266
267    #[test]
268    fn a_row_no_piece_claims_is_null() {
269        // Which is what a `CASE` with no `ELSE` leaves behind, and the case where a run of data with
270        // a hole in it would put every value after the hole at the wrong index.
271        let piece =
272            Vector::from_values(LogicalType::BigInt, &[Value::BigInt(7)]).expect("a vector");
273        let built = agrees(&LogicalType::BigInt, 3, &[(vec![1], piece)]);
274        assert_eq!(values(&built), vec![Value::Null, Value::BigInt(7), Value::Null]);
275    }
276
277    #[test]
278    fn no_pieces_at_all_is_a_column_of_nulls_of_the_right_length() {
279        let built = agrees(&LogicalType::Integer, 5, &[]);
280        assert!(built.is_null_at(4), "every row of it is null");
281    }
282
283    #[test]
284    fn a_null_inside_a_piece_stays_null_where_the_piece_put_it() {
285        // The validity has to survive the copy, and the value under it has to not be read, which is
286        // two different things a single run of data with a mask over it can get wrong separately.
287        let piece = Vector::from_values(
288            LogicalType::BigInt,
289            &[Value::BigInt(1), Value::Null, Value::BigInt(3)],
290        )
291        .expect("a vector");
292        let built = agrees(&LogicalType::BigInt, 3, &[(vec![2, 0, 1], piece)]);
293        assert!(built.is_null_at(0), "the null landed where the piece put it");
294        assert_eq!(built.value_at(2), Value::BigInt(1));
295    }
296
297    #[test]
298    fn strings_are_assembled_without_going_through_a_value_each() {
299        let left = Vector::from_values(
300            LogicalType::Varchar,
301            &[Value::Varchar("a short one".into()), Value::Varchar("another".into())],
302        )
303        .expect("a vector");
304        let right = Vector::from_values(
305            LogicalType::Varchar,
306            &[Value::Varchar("a string that is far too long to live inline in a view".into())],
307        )
308        .expect("a vector");
309        let built = agrees(&LogicalType::Varchar, 3, &[(vec![0, 2], left), (vec![1], right)]);
310        assert_eq!(built.value_at(0), Value::Varchar("a short one".into()));
311        assert_eq!(
312            built.value_at(1),
313            Value::Varchar("a string that is far too long to live inline in a view".into())
314        );
315        assert_eq!(built.value_at(2), Value::Varchar("another".into()));
316    }
317
318    #[test]
319    fn a_constant_piece_is_written_out_rather_than_read_a_row_at_a_time() {
320        // The `ELSE ''` half of the ClickBench query this was built for, which arrives as a constant
321        // over however many rows the arms did not claim.
322        let arm = Vector::from_values(LogicalType::Varchar, &[Value::Varchar("kept".into())])
323            .expect("a vector");
324        let otherwise = Vector::constant(LogicalType::Varchar, Value::Varchar("".into()), 3);
325        let built = agrees(&LogicalType::Varchar, 4, &[(vec![2], arm), (vec![0, 1, 3], otherwise)]);
326        assert_eq!(built.value_at(0), Value::Varchar("".into()));
327        assert_eq!(built.value_at(2), Value::Varchar("kept".into()));
328    }
329
330    #[test]
331    fn a_dictionary_piece_is_walked_to_its_values() {
332        // A scanned string column arrives as codes over a shared dictionary, so this is the form the
333        // `THEN Referer` arm of the ClickBench query actually hands over.
334        let dictionary = Vector::from_values(
335            LogicalType::Varchar,
336            &[Value::Varchar("one".into()), Value::Varchar("two".into())],
337        )
338        .expect("a dictionary");
339        let piece = Vector::dictionary(vec![1, 0, 1], dictionary).expect("a dictionary vector");
340        let built = agrees(&LogicalType::Varchar, 3, &[(vec![0, 1, 2], piece)]);
341        assert_eq!(
342            values(&built),
343            vec![
344                Value::Varchar("two".into()),
345                Value::Varchar("one".into()),
346                Value::Varchar("two".into())
347            ]
348        );
349    }
350
351    #[test]
352    fn a_piece_placed_at_the_wrong_number_of_positions_is_an_error() {
353        let piece =
354            Vector::from_values(LogicalType::BigInt, &[Value::BigInt(1)]).expect("a vector");
355        let mut assembly = Assembly::new(LogicalType::BigInt, 4).expect("an assembly");
356        assert!(assembly.place(&[0, 1], &piece).is_err(), "two positions for one row");
357    }
358
359    #[test]
360    fn a_position_past_the_end_is_an_error_rather_than_a_lost_row() {
361        let piece =
362            Vector::from_values(LogicalType::BigInt, &[Value::BigInt(1)]).expect("a vector");
363        let mut assembly = Assembly::new(LogicalType::BigInt, 2).expect("an assembly");
364        assert!(assembly.place(&[9], &piece).is_err(), "a row past the end of the assembly");
365    }
366
367    #[test]
368    fn a_piece_of_the_wrong_layout_is_an_error_rather_than_a_wrong_answer() {
369        // Two runs of data that cannot be laid end to end, which is a bug in whoever built the
370        // pieces and has to say so rather than silently keep the first one.
371        let piece =
372            Vector::from_values(LogicalType::Varchar, &[Value::Varchar("x".into())]).expect("text");
373        let mut assembly = Assembly::new(LogicalType::BigInt, 1).expect("an assembly");
374        assert!(assembly.place(&[0], &piece).is_err(), "text laid after integers");
375    }
376
377    #[test]
378    fn every_layout_assembles_the_way_it_scatters() {
379        // One case per physical layout, because the copy loop is generated per layout and a layout
380        // missing from it is a wrong answer for that type alone, which no single typed test finds.
381        let cases: Vec<(LogicalType, Vec<Value>)> = vec![
382            (LogicalType::Boolean, vec![Value::Boolean(true), Value::Boolean(false)]),
383            (LogicalType::TinyInt, vec![Value::TinyInt(1), Value::TinyInt(-2)]),
384            (LogicalType::SmallInt, vec![Value::SmallInt(3), Value::SmallInt(-4)]),
385            (LogicalType::Integer, vec![Value::Integer(5), Value::Integer(-6)]),
386            (LogicalType::BigInt, vec![Value::BigInt(7), Value::BigInt(-8)]),
387            (LogicalType::HugeInt, vec![Value::HugeInt(9), Value::HugeInt(-10)]),
388            (LogicalType::UTinyInt, vec![Value::UTinyInt(11), Value::UTinyInt(12)]),
389            (LogicalType::USmallInt, vec![Value::USmallInt(13), Value::USmallInt(14)]),
390            (LogicalType::UInteger, vec![Value::UInteger(15), Value::UInteger(16)]),
391            (LogicalType::UBigInt, vec![Value::UBigInt(17), Value::UBigInt(18)]),
392            (LogicalType::Float, vec![Value::Float(1.5), Value::Float(-2.5)]),
393            (LogicalType::Double, vec![Value::Double(3.5), Value::Double(-4.5)]),
394            (
395                LogicalType::Varchar,
396                vec![Value::Varchar("first".into()), Value::Varchar("second".into())],
397            ),
398            (LogicalType::Date, vec![Value::Date(19), Value::Date(20)]),
399        ];
400        for (ty, pair) in cases {
401            let left = Vector::from_values(ty.clone(), &pair[..1]).expect("a vector");
402            let right = Vector::from_values(ty.clone(), &pair[1..]).expect("a vector");
403            let built = agrees(&ty, 2, &[(vec![1], left), (vec![0], right)]);
404            assert_eq!(built.value_at(0), pair[1], "{ty:?} at row 0");
405            assert_eq!(built.value_at(1), pair[0], "{ty:?} at row 1");
406        }
407    }
408
409    #[test]
410    fn an_assembly_is_a_chunk_column_like_any_other() {
411        // The point of building a vector rather than a `Vec<Value>` is that what comes out goes
412        // straight into a chunk, so this checks it actually does.
413        let piece = Vector::from_values(LogicalType::BigInt, &[Value::BigInt(1), Value::BigInt(2)])
414            .expect("a vector");
415        let built = agrees(&LogicalType::BigInt, 2, &[(vec![1, 0], piece)]);
416        let chunk = Chunk::new(vec![built]).expect("a chunk of one column");
417        assert_eq!(chunk.len(), 2, "two rows");
418    }
419}