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