Skip to main content

vortex_scan/
selection.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Defines a selection mask over a scan.
5
6use std::ops::Not;
7use std::ops::Range;
8
9use vortex_buffer::BitBufferMut;
10use vortex_error::vortex_panic;
11use vortex_mask::Mask;
12
13use crate::row_mask::RowMask;
14use crate::strict_sorted_buffer::StrictSortedBuffer;
15
16/// A selection identifies a set of rows to include in the scan (in addition to applying any
17/// filter predicates).
18#[derive(Default, Clone, Debug)]
19pub enum Selection {
20    /// No selection, all rows are included.
21    #[default]
22    All,
23    /// A selection of sorted, unique rows to include by index.
24    IncludeByIndex(StrictSortedBuffer<u64>),
25    /// A selection of sorted, unique rows to exclude by index.
26    ExcludeByIndex(StrictSortedBuffer<u64>),
27    /// A selection of rows to include using a [`roaring::RoaringTreemap`].
28    IncludeRoaring(roaring::RoaringTreemap),
29    /// A selection of rows to exclude using a [`roaring::RoaringTreemap`].
30    ExcludeRoaring(roaring::RoaringTreemap),
31}
32
33impl Selection {
34    /// Return the row count for this selection.
35    pub fn row_count(&self, total_rows: u64) -> u64 {
36        match self {
37            Selection::All => total_rows,
38            Selection::IncludeByIndex(include) => include.len() as u64,
39            Selection::ExcludeByIndex(exclude) => total_rows.saturating_sub(exclude.len() as u64),
40            Selection::IncludeRoaring(roaring) => roaring.len(),
41            Selection::ExcludeRoaring(roaring) => total_rows.saturating_sub(roaring.len()),
42        }
43    }
44
45    /// Count the selected rows that fall within `range`.
46    pub fn row_count_in_range(&self, range: &Range<u64>) -> u64 {
47        let range_len = range.end.saturating_sub(range.start);
48        match self {
49            Selection::All => range_len,
50            Selection::IncludeByIndex(include) => count_in_range(range, include),
51            Selection::ExcludeByIndex(exclude) => {
52                range_len.saturating_sub(count_in_range(range, exclude))
53            }
54            Selection::IncludeRoaring(roaring) => roaring.range_cardinality(range.clone()),
55            Selection::ExcludeRoaring(roaring) => {
56                range_len.saturating_sub(roaring.range_cardinality(range.clone()))
57            }
58        }
59    }
60
61    /// Extract the [`RowMask`] for the given range from this selection.
62    pub fn row_mask(&self, range: &Range<u64>) -> RowMask {
63        if range.start >= range.end {
64            return RowMask::new(0, Mask::AllFalse(0));
65        }
66
67        // Saturating subtraction to prevent underflow, though range should be valid
68        let range_diff = range.end.saturating_sub(range.start);
69        let range_len = usize::try_from(range_diff).unwrap_or_else(|_| {
70            // If the range is too large for usize, cap it at usize::MAX
71            // This is a defensive measure; in practice, ranges should be reasonable
72            tracing::warn!(
73                "Range length {} exceeds usize::MAX, capping at usize::MAX",
74                range_diff
75            );
76            usize::MAX
77        });
78
79        match self {
80            Selection::All => RowMask::new(range.start, Mask::new_true(range_len)),
81            Selection::IncludeByIndex(include) => {
82                RowMask::new(range.start, index_mask(range, range_len, include))
83            }
84            Selection::ExcludeByIndex(exclude) => {
85                RowMask::new(range.start, index_mask(range, range_len, exclude).not())
86            }
87            Selection::IncludeRoaring(roaring) => {
88                RowMask::new(range.start, roaring_mask(roaring, range, range_len))
89            }
90            Selection::ExcludeRoaring(roaring) => {
91                RowMask::new(range.start, roaring_mask(roaring, range, range_len).not())
92            }
93        }
94    }
95}
96
97/// Build the mask of positions within `range` that are set in `roaring`.
98///
99/// Seeks straight to `range` rather than intersecting the whole treemap, and short-circuits splits
100/// that are entirely outside or inside the selection.
101fn roaring_mask(roaring: &roaring::RoaringTreemap, range: &Range<u64>, range_len: usize) -> Mask {
102    let selected = roaring.range_cardinality(range.clone());
103    if selected == 0 {
104        return Mask::new_false(range_len);
105    }
106
107    if selected == range_len as u64 {
108        return Mask::new_true(range_len);
109    }
110
111    let mut bits = BitBufferMut::new_unset(range_len);
112    let mut iter = roaring.iter();
113    iter.advance_to(range.start);
114    for idx in iter.take_while(|&idx| idx < range.end) {
115        bits.set(relativize(range, idx));
116    }
117
118    Mask::from_buffer(bits.freeze())
119}
120
121/// Count the entries of the sorted `row_indices` that fall within `range`.
122fn count_in_range(range: &Range<u64>, row_indices: &[u64]) -> u64 {
123    indices_range(range, row_indices).map_or(0, |idx_range| idx_range.len() as u64)
124}
125
126/// Build the mask of positions within `range` that are named by the given sorted row indices.
127fn index_mask(range: &Range<u64>, range_len: usize, row_indices: &[u64]) -> Mask {
128    indices_range(range, row_indices)
129        .map(|idx_range| {
130            Mask::from_indices(
131                range_len,
132                row_indices[idx_range]
133                    .iter()
134                    .map(|&idx| relativize(range, idx)),
135            )
136        })
137        .unwrap_or_else(|| Mask::new_false(range_len))
138}
139
140/// Shift an absolute row index to be relative to the start of `range`.
141///
142/// Panics if the index is not a `usize`-sized offset into `range`. Callers have already narrowed
143/// the indices to `range`, whose own length had to fit in a `usize` to size the mask, so a failure
144/// here means that invariant was broken rather than that the index was merely out of bounds.
145fn relativize(range: &Range<u64>, idx: u64) -> usize {
146    idx.checked_sub(range.start)
147        .and_then(|relative| usize::try_from(relative).ok())
148        .unwrap_or_else(|| {
149            vortex_panic!(
150                "index {:?} is not a usize offset into range {:?}",
151                idx,
152                range
153            )
154        })
155}
156
157/// Find the positional range within row_indices that covers all rows in the given range.
158fn indices_range(range: &Range<u64>, row_indices: &[u64]) -> Option<Range<usize>> {
159    if row_indices.first().is_some_and(|&first| first >= range.end)
160        || row_indices.last().is_some_and(|&last| range.start > last)
161    {
162        return None;
163    }
164
165    // For the given row range, find the indices that are within the row_indices.
166    let start_idx = row_indices
167        .binary_search(&range.start)
168        .unwrap_or_else(|x| x);
169    let end_idx = row_indices.binary_search(&range.end).unwrap_or_else(|x| x);
170
171    (start_idx != end_idx).then_some(start_idx..end_idx)
172}
173
174#[cfg(test)]
175mod tests {
176    use vortex_buffer::Buffer;
177
178    use super::Selection;
179    use crate::strict_sorted_buffer::StrictSortedBuffer;
180
181    fn strict_sorted(indices: impl IntoIterator<Item = u64>) -> StrictSortedBuffer<u64> {
182        StrictSortedBuffer::try_new(Buffer::from_iter(indices))
183            .expect("test indices should be strictly increasing")
184    }
185
186    fn include(indices: impl IntoIterator<Item = u64>) -> Selection {
187        Selection::IncludeByIndex(strict_sorted(indices))
188    }
189
190    fn exclude(indices: impl IntoIterator<Item = u64>) -> Selection {
191        Selection::ExcludeByIndex(strict_sorted(indices))
192    }
193
194    #[test]
195    fn test_row_count_in_range() {
196        let range = 2..8;
197        assert_eq!(Selection::All.row_count_in_range(&range), 6);
198        // Indices 3, 5, 7 fall inside the range; 1 and 9 do not.
199        assert_eq!(include([1, 3, 5, 7, 9]).row_count_in_range(&range), 3);
200        assert_eq!(exclude([1, 3, 5, 7, 9]).row_count_in_range(&range), 3);
201        assert_eq!(include([10, 11]).row_count_in_range(&range), 0);
202        assert_eq!(include([1, 3]).row_count_in_range(&(8..8)), 0);
203    }
204
205    #[test]
206    fn test_row_count_in_range_roaring() {
207        let mut roaring = roaring::RoaringTreemap::new();
208        roaring.insert_range(1..10u64);
209
210        let range = 2..8;
211        assert_eq!(
212            Selection::IncludeRoaring(roaring.clone()).row_count_in_range(&range),
213            6
214        );
215        assert_eq!(
216            Selection::ExcludeRoaring(roaring).row_count_in_range(&range),
217            0
218        );
219    }
220
221    #[test]
222    fn test_row_count_in_range_roaring_boundaries() {
223        let roaring = [0, 3, u64::from(u32::MAX) + 1, u64::MAX - 1, u64::MAX]
224            .into_iter()
225            .collect::<roaring::RoaringTreemap>();
226        let include = Selection::IncludeRoaring(roaring.clone());
227        let exclude = Selection::ExcludeRoaring(roaring);
228
229        for (range, expected) in [
230            (0..0, 0),
231            (0..1, 1),
232            (1..3, 0),
233            (3..4, 1),
234            (u64::from(u32::MAX)..u64::from(u32::MAX) + 2, 1),
235            (u64::MAX - 1..u64::MAX, 1),
236            (0..u64::MAX, 4),
237        ] {
238            assert_eq!(include.row_count_in_range(&range), expected);
239            assert_eq!(
240                exclude.row_count_in_range(&range),
241                range.end - range.start - expected
242            );
243        }
244    }
245
246    #[test]
247    fn test_row_mask_all() {
248        let selection = include([1, 3, 5, 7]);
249        let range = 1..8;
250        let row_mask = selection.row_mask(&range);
251
252        assert_eq!(row_mask.mask().values().unwrap().indices(), &[0, 2, 4, 6]);
253    }
254
255    #[test]
256    fn test_row_mask_slice() {
257        let selection = include([1, 3, 5, 7]);
258        let range = 3..6;
259        let row_mask = selection.row_mask(&range);
260
261        assert_eq!(row_mask.mask().values().unwrap().indices(), &[0, 2]);
262    }
263
264    #[test]
265    fn test_row_mask_exclusive() {
266        let selection = include([1, 3, 5, 7]);
267        let range = 3..5;
268        let row_mask = selection.row_mask(&range);
269
270        assert_eq!(row_mask.mask().values().unwrap().indices(), &[0]);
271    }
272
273    #[test]
274    fn test_row_mask_all_false() {
275        let selection = include([1, 3, 5, 7]);
276        let range = 8..10;
277        let row_mask = selection.row_mask(&range);
278
279        assert!(row_mask.mask().all_false());
280    }
281
282    #[test]
283    fn test_row_mask_all_true() {
284        let selection = include([1, 3, 4, 5, 6]);
285        let range = 3..7;
286        let row_mask = selection.row_mask(&range);
287
288        assert!(row_mask.mask().all_true());
289    }
290
291    #[test]
292    fn test_row_mask_zero() {
293        let selection = include([0]);
294        let range = 0..5;
295        let row_mask = selection.row_mask(&range);
296
297        assert_eq!(row_mask.mask().values().unwrap().indices(), &[0]);
298    }
299
300    mod roaring_tests {
301        use roaring::RoaringTreemap;
302
303        use super::*;
304
305        #[test]
306        fn test_roaring_include_basic() {
307            let mut roaring = RoaringTreemap::new();
308            roaring.insert(1);
309            roaring.insert(3);
310            roaring.insert(5);
311            roaring.insert(7);
312
313            let selection = Selection::IncludeRoaring(roaring);
314            let range = 1..8;
315            let row_mask = selection.row_mask(&range);
316
317            assert_eq!(row_mask.mask().values().unwrap().indices(), &[0, 2, 4, 6]);
318        }
319
320        #[test]
321        fn test_roaring_include_slice() {
322            let mut roaring = RoaringTreemap::new();
323            roaring.insert(1);
324            roaring.insert(3);
325            roaring.insert(5);
326            roaring.insert(7);
327
328            let selection = Selection::IncludeRoaring(roaring);
329            let range = 3..6;
330            let row_mask = selection.row_mask(&range);
331
332            assert_eq!(row_mask.mask().values().unwrap().indices(), &[0, 2]);
333        }
334
335        #[test]
336        fn test_roaring_include_disjoint() {
337            let mut roaring = RoaringTreemap::new();
338            roaring.insert(1);
339            roaring.insert(3);
340            roaring.insert(5);
341            roaring.insert(7);
342
343            let selection = Selection::IncludeRoaring(roaring);
344            let range = 8..10;
345            let row_mask = selection.row_mask(&range);
346
347            assert!(row_mask.mask().all_false());
348        }
349
350        #[test]
351        fn test_roaring_include_large_range() {
352            let mut roaring = RoaringTreemap::new();
353            // Insert a large number of indices
354            for i in (0..1000000).step_by(2) {
355                roaring.insert(i);
356            }
357
358            let selection = Selection::IncludeRoaring(roaring);
359            let range = 1000..2000;
360            let row_mask = selection.row_mask(&range);
361
362            assert_eq!(row_mask.row_range(), range);
363            assert_eq!(
364                row_mask.mask().values().unwrap().indices(),
365                &(0..1000).step_by(2).collect::<Vec<_>>()
366            );
367        }
368
369        #[test]
370        fn test_roaring_exclude_basic() {
371            let mut roaring = RoaringTreemap::new();
372            roaring.insert(1);
373            roaring.insert(3);
374            roaring.insert(5);
375
376            let selection = Selection::ExcludeRoaring(roaring);
377            let range = 0..7;
378            let row_mask = selection.row_mask(&range);
379
380            // Should exclude indices 1, 3, 5, so we get 0, 2, 4, 6
381            assert_eq!(row_mask.mask().values().unwrap().indices(), &[0, 2, 4, 6]);
382        }
383
384        #[test]
385        fn test_roaring_exclude_all() {
386            let mut roaring = RoaringTreemap::new();
387            // Exclude all indices in range
388            for i in 10..20 {
389                roaring.insert(i);
390            }
391
392            let selection = Selection::ExcludeRoaring(roaring);
393            let range = 10..20;
394            let row_mask = selection.row_mask(&range);
395
396            assert!(row_mask.mask().all_false());
397        }
398
399        #[test]
400        fn test_roaring_exclude_none() {
401            let mut roaring = RoaringTreemap::new();
402            roaring.insert(100);
403            roaring.insert(101);
404
405            let selection = Selection::ExcludeRoaring(roaring);
406            let range = 0..10;
407            let row_mask = selection.row_mask(&range);
408
409            // Nothing to exclude in this range
410            assert!(row_mask.mask().all_true());
411        }
412
413        #[test]
414        fn test_roaring_exclude_partial() {
415            let mut roaring = RoaringTreemap::new();
416            roaring.insert(5);
417            roaring.insert(6);
418            roaring.insert(7);
419            roaring.insert(15); // Outside range
420
421            let selection = Selection::ExcludeRoaring(roaring);
422            let range = 5..10;
423            let row_mask = selection.row_mask(&range);
424
425            // Should exclude 5, 6, 7 (mapped to 0, 1, 2), keep 8, 9 (mapped to 3, 4)
426            assert_eq!(row_mask.mask().values().unwrap().indices(), &[3, 4]);
427        }
428
429        #[test]
430        fn test_roaring_include_empty() {
431            let roaring = RoaringTreemap::new();
432            let selection = Selection::IncludeRoaring(roaring);
433            let range = 0..100;
434            let row_mask = selection.row_mask(&range);
435
436            assert!(row_mask.mask().all_false());
437        }
438
439        #[test]
440        fn test_roaring_exclude_empty() {
441            let roaring = RoaringTreemap::new();
442            let selection = Selection::ExcludeRoaring(roaring);
443            let range = 0..100;
444            let row_mask = selection.row_mask(&range);
445
446            assert!(row_mask.mask().all_true());
447        }
448
449        #[test]
450        fn test_roaring_include_boundary() {
451            let mut roaring = RoaringTreemap::new();
452            roaring.insert(0);
453            roaring.insert(99);
454
455            let selection = Selection::IncludeRoaring(roaring);
456            let range = 0..100;
457            let row_mask = selection.row_mask(&range);
458
459            assert_eq!(row_mask.mask().values().unwrap().indices(), &[0, 99]);
460        }
461
462        #[test]
463        fn test_roaring_include_range_insertion() {
464            let mut roaring = RoaringTreemap::new();
465            // Use insert_range for efficiency
466            roaring.insert_range(10..20);
467            roaring.insert_range(30..40);
468
469            let selection = Selection::IncludeRoaring(roaring);
470            let range = 15..35;
471            let row_mask = selection.row_mask(&range);
472
473            // Should include 15-19 (mapped to 0-4) and 30-34 (mapped to 15-19)
474            let expected: Vec<usize> = (0..5).chain(15..20).collect();
475            assert_eq!(row_mask.mask().values().unwrap().indices(), &expected);
476        }
477
478        #[test]
479        fn test_roaring_overflow_protection() {
480            let mut roaring = RoaringTreemap::new();
481            // Insert very large indices
482            roaring.insert(u64::MAX - 1);
483            roaring.insert(u64::MAX);
484
485            let selection = Selection::IncludeRoaring(roaring);
486            let range = u64::MAX - 10..u64::MAX;
487            let row_mask = selection.row_mask(&range);
488
489            assert_eq!(row_mask.row_range(), range);
490            assert_eq!(row_mask.mask().values().unwrap().indices(), &[9]);
491        }
492
493        #[test]
494        fn test_roaring_exclude_overflow_protection() {
495            let mut roaring = RoaringTreemap::new();
496            roaring.insert(u64::MAX - 1);
497
498            let selection = Selection::ExcludeRoaring(roaring);
499            let range = u64::MAX - 10..u64::MAX;
500            let row_mask = selection.row_mask(&range);
501
502            assert_eq!(row_mask.row_range(), range);
503            assert_eq!(
504                row_mask.mask().values().unwrap().indices(),
505                &(0..9).collect::<Vec<_>>()
506            );
507        }
508
509        #[test]
510        fn test_roaring_include_vs_buffer_equivalence() {
511            // Test that RoaringTreemap and Buffer produce same results
512            let indices = vec![1, 3, 5, 7, 9];
513
514            let buffer_selection = include(indices.clone());
515
516            let mut roaring = RoaringTreemap::new();
517            for idx in &indices {
518                roaring.insert(*idx);
519            }
520            let roaring_selection = Selection::IncludeRoaring(roaring);
521
522            let range = 0..12;
523            let buffer_mask = buffer_selection.row_mask(&range);
524            let roaring_mask = roaring_selection.row_mask(&range);
525
526            for mask in [buffer_mask, roaring_mask] {
527                assert_eq!(mask.row_range(), range);
528                assert_eq!(mask.mask().values().unwrap().indices(), &[1, 3, 5, 7, 9]);
529            }
530        }
531
532        #[test]
533        fn test_roaring_exclude_vs_buffer_equivalence() {
534            // Test that ExcludeRoaring and ExcludeByIndex produce same results
535            let indices = vec![2, 4, 6, 8];
536
537            let buffer_selection = exclude(indices.clone());
538
539            let mut roaring = RoaringTreemap::new();
540            for idx in &indices {
541                roaring.insert(*idx);
542            }
543            let roaring_selection = Selection::ExcludeRoaring(roaring);
544
545            let range = 0..10;
546            let buffer_mask = buffer_selection.row_mask(&range);
547            let roaring_mask = roaring_selection.row_mask(&range);
548
549            for mask in [buffer_mask, roaring_mask] {
550                assert_eq!(mask.row_range(), range);
551                assert_eq!(mask.mask().values().unwrap().indices(), &[0, 1, 3, 5, 7, 9]);
552            }
553        }
554    }
555}