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