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 connective produces flags over the whole chunk
63/// and then has to be intersected with what the conjuncts before it left. The second one is worth
64/// having because a predicate with one awkward conjunct in it would otherwise put every conjunct
65/// back on the unthreaded path.
66///
67/// The branches of a threaded `OR` come through here too. What `kept` holds there is the rows no
68/// branch has accepted yet rather than the rows every conjunct has kept, which is the caller's
69/// business and not this one's: either way it is the rows still worth looking at.
70///
71/// # Errors
72///
73/// If a position in `kept` is past the end of `flags`.
74pub fn refine(flags: &Vector, kept: &Selection) -> Result<Selection> {
75    if kept.indices().iter().any(|&row| row as usize >= flags.len()) {
76        return Err(Error::internal(format!(
77            "a selection past the end of a {} row vector",
78            flags.len()
79        )));
80    }
81    if kept.is_empty() {
82        return Ok(Selection::empty());
83    }
84    if let Some(narrowed) = swept_within(flags, kept) {
85        return Ok(narrowed);
86    }
87    fallback::record(Kernel::Select, flags.form(), flags.form());
88    let mut out = Vec::with_capacity(kept.len());
89    // row at a time: the path recorded on the line above, for a flag vector in a form with no loop
90    // here, reading only the rows the conjuncts before this one kept.
91    for &row in kept.indices() {
92        if is_true(&flags.value_at(row as usize)) {
93            out.push(row);
94        }
95    }
96    Ok(Selection::from_indices(out))
97}
98
99fn swept_within(flags: &Vector, kept: &Selection) -> Option<Selection> {
100    if *flags.logical_type() != LogicalType::Boolean {
101        return None;
102    }
103    match flags.form() {
104        Form::Constant => {
105            Some(if is_true(flags.constant_value()?) { kept.clone() } else { Selection::empty() })
106        }
107        Form::Flat => {
108            let Data::Bool(values) = flags.data()? else {
109                return None;
110            };
111            if values.len() < flags.len() {
112                return None;
113            }
114            Some(picked_within(values, identity, kept.indices(), &nulls_of(flags)))
115        }
116        Form::Dictionary | Form::Rle => {
117            let (codes, inner) = flags.positions()?;
118            if codes.len() < flags.len() {
119                return None;
120            }
121            let Data::Bool(values) = inner.data()? else {
122                return None;
123            };
124            Some(picked_within(values, |row| codes[row] as usize, kept.indices(), &nulls_of(flags)))
125        }
126        _ => None,
127    }
128}
129
130/// The same branchless loop as [`picked`], over the rows a selection names rather than over a range.
131///
132/// The validity is read a bit at a time here where [`picked`] reads a word at a time, because the
133/// rows are scattered by construction and a word oriented loop would reread most of them.
134fn picked_within<M: Fn(usize) -> usize>(
135    values: &[bool],
136    at: M,
137    rows: &[u32],
138    nulls: &Validity,
139) -> Selection {
140    let mut out = vec![0_u32; rows.len()];
141    let mut count = 0;
142    match nulls {
143        Validity::AllValid => {
144            for &row in rows {
145                out[count] = row;
146                count += usize::from(values[at(row as usize)]);
147            }
148        }
149        Validity::AllInvalid => {}
150        Validity::Mask(mask) => {
151            for &row in rows {
152                out[count] = row;
153                // A single `&` rather than `&&`, because the short circuit would put back the
154                // branch this whole loop is shaped to avoid.
155                count += usize::from(mask.get(row as usize) & values[at(row as usize)]);
156            }
157        }
158    }
159    out.truncate(count);
160    Selection::from_indices(out)
161}
162
163fn swept(flags: &Vector, rows: usize) -> Option<Selection> {
164    if *flags.logical_type() != LogicalType::Boolean {
165        return None;
166    }
167    // The indices are written as `u32`, which is what a selection holds. A vector is 1024 rows and
168    // a row group is 122,880, so this is a bound the callers are nowhere near rather than a limit.
169    if rows > u32::MAX as usize {
170        return None;
171    }
172    match flags.form() {
173        // One value decides the whole vector, and the answer is every row or no rows.
174        Form::Constant => Some(if is_true(flags.constant_value()?) {
175            Selection::identity(rows)
176        } else {
177            Selection::empty()
178        }),
179        Form::Flat => {
180            let Data::Bool(values) = flags.data()? else {
181                return None;
182            };
183            if values.len() < rows {
184                return None;
185            }
186            Some(picked(values, identity, rows, &nulls_of(flags)))
187        }
188        Form::Dictionary | Form::Rle => {
189            let (codes, inner) = flags.positions()?;
190            if codes.len() < rows {
191                return None;
192            }
193            let Data::Bool(values) = inner.data()? else {
194                return None;
195            };
196            // Every code is inside the dictionary because `Vector::dictionary` checks that on the
197            // way in, so the gather below indexes without a bound of its own.
198            Some(picked(values, |index| codes[index] as usize, rows, &nulls_of(flags)))
199        }
200        _ => None,
201    }
202}
203
204#[expect(
205    clippy::cast_possible_truncation,
206    reason = "the caller checked that the row count fits in a u32 before getting here"
207)]
208fn picked<M: Fn(usize) -> usize>(
209    values: &[bool],
210    at: M,
211    rows: usize,
212    nulls: &Validity,
213) -> Selection {
214    let mut out = vec![0_u32; rows];
215    let mut kept = 0;
216    match nulls {
217        Validity::AllValid => {
218            for index in 0..rows {
219                out[kept] = index as u32;
220                kept += usize::from(values[at(index)]);
221            }
222        }
223        Validity::AllInvalid => {}
224        Validity::Mask(mask) => {
225            for start in (0..rows).step_by(64) {
226                let word = mask.word(start / 64);
227                for index in start..(start + 64).min(rows) {
228                    out[kept] = index as u32;
229                    let live = word >> (index - start) & 1 == 1;
230                    // A single `&` rather than `&&`, because the short circuit would put back the
231                    // branch this whole loop is shaped to avoid.
232                    kept += usize::from(live & values[at(index)]);
233                }
234            }
235        }
236    }
237    out.truncate(kept);
238    Selection::from_indices(out)
239}
240
241#[cfg(test)]
242mod tests {
243    use rudb_common::Value;
244
245    use super::*;
246
247    fn flags(values: &[Value]) -> Vector {
248        Vector::from_values(LogicalType::Boolean, values).expect("a vector of booleans")
249    }
250
251    const YES: Value = Value::Boolean(true);
252    const NO: Value = Value::Boolean(false);
253
254    /// The row at a time path, which is what the loop above has to agree with.
255    fn oracle(vector: &Vector, rows: usize) -> Selection {
256        Selection::from_predicate(rows, |index| is_true(&vector.value_at(index)))
257    }
258
259    struct Rng(u64);
260
261    impl Rng {
262        fn next(&mut self) -> u64 {
263            self.0 ^= self.0 << 13;
264            self.0 ^= self.0 >> 7;
265            self.0 ^= self.0 << 17;
266            self.0
267        }
268    }
269
270    #[test]
271    fn a_null_flag_is_not_a_true_flag() {
272        let vector = flags(&[YES, Value::Null, NO, YES]);
273        let kept = selection(&vector, 4);
274        assert_eq!(kept.indices(), &[0, 3]);
275        assert_eq!(kept, oracle(&vector, 4));
276    }
277
278    /// Every selectivity from nothing to everything, at four null densities, in both forms that
279    /// have a loop, against reading the flags a value at a time.
280    #[test]
281    fn the_rows_kept_are_the_rows_the_row_at_a_time_path_keeps() {
282        let mut rng = Rng(0x5eed_ca11_ab1e_0005);
283        for nulls in [0_usize, 8, 3, 1] {
284            for share in [0_u64, 1, 16, 50, 84, 99, 100] {
285                let values: Vec<Value> = (0..251)
286                    .map(|index| {
287                        if nulls > 0 && index % nulls == 0 {
288                            Value::Null
289                        } else {
290                            Value::Boolean(rng.next() % 100 < share)
291                        }
292                    })
293                    .collect();
294                let vector = flags(&values);
295                let note = format!("{share} percent true, one null in {nulls}");
296                assert_eq!(selection(&vector, 251), oracle(&vector, 251), "{note}, flat");
297                let codes: Vec<u32> = (0..251).map(|index| (index % 37) as u32).collect();
298                let coded = Vector::dictionary(codes, vector).expect("codes are in range");
299                assert_eq!(selection(&coded, 251), oracle(&coded, 251), "{note}, dictionary");
300            }
301        }
302    }
303
304    /// The rows of a selection the row at a time path keeps, which is what [`refine`] has to say.
305    fn within(vector: &Vector, kept: &Selection) -> Selection {
306        let mut out = Vec::new();
307        // row at a time: the oracle the threaded loop is checked against, which is the whole
308        // reason the row at a time path is kept rather than deleted.
309        for &row in kept.indices() {
310            if is_true(&vector.value_at(row as usize)) {
311                out.push(row);
312            }
313        }
314        Selection::from_indices(out)
315    }
316
317    /// Threading a selection through a flag vector is the rows a full pass would have kept that
318    /// were still in play. Every selectivity, four null densities, both forms with a loop, and the
319    /// four shapes of selection a conjunct chain actually reaches.
320    #[test]
321    fn a_threaded_selection_keeps_what_was_still_in_play_and_true() {
322        let mut rng = Rng(0x5eed_ca11_ab1e_0006);
323        let len = 251;
324        let selections = [
325            Selection::identity(len),
326            Selection::from_indices((0..len as u32).filter(|row| row % 7 == 0).collect()),
327            Selection::from_indices(vec![0, 1, 128, 250]),
328            Selection::empty(),
329        ];
330        for nulls in [0_usize, 8, 3, 1] {
331            for share in [0_u64, 1, 16, 50, 84, 99, 100] {
332                let values: Vec<Value> = (0..len)
333                    .map(|index| {
334                        if nulls > 0 && index % nulls == 0 {
335                            Value::Null
336                        } else {
337                            Value::Boolean(rng.next() % 100 < share)
338                        }
339                    })
340                    .collect();
341                let vector = flags(&values);
342                let codes: Vec<u32> = (0..len).map(|index| (index % 37) as u32).collect();
343                let coded = Vector::dictionary(codes, vector.clone()).expect("codes are in range");
344                let all = Vector::constant(LogicalType::Boolean, YES, len);
345                for kept in &selections {
346                    let note = format!("{share} percent true, one null in {nulls}");
347                    let threaded = refine(&vector, kept).expect("in range");
348                    assert_eq!(threaded, within(&vector, kept), "{note}, flat");
349                    assert_eq!(
350                        refine(&coded, kept).expect("in range"),
351                        within(&coded, kept),
352                        "{note}, dictionary"
353                    );
354                    // A constant true keeps everything that was in play and reads nothing.
355                    assert_eq!(refine(&all, kept).expect("in range"), *kept, "{note}, constant");
356                    // And what a threaded pass keeps is always a subset of what a full pass does.
357                    let full = selection(&vector, len);
358                    assert!(
359                        threaded.indices().iter().all(|row| full.indices().contains(row)),
360                        "{note}, threaded is within the full pass"
361                    );
362                }
363            }
364        }
365    }
366
367    #[test]
368    fn a_threaded_selection_past_the_end_is_caught() {
369        let vector = flags(&[YES, YES]);
370        let past = Selection::from_indices(vec![0, 2]);
371        let error = refine(&vector, &past).expect_err("out of range");
372        assert!(error.message().contains("2 row vector"), "{error}");
373    }
374
375    #[test]
376    fn a_constant_is_answered_without_a_loop_and_a_non_boolean_is_not_answered_at_all() {
377        fallback::reset();
378        let all = Vector::constant(LogicalType::Boolean, YES, 500);
379        assert_eq!(selection(&all, 500), Selection::identity(500));
380        let none = Vector::constant(LogicalType::Boolean, Value::Null, 500);
381        assert!(selection(&none, 500).is_empty());
382        assert_eq!(fallback::count(Kernel::Select, Form::Constant, Form::Constant), 0);
383
384        // Nothing binds a filter to a non boolean, and if something did it would be wrong rather
385        // than fast, so the loop refuses it and the value at a time path decides.
386        let numbers = Vector::from_values(LogicalType::Integer, &[Value::Integer(1)])
387            .expect("a vector of integers");
388        assert!(selection(&numbers, 1).is_empty());
389        assert_eq!(fallback::count(Kernel::Select, Form::Flat, Form::Flat), 1);
390        fallback::reset();
391    }
392
393    /// Fewer rows than the vector holds, which is what a partly filled chunk is.
394    #[test]
395    fn only_the_rows_asked_for_are_looked_at() {
396        let vector = flags(&[YES, YES, YES, YES]);
397        assert_eq!(selection(&vector, 2).indices(), &[0, 1]);
398        // And more rows than there are is the vector's length, not a panic.
399        assert_eq!(selection(&vector, 9).indices(), &[0, 1, 2, 3]);
400    }
401}