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)
132        | Value::Time(held)
133        | Value::TimeTz(held)
134        | Value::Timestamp(held)
135        | Value::TimestampTz(held) => Some(i128::from(held)),
136        Value::HugeInt(held) => Some(held),
137        Value::UTinyInt(held) => Some(i128::from(held)),
138        Value::USmallInt(held) => Some(i128::from(held)),
139        Value::UInteger(held) => Some(i128::from(held)),
140        Value::UBigInt(held) => Some(i128::from(held)),
141        _ => None,
142    }
143}
144
145/// Which rows of `input` are in the list.
146///
147/// # Errors
148///
149/// If the answer vector cannot be built, which is the same check every kernel here makes.
150pub fn in_set(input: &Vector, members: &Members, returns: &LogicalType) -> Result<Vector> {
151    let rows = input.len();
152    let base = nulls_of(input);
153    match input.form() {
154        Form::Flat => match input.data() {
155            Some(data) => look(data, identity, members, &base, rows, returns),
156            None => row_at_a_time(input, members, &base, rows, returns),
157        },
158        Form::Dictionary | Form::Rle => {
159            let Some((codes, values)) = input.positions() else {
160                return row_at_a_time(input, members, &base, rows, returns);
161            };
162            let Some(data) = values.data().filter(|_| codes.len() >= rows) else {
163                return row_at_a_time(input, members, &base, rows, returns);
164            };
165            let at = move |index: usize| codes[index] as usize;
166            look(data, at, members, &base, rows, returns)
167        }
168        Form::Constant => {
169            let Some(value) = input.constant_value() else {
170                return row_at_a_time(input, members, &base, rows, returns);
171            };
172            let Some(held) = single(input.logical_type(), value) else {
173                return row_at_a_time(input, members, &base, rows, returns);
174            };
175            match held.data() {
176                Some(data) => look(data, first, members, &base, rows, returns),
177                None => row_at_a_time(input, members, &base, rows, returns),
178            }
179        }
180        _ => row_at_a_time(input, members, &base, rows, returns),
181    }
182}
183
184/// The lookup loop, once per physical layout the column can arrive in.
185///
186/// The index mapping is a generic parameter rather than a function pointer for the reason the
187/// `by_form` macro in `scalar` gives, which is that a function pointer here is an indirect call per
188/// row.
189fn look<A: Fn(usize) -> usize>(
190    data: &Data,
191    at: A,
192    members: &Members,
193    base: &Validity,
194    rows: usize,
195    returns: &LogicalType,
196) -> Result<Vector> {
197    match (&members.held, data) {
198        (Held::Text(set), Data::Varlen(column)) => answer(rows, base, members, returns, |index| {
199            column.get(at(index)).is_some_and(|text| set.contains(text))
200        }),
201        (Held::Whole(set), Data::Int8(held)) => {
202            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
203        }
204        (Held::Whole(set), Data::Int16(held)) => {
205            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
206        }
207        (Held::Whole(set), Data::Int32(held)) => {
208            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
209        }
210        (Held::Whole(set), Data::Int64(held)) => {
211            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
212        }
213        (Held::Whole(set), Data::Int128(held)) => {
214            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
215        }
216        (Held::Whole(set), Data::UInt8(held)) => {
217            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
218        }
219        (Held::Whole(set), Data::UInt16(held)) => {
220            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
221        }
222        (Held::Whole(set), Data::UInt32(held)) => {
223            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
224        }
225        (Held::Whole(set), Data::UInt64(held)) => {
226            answer(rows, base, members, returns, |index| holds(set, held.as_slice(), at(index)))
227        }
228        // A layout the set cannot be keyed on, which means the list and the column disagree about
229        // what they hold. `Members::of` refuses the lists that would get here, so this is the arm
230        // that keeps that true rather than assumed.
231        _ => Err(rudb_common::Error::internal(format!(
232            "an IN list over a column this kernel does not read, which is {returns}"
233        ))),
234    }
235}
236
237/// Whether the set holds the value at `index`, for any integer narrower than the key.
238fn holds<T: Copy>(set: &HashSet<i128>, values: &[T], index: usize) -> bool
239where
240    i128: From<T>,
241{
242    values.get(index).is_some_and(|&held| set.contains(&i128::from(held)))
243}
244
245/// The answer, given a lookup that says whether a row is in the list.
246fn answer(
247    rows: usize,
248    base: &Validity,
249    members: &Members,
250    returns: &LogicalType,
251    found: impl Fn(usize) -> bool,
252) -> Result<Vector> {
253    let mut out = vec![false; rows];
254    let mut live = vec![false; rows];
255    for index in 0..rows {
256        if !base.is_valid(index) {
257            continue;
258        }
259        let hit = found(index);
260        // A miss against a list with a null in it is null and not false, because the row might have
261        // equalled whatever that null stands for. A hit is a hit whatever else the list holds.
262        live[index] = hit || !members.has_null;
263        out[index] = hit != members.negated;
264    }
265    let validity = Validity::from_run(&live).normalize(rows);
266    Ok(Vector::flat(returns.clone(), Data::Bool(out.into()))?.with_validity(validity))
267}
268
269/// The path for a form or a layout with no loop above, which reads a value per row.
270///
271/// It counts itself nowhere, because there is nothing here for the fallback table to tell anybody:
272/// `Members::of` decides what folds, so a column that reaches this is one the fold should not have
273/// happened for, and the answer to that is a line in `Members::of` rather than a number in a report.
274fn row_at_a_time(
275    input: &Vector,
276    members: &Members,
277    base: &Validity,
278    rows: usize,
279    returns: &LogicalType,
280) -> Result<Vector> {
281    let held: Vec<Value> =
282        (0..rows).map(|index| input.try_value_at(index)).collect::<Result<_>>()?;
283    answer(rows, base, members, returns, |index| match (&members.held, &held[index]) {
284        (Held::Text(set), Value::Varchar(text)) => set.contains(text.as_str()),
285        (Held::Whole(set), value) => number(value).is_some_and(|held| set.contains(&held)),
286        _ => false,
287    })
288}
289
290#[cfg(test)]
291mod tests {
292    use rudb_common::{LogicalType, Value};
293    use rudb_vector::Vector;
294
295    use super::{Members, in_set};
296
297    /// What the kernel answers for each row, as the values a caller would read back.
298    fn over(input: &Vector, list: &[Value], negated: bool) -> Vec<Value> {
299        let members = Members::of(list, negated).expect("this list folds");
300        let answer = in_set(input, &members, &LogicalType::Boolean).expect("the lookup runs");
301        (0..input.len()).map(|row| answer.value_at(row)).collect()
302    }
303
304    fn numbers() -> Vector {
305        Vector::from_values(
306            LogicalType::Integer,
307            &[Value::Integer(1), Value::Integer(7), Value::Null, Value::Integer(3)],
308        )
309        .expect("four integers")
310    }
311
312    #[test]
313    fn a_row_in_the_list_is_true_and_a_row_outside_it_is_false() {
314        assert_eq!(
315            over(&numbers(), &[Value::Integer(1), Value::Integer(3)], false),
316            [Value::Boolean(true), Value::Boolean(false), Value::Null, Value::Boolean(true)]
317        );
318    }
319
320    #[test]
321    fn a_not_in_is_the_same_lookup_read_the_other_way() {
322        assert_eq!(
323            over(&numbers(), &[Value::Integer(1), Value::Integer(3)], true),
324            [Value::Boolean(false), Value::Boolean(true), Value::Null, Value::Boolean(false)]
325        );
326    }
327
328    /// The rule that separates a set lookup from an `IN`. `7 IN (1, NULL)` is null rather than
329    /// false, because the row might have equalled whatever the null stands for, and `7 NOT IN
330    /// (1, NULL)` is null for the same reason.
331    #[test]
332    fn a_miss_against_a_list_with_a_null_in_it_is_null() {
333        let list = [Value::Integer(1), Value::Null, Value::Integer(3)];
334        assert_eq!(
335            over(&numbers(), &list, false),
336            [Value::Boolean(true), Value::Null, Value::Null, Value::Boolean(true)]
337        );
338        assert_eq!(
339            over(&numbers(), &list, true),
340            [Value::Boolean(false), Value::Null, Value::Null, Value::Boolean(false)]
341        );
342    }
343
344    #[test]
345    fn a_dictionary_column_is_read_through_its_codes() {
346        let values = Vector::from_values(
347            LogicalType::Varchar,
348            &[Value::Varchar("a".into()), Value::Varchar("b".into()), Value::Null],
349        )
350        .expect("builds");
351        let text = Vector::dictionary(vec![0, 2, 1, 0], values).expect("codes are in range");
352        let list = [Value::Varchar("a".into()), Value::Varchar("c".into())];
353        assert_eq!(
354            over(&text, &list, false),
355            [Value::Boolean(true), Value::Null, Value::Boolean(false), Value::Boolean(true)]
356        );
357    }
358
359    #[test]
360    fn a_constant_column_answers_every_row_the_same() {
361        let held = Vector::constant(LogicalType::Integer, Value::Integer(3), 3);
362        let list = [Value::Integer(1), Value::Integer(3)];
363        assert_eq!(over(&held, &list, false), vec![Value::Boolean(true); 3]);
364    }
365
366    #[test]
367    fn a_run_length_column_reads_the_same_as_the_flat_one_it_stands_for() {
368        let flat = Vector::from_values(
369            LogicalType::Integer,
370            &[Value::Integer(1), Value::Integer(1), Value::Integer(7), Value::Integer(7)],
371        )
372        .expect("four integers");
373        let runs = flat.clone().run_encoded().expect("two runs");
374        let list = [Value::Integer(1), Value::Integer(3)];
375        assert_eq!(over(&runs, &list, false), over(&flat, &list, false));
376    }
377
378    #[test]
379    fn a_list_of_one_is_left_alone_because_a_comparison_is_already_that() {
380        assert!(Members::of(&[Value::Integer(1)], false).is_none());
381    }
382
383    #[test]
384    fn a_list_of_floats_does_not_fold() {
385        // Two nans are equal to DuckDB's `=` and not to a hash set, and a positive and a negative
386        // zero are equal to both but hash differently. Neither is worth a special case.
387        assert!(Members::of(&[Value::Double(1.0), Value::Double(2.0)], false).is_none());
388    }
389
390    #[test]
391    fn a_list_of_two_kinds_does_not_fold() {
392        let mixed = [Value::Integer(1), Value::Varchar("a".into())];
393        assert!(Members::of(&mixed, false).is_none());
394    }
395
396    #[test]
397    fn a_list_of_nothing_but_nulls_does_not_fold() {
398        assert!(Members::of(&[Value::Null, Value::Null], false).is_none());
399    }
400
401    #[test]
402    fn a_list_says_how_many_distinct_values_it_holds() {
403        let list = [Value::Integer(1), Value::Integer(1), Value::Integer(2), Value::Null];
404        let members = Members::of(&list, false).expect("this list folds");
405        assert_eq!(members.len(), 2);
406        assert!(!members.is_empty());
407    }
408}