Skip to main content

rudb_kernels/
select.rs

1//! Turning a vector of flags into the rows it keeps.
2//!
3//! This is the other half of a filter. The comparison kernel produces a vector of booleans fast, and
4//! then something has to turn that vector into the positions that survived, which is what an
5//! operator hands downstream. Reading the flags back out one [`rudb_common::Value`] at a time
6//! undoes the comparison kernel's work and then some: on a two million row table a comparison that
7//! costs under a nanosecond a row was followed by a read that cost twenty seven.
8//!
9//! # Why the loop has no branch in it
10//!
11//! The obvious loop is `if kept { push(index) }`, and the branch in it is unpredictable by
12//! construction. A filter that keeps every row or no rows predicts perfectly and is also a filter
13//! nobody needed; the filters that matter keep some rows, and which rows is exactly the thing the
14//! data decides rather than the code. A mispredict is somewhere between fifteen and twenty cycles,
15//! so at thirty percent selectivity the branch alone can cost more than everything else in the loop.
16//!
17//! So every row writes its own index at the current length and only a row that is kept moves the
18//! length on. The write is unconditional and lands in the same cache line most of the time, the
19//! addition is of a zero or a one, and there is no branch for a predictor to get wrong. That is why
20//! [`rudb_vector::Selection::from_indices`] exists: the buffer is filled and counted here and handed
21//! over whole, rather than being pushed into one position at a time with a capacity check per row.
22//!
23//! A conjunct that is not the first one does not start from a range of rows, it starts from what
24//! the conjuncts before it left, which is what [`refine`] is for. Most conjuncts never get here at
25//! all because [`crate::compare::refine`] threads the selection into the comparison itself and
26//! produces the rows directly, and this one catches the conjuncts that are not comparisons.
27//!
28//! Three valued logic is what makes this a kernel rather than a line. A filter keeps a row when the
29//! predicate is true, and null is not true, which is what makes `WHERE x <> 5` leave out the rows
30//! where `x` is null. So a row is kept when its flag is set and its validity bit is set, and the
31//! second half of that is the reason the null path reads a word of the mask at a time rather than
32//! asking the vector per row.
33
34use rudb_common::{Error, LogicalType, Result};
35use rudb_vector::{Data, Form, Selection, Validity, Vector};
36
37use crate::fallback::{self, Kernel};
38use crate::logic::is_true;
39use crate::shape::{identity, nulls_of};
40
41/// The first `rows` positions of `flags` where the flag is true and not null.
42///
43/// A vector that is not boolean, or is in a form with no loop here, falls through to reading it a
44/// value at a time and records itself in [`crate::fallback`]. The answer is the same either way.
45#[must_use]
46pub fn selection(flags: &Vector, rows: usize) -> Selection {
47    let rows = rows.min(flags.len());
48    if let Some(kept) = swept(flags, rows) {
49        return kept;
50    }
51    // One vector in, so its form goes in both halves of the report rather than leaving a column of
52    // zeros next to every row of it.
53    fallback::record(Kernel::Select, flags.form(), flags.form());
54    Selection::from_predicate(rows, |index| is_true(&flags.value_at(index)))
55}
56
57/// The rows of `kept` whose flag is true and not null.
58///
59/// [`selection`] for a conjunct that is not the first one. A predicate is threaded through the
60/// comparison kernel where it can be, because [`crate::compare::refine`] reads only the rows it is
61/// given and never builds a flag vector at all, and through this where it cannot: a conjunct that is
62/// a bare boolean column, a function call or a nested `OR` produces flags over the whole chunk and
63/// then has to be intersected with what the conjuncts before it left. The second one is worth having
64/// because a predicate with one awkward conjunct in it would otherwise put every conjunct back on
65/// the unthreaded path.
66///
67/// # Errors
68///
69/// If a position in `kept` is past the end of `flags`.
70pub fn refine(flags: &Vector, kept: &Selection) -> Result<Selection> {
71    if kept.indices().iter().any(|&row| row as usize >= flags.len()) {
72        return Err(Error::internal(format!(
73            "a selection past the end of a {} row vector",
74            flags.len()
75        )));
76    }
77    if kept.is_empty() {
78        return Ok(Selection::empty());
79    }
80    if let Some(narrowed) = swept_within(flags, kept) {
81        return Ok(narrowed);
82    }
83    fallback::record(Kernel::Select, flags.form(), flags.form());
84    let mut out = Vec::with_capacity(kept.len());
85    // row at a time: the path recorded on the line above, for a flag vector in a form with no loop
86    // here, reading only the rows the conjuncts before this one kept.
87    for &row in kept.indices() {
88        if is_true(&flags.value_at(row as usize)) {
89            out.push(row);
90        }
91    }
92    Ok(Selection::from_indices(out))
93}
94
95fn swept_within(flags: &Vector, kept: &Selection) -> Option<Selection> {
96    if *flags.logical_type() != LogicalType::Boolean {
97        return None;
98    }
99    match flags.form() {
100        Form::Constant => {
101            Some(if is_true(flags.constant_value()?) { kept.clone() } else { Selection::empty() })
102        }
103        Form::Flat => {
104            let Data::Bool(values) = flags.data()? else {
105                return None;
106            };
107            if values.len() < flags.len() {
108                return None;
109            }
110            Some(picked_within(values, identity, kept.indices(), &nulls_of(flags)))
111        }
112        Form::Dictionary => {
113            let (codes, inner) = flags.dictionary_parts()?;
114            if codes.len() < flags.len() {
115                return None;
116            }
117            let Data::Bool(values) = inner.data()? else {
118                return None;
119            };
120            Some(picked_within(values, |row| codes[row] as usize, kept.indices(), &nulls_of(flags)))
121        }
122        _ => None,
123    }
124}
125
126/// The same branchless loop as [`picked`], over the rows a selection names rather than over a range.
127///
128/// The validity is read a bit at a time here where [`picked`] reads a word at a time, because the
129/// rows are scattered by construction and a word oriented loop would reread most of them.
130fn picked_within<M: Fn(usize) -> usize>(
131    values: &[bool],
132    at: M,
133    rows: &[u32],
134    nulls: &Validity,
135) -> Selection {
136    let mut out = vec![0_u32; rows.len()];
137    let mut count = 0;
138    match nulls {
139        Validity::AllValid => {
140            for &row in rows {
141                out[count] = row;
142                count += usize::from(values[at(row as usize)]);
143            }
144        }
145        Validity::AllInvalid => {}
146        Validity::Mask(mask) => {
147            for &row in rows {
148                out[count] = row;
149                // A single `&` rather than `&&`, because the short circuit would put back the
150                // branch this whole loop is shaped to avoid.
151                count += usize::from(mask.get(row as usize) & values[at(row as usize)]);
152            }
153        }
154    }
155    out.truncate(count);
156    Selection::from_indices(out)
157}
158
159fn swept(flags: &Vector, rows: usize) -> Option<Selection> {
160    if *flags.logical_type() != LogicalType::Boolean {
161        return None;
162    }
163    // The indices are written as `u32`, which is what a selection holds. A vector is 1024 rows and
164    // a row group is 122,880, so this is a bound the callers are nowhere near rather than a limit.
165    if rows > u32::MAX as usize {
166        return None;
167    }
168    match flags.form() {
169        // One value decides the whole vector, and the answer is every row or no rows.
170        Form::Constant => Some(if is_true(flags.constant_value()?) {
171            Selection::identity(rows)
172        } else {
173            Selection::empty()
174        }),
175        Form::Flat => {
176            let Data::Bool(values) = flags.data()? else {
177                return None;
178            };
179            if values.len() < rows {
180                return None;
181            }
182            Some(picked(values, identity, rows, &nulls_of(flags)))
183        }
184        Form::Dictionary => {
185            let (codes, inner) = flags.dictionary_parts()?;
186            if codes.len() < rows {
187                return None;
188            }
189            let Data::Bool(values) = inner.data()? else {
190                return None;
191            };
192            // Every code is inside the dictionary because `Vector::dictionary` checks that on the
193            // way in, so the gather below indexes without a bound of its own.
194            Some(picked(values, |index| codes[index] as usize, rows, &nulls_of(flags)))
195        }
196        _ => None,
197    }
198}
199
200#[expect(
201    clippy::cast_possible_truncation,
202    reason = "the caller checked that the row count fits in a u32 before getting here"
203)]
204fn picked<M: Fn(usize) -> usize>(
205    values: &[bool],
206    at: M,
207    rows: usize,
208    nulls: &Validity,
209) -> Selection {
210    let mut out = vec![0_u32; rows];
211    let mut kept = 0;
212    match nulls {
213        Validity::AllValid => {
214            for index in 0..rows {
215                out[kept] = index as u32;
216                kept += usize::from(values[at(index)]);
217            }
218        }
219        Validity::AllInvalid => {}
220        Validity::Mask(mask) => {
221            for start in (0..rows).step_by(64) {
222                let word = mask.word(start / 64);
223                for index in start..(start + 64).min(rows) {
224                    out[kept] = index as u32;
225                    let live = word >> (index - start) & 1 == 1;
226                    // A single `&` rather than `&&`, because the short circuit would put back the
227                    // branch this whole loop is shaped to avoid.
228                    kept += usize::from(live & values[at(index)]);
229                }
230            }
231        }
232    }
233    out.truncate(kept);
234    Selection::from_indices(out)
235}
236
237#[cfg(test)]
238mod tests {
239    use rudb_common::Value;
240
241    use super::*;
242
243    fn flags(values: &[Value]) -> Vector {
244        Vector::from_values(LogicalType::Boolean, values).expect("a vector of booleans")
245    }
246
247    const YES: Value = Value::Boolean(true);
248    const NO: Value = Value::Boolean(false);
249
250    /// The row at a time path, which is what the loop above has to agree with.
251    fn oracle(vector: &Vector, rows: usize) -> Selection {
252        Selection::from_predicate(rows, |index| is_true(&vector.value_at(index)))
253    }
254
255    struct Rng(u64);
256
257    impl Rng {
258        fn next(&mut self) -> u64 {
259            self.0 ^= self.0 << 13;
260            self.0 ^= self.0 >> 7;
261            self.0 ^= self.0 << 17;
262            self.0
263        }
264    }
265
266    #[test]
267    fn a_null_flag_is_not_a_true_flag() {
268        let vector = flags(&[YES, Value::Null, NO, YES]);
269        let kept = selection(&vector, 4);
270        assert_eq!(kept.indices(), &[0, 3]);
271        assert_eq!(kept, oracle(&vector, 4));
272    }
273
274    /// Every selectivity from nothing to everything, at four null densities, in both forms that
275    /// have a loop, against reading the flags a value at a time.
276    #[test]
277    fn the_rows_kept_are_the_rows_the_row_at_a_time_path_keeps() {
278        let mut rng = Rng(0x5eed_ca11_ab1e_0005);
279        for nulls in [0_usize, 8, 3, 1] {
280            for share in [0_u64, 1, 16, 50, 84, 99, 100] {
281                let values: Vec<Value> = (0..251)
282                    .map(|index| {
283                        if nulls > 0 && index % nulls == 0 {
284                            Value::Null
285                        } else {
286                            Value::Boolean(rng.next() % 100 < share)
287                        }
288                    })
289                    .collect();
290                let vector = flags(&values);
291                let note = format!("{share} percent true, one null in {nulls}");
292                assert_eq!(selection(&vector, 251), oracle(&vector, 251), "{note}, flat");
293                let codes: Vec<u32> = (0..251).map(|index| (index % 37) as u32).collect();
294                let coded = Vector::dictionary(codes, vector).expect("codes are in range");
295                assert_eq!(selection(&coded, 251), oracle(&coded, 251), "{note}, dictionary");
296            }
297        }
298    }
299
300    /// The rows of a selection the row at a time path keeps, which is what [`refine`] has to say.
301    fn within(vector: &Vector, kept: &Selection) -> Selection {
302        let mut out = Vec::new();
303        // row at a time: the oracle the threaded loop is checked against, which is the whole
304        // reason the row at a time path is kept rather than deleted.
305        for &row in kept.indices() {
306            if is_true(&vector.value_at(row as usize)) {
307                out.push(row);
308            }
309        }
310        Selection::from_indices(out)
311    }
312
313    /// Threading a selection through a flag vector is the rows a full pass would have kept that
314    /// were still in play. Every selectivity, four null densities, both forms with a loop, and the
315    /// four shapes of selection a conjunct chain actually reaches.
316    #[test]
317    fn a_threaded_selection_keeps_what_was_still_in_play_and_true() {
318        let mut rng = Rng(0x5eed_ca11_ab1e_0006);
319        let len = 251;
320        let selections = [
321            Selection::identity(len),
322            Selection::from_indices((0..len as u32).filter(|row| row % 7 == 0).collect()),
323            Selection::from_indices(vec![0, 1, 128, 250]),
324            Selection::empty(),
325        ];
326        for nulls in [0_usize, 8, 3, 1] {
327            for share in [0_u64, 1, 16, 50, 84, 99, 100] {
328                let values: Vec<Value> = (0..len)
329                    .map(|index| {
330                        if nulls > 0 && index % nulls == 0 {
331                            Value::Null
332                        } else {
333                            Value::Boolean(rng.next() % 100 < share)
334                        }
335                    })
336                    .collect();
337                let vector = flags(&values);
338                let codes: Vec<u32> = (0..len).map(|index| (index % 37) as u32).collect();
339                let coded = Vector::dictionary(codes, vector.clone()).expect("codes are in range");
340                let all = Vector::constant(LogicalType::Boolean, YES, len);
341                for kept in &selections {
342                    let note = format!("{share} percent true, one null in {nulls}");
343                    let threaded = refine(&vector, kept).expect("in range");
344                    assert_eq!(threaded, within(&vector, kept), "{note}, flat");
345                    assert_eq!(
346                        refine(&coded, kept).expect("in range"),
347                        within(&coded, kept),
348                        "{note}, dictionary"
349                    );
350                    // A constant true keeps everything that was in play and reads nothing.
351                    assert_eq!(refine(&all, kept).expect("in range"), *kept, "{note}, constant");
352                    // And what a threaded pass keeps is always a subset of what a full pass does.
353                    let full = selection(&vector, len);
354                    assert!(
355                        threaded.indices().iter().all(|row| full.indices().contains(row)),
356                        "{note}, threaded is within the full pass"
357                    );
358                }
359            }
360        }
361    }
362
363    #[test]
364    fn a_threaded_selection_past_the_end_is_caught() {
365        let vector = flags(&[YES, YES]);
366        let past = Selection::from_indices(vec![0, 2]);
367        let error = refine(&vector, &past).expect_err("out of range");
368        assert!(error.message().contains("2 row vector"), "{error}");
369    }
370
371    #[test]
372    fn a_constant_is_answered_without_a_loop_and_a_non_boolean_is_not_answered_at_all() {
373        fallback::reset();
374        let all = Vector::constant(LogicalType::Boolean, YES, 500);
375        assert_eq!(selection(&all, 500), Selection::identity(500));
376        let none = Vector::constant(LogicalType::Boolean, Value::Null, 500);
377        assert!(selection(&none, 500).is_empty());
378        assert_eq!(fallback::count(Kernel::Select, Form::Constant, Form::Constant), 0);
379
380        // Nothing binds a filter to a non boolean, and if something did it would be wrong rather
381        // than fast, so the loop refuses it and the value at a time path decides.
382        let numbers = Vector::from_values(LogicalType::Integer, &[Value::Integer(1)])
383            .expect("a vector of integers");
384        assert!(selection(&numbers, 1).is_empty());
385        assert_eq!(fallback::count(Kernel::Select, Form::Flat, Form::Flat), 1);
386        fallback::reset();
387    }
388
389    /// Fewer rows than the vector holds, which is what a partly filled chunk is.
390    #[test]
391    fn only_the_rows_asked_for_are_looked_at() {
392        let vector = flags(&[YES, YES, YES, YES]);
393        assert_eq!(selection(&vector, 2).indices(), &[0, 1]);
394        // And more rows than there are is the vector's length, not a panic.
395        assert_eq!(selection(&vector, 9).indices(), &[0, 1, 2, 3]);
396    }
397}