Skip to main content

rudb_vector/
selection.rs

1//! Selection vectors.
2//!
3//! `spec/07-execution.md` section 7.1: a filter produces a `u32` selection vector rather than
4//! compacting. Compaction happens when a measured selectivity threshold is crossed and the
5//! downstream operator is one that benefits, and the threshold is per operator and measured rather
6//! than one global constant somebody picked.
7//!
8//! The reason not to compact by default is that a filter over five columns which compacts has
9//! copied five columns to save the next operator a redirection. On a query that filters and then
10//! projects two of those columns, three of the copies were free work.
11
12/// Which positions of a vector are still in play, as indices into it.
13///
14/// An empty selection means nothing survived, which is different from no selection at all. The
15/// distinction is why this is a type rather than an `Option<Vec<u32>>` that everybody interprets
16/// slightly differently.
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct Selection {
19    indices: Vec<u32>,
20}
21
22impl Selection {
23    /// A selection of nothing.
24    #[must_use]
25    pub fn empty() -> Self {
26        Self::default()
27    }
28
29    /// A selection of nothing, with room for `capacity` positions.
30    #[must_use]
31    pub fn with_capacity(capacity: usize) -> Self {
32        Self { indices: Vec::with_capacity(capacity) }
33    }
34
35    /// A selection of the first `len` positions in order.
36    ///
37    /// Materialized rather than represented as an absent selection, so this is what a caller uses
38    /// when it genuinely wants the identity written down. A scan that has not filtered anything
39    /// carries no selection at all, which is cheaper and is the common case.
40    #[must_use]
41    pub fn identity(len: usize) -> Self {
42        Self { indices: (0..len as u32).collect() }
43    }
44
45    /// A selection from positions a caller has already worked out, in order.
46    ///
47    /// For a kernel that fills a buffer of its own and counts as it goes, which is how a selection
48    /// loop is written without a branch in it: every row writes its index at the current length and
49    /// only a row that is kept moves the length on. Pushing one at a time would put a capacity check
50    /// and a conversion on a loop whose whole point is that it has neither.
51    #[must_use]
52    pub fn from_indices(indices: Vec<u32>) -> Self {
53        Self { indices }
54    }
55
56    /// A selection of the positions a predicate accepts.
57    pub fn from_predicate(len: usize, keep: impl Fn(usize) -> bool) -> Self {
58        let mut selection = Self::with_capacity(len);
59        for index in 0..len {
60            if keep(index) {
61                selection.push(index);
62            }
63        }
64        selection
65    }
66
67    /// Adds a position to the end.
68    ///
69    /// # Panics
70    ///
71    /// If the index does not fit in a `u32`. A vector holds 1024 values and a row group holds
72    /// 122,880, so an index that large is a bug several layers up rather than a large query.
73    pub fn push(&mut self, index: usize) {
74        self.indices.push(u32::try_from(index).expect("a position past four billion"));
75    }
76
77    /// How many positions survived.
78    #[must_use]
79    pub fn len(&self) -> usize {
80        self.indices.len()
81    }
82
83    /// Whether nothing survived.
84    #[must_use]
85    pub fn is_empty(&self) -> bool {
86        self.indices.is_empty()
87    }
88
89    /// The position at `slot`, where `slot` counts through the survivors.
90    #[must_use]
91    pub fn get(&self, slot: usize) -> Option<usize> {
92        self.indices.get(slot).map(|&index| index as usize)
93    }
94
95    /// The positions, in order.
96    #[must_use]
97    pub fn indices(&self) -> &[u32] {
98        &self.indices
99    }
100
101    /// The positions as `usize`, in order.
102    pub fn iter(&self) -> impl Iterator<Item = usize> + '_ {
103        self.indices.iter().map(|&index| index as usize)
104    }
105
106    /// What fraction of `len` positions survived.
107    ///
108    /// This is the number the compaction decision is made on, and it is measured rather than
109    /// assumed, per section 7.1. Zero length reports 1.0, because a filter over nothing has not
110    /// rejected anything.
111    #[must_use]
112    pub fn selectivity(&self, len: usize) -> f64 {
113        if len == 0 { 1.0 } else { self.len() as f64 / len as f64 }
114    }
115
116    /// This selection composed with an earlier one, so that filtering twice does not need the
117    /// intermediate to be materialized.
118    ///
119    /// `self` indexes into `earlier`, and the result indexes into whatever `earlier` indexed into.
120    /// Getting this backwards produces a query that returns the wrong rows rather than an error,
121    /// which is why the direction is spelled out here and tested below.
122    #[must_use]
123    pub fn compose(&self, earlier: &Self) -> Self {
124        let indices =
125            self.indices.iter().filter_map(|&slot| earlier.indices.get(slot as usize).copied());
126        Self { indices: indices.collect() }
127    }
128
129    /// The positions this selection holds that `taken` does not.
130    ///
131    /// Both sides have to be in ascending order, which every selection in this engine is: a kernel
132    /// fills one by walking the rows upward and a composed one keeps that order. So this is one
133    /// merge over the pair rather than a search per position.
134    ///
135    /// This is what a threaded `OR` narrows its work with. Each branch is given the rows no branch
136    /// before it accepted, and the rows it accepts come out of that set for the branch after.
137    #[must_use]
138    pub fn without(&self, taken: &Self) -> Self {
139        let mut indices = Vec::with_capacity(self.indices.len().saturating_sub(taken.len()));
140        let mut next = taken.indices.iter().copied().peekable();
141        for &index in &self.indices {
142            while next.peek().is_some_and(|&other| other < index) {
143                next.next();
144            }
145            if next.peek() == Some(&index) {
146                next.next();
147            } else {
148                indices.push(index);
149            }
150        }
151        Self { indices }
152    }
153
154    /// The positions below `len` that this selection does not hold.
155    ///
156    /// The other half of a threaded `OR`. What the branches leave behind is the rows none of them
157    /// accepted, and the rows the filter keeps are all the others.
158    #[must_use]
159    pub fn complement(&self, len: usize) -> Self {
160        let mut indices = Vec::with_capacity(len.saturating_sub(self.indices.len()));
161        let mut next = self.indices.iter().copied().peekable();
162        for index in 0..len as u32 {
163            while next.peek().is_some_and(|&held| held < index) {
164                next.next();
165            }
166            if next.peek() == Some(&index) {
167                next.next();
168            } else {
169                indices.push(index);
170            }
171        }
172        Self { indices }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::Selection;
179
180    #[test]
181    fn nothing_selected_is_not_the_same_as_no_selection() {
182        // The reason this is a type. An operator that treats an empty selection as "everything"
183        // returns every row for a predicate that matched none, which is a wrong answer and the
184        // worst thing this project can ship.
185        let none = Selection::empty();
186        assert_eq!(none.len(), 0);
187        assert!(none.is_empty());
188        assert_eq!(none.selectivity(1024), 0.0);
189    }
190
191    #[test]
192    fn a_predicate_selection_keeps_the_positions_in_order() {
193        let selection = Selection::from_predicate(10, |i| i % 3 == 0);
194        assert_eq!(selection.indices(), &[0, 3, 6, 9]);
195        assert_eq!(selection.get(2), Some(6));
196        assert_eq!(selection.get(4), None);
197        assert!((selection.selectivity(10) - 0.4).abs() < f64::EPSILON);
198    }
199
200    #[test]
201    fn composing_two_filters_indexes_all_the_way_back() {
202        // First filter keeps the even positions of sixteen. Second keeps every third survivor,
203        // meaning slots 0, 3 and 6 of the first result, which are positions 0, 6 and 12.
204        let first = Selection::from_predicate(16, |i| i % 2 == 0);
205        let second = Selection::from_predicate(first.len(), |i| i % 3 == 0);
206        assert_eq!(second.compose(&first).indices(), &[0, 6, 12]);
207    }
208
209    #[test]
210    fn composing_with_the_identity_changes_nothing() {
211        let selection = Selection::from_predicate(8, |i| i > 4);
212        assert_eq!(selection.compose(&Selection::identity(8)), selection);
213    }
214
215    #[test]
216    fn taking_rows_out_of_a_selection_leaves_the_rest_in_order() {
217        let live = Selection::from_indices(vec![1, 4, 5, 9, 12]);
218        let taken = Selection::from_indices(vec![4, 9]);
219        assert_eq!(live.without(&taken).indices(), &[1, 5, 12]);
220        // Taking nothing and taking everything are the two ends a threaded `OR` hits on its first
221        // branch, and neither of them is allowed to be a special case at the call site.
222        assert_eq!(live.without(&Selection::empty()), live);
223        assert!(live.without(&live).is_empty());
224    }
225
226    #[test]
227    fn taking_rows_that_are_not_there_changes_nothing() {
228        let live = Selection::from_indices(vec![2, 6]);
229        assert_eq!(live.without(&Selection::from_indices(vec![0, 3, 7])), live);
230    }
231
232    #[test]
233    fn the_complement_is_every_position_the_selection_left_out() {
234        let selection = Selection::from_indices(vec![0, 2, 3]);
235        assert_eq!(selection.complement(6).indices(), &[1, 4, 5]);
236        assert_eq!(Selection::empty().complement(3), Selection::identity(3));
237        assert!(Selection::identity(3).complement(3).is_empty());
238        assert!(Selection::empty().complement(0).is_empty());
239    }
240
241    #[test]
242    fn selectivity_over_nothing_is_one_rather_than_a_division_by_zero() {
243        assert!((Selection::empty().selectivity(0) - 1.0).abs() < f64::EPSILON);
244    }
245}