Skip to main content

rudb_kernels/
logic.rs

1//! `AND` and `OR` over three values.
2//!
3//! The two rules that matter are that `FALSE AND NULL` is false and `TRUE OR NULL` is true. Both
4//! are the reason a conjunction cannot be evaluated by treating null as false and then fixing it up
5//! afterwards: `NULL AND FALSE` is false, `NULL AND TRUE` is null, and no single substitution for
6//! null gets both.
7//!
8//! A conjunction here is flat, over two or more children, because that is the shape the binder
9//! produces and the shape filter pushdown wants. Evaluating a flat one is a fold with an early
10//! answer, which is also why the null case is cheap: once a false has been seen in an `AND` nothing
11//! any other child says can change the result.
12//!
13//! # How the vectorized path is put together
14//!
15//! The fold above is stated per row, and per row is exactly what it must not be. The shape that
16//! runs fast is the same fold turned inside out: one pass per child over all the rows, carrying two
17//! boolean runs rather than one three-valued answer.
18//!
19//! The first run says a child has already produced the value that decides the answer, which is
20//! false for `AND` and true for `OR`. The second says a child was null. A row where the first is set
21//! is the deciding value and is not null, however many nulls it saw. A row where only the second is
22//! set is null. A row where neither is set is the other value. That is the whole of three-valued
23//! logic with no branch in it, because both runs are accumulated with `or` rather than tested.
24//!
25//! Writing it that way also makes the number of children free. Ten conjuncts are ten passes over a
26//! run of bytes that stays in L1, rather than ten `Value` constructions per row, and the pass for a
27//! child whose validity is `AllInvalid` does not look at the data at all: every row it can speak to
28//! is unknown, so it sets the second run and returns.
29//!
30//! A constant child does not get a pass. It either decides every row, in which case one `fill` says
31//! so, or it decides none of them, in which case it is dropped. `WHERE a AND true` costs nothing
32//! after binding, which matters because that is the shape a pushed down filter with one conjunct
33//! removed actually has.
34
35use rudb_common::{Error, LogicalType, Result, Value};
36use rudb_vector::{Buffer, Data, Form, Validity, Vector};
37
38use crate::fallback::{self, Kernel};
39use crate::shape::{identity, nulls_of};
40
41/// Which connective.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum Connective {
44    /// `AND`.
45    And,
46    /// `OR`.
47    Or,
48}
49
50/// Combines two or more boolean vectors.
51///
52/// # Errors
53///
54/// If there are no children, if they are not all the same length, or if one of them is not boolean.
55pub fn combine(op: Connective, children: &[Vector]) -> Result<Vector> {
56    let first =
57        children.first().ok_or_else(|| Error::internal("a conjunction with no children"))?;
58    let rows = first.len();
59    for (at, child) in children.iter().enumerate() {
60        if child.len() != rows {
61            return Err(Error::internal(format!(
62                "child {at} of a conjunction is {} rows and child 0 is {rows}",
63                child.len()
64            )));
65        }
66    }
67    if let Some(vector) = folded(op, children, rows) {
68        return Ok(vector);
69    }
70    let left = first.form();
71    fallback::record(Kernel::Logic, left, children.get(1).map_or(left, Vector::form));
72    let mut values = Vec::with_capacity(rows);
73    // row at a time: the path recorded on the line above, which exists to be correct for a set of
74    // forms `folded` does not cover and counts itself so that set shows up.
75    for index in 0..rows {
76        let mut answer = Some(matches!(op, Connective::And));
77        for child in children {
78            let held = match child.value_at(index) {
79                Value::Boolean(held) => Some(held),
80                Value::Null => None,
81                other => {
82                    return Err(Error::internal(format!(
83                        "a conjunction over a {} value",
84                        other.logical_type()
85                    )));
86                }
87            };
88            answer = fold(op, answer, held);
89        }
90        values.push(match answer {
91            Some(held) => Value::Boolean(held),
92            None => Value::Null,
93        });
94    }
95    Vector::from_values(LogicalType::Boolean, &values)
96}
97
98/// The vectorized fold, or `None` for a shape it does not handle.
99///
100/// The connective becomes a constant generic here and nowhere else. Which value decides the answer
101/// is the only thing that differs between `AND` and `OR` in the loop below, and passing it as a
102/// value would put a comparison against it inside the loop for something that cannot change while
103/// the loop runs.
104fn folded(op: Connective, children: &[Vector], rows: usize) -> Option<Vector> {
105    match op {
106        Connective::And => fold_runs::<false>(children, rows),
107        Connective::Or => fold_runs::<true>(children, rows),
108    }
109}
110
111/// One pass per child, carrying the run that says decided and the run that says unknown.
112///
113/// `DOMINANT` is the value that ends the question for a row: false for `AND`, true for `OR`.
114fn fold_runs<const DOMINANT: bool>(children: &[Vector], rows: usize) -> Option<Vector> {
115    if rows == 0 {
116        // What `from_values` builds from no values at all, which is the empty run rather than
117        // `Data::Empty` and validity that normalizes to all valid. Written out rather than reached
118        // by falling through so that an empty chunk does not show up in the fallback counters as a
119        // form pair worth specializing.
120        return Vector::flat(LogicalType::Boolean, Data::Bool(Buffer::new())).ok();
121    }
122    // A child that is not boolean is an error the row at a time path raises with the type in the
123    // message, and it raises it only for rows that are not null, so the fast path cannot answer for
124    // it at all. It hands the whole call back rather than guessing.
125    if children.iter().any(|child| child.logical_type() != &LogicalType::Boolean) {
126        return None;
127    }
128
129    let mut decided = vec![false; rows];
130    let mut unknown = vec![false; rows];
131    let mut nullable = false;
132
133    for child in children {
134        let nulls = nulls_of(child);
135        nullable |= nulls.has_nulls(rows);
136        match child.form() {
137            Form::Constant => match child.value_at(0) {
138                Value::Boolean(held) if held == DOMINANT => decided.fill(true),
139                Value::Boolean(_) => {}
140                Value::Null => unknown.fill(true),
141                _ => return None,
142            },
143            Form::Flat => {
144                let Some(Data::Bool(values)) = child.data() else {
145                    return None;
146                };
147                if values.len() < rows {
148                    return None;
149                }
150                absorb::<DOMINANT, _>(values, identity, &nulls, &mut decided, &mut unknown);
151            }
152            Form::Dictionary => {
153                let (codes, values) = child.dictionary_parts()?;
154                let Some(Data::Bool(held)) = values.data() else {
155                    return None;
156                };
157                if codes.len() < rows {
158                    return None;
159                }
160                absorb::<DOMINANT, _>(
161                    held,
162                    |index| codes[index] as usize,
163                    &nulls,
164                    &mut decided,
165                    &mut unknown,
166                );
167            }
168            _ => return None,
169        }
170    }
171
172    // All valid against all valid is the discriminant comparison the whole run was tracking, and it
173    // skips this pass entirely rather than walking a bitmap that is going to say valid every time.
174    let validity = if nullable {
175        // Packed a word at a time from the two runs, because building it a bit at a time is a read
176        // modify write per row that depends on the row before it.
177        let live: Vec<bool> =
178            decided.iter().zip(&unknown).map(|(&hit, &null)| hit || !null).collect();
179        Validity::from_run(&live)
180    } else {
181        Validity::AllValid
182    };
183    let data = if DOMINANT {
184        decided
185    } else {
186        // The filler under a null has to be the false that `push_value` writes, and a row that is
187        // unknown is a row nothing decided, so the same expression produces both.
188        decided.iter().zip(&unknown).map(|(&hit, &null)| !(hit | null)).collect()
189    };
190    Some(Vector::flat(LogicalType::Boolean, Data::Bool(data.into())).ok()?.with_validity(validity))
191}
192
193/// Folds one child into the two runs.
194///
195/// `at` is a generic parameter rather than a function pointer, so that the flat pass and the
196/// dictionary pass are two monomorphizations with the indexing inlined into each rather than one
197/// loop with an indirect call in it. That difference measured at ten nanoseconds a row in
198/// `scalar.rs` and there is no reason to rediscover it here.
199fn absorb<const DOMINANT: bool, M: Fn(usize) -> usize>(
200    values: &[bool],
201    at: M,
202    nulls: &Validity,
203    decided: &mut [bool],
204    unknown: &mut [bool],
205) {
206    match nulls {
207        Validity::AllValid => {
208            for (index, slot) in decided.iter_mut().enumerate() {
209                *slot |= values[at(index)] == DOMINANT;
210            }
211        }
212        // Every row this child could speak to is unknown, so the data is not read at all.
213        Validity::AllInvalid => unknown.fill(true),
214        Validity::Mask(mask) => {
215            // Sixty four rows to a word, so the validity bits cost one load for the run rather
216            // than a bounds check and a shift each.
217            for (word_at, (hits, nulls)) in
218                decided.chunks_mut(64).zip(unknown.chunks_mut(64)).enumerate()
219            {
220                let word = mask.word(word_at);
221                let base = word_at * 64;
222                for (bit, (hit, null)) in hits.iter_mut().zip(nulls.iter_mut()).enumerate() {
223                    let valid = word >> bit & 1 == 1;
224                    // Not `&&`, because a branch per row on the validity bit is the thing being
225                    // removed, and the index is in range for a null row as much as for a live one.
226                    *hit |= valid & (values[at(base + bit)] == DOMINANT);
227                    *null |= !valid;
228                }
229            }
230        }
231    }
232}
233
234/// One step of the fold, where `None` is unknown.
235///
236/// The short circuit is on the value rather than on the position: a false anywhere in an `AND`
237/// wins over an unknown that came before it, which is exactly the case a two-valued fold gets
238/// wrong.
239fn fold(op: Connective, left: Option<bool>, right: Option<bool>) -> Option<bool> {
240    match op {
241        Connective::And => match (left, right) {
242            (Some(false), _) | (_, Some(false)) => Some(false),
243            (Some(true), Some(true)) => Some(true),
244            _ => None,
245        },
246        Connective::Or => match (left, right) {
247            (Some(true), _) | (_, Some(true)) => Some(true),
248            (Some(false), Some(false)) => Some(false),
249            _ => None,
250        },
251    }
252}
253
254/// Whether a predicate keeps a row.
255///
256/// True keeps it, and false and null both drop it. That is `WHERE`'s rule and it is not `CHECK`'s,
257/// which keeps a row whose constraint is unknown.
258#[must_use]
259pub fn is_true(value: &Value) -> bool {
260    matches!(value, Value::Boolean(true))
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    fn vector(values: &[Value]) -> Vector {
268        Vector::from_values(LogicalType::Boolean, values).expect("booleans")
269    }
270
271    const TRUE: Value = Value::Boolean(true);
272    const FALSE: Value = Value::Boolean(false);
273
274    #[test]
275    fn a_false_wins_an_and_even_against_an_unknown() {
276        let result = combine(Connective::And, &[vector(&[Value::Null]), vector(&[FALSE])])
277            .expect("two booleans");
278        assert_eq!(result.value_at(0), FALSE);
279    }
280
281    #[test]
282    fn a_true_wins_an_or_even_against_an_unknown() {
283        let result = combine(Connective::Or, &[vector(&[Value::Null]), vector(&[TRUE])])
284            .expect("two booleans");
285        assert_eq!(result.value_at(0), TRUE);
286    }
287
288    #[test]
289    fn an_unknown_survives_when_nothing_decides_it() {
290        let result = combine(Connective::And, &[vector(&[Value::Null]), vector(&[TRUE])])
291            .expect("two booleans");
292        assert_eq!(result.value_at(0), Value::Null);
293        let result = combine(Connective::Or, &[vector(&[Value::Null]), vector(&[FALSE])])
294            .expect("two booleans");
295        assert_eq!(result.value_at(0), Value::Null);
296    }
297
298    #[test]
299    fn a_flat_conjunction_of_more_than_two_children_is_one_pass() {
300        let result = combine(
301            Connective::And,
302            &[vector(&[TRUE]), vector(&[TRUE]), vector(&[TRUE]), vector(&[FALSE])],
303        )
304        .expect("four booleans");
305        assert_eq!(result.value_at(0), FALSE);
306    }
307
308    #[test]
309    fn a_where_clause_drops_the_rows_it_cannot_decide() {
310        assert!(is_true(&TRUE));
311        assert!(!is_true(&FALSE));
312        assert!(!is_true(&Value::Null));
313    }
314
315    #[test]
316    fn a_conjunction_with_no_children_is_caught() {
317        let error = combine(Connective::And, &[]).expect_err("nothing to combine");
318        assert!(error.message().contains("no children"), "{error}");
319    }
320
321    /// The loop this file used to be, kept verbatim as the thing the fast path is checked against.
322    ///
323    /// It is the oracle rather than dead code. Every property test below runs both and compares
324    /// whole vectors, so a disagreement about which rows are null, or about the filler stored under
325    /// a null, is a failure and not something that has to be noticed by eye later.
326    fn oracle(op: Connective, children: &[Vector]) -> Result<Vector> {
327        let rows = children.first().map_or(0, Vector::len);
328        let mut values = Vec::with_capacity(rows);
329        for index in 0..rows {
330            let mut answer = Some(matches!(op, Connective::And));
331            for child in children {
332                let held = match child.value_at(index) {
333                    Value::Boolean(held) => Some(held),
334                    Value::Null => None,
335                    other => {
336                        return Err(Error::internal(format!(
337                            "a conjunction over a {} value",
338                            other.logical_type()
339                        )));
340                    }
341                };
342                answer = fold(op, answer, held);
343            }
344            values.push(match answer {
345                Some(held) => Value::Boolean(held),
346                None => Value::Null,
347            });
348        }
349        Vector::from_values(LogicalType::Boolean, &values)
350    }
351
352    fn agrees(op: Connective, children: &[Vector]) {
353        let fast = combine(op, children);
354        let slow = oracle(op, children);
355        match (fast, slow) {
356            (Ok(fast), Ok(slow)) => assert_eq!(fast, slow, "{op:?} over {children:?}"),
357            (Err(fast), Err(slow)) => {
358                assert_eq!(fast.message(), slow.message(), "{op:?} over {children:?}");
359            }
360            (fast, slow) => panic!("{op:?} over {children:?} gave {fast:?} and {slow:?}"),
361        }
362    }
363
364    /// Reproducible noise. The seed is written down so a failure is a failure twice.
365    struct Rng(u64);
366
367    impl Rng {
368        fn next(&mut self) -> u64 {
369            self.0 ^= self.0 << 13;
370            self.0 ^= self.0 >> 7;
371            self.0 ^= self.0 << 17;
372            self.0
373        }
374    }
375
376    /// A boolean vector of `rows` rows with one null in `nulls` when `nulls` is not zero.
377    fn sample(rng: &mut Rng, rows: usize, nulls: u64) -> Vector {
378        let values: Vec<Value> = (0..rows)
379            .map(|_| {
380                let draw = rng.next();
381                if nulls > 0 && draw % nulls == 0 {
382                    Value::Null
383                } else {
384                    Value::Boolean(draw % 2 == 0)
385                }
386            })
387            .collect();
388        vector(&values)
389    }
390
391    #[test]
392    fn every_form_and_null_density_agrees_with_the_row_at_a_time_path() {
393        let mut rng = Rng(0x5eed_1eaf_c0ff_ee01);
394        let rows = 97;
395        for op in [Connective::And, Connective::Or] {
396            for nulls in [0, 2, 7] {
397                let flat = sample(&mut rng, rows, nulls);
398                let other = sample(&mut rng, rows, nulls);
399                let third = sample(&mut rng, rows, nulls);
400
401                // Flat against flat, which is what a scan produces.
402                agrees(op, &[flat.clone(), other.clone()]);
403                // A flat conjunction of more than two, which is what the binder produces.
404                agrees(op, &[flat.clone(), other.clone(), third.clone()]);
405                // One child on its own, which is what a filter with one conjunct is.
406                agrees(op, std::slice::from_ref(&flat));
407
408                // Every constant a boolean column can be, on both sides.
409                for held in [TRUE, FALSE, Value::Null] {
410                    let constant = Vector::constant(LogicalType::Boolean, held, rows);
411                    agrees(op, &[flat.clone(), constant.clone()]);
412                    agrees(op, &[constant.clone(), flat.clone()]);
413                    agrees(op, &[constant.clone(), flat.clone(), other.clone()]);
414                }
415
416                // A dictionary, whose nulls live in the vector it points at rather than in its own
417                // validity, which is the wrong answer this crate is most likely to produce.
418                let dictionary = Vector::dictionary(
419                    (0..rows)
420                        .map(|index| u32::try_from(index % 3).expect("a code under three"))
421                        .collect(),
422                    vector(&[TRUE, FALSE, Value::Null]),
423                )
424                .expect("three codes into three values");
425                agrees(op, &[dictionary.clone(), flat.clone()]);
426                agrees(op, &[flat.clone(), dictionary.clone()]);
427                agrees(op, &[dictionary.clone(), dictionary.clone()]);
428            }
429        }
430    }
431
432    #[test]
433    fn a_child_that_is_all_null_still_lets_a_decided_row_through() {
434        // The pass for an all invalid child does not read its data at all, and the risk in that is
435        // forgetting that a false elsewhere still decides the row. `NULL AND FALSE` is false.
436        let rows = 8;
437        let gone = Vector::constant(LogicalType::Boolean, Value::Null, rows);
438        let mixed = vector(&[TRUE, FALSE, TRUE, FALSE, TRUE, FALSE, TRUE, FALSE]);
439        agrees(Connective::And, &[gone.clone(), mixed.clone()]);
440        agrees(Connective::Or, &[gone.clone(), mixed.clone()]);
441        let result = combine(Connective::And, &[gone, mixed]).expect("two booleans");
442        assert_eq!(result.value_at(0), Value::Null);
443        assert_eq!(result.value_at(1), FALSE);
444    }
445
446    #[test]
447    fn an_empty_conjunction_of_empty_children_is_an_empty_answer() {
448        let empty = vector(&[]);
449        agrees(Connective::And, &[empty.clone(), empty.clone()]);
450        agrees(Connective::Or, &[empty.clone(), empty]);
451    }
452
453    #[test]
454    fn a_child_that_is_not_boolean_is_still_caught_by_name() {
455        let numbers = Vector::from_values(
456            LogicalType::Integer,
457            &[Value::Integer(1), Value::Integer(0), Value::Integer(3)],
458        )
459        .expect("integers");
460        let error = combine(Connective::And, &[vector(&[TRUE, TRUE, TRUE]), numbers])
461            .expect_err("a conjunction over integers");
462        assert!(error.message().contains("conjunction over"), "{error}");
463        assert!(error.message().contains("INTEGER"), "{error}");
464    }
465
466    #[test]
467    fn a_form_pair_with_no_loop_is_still_right_and_says_so() {
468        // The counters are process wide and another test in this crate resets them, so the ones
469        // that read a count take turns.
470        let _turn = fallback::TURN.lock().expect("no test panics while holding this");
471        let before = fallback::count(Kernel::Logic, Form::Sequence, Form::Flat);
472        let rows = 4;
473        let ids = Vector::sequence(0, 1, rows);
474        let flat = vector(&[TRUE, FALSE, TRUE, FALSE]);
475        // A sequence is a run of integers whatever anybody wants it to be, so this is the error
476        // path, and it has to be the same error the row at a time path raises.
477        let error = combine(Connective::And, &[ids, flat]).expect_err("a conjunction over bigints");
478        assert!(error.message().contains("conjunction over"), "{error}");
479        assert!(fallback::count(Kernel::Logic, Form::Sequence, Form::Flat) > before);
480    }
481
482    #[test]
483    fn a_children_length_mismatch_names_the_child_that_is_wrong() {
484        let error = combine(Connective::And, &[vector(&[TRUE, TRUE]), vector(&[TRUE])])
485            .expect_err("two lengths");
486        assert!(error.message().contains("child 1"), "{error}");
487    }
488}