Skip to main content

lance_core/utils/mask/
nullable.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use deepsize::DeepSizeOf;
5
6use super::{RowAddrMask, RowAddrTreeMap, RowSetOps};
7
8/// A set of row ids, with optional set of nulls.
9///
10/// This is often a result of a filter, where `selected` represents the rows that
11/// passed the filter, and `nulls` represents the rows where the filter evaluated
12/// to null. For example, in SQL `NULL > 5` evaluates to null. This is distinct
13/// from being deselected to support proper three-valued logic for NOT.
14/// (`NOT FALSE` is TRUE, `NOT TRUE` is FALSE, but `NOT NULL` is NULL.
15/// `NULL | TRUE = TRUE`, `NULL & FALSE = FALSE`, but `NULL | FALSE = NULL`
16/// and `NULL & TRUE = NULL`).
17#[derive(Clone, Debug, Default, DeepSizeOf)]
18pub struct NullableRowAddrSet {
19    selected: RowAddrTreeMap,
20    // Rows that are NULL. These rows are considered NULL even if they are also in `selected`.
21    nulls: RowAddrTreeMap,
22}
23
24impl NullableRowAddrSet {
25    /// Create a new RowSelection from selected rows and null rows.
26    ///
27    /// `nulls` may have overlap with `selected`. Rows in `nulls` are considered NULL,
28    /// even if they are also in `selected`.
29    pub fn new(selected: RowAddrTreeMap, nulls: RowAddrTreeMap) -> Self {
30        Self { selected, nulls }
31    }
32
33    pub fn with_nulls(mut self, nulls: RowAddrTreeMap) -> Self {
34        self.nulls = nulls;
35        self
36    }
37
38    /// Create an empty selection. Alias for [Default::default]
39    pub fn empty() -> Self {
40        Default::default()
41    }
42
43    /// Get the number of TRUE rows (selected but not null).
44    ///
45    /// Returns None if the number of TRUE rows cannot be determined. This happens
46    /// if the underlying RowAddrTreeMap has full fragments selected.
47    pub fn len(&self) -> Option<u64> {
48        self.true_rows().len()
49    }
50
51    pub fn is_empty(&self) -> bool {
52        self.selected.is_empty()
53    }
54
55    /// Check if a row_id is selected (TRUE)
56    pub fn selected(&self, row_id: u64) -> bool {
57        self.selected.contains(row_id) && !self.nulls.contains(row_id)
58    }
59
60    /// Get the null rows
61    pub fn null_rows(&self) -> &RowAddrTreeMap {
62        &self.nulls
63    }
64
65    /// Get the TRUE rows (selected but not null)
66    pub fn true_rows(&self) -> RowAddrTreeMap {
67        self.selected.clone() - self.nulls.clone()
68    }
69
70    pub fn union_all(selections: &[Self]) -> Self {
71        let true_rows = selections
72            .iter()
73            .map(|s| s.true_rows())
74            .collect::<Vec<RowAddrTreeMap>>();
75        let true_rows_refs = true_rows.iter().collect::<Vec<&RowAddrTreeMap>>();
76        let selected = RowAddrTreeMap::union_all(&true_rows_refs);
77        let nulls = RowAddrTreeMap::union_all(
78            &selections
79                .iter()
80                .map(|s| &s.nulls)
81                .collect::<Vec<&RowAddrTreeMap>>(),
82        );
83        // TRUE | NULL = TRUE, so remove any TRUE rows from nulls
84        let nulls = nulls - &selected;
85        Self { selected, nulls }
86    }
87}
88
89impl PartialEq for NullableRowAddrSet {
90    fn eq(&self, other: &Self) -> bool {
91        self.true_rows() == other.true_rows() && self.nulls == other.nulls
92    }
93}
94
95impl std::ops::BitAndAssign<&Self> for NullableRowAddrSet {
96    fn bitand_assign(&mut self, rhs: &Self) {
97        self.nulls = if self.nulls.is_empty() && rhs.nulls.is_empty() {
98            RowAddrTreeMap::new() // Fast path
99        } else {
100            (self.nulls.clone() & &rhs.nulls) // null and null -> null
101            | (self.nulls.clone() & &rhs.selected) // null and true -> null
102            | (rhs.nulls.clone() & &self.selected) // true and null -> null
103        };
104
105        self.selected &= &rhs.selected;
106    }
107}
108
109impl std::ops::BitOrAssign<&Self> for NullableRowAddrSet {
110    fn bitor_assign(&mut self, rhs: &Self) {
111        self.nulls = if self.nulls.is_empty() && rhs.nulls.is_empty() {
112            RowAddrTreeMap::new() // Fast path
113        } else {
114            // null or null -> null (excluding rows that are true in either)
115            let true_rows =
116                (self.selected.clone() - &self.nulls) | (rhs.selected.clone() - &rhs.nulls);
117            (self.nulls.clone() | &rhs.nulls) - true_rows
118        };
119
120        self.selected |= &rhs.selected;
121    }
122}
123
124/// A version of [`RowAddrMask`] that supports nulls.
125///
126/// This mask handles three-valued logic for SQL expressions, where a filter can
127/// evaluate to TRUE, FALSE, or NULL. The `selected` set includes rows that are
128/// TRUE or NULL. The `nulls` set includes rows that are NULL.
129#[derive(Clone, Debug)]
130pub enum NullableRowAddrMask {
131    AllowList(NullableRowAddrSet),
132    BlockList(NullableRowAddrSet),
133}
134
135impl NullableRowAddrMask {
136    pub fn selected(&self, row_id: u64) -> bool {
137        match self {
138            Self::AllowList(NullableRowAddrSet { selected, nulls }) => {
139                selected.contains(row_id) && !nulls.contains(row_id)
140            }
141            Self::BlockList(NullableRowAddrSet { selected, nulls }) => {
142                !selected.contains(row_id) && !nulls.contains(row_id)
143            }
144        }
145    }
146
147    pub fn drop_nulls(self) -> RowAddrMask {
148        match self {
149            Self::AllowList(NullableRowAddrSet { selected, nulls }) => {
150                RowAddrMask::AllowList(selected - nulls)
151            }
152            Self::BlockList(NullableRowAddrSet { selected, nulls }) => {
153                RowAddrMask::BlockList(selected | nulls)
154            }
155        }
156    }
157}
158
159impl std::ops::Not for NullableRowAddrMask {
160    type Output = Self;
161
162    fn not(self) -> Self::Output {
163        match self {
164            Self::AllowList(set) => Self::BlockList(set),
165            Self::BlockList(set) => Self::AllowList(set),
166        }
167    }
168}
169
170impl std::ops::BitAnd for NullableRowAddrMask {
171    type Output = Self;
172
173    fn bitand(self, rhs: Self) -> Self::Output {
174        // Null handling:
175        // * null and true -> null
176        // * null and null -> null
177        // * null and false -> false
178        match (self, rhs) {
179            (Self::AllowList(a), Self::AllowList(b)) => {
180                let nulls = if a.nulls.is_empty() && b.nulls.is_empty() {
181                    RowAddrTreeMap::new() // Fast path
182                } else {
183                    (a.nulls.clone() & &b.nulls) // null and null -> null
184                    | (a.nulls & &b.selected) // null and true -> null
185                    | (b.nulls & &a.selected) // true and null -> null
186                };
187                let selected = a.selected & b.selected;
188                Self::AllowList(NullableRowAddrSet { selected, nulls })
189            }
190            (Self::AllowList(allow), Self::BlockList(block))
191            | (Self::BlockList(block), Self::AllowList(allow)) => {
192                let nulls = if allow.nulls.is_empty() && block.nulls.is_empty() {
193                    RowAddrTreeMap::new() // Fast path
194                } else {
195                    (allow.nulls.clone() & &block.nulls) // null and null -> null
196                    | (allow.nulls - &block.selected) // null and true -> null
197                    | (block.nulls & &allow.selected) // true and null -> null
198                };
199                let selected = allow.selected - block.selected;
200                Self::AllowList(NullableRowAddrSet { selected, nulls })
201            }
202            (Self::BlockList(a), Self::BlockList(b)) => {
203                let nulls = if a.nulls.is_empty() && b.nulls.is_empty() {
204                    RowAddrTreeMap::new() // Fast path
205                } else {
206                    (a.nulls.clone() & &b.nulls) // null and null -> null
207                    | (a.nulls - &b.selected) // null and true -> null
208                    | (b.nulls - &a.selected) // true and null -> null
209                };
210                let selected = a.selected | b.selected;
211                Self::BlockList(NullableRowAddrSet { selected, nulls })
212            }
213        }
214    }
215}
216
217impl std::ops::BitOr for NullableRowAddrMask {
218    type Output = Self;
219
220    fn bitor(self, rhs: Self) -> Self::Output {
221        // Null handling:
222        // * null or true -> true
223        // * null or null -> null
224        // * null or false -> null
225        match (self, rhs) {
226            (Self::AllowList(a), Self::AllowList(b)) => {
227                let nulls = if a.nulls.is_empty() && b.nulls.is_empty() {
228                    RowAddrTreeMap::new() // Fast path
229                } else {
230                    // null or null -> null (excluding rows that are true in either)
231                    let true_rows =
232                        (a.selected.clone() - &a.nulls) | (b.selected.clone() - &b.nulls);
233                    (a.nulls | b.nulls) - true_rows
234                };
235                let selected = (a.selected | b.selected) | &nulls;
236                Self::AllowList(NullableRowAddrSet { selected, nulls })
237            }
238            (Self::AllowList(allow), Self::BlockList(block))
239            | (Self::BlockList(block), Self::AllowList(allow)) => {
240                let nulls = if allow.nulls.is_empty() && block.nulls.is_empty() {
241                    RowAddrTreeMap::new() // Fast path
242                } else {
243                    // null or null -> null (excluding rows that are true in either)
244                    let allow_true = allow.selected.clone() - &allow.nulls;
245                    ((allow.nulls | block.nulls) & block.selected.clone()) - allow_true
246                };
247                let selected = (block.selected - allow.selected) | &nulls;
248                Self::BlockList(NullableRowAddrSet { selected, nulls })
249            }
250            (Self::BlockList(a), Self::BlockList(b)) => {
251                let nulls = if a.nulls.is_empty() && b.nulls.is_empty() {
252                    RowAddrTreeMap::new() // Fast path
253                } else {
254                    // null or null -> null (excluding rows that are true in either)
255                    let false_rows =
256                        (a.selected.clone() - &a.nulls) & (b.selected.clone() - &b.nulls);
257                    (a.nulls | &b.nulls) - false_rows
258                };
259                let selected = (a.selected & b.selected) | &nulls;
260                Self::BlockList(NullableRowAddrSet { selected, nulls })
261            }
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    fn rows(ids: &[u64]) -> RowAddrTreeMap {
271        RowAddrTreeMap::from_iter(ids)
272    }
273
274    fn nullable_set(selected: &[u64], nulls: &[u64]) -> NullableRowAddrSet {
275        NullableRowAddrSet::new(rows(selected), rows(nulls))
276    }
277
278    fn allow(selected: &[u64], nulls: &[u64]) -> NullableRowAddrMask {
279        NullableRowAddrMask::AllowList(nullable_set(selected, nulls))
280    }
281
282    fn block(selected: &[u64], nulls: &[u64]) -> NullableRowAddrMask {
283        NullableRowAddrMask::BlockList(nullable_set(selected, nulls))
284    }
285
286    fn assert_mask_selects(mask: &NullableRowAddrMask, selected: &[u64], not_selected: &[u64]) {
287        for &id in selected {
288            assert!(mask.selected(id), "Expected row {} to be selected", id);
289        }
290        for &id in not_selected {
291            assert!(!mask.selected(id), "Expected row {} to NOT be selected", id);
292        }
293    }
294
295    #[test]
296    fn test_not_with_nulls() {
297        // Test case from issue #4756: x != 5 on data [0, 5, null]
298        // x = 5 should return: AllowList with selected=[1,2], nulls=[2]
299        // NOT(x = 5) should return: BlockList with selected=[1,2], nulls=[2]
300        // selected() should return TRUE for row 0, FALSE for rows 1 and 2
301        let mask = allow(&[1, 2], &[2]);
302        let not_mask = !mask;
303
304        // Row 0: selected (x=0, which is != 5)
305        // Row 1: NOT selected (x=5, which is == 5)
306        // Row 2: NOT selected (x=null, comparison result is null)
307        assert_mask_selects(&not_mask, &[0], &[1, 2]);
308    }
309
310    #[test]
311    fn test_and_with_nulls() {
312        // Test Kleene AND logic: true AND null = null, false AND null = false
313
314        // Case 1: TRUE mask AND mask with nulls
315        let true_mask = allow(&[0, 1, 2, 3, 4], &[]);
316        let null_mask = allow(&[0, 1, 2, 3, 4], &[1, 3]);
317        let result = true_mask & null_mask.clone();
318
319        // TRUE AND TRUE = TRUE; TRUE AND NULL = NULL (filtered out)
320        assert_mask_selects(&result, &[0, 2, 4], &[1, 3]);
321
322        // Case 2: FALSE mask AND mask with nulls
323        let false_mask = block(&[0, 1, 2, 3, 4], &[]);
324        let result = false_mask & null_mask;
325
326        // FALSE AND anything = FALSE
327        assert_mask_selects(&result, &[], &[0, 1, 2, 3, 4]);
328
329        // Case 3: Both masks have nulls - union of null sets
330        let mask1 = allow(&[0, 1, 2], &[1]);
331        let mask2 = allow(&[0, 2, 3], &[2]);
332        let result = mask1 & mask2;
333
334        // Only row 0 is TRUE in both; rows 1,2 are null in at least one; row 3 not in first
335        assert_mask_selects(&result, &[0], &[1, 2, 3]);
336    }
337
338    #[test]
339    fn test_or_with_nulls() {
340        // Test Kleene OR logic: true OR null = true, false OR null = null
341
342        // Case 1: FALSE mask OR mask with nulls
343        let false_mask = block(&[0, 1, 2], &[]);
344        let null_mask = allow(&[0, 1, 2], &[1, 2]);
345        let result = false_mask | null_mask.clone();
346
347        // FALSE OR TRUE = TRUE; FALSE OR NULL = NULL (filtered out)
348        assert_mask_selects(&result, &[0], &[1, 2]);
349
350        // Case 2: TRUE mask OR mask with nulls
351        let true_mask = allow(&[0, 1, 2], &[]);
352        let result = true_mask | null_mask;
353
354        // TRUE OR anything = TRUE
355        assert_mask_selects(&result, &[0, 1, 2], &[]);
356
357        // Case 3: Both have nulls
358        let mask1 = block(&[0, 1, 2, 3], &[1, 2]);
359        let mask2 = block(&[0, 1, 2, 3], &[2, 3]);
360        let result = mask1 | mask2;
361
362        // Row 0: FALSE in both; Rows 1,2,3: NULL in at least one
363        assert_mask_selects(&result, &[], &[0, 1, 2, 3]);
364    }
365
366    #[test]
367    fn test_row_selection_bit_or() {
368        // [T, N, T, N, F, F, F]
369        let left = nullable_set(&[1, 2, 3, 4], &[2, 4]);
370        // [F, F, T, N, T, N, N]
371        let right = nullable_set(&[3, 4, 5, 6], &[4, 6, 7]);
372        // [T, N, T, N, T, N, N]
373        let expected_true = rows(&[1, 3, 5]);
374        let expected_nulls = rows(&[2, 4, 6, 7]);
375
376        let mut result = left.clone();
377        result |= &right;
378        assert_eq!(&result.true_rows(), &expected_true);
379        assert_eq!(result.null_rows(), &expected_nulls);
380
381        // Commutative property holds
382        let mut result = right.clone();
383        result |= &left;
384        assert_eq!(&result.true_rows(), &expected_true);
385        assert_eq!(result.null_rows(), &expected_nulls);
386    }
387
388    #[test]
389    fn test_row_selection_bit_and() {
390        // [T, N, T, N, F, F, F]
391        let left = nullable_set(&[1, 2, 3, 4], &[2, 4]);
392        // [F, F, T, N, T, N, N]
393        let right = nullable_set(&[3, 4, 5, 6], &[4, 6, 7]);
394        // [F, F, T, N, F, F, F]
395        let expected_true = rows(&[3]);
396        let expected_nulls = rows(&[4]);
397
398        let mut result = left.clone();
399        result &= &right;
400        assert_eq!(&result.true_rows(), &expected_true);
401        assert_eq!(result.null_rows(), &expected_nulls);
402
403        // Commutative property holds
404        let mut result = right.clone();
405        result &= &left;
406        assert_eq!(&result.true_rows(), &expected_true);
407        assert_eq!(result.null_rows(), &expected_nulls);
408    }
409
410    #[test]
411    fn test_union_all() {
412        // Union all is basically a series of ORs.
413        // [T, T, T, N, N, N, F, F, F]
414        let set1 = nullable_set(&[1, 2, 3, 4], &[4, 5, 6]);
415        // [T, N, F, T, N, F, T, N, F]
416        let set2 = nullable_set(&[1, 4, 7, 8], &[2, 5, 8]);
417        let set3 = NullableRowAddrSet::empty();
418
419        let result = NullableRowAddrSet::union_all(&[set1, set2, set3]);
420
421        // [T, T, T, T, N, N, T, N, F]
422        assert_eq!(&result.true_rows(), &rows(&[1, 2, 3, 4, 7]));
423        assert_eq!(result.null_rows(), &rows(&[5, 6, 8]));
424    }
425
426    #[test]
427    fn test_nullable_row_addr_set_with_nulls() {
428        let set = NullableRowAddrSet::new(rows(&[1, 2, 3]), RowAddrTreeMap::new());
429        let set_with_nulls = set.with_nulls(rows(&[2]));
430
431        assert!(set_with_nulls.selected(1) && set_with_nulls.selected(3));
432        assert!(!set_with_nulls.selected(2)); // null
433    }
434
435    #[test]
436    fn test_nullable_row_addr_set_len_and_is_empty() {
437        let set = nullable_set(&[1, 2, 3, 4, 5], &[2, 4]);
438
439        // len() returns count of TRUE rows (selected - nulls)
440        assert_eq!(set.len(), Some(3)); // 1, 3, 5
441        assert!(!set.is_empty());
442
443        let empty_set = NullableRowAddrSet::empty();
444        assert!(empty_set.is_empty());
445        assert_eq!(empty_set.len(), Some(0));
446    }
447
448    #[test]
449    fn test_nullable_row_addr_set_selected() {
450        let set = nullable_set(&[1, 2, 3], &[2]);
451
452        // selected() returns true only for TRUE rows (in selected and not in nulls)
453        assert!(set.selected(1) && set.selected(3));
454        assert!(!set.selected(2)); // null
455        assert!(!set.selected(4)); // not in selected
456    }
457
458    #[test]
459    fn test_nullable_row_addr_set_partial_eq() {
460        let set1 = nullable_set(&[1, 2, 3], &[2]);
461        let set2 = nullable_set(&[1, 2, 3], &[2]);
462        // set3 has same true_rows but different nulls
463        let set3 = nullable_set(&[1, 3], &[3]);
464
465        assert_eq!(set1, set2);
466        assert_ne!(set1, set3); // different nulls
467    }
468
469    #[test]
470    fn test_nullable_row_addr_set_bitand_fast_path() {
471        // Test fast path when both have no nulls
472        let set1 = nullable_set(&[1, 2, 3], &[]);
473        let set2 = nullable_set(&[2, 3, 4], &[]);
474
475        let mut result = set1;
476        result &= &set2;
477
478        // Intersection: [2, 3]
479        assert!(result.selected(2) && result.selected(3));
480        assert!(!result.selected(1) && !result.selected(4));
481        assert!(result.null_rows().is_empty());
482    }
483
484    #[test]
485    fn test_nullable_row_addr_set_bitor_fast_path() {
486        // Test fast path when both have no nulls
487        let set1 = nullable_set(&[1, 2], &[]);
488        let set2 = nullable_set(&[3, 4], &[]);
489
490        let mut result = set1;
491        result |= &set2;
492
493        // Union: [1, 2, 3, 4]
494        for id in [1, 2, 3, 4] {
495            assert!(result.selected(id));
496        }
497        assert!(result.null_rows().is_empty());
498    }
499
500    #[test]
501    fn test_nullable_row_id_mask_drop_nulls() {
502        // Test drop_nulls for AllowList
503        let allow_mask = allow(&[1, 2, 3, 4], &[2, 4]);
504        let dropped = allow_mask.drop_nulls();
505        // Should be AllowList([1, 3]) after removing nulls
506        assert!(dropped.selected(1) && dropped.selected(3));
507        assert!(!dropped.selected(2) && !dropped.selected(4));
508
509        // Test drop_nulls for BlockList
510        let block_mask = block(&[1, 2], &[3]);
511        let dropped = block_mask.drop_nulls();
512        // BlockList: blocked = [1, 2] | [3] = [1, 2, 3]
513        assert!(!dropped.selected(1) && !dropped.selected(2) && !dropped.selected(3));
514        assert!(dropped.selected(4) && dropped.selected(5));
515    }
516
517    #[test]
518    fn test_nullable_row_id_mask_not_blocklist() {
519        let block_mask = block(&[1, 2], &[2]);
520        let not_mask = !block_mask;
521
522        // NOT(BlockList) = AllowList
523        assert!(matches!(not_mask, NullableRowAddrMask::AllowList(_)));
524    }
525
526    #[test]
527    fn test_nullable_row_id_mask_bitand_allow_allow_fast_path() {
528        // Test AllowList & AllowList with no nulls (fast path)
529        let mask1 = allow(&[1, 2, 3], &[]);
530        let mask2 = allow(&[2, 3, 4], &[]);
531
532        let result = mask1 & mask2;
533        assert_mask_selects(&result, &[2, 3], &[1, 4]);
534    }
535
536    #[test]
537    fn test_nullable_row_id_mask_bitand_allow_block() {
538        let allow_mask = allow(&[1, 2, 3, 4, 5], &[2]);
539        let block_mask = block(&[3, 4], &[4]);
540
541        let result = allow_mask & block_mask;
542        // allow: T=[1,3,4,5], N=[2]
543        // block: F=[3,4], N=[4]
544        // T & T = T; N & T = N (filtered); T & F = F; T & N = N (filtered)
545        assert_mask_selects(&result, &[1, 5], &[2, 3, 4]);
546    }
547
548    #[test]
549    fn test_nullable_row_id_mask_bitand_allow_block_fast_path() {
550        // Test AllowList & BlockList fast path (no nulls)
551        let allow_mask = allow(&[1, 2, 3], &[]);
552        let block_mask = block(&[2], &[]);
553
554        let result = allow_mask & block_mask;
555        assert_mask_selects(&result, &[1, 3], &[2]);
556    }
557
558    #[test]
559    fn test_nullable_row_id_mask_bitand_block_block() {
560        let block1 = block(&[1, 2], &[2]);
561        let block2 = block(&[2, 3], &[3]);
562
563        let result = block1 & block2;
564        // block1: F=[1], N=[2]; block2: F=[2], N=[3]
565        // F & T = F; N & F = F; T & N = N (filtered); T & T = T
566        assert_mask_selects(&result, &[4], &[1, 2, 3]);
567    }
568
569    #[test]
570    fn test_nullable_row_id_mask_bitand_block_block_fast_path() {
571        // Test BlockList & BlockList fast path (no nulls)
572        let block1 = block(&[1], &[]);
573        let block2 = block(&[2], &[]);
574
575        let result = block1 & block2;
576        assert_mask_selects(&result, &[3], &[1, 2]);
577    }
578
579    #[test]
580    fn test_nullable_row_id_mask_bitor_allow_allow_fast_path() {
581        // Test AllowList | AllowList with no nulls (fast path)
582        let mask1 = allow(&[1, 2], &[]);
583        let mask2 = allow(&[3, 4], &[]);
584
585        let result = mask1 | mask2;
586        assert_mask_selects(&result, &[1, 2, 3, 4], &[5]);
587    }
588
589    #[test]
590    fn test_nullable_row_id_mask_bitor_allow_block() {
591        let allow_mask = allow(&[1, 2, 3], &[2]);
592        let block_mask = block(&[1, 4], &[4]);
593
594        let result = allow_mask | block_mask;
595        // allow: T=[1,3], N=[2]; block: F=[1], N=[4], T=everything else
596        // T|F=T, T|T=T, N|T=T
597        assert_mask_selects(&result, &[1, 2, 3], &[]);
598    }
599
600    #[test]
601    fn test_nullable_row_id_mask_bitor_allow_block_fast_path() {
602        // Test AllowList | BlockList fast path (no nulls)
603        let allow_mask = allow(&[1], &[]);
604        let block_mask = block(&[2], &[]);
605
606        let result = allow_mask | block_mask;
607        // AllowList([1]) | BlockList([2]) = BlockList([2] - [1]) = BlockList([2])
608        assert_mask_selects(&result, &[1, 3], &[2]);
609    }
610
611    #[test]
612    fn test_nullable_row_id_mask_bitor_block_block_fast_path() {
613        // Test BlockList | BlockList with no nulls (fast path)
614        let block1 = block(&[1, 2], &[]);
615        let block2 = block(&[2, 3], &[]);
616
617        let result = block1 | block2;
618        // OR of BlockLists: BlockList([1,2] & [2,3]) = BlockList([2])
619        assert_mask_selects(&result, &[1, 3, 4], &[2]);
620    }
621}