Skip to main content

rudb_kernels/
membership.rs

1//! `IN` over a list the query wrote out.
2//!
3//! The binder has no `IN` node. `x IN (1, 2, 3)` is bound as `x = 1 OR x = 2 OR x = 3` and
4//! `x NOT IN (1, 2, 3)` as `x <> 1 AND x <> 2 AND x <> 3`, which is the right thing for the binder
5//! to do because it means nothing after it has to know a second set of rules for null. What it
6//! costs is a pass over the column and an output vector per list entry, and TPC-H query 16 has
7//! eight entries in one list.
8//!
9//! This file is the other end of that. A caller that can see the whole conjunction folds it back
10//! into a set, and then the column is read once and each row is one lookup. What is being removed
11//! is the pass and the allocation per entry rather than the comparison per row, which is why two
12//! entries is already worth folding rather than four or eight.
13//!
14//! # What it will not fold
15//!
16//! Whole numbers and strings, and nothing else. A float will not fold because DuckDB's `=` on a
17//! float is not the equality a hash set has: it says that two nans are equal and that a positive
18//! and a negative zero are equal, and the second one also breaks hashing rather than only the
19//! comparison. A decimal will not fold because two unscaled integers at two scales are the same
20//! number, and the check that the scales match is not worth writing for a list nobody writes. An
21//! interval will not fold because interval equality is by length and a month is not thirty days.
22//! Anything this refuses stays the `OR` the binder built, which is correct and is counted.
23
24use std::collections::HashSet;
25
26use rudb_common::{LogicalType, Result, Value};
27use rudb_vector::{Data, Form, Validity, Vector};
28
29use crate::shape::{first, identity, nulls_of, single};
30
31/// The list of an `IN`, in the shape a loop can look a row up in.
32///
33/// Built once when the pipeline is built, because the list is literals the user wrote and cannot
34/// change from chunk to chunk. This is the same idea as [`crate::prepare`] and is a separate type
35/// only because an `IN` is not a function call by the time it reaches here.
36#[derive(Debug)]
37pub struct Members {
38    held: Held,
39    /// Whether the list held a null.
40    ///
41    /// A row that is not in the list is null rather than false when it did, because the row might
42    /// have equalled whatever the null stands for. This is the whole of the difference between an
43    /// `IN` and a set lookup and it is the thing a hand written version gets wrong.
44    has_null: bool,
45    /// Whether this was a `NOT IN`, which the binder wrote as an `AND` of inequalities.
46    negated: bool,
47}
48
49/// The set itself, in the one layout per kind of value that hashes the way SQL compares.
50#[derive(Debug)]
51enum Held {
52    /// Every integral type and the three whole calendar ones, widened to the widest signed integer.
53    /// Widening is exact for all of them, and the binder has already cast the column and the list
54    /// to one type, so two entries that differ here differ in SQL too.
55    Whole(HashSet<i128>),
56    /// Strings, compared by bytes, which is what DuckDB's `=` on a varchar does.
57    Text(HashSet<String>),
58}
59
60impl Members {
61    /// The list as a set, or `None` for a list this file will not fold.
62    ///
63    /// `None` covers a list of fewer than two entries, which is not worth a set, a list holding a
64    /// kind of value that does not hash the way SQL compares, and a list mixing two kinds, which
65    /// the binder does not produce but which is cheaper to refuse than to reason about.
66    #[must_use]
67    pub fn of(values: &[Value], negated: bool) -> Option<Self> {
68        if values.len() < 2 {
69            return None;
70        }
71        let mut whole: HashSet<i128> = HashSet::new();
72        let mut text: HashSet<String> = HashSet::new();
73        let mut has_null = false;
74        let mut kind: Option<std::mem::Discriminant<Value>> = None;
75        for value in values {
76            if matches!(value, Value::Null) {
77                has_null = true;
78                continue;
79            }
80            // One kind for the whole list. The binder casts every entry to the type the comparison
81            // happens at, so a list that reaches here is already uniform, and a list that is not is
82            // one this file has no business guessing about.
83            let held = std::mem::discriminant(value);
84            if *kind.get_or_insert(held) != held {
85                return None;
86            }
87            match value {
88                Value::Varchar(held) => {
89                    text.insert(held.clone());
90                }
91                other => {
92                    whole.insert(number(other)?);
93                }
94            }
95        }
96        let held = if text.is_empty() {
97            if whole.is_empty() {
98                // Every entry was null, so every row is null and there is nothing to look up. Rare
99                // enough that the `OR` can have it.
100                return None;
101            }
102            Held::Whole(whole)
103        } else {
104            Held::Text(text)
105        };
106        Some(Self { held, has_null, negated })
107    }
108
109    /// How many distinct values the list holds, for a caller that wants to say so.
110    #[must_use]
111    pub fn len(&self) -> usize {
112        match &self.held {
113            Held::Whole(set) => set.len(),
114            Held::Text(set) => set.len(),
115        }
116    }
117
118    /// Whether the list holds no value at all, which [`Members::of`] never builds.
119    #[must_use]
120    pub fn is_empty(&self) -> bool {
121        self.len() == 0
122    }
123}
124
125/// A value as the integer the set is keyed on, or `None` for a kind that does not belong in one.
126fn number(value: &Value) -> Option<i128> {
127    match *value {
128        Value::TinyInt(held) => Some(i128::from(held)),
129        Value::SmallInt(held) => Some(i128::from(held)),
130        Value::Integer(held) | Value::Date(held) => Some(i128::from(held)),
131        Value::BigInt(held) | Value::Time(held) | Value::Timestamp(held) => Some(i128::from(held)),
132        Value::HugeInt(held) => Some(held),
133        Value::UTinyInt(held) => Some(i128::from(held)),
134        Value::USmallInt(held) => Some(i128::from(held)),
135        Value::UInteger(held) => Some(i128::from(held)),
136        Value::UBigInt(held) => Some(i128::from(held)),
137        _ => None,
138    }
139}
140
141/// Which rows of `input` are in the list.
142///
143/// # Errors
144///
145/// If the answer vector cannot be built, which is the same check every kernel here makes.
146pub fn in_set(input: &Vector, members: &Members, returns: &LogicalType) -> Result<Vector> {
147    let rows = input.len();
148    let base = nulls_of(input);
149    match input.form() {
150        Form::Flat => match input.data() {
151            Some(data) => look(data, identity, members, &base, rows, returns),
152            None => row_at_a_time(input, members, &base, rows, returns),
153        },
154        Form::Dictionary | Form::Rle => {
155            let Some((codes, values)) = input.positions() else {
156                return row_at_a_time(input, members, &base, rows, returns);
157            };
158            let Some(data) = values.data().filter(|_| codes.len() >= rows) else {
159                return row_at_a_time(input, members, &base, rows, returns);
160            };
161            let at = move |index: usize| codes[index] as usize;
162            look(data, at, members, &base, rows, returns)
163        }
164        Form::Constant => {
165            let Some(value) = input.constant_value() else {
166                return row_at_a_time(input, members, &base, rows, returns);
167            };
168            let Some(held) = single(input.logical_type(), value) else {
169                return row_at_a_time(input, members, &base, rows, returns);
170            };
171            match held.data() {
172                Some(data) => look(data, first, members, &base, rows, returns),
173                None => row_at_a_time(input, members, &base, rows, returns),
174            }
175        }
176        _ => row_at_a_time(input, members, &base, rows, returns),
177    }
178}
179
180/// The lookup loop, once per physical layout the column can arrive in.
181///
182/// The index mapping is a generic parameter rather than a function pointer for the reason the
183/// `by_form` macro in `scalar` gives, which is that a function pointer here is an indirect call per
184/// row.
185fn look<A: Fn(usize) -> usize>(
186    data: &Data,
187    at: A,
188    members: &Members,
189    base: &Validity,
190    rows: usize,
191    returns: &LogicalType,
192) -> Result<Vector> {
193    match (&members.held, data) {
194        (Held::Text(set), Data::Varlen(column)) => answer(rows, base, members, returns, |index| {
195            column.get(at(index)).is_some_and(|text| set.contains(text))
196        }),
197        (Held::Whole(set), Data::Int8(held)) => {
198            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
199        }
200        (Held::Whole(set), Data::Int16(held)) => {
201            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
202        }
203        (Held::Whole(set), Data::Int32(held)) => {
204            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
205        }
206        (Held::Whole(set), Data::Int64(held)) => {
207            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
208        }
209        (Held::Whole(set), Data::Int128(held)) => {
210            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
211        }
212        (Held::Whole(set), Data::UInt8(held)) => {
213            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
214        }
215        (Held::Whole(set), Data::UInt16(held)) => {
216            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
217        }
218        (Held::Whole(set), Data::UInt32(held)) => {
219            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
220        }
221        (Held::Whole(set), Data::UInt64(held)) => {
222            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
223        }
224        // A layout the set cannot be keyed on, which means the list and the column disagree about
225        // what they hold. `Members::of` refuses the lists that would get here, so this is the arm
226        // that keeps that true rather than assumed.
227        _ => Err(rudb_common::Error::internal(format!(
228            "an IN list over a column this kernel does not read, which is {returns}"
229        ))),
230    }
231}
232
233/// Whether the set holds the value at `index`, for any integer narrower than the key.
234fn holds<T: Copy>(set: &HashSet<i128>, values: &[T], index: usize) -> bool
235where
236    i128: From<T>,
237{
238    values.get(index).is_some_and(|&held| set.contains(&i128::from(held)))
239}
240
241/// The answer, given a lookup that says whether a row is in the list.
242fn answer(
243    rows: usize,
244    base: &Validity,
245    members: &Members,
246    returns: &LogicalType,
247    found: impl Fn(usize) -> bool,
248) -> Result<Vector> {
249    let mut out = vec![false; rows];
250    let mut live = vec![false; rows];
251    for index in 0..rows {
252        if !base.is_valid(index) {
253            continue;
254        }
255        let hit = found(index);
256        // A miss against a list with a null in it is null and not false, because the row might have
257        // equalled whatever that null stands for. A hit is a hit whatever else the list holds.
258        live[index] = hit || !members.has_null;
259        out[index] = hit != members.negated;
260    }
261    let validity = Validity::from_run(&live).normalize(rows);
262    Ok(Vector::flat(returns.clone(), Data::Bool(out.into()))?.with_validity(validity))
263}
264
265/// The path for a form or a layout with no loop above, which reads a value per row.
266///
267/// It counts itself nowhere, because there is nothing here for the fallback table to tell anybody:
268/// `Members::of` decides what folds, so a column that reaches this is one the fold should not have
269/// happened for, and the answer to that is a line in `Members::of` rather than a number in a report.
270fn row_at_a_time(
271    input: &Vector,
272    members: &Members,
273    base: &Validity,
274    rows: usize,
275    returns: &LogicalType,
276) -> Result<Vector> {
277    let held: Vec<Value> = (0..rows).map(|index| input.value_at(index)).collect();
278    answer(rows, base, members, returns, |index| match (&members.held, &held[index]) {
279        (Held::Text(set), Value::Varchar(text)) => set.contains(text.as_str()),
280        (Held::Whole(set), value) => number(value).is_some_and(|held| set.contains(&held)),
281        _ => false,
282    })
283}
284
285#[cfg(test)]
286mod tests {
287    use rudb_common::{LogicalType, Value};
288    use rudb_vector::Vector;
289
290    use super::{Members, in_set};
291
292    /// What the kernel answers for each row, as the values a caller would read back.
293    fn over(input: &Vector, list: &[Value], negated: bool) -> Vec<Value> {
294        let members = Members::of(list, negated).expect("this list folds");
295        let answer = in_set(input, &members, &LogicalType::Boolean).expect("the lookup runs");
296        (0..input.len()).map(|row| answer.value_at(row)).collect()
297    }
298
299    fn numbers() -> Vector {
300        Vector::from_values(
301            LogicalType::Integer,
302            &[Value::Integer(1), Value::Integer(7), Value::Null, Value::Integer(3)],
303        )
304        .expect("four integers")
305    }
306
307    #[test]
308    fn a_row_in_the_list_is_true_and_a_row_outside_it_is_false() {
309        assert_eq!(
310            over(&numbers(), &[Value::Integer(1), Value::Integer(3)], false),
311            [Value::Boolean(true), Value::Boolean(false), Value::Null, Value::Boolean(true)]
312        );
313    }
314
315    #[test]
316    fn a_not_in_is_the_same_lookup_read_the_other_way() {
317        assert_eq!(
318            over(&numbers(), &[Value::Integer(1), Value::Integer(3)], true),
319            [Value::Boolean(false), Value::Boolean(true), Value::Null, Value::Boolean(false)]
320        );
321    }
322
323    /// The rule that separates a set lookup from an `IN`. `7 IN (1, NULL)` is null rather than
324    /// false, because the row might have equalled whatever the null stands for, and `7 NOT IN
325    /// (1, NULL)` is null for the same reason.
326    #[test]
327    fn a_miss_against_a_list_with_a_null_in_it_is_null() {
328        let list = [Value::Integer(1), Value::Null, Value::Integer(3)];
329        assert_eq!(
330            over(&numbers(), &list, false),
331            [Value::Boolean(true), Value::Null, Value::Null, Value::Boolean(true)]
332        );
333        assert_eq!(
334            over(&numbers(), &list, true),
335            [Value::Boolean(false), Value::Null, Value::Null, Value::Boolean(false)]
336        );
337    }
338
339    #[test]
340    fn a_dictionary_column_is_read_through_its_codes() {
341        let values = Vector::from_values(
342            LogicalType::Varchar,
343            &[Value::Varchar("a".into()), Value::Varchar("b".into()), Value::Null],
344        )
345        .expect("builds");
346        let text = Vector::dictionary(vec![0, 2, 1, 0], values).expect("codes are in range");
347        let list = [Value::Varchar("a".into()), Value::Varchar("c".into())];
348        assert_eq!(
349            over(&text, &list, false),
350            [Value::Boolean(true), Value::Null, Value::Boolean(false), Value::Boolean(true)]
351        );
352    }
353
354    #[test]
355    fn a_constant_column_answers_every_row_the_same() {
356        let held = Vector::constant(LogicalType::Integer, Value::Integer(3), 3);
357        let list = [Value::Integer(1), Value::Integer(3)];
358        assert_eq!(over(&held, &list, false), vec![Value::Boolean(true); 3]);
359    }
360
361    #[test]
362    fn a_run_length_column_reads_the_same_as_the_flat_one_it_stands_for() {
363        let flat = Vector::from_values(
364            LogicalType::Integer,
365            &[Value::Integer(1), Value::Integer(1), Value::Integer(7), Value::Integer(7)],
366        )
367        .expect("four integers");
368        let runs = flat.clone().run_encoded().expect("two runs");
369        let list = [Value::Integer(1), Value::Integer(3)];
370        assert_eq!(over(&runs, &list, false), over(&flat, &list, false));
371    }
372
373    #[test]
374    fn a_list_of_one_is_left_alone_because_a_comparison_is_already_that() {
375        assert!(Members::of(&[Value::Integer(1)], false).is_none());
376    }
377
378    #[test]
379    fn a_list_of_floats_does_not_fold() {
380        // Two nans are equal to DuckDB's `=` and not to a hash set, and a positive and a negative
381        // zero are equal to both but hash differently. Neither is worth a special case.
382        assert!(Members::of(&[Value::Double(1.0), Value::Double(2.0)], false).is_none());
383    }
384
385    #[test]
386    fn a_list_of_two_kinds_does_not_fold() {
387        let mixed = [Value::Integer(1), Value::Varchar("a".into())];
388        assert!(Members::of(&mixed, false).is_none());
389    }
390
391    #[test]
392    fn a_list_of_nothing_but_nulls_does_not_fold() {
393        assert!(Members::of(&[Value::Null, Value::Null], false).is_none());
394    }
395
396    #[test]
397    fn a_list_says_how_many_distinct_values_it_holds() {
398        let list = [Value::Integer(1), Value::Integer(1), Value::Integer(2), Value::Null];
399        let members = Members::of(&list, false).expect("this list folds");
400        assert_eq!(members.len(), 2);
401        assert!(!members.is_empty());
402    }
403}