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 allow_true = allow.selected.clone() - &allow.nulls;
241                let block_false = block.selected.clone() - &block.nulls;
242
243                let nulls = if allow.nulls.is_empty() && block.nulls.is_empty() {
244                    RowAddrTreeMap::new() // Fast path
245                } else {
246                    // NULL|FALSE=NULL, FALSE|NULL=NULL, NULL|NULL=NULL, TRUE|NULL=TRUE.
247                    // So NULL rows are: (allow NULL & block FALSE) or (block NULL & allow not TRUE).
248                    (allow.nulls & &block_false) | (block.nulls - &allow_true)
249                };
250                let selected = (block_false - &allow_true) | &nulls;
251                Self::BlockList(NullableRowAddrSet { selected, nulls })
252            }
253            (Self::BlockList(a), Self::BlockList(b)) => {
254                let a_false = a.selected.clone() - &a.nulls;
255                let b_false = b.selected.clone() - &b.nulls;
256                let nulls = if a.nulls.is_empty() && b.nulls.is_empty() {
257                    RowAddrTreeMap::new() // Fast path
258                } else {
259                    // NULL if: (A NULL & B FALSE) or (A FALSE & B NULL) or (A NULL & B NULL).
260                    (a.nulls.clone() & &b_false)
261                        | (b.nulls.clone() & &a_false)
262                        | (a.nulls & &b.nulls)
263                };
264                let selected = (a_false & b_false) | &nulls;
265                Self::BlockList(NullableRowAddrSet { selected, nulls })
266            }
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    fn rows(ids: &[u64]) -> RowAddrTreeMap {
276        RowAddrTreeMap::from_iter(ids)
277    }
278
279    fn nullable_set(selected: &[u64], nulls: &[u64]) -> NullableRowAddrSet {
280        NullableRowAddrSet::new(rows(selected), rows(nulls))
281    }
282
283    fn allow(selected: &[u64], nulls: &[u64]) -> NullableRowAddrMask {
284        NullableRowAddrMask::AllowList(nullable_set(selected, nulls))
285    }
286
287    fn block(selected: &[u64], nulls: &[u64]) -> NullableRowAddrMask {
288        NullableRowAddrMask::BlockList(nullable_set(selected, nulls))
289    }
290
291    fn assert_mask_selects(mask: &NullableRowAddrMask, selected: &[u64], not_selected: &[u64]) {
292        for &id in selected {
293            assert!(mask.selected(id), "Expected row {} to be selected", id);
294        }
295        for &id in not_selected {
296            assert!(!mask.selected(id), "Expected row {} to NOT be selected", id);
297        }
298    }
299
300    #[test]
301    fn test_not_with_nulls() {
302        // Test case from issue #4756: x != 5 on data [0, 5, null]
303        // x = 5 should return: AllowList with selected=[1,2], nulls=[2]
304        // NOT(x = 5) should return: BlockList with selected=[1,2], nulls=[2]
305        // selected() should return TRUE for row 0, FALSE for rows 1 and 2
306        let mask = allow(&[1, 2], &[2]);
307        let not_mask = !mask;
308
309        // Row 0: selected (x=0, which is != 5)
310        // Row 1: NOT selected (x=5, which is == 5)
311        // Row 2: NOT selected (x=null, comparison result is null)
312        assert_mask_selects(&not_mask, &[0], &[1, 2]);
313    }
314
315    #[test]
316    fn test_and_with_nulls() {
317        // Test Kleene AND logic: true AND null = null, false AND null = false
318
319        // Case 1: TRUE mask AND mask with nulls
320        let true_mask = allow(&[0, 1, 2, 3, 4], &[]);
321        let null_mask = allow(&[0, 1, 2, 3, 4], &[1, 3]);
322        let result = true_mask & null_mask.clone();
323
324        // TRUE AND TRUE = TRUE; TRUE AND NULL = NULL (filtered out)
325        assert_mask_selects(&result, &[0, 2, 4], &[1, 3]);
326
327        // Case 2: FALSE mask AND mask with nulls
328        let false_mask = block(&[0, 1, 2, 3, 4], &[]);
329        let result = false_mask & null_mask;
330
331        // FALSE AND anything = FALSE
332        assert_mask_selects(&result, &[], &[0, 1, 2, 3, 4]);
333
334        // Case 3: Both masks have nulls - union of null sets
335        let mask1 = allow(&[0, 1, 2], &[1]);
336        let mask2 = allow(&[0, 2, 3], &[2]);
337        let result = mask1 & mask2;
338
339        // Only row 0 is TRUE in both; rows 1,2 are null in at least one; row 3 not in first
340        assert_mask_selects(&result, &[0], &[1, 2, 3]);
341    }
342
343    #[test]
344    fn test_or_with_nulls() {
345        // Test Kleene OR logic: true OR null = true, false OR null = null
346
347        // Case 1: FALSE mask OR mask with nulls
348        let false_mask = block(&[0, 1, 2], &[]);
349        let null_mask = allow(&[0, 1, 2], &[1, 2]);
350        let result = false_mask | null_mask.clone();
351
352        // FALSE OR TRUE = TRUE; FALSE OR NULL = NULL (filtered out)
353        assert_mask_selects(&result, &[0], &[1, 2]);
354
355        // Case 2: TRUE mask OR mask with nulls
356        let true_mask = allow(&[0, 1, 2], &[]);
357        let result = true_mask | null_mask;
358
359        // TRUE OR anything = TRUE
360        assert_mask_selects(&result, &[0, 1, 2], &[]);
361
362        // Case 3: Both have nulls
363        let mask1 = block(&[0, 1, 2, 3], &[1, 2]);
364        let mask2 = block(&[0, 1, 2, 3], &[2, 3]);
365        let result = mask1 | mask2;
366
367        // Row 0: FALSE in both; Rows 1,2,3: NULL in at least one
368        assert_mask_selects(&result, &[], &[0, 1, 2, 3]);
369    }
370
371    #[test]
372    fn test_or_allow_block_keeps_block_nulls() {
373        // Allow|Block OR must preserve NULLs from block even when block.selected is empty.
374        // allow: TRUE=[1], NULL=[0]; block: FALSE=[], NULL=[0]
375        let allow_mask = allow(&[1], &[0]);
376        let block_mask = block(&[], &[0]);
377        let result = allow_mask | block_mask;
378
379        // Row 1 is TRUE; row 0 remains NULL (not selected)
380        assert_mask_selects(&result, &[1], &[0]);
381    }
382
383    #[test]
384    fn test_or_allow_block_keeps_block_nulls_with_false_rows() {
385        // Ensure FALSE stays FALSE and NULL stays NULL when both appear on the block side.
386        // allow: TRUE=[2], NULL=[]; block: FALSE=[1], NULL=[0]
387        let allow_mask = allow(&[2], &[]);
388        let block_mask = block(&[1], &[0]);
389        let result = allow_mask | block_mask;
390
391        // Row 2 is TRUE; row 1 is FALSE; row 0 remains NULL (not selected)
392        assert_mask_selects(&result, &[2], &[0, 1]);
393    }
394
395    #[test]
396    fn test_or_block_block_true_overrides_null() {
397        // TRUE OR NULL should be TRUE, even when both sides are BlockList.
398        let true_mask = block(&[], &[]);
399        let null_mask = block(&[], &[0]);
400        let result = true_mask | null_mask;
401
402        // Row 0 should be TRUE.
403        assert_mask_selects(&result, &[0], &[]);
404    }
405
406    #[test]
407    fn test_row_selection_bit_or() {
408        // [T, N, T, N, F, F, F]
409        let left = nullable_set(&[1, 2, 3, 4], &[2, 4]);
410        // [F, F, T, N, T, N, N]
411        let right = nullable_set(&[3, 4, 5, 6], &[4, 6, 7]);
412        // [T, N, T, N, T, N, N]
413        let expected_true = rows(&[1, 3, 5]);
414        let expected_nulls = rows(&[2, 4, 6, 7]);
415
416        let mut result = left.clone();
417        result |= &right;
418        assert_eq!(&result.true_rows(), &expected_true);
419        assert_eq!(result.null_rows(), &expected_nulls);
420
421        // Commutative property holds
422        let mut result = right.clone();
423        result |= &left;
424        assert_eq!(&result.true_rows(), &expected_true);
425        assert_eq!(result.null_rows(), &expected_nulls);
426    }
427
428    #[test]
429    fn test_row_selection_bit_and() {
430        // [T, N, T, N, F, F, F]
431        let left = nullable_set(&[1, 2, 3, 4], &[2, 4]);
432        // [F, F, T, N, T, N, N]
433        let right = nullable_set(&[3, 4, 5, 6], &[4, 6, 7]);
434        // [F, F, T, N, F, F, F]
435        let expected_true = rows(&[3]);
436        let expected_nulls = rows(&[4]);
437
438        let mut result = left.clone();
439        result &= &right;
440        assert_eq!(&result.true_rows(), &expected_true);
441        assert_eq!(result.null_rows(), &expected_nulls);
442
443        // Commutative property holds
444        let mut result = right.clone();
445        result &= &left;
446        assert_eq!(&result.true_rows(), &expected_true);
447        assert_eq!(result.null_rows(), &expected_nulls);
448    }
449
450    #[test]
451    fn test_union_all() {
452        // Union all is basically a series of ORs.
453        // [T, T, T, N, N, N, F, F, F]
454        let set1 = nullable_set(&[1, 2, 3, 4], &[4, 5, 6]);
455        // [T, N, F, T, N, F, T, N, F]
456        let set2 = nullable_set(&[1, 4, 7, 8], &[2, 5, 8]);
457        let set3 = NullableRowAddrSet::empty();
458
459        let result = NullableRowAddrSet::union_all(&[set1, set2, set3]);
460
461        // [T, T, T, T, N, N, T, N, F]
462        assert_eq!(&result.true_rows(), &rows(&[1, 2, 3, 4, 7]));
463        assert_eq!(result.null_rows(), &rows(&[5, 6, 8]));
464    }
465
466    #[test]
467    fn test_nullable_row_addr_set_with_nulls() {
468        let set = NullableRowAddrSet::new(rows(&[1, 2, 3]), RowAddrTreeMap::new());
469        let set_with_nulls = set.with_nulls(rows(&[2]));
470
471        assert!(set_with_nulls.selected(1) && set_with_nulls.selected(3));
472        assert!(!set_with_nulls.selected(2)); // null
473    }
474
475    #[test]
476    fn test_nullable_row_addr_set_len_and_is_empty() {
477        let set = nullable_set(&[1, 2, 3, 4, 5], &[2, 4]);
478
479        // len() returns count of TRUE rows (selected - nulls)
480        assert_eq!(set.len(), Some(3)); // 1, 3, 5
481        assert!(!set.is_empty());
482
483        let empty_set = NullableRowAddrSet::empty();
484        assert!(empty_set.is_empty());
485        assert_eq!(empty_set.len(), Some(0));
486    }
487
488    #[test]
489    fn test_nullable_row_addr_set_selected() {
490        let set = nullable_set(&[1, 2, 3], &[2]);
491
492        // selected() returns true only for TRUE rows (in selected and not in nulls)
493        assert!(set.selected(1) && set.selected(3));
494        assert!(!set.selected(2)); // null
495        assert!(!set.selected(4)); // not in selected
496    }
497
498    #[test]
499    fn test_nullable_row_addr_set_partial_eq() {
500        let set1 = nullable_set(&[1, 2, 3], &[2]);
501        let set2 = nullable_set(&[1, 2, 3], &[2]);
502        // set3 has same true_rows but different nulls
503        let set3 = nullable_set(&[1, 3], &[3]);
504
505        assert_eq!(set1, set2);
506        assert_ne!(set1, set3); // different nulls
507    }
508
509    #[test]
510    fn test_nullable_row_addr_set_bitand_fast_path() {
511        // Test fast path when both have no nulls
512        let set1 = nullable_set(&[1, 2, 3], &[]);
513        let set2 = nullable_set(&[2, 3, 4], &[]);
514
515        let mut result = set1;
516        result &= &set2;
517
518        // Intersection: [2, 3]
519        assert!(result.selected(2) && result.selected(3));
520        assert!(!result.selected(1) && !result.selected(4));
521        assert!(result.null_rows().is_empty());
522    }
523
524    #[test]
525    fn test_nullable_row_addr_set_bitor_fast_path() {
526        // Test fast path when both have no nulls
527        let set1 = nullable_set(&[1, 2], &[]);
528        let set2 = nullable_set(&[3, 4], &[]);
529
530        let mut result = set1;
531        result |= &set2;
532
533        // Union: [1, 2, 3, 4]
534        for id in [1, 2, 3, 4] {
535            assert!(result.selected(id));
536        }
537        assert!(result.null_rows().is_empty());
538    }
539
540    #[test]
541    fn test_nullable_row_id_mask_drop_nulls() {
542        // Test drop_nulls for AllowList
543        let allow_mask = allow(&[1, 2, 3, 4], &[2, 4]);
544        let dropped = allow_mask.drop_nulls();
545        // Should be AllowList([1, 3]) after removing nulls
546        assert!(dropped.selected(1) && dropped.selected(3));
547        assert!(!dropped.selected(2) && !dropped.selected(4));
548
549        // Test drop_nulls for BlockList
550        let block_mask = block(&[1, 2], &[3]);
551        let dropped = block_mask.drop_nulls();
552        // BlockList: blocked = [1, 2] | [3] = [1, 2, 3]
553        assert!(!dropped.selected(1) && !dropped.selected(2) && !dropped.selected(3));
554        assert!(dropped.selected(4) && dropped.selected(5));
555    }
556
557    #[test]
558    fn test_nullable_row_id_mask_not_blocklist() {
559        let block_mask = block(&[1, 2], &[2]);
560        let not_mask = !block_mask;
561
562        // NOT(BlockList) = AllowList
563        assert!(matches!(not_mask, NullableRowAddrMask::AllowList(_)));
564    }
565
566    #[test]
567    fn test_nullable_row_id_mask_bitand_allow_allow_fast_path() {
568        // Test AllowList & AllowList with no nulls (fast path)
569        let mask1 = allow(&[1, 2, 3], &[]);
570        let mask2 = allow(&[2, 3, 4], &[]);
571
572        let result = mask1 & mask2;
573        assert_mask_selects(&result, &[2, 3], &[1, 4]);
574    }
575
576    #[test]
577    fn test_nullable_row_id_mask_bitand_allow_block() {
578        let allow_mask = allow(&[1, 2, 3, 4, 5], &[2]);
579        let block_mask = block(&[3, 4], &[4]);
580
581        let result = allow_mask & block_mask;
582        // allow: T=[1,3,4,5], N=[2]
583        // block: F=[3,4], N=[4]
584        // T & T = T; N & T = N (filtered); T & F = F; T & N = N (filtered)
585        assert_mask_selects(&result, &[1, 5], &[2, 3, 4]);
586    }
587
588    #[test]
589    fn test_nullable_row_id_mask_bitand_allow_block_fast_path() {
590        // Test AllowList & BlockList fast path (no nulls)
591        let allow_mask = allow(&[1, 2, 3], &[]);
592        let block_mask = block(&[2], &[]);
593
594        let result = allow_mask & block_mask;
595        assert_mask_selects(&result, &[1, 3], &[2]);
596    }
597
598    #[test]
599    fn test_nullable_row_id_mask_bitand_block_block() {
600        let block1 = block(&[1, 2], &[2]);
601        let block2 = block(&[2, 3], &[3]);
602
603        let result = block1 & block2;
604        // block1: F=[1], N=[2]; block2: F=[2], N=[3]
605        // F & T = F; N & F = F; T & N = N (filtered); T & T = T
606        assert_mask_selects(&result, &[4], &[1, 2, 3]);
607    }
608
609    #[test]
610    fn test_nullable_row_id_mask_bitand_block_block_fast_path() {
611        // Test BlockList & BlockList fast path (no nulls)
612        let block1 = block(&[1], &[]);
613        let block2 = block(&[2], &[]);
614
615        let result = block1 & block2;
616        assert_mask_selects(&result, &[3], &[1, 2]);
617    }
618
619    #[test]
620    fn test_nullable_row_id_mask_bitor_allow_allow_fast_path() {
621        // Test AllowList | AllowList with no nulls (fast path)
622        let mask1 = allow(&[1, 2], &[]);
623        let mask2 = allow(&[3, 4], &[]);
624
625        let result = mask1 | mask2;
626        assert_mask_selects(&result, &[1, 2, 3, 4], &[5]);
627    }
628
629    #[test]
630    fn test_nullable_row_id_mask_bitor_allow_block() {
631        let allow_mask = allow(&[1, 2, 3], &[2]);
632        let block_mask = block(&[1, 4], &[4]);
633
634        let result = allow_mask | block_mask;
635        // allow: T=[1,3], N=[2]; block: F=[1], N=[4], T=everything else
636        // T|F=T, T|T=T, N|T=T
637        assert_mask_selects(&result, &[1, 2, 3], &[]);
638    }
639
640    #[test]
641    fn test_nullable_row_id_mask_bitor_allow_block_fast_path() {
642        // Test AllowList | BlockList fast path (no nulls)
643        let allow_mask = allow(&[1], &[]);
644        let block_mask = block(&[2], &[]);
645
646        let result = allow_mask | block_mask;
647        // AllowList([1]) | BlockList([2]) = BlockList([2] - [1]) = BlockList([2])
648        assert_mask_selects(&result, &[1, 3], &[2]);
649    }
650
651    #[test]
652    fn test_nullable_row_id_mask_bitor_block_block_fast_path() {
653        // Test BlockList | BlockList with no nulls (fast path)
654        let block1 = block(&[1, 2], &[]);
655        let block2 = block(&[2, 3], &[]);
656
657        let result = block1 | block2;
658        // OR of BlockLists: BlockList([1,2] & [2,3]) = BlockList([2])
659        assert_mask_selects(&result, &[1, 3, 4], &[2]);
660    }
661}