Skip to main content

volas_core/
validity.rs

1//! Validity: an optional, packed missing-value mask for nullable column
2//! variants (`I64` / `I32` / `Bool`).
3//!
4//! `volas.NA` is one logical missing value, but each dtype stores it in its
5//! natural place: a float column uses `NaN` in-band (no mask), so only the
6//! integer and boolean variants carry a [`Validity`]. A [`Validity`] is `None`
7//! in the common dense case — every value present, **zero allocation** — and a
8//! packed [`Bitmap`] (1 bit per element, **set = present**) only once a missing
9//! value actually appears. This is the Arrow / pandas-nullable physical layout,
10//! kept behind a uniform `is_valid` interface so the rest of the library never
11//! sees the per-dtype storage difference.
12
13use std::sync::Arc;
14
15/// A packed validity mask: 1 bit per element, **set = present (valid)**, clear =
16/// missing (`volas.NA`). Bits are stored LSB-first in `u64` words; bits beyond
17/// `len` are kept clear so a popcount over every word counts exactly the present
18/// values.
19#[derive(Clone, Debug)]
20pub struct Bitmap {
21    words: Vec<u64>,
22    len: usize,
23}
24
25/// Words needed to hold `len` bits.
26fn words_for(len: usize) -> usize {
27    len.div_ceil(64)
28}
29
30impl Bitmap {
31    /// An all-present mask of `len` bits (every value valid).
32    pub fn all_valid(len: usize) -> Bitmap {
33        let mut words = vec![u64::MAX; words_for(len)];
34        // Clear the unused high bits of the final word so popcount == len.
35        if let Some(last) = words.last_mut() {
36            let used = len % 64;
37            if used != 0 {
38                *last = (1u64 << used) - 1;
39            }
40        }
41        Bitmap { words, len }
42    }
43
44    /// Build from an iterator of `valid` flags (true = present).
45    pub fn from_valid_iter(len: usize, valid: impl IntoIterator<Item = bool>) -> Bitmap {
46        let mut bm = Bitmap { words: vec![0; words_for(len)], len };
47        for (i, v) in valid.into_iter().enumerate().take(len) {
48            if v {
49                bm.words[i / 64] |= 1u64 << (i % 64);
50            }
51        }
52        bm
53    }
54
55    /// Number of elements.
56    pub fn len(&self) -> usize {
57        self.len
58    }
59
60    /// Whether the mask covers no elements.
61    pub fn is_empty(&self) -> bool {
62        self.len == 0
63    }
64
65    /// Whether element `i` is present (valid). `i` is assumed in bounds.
66    pub fn get(&self, i: usize) -> bool {
67        (self.words[i / 64] >> (i % 64)) & 1 == 1
68    }
69
70    /// Mark element `i` present (`valid = true`) or missing. `i` in bounds.
71    pub fn set(&mut self, i: usize, valid: bool) {
72        let (w, bit) = (i / 64, 1u64 << (i % 64));
73        if valid {
74            self.words[w] |= bit;
75        } else {
76            self.words[w] &= !bit;
77        }
78    }
79
80    /// Count of missing values (clear bits among the first `len`).
81    pub fn null_count(&self) -> usize {
82        let present: u32 = self.words.iter().map(|w| w.count_ones()).sum();
83        self.len - present as usize
84    }
85}
86
87impl PartialEq for Bitmap {
88    /// Equal when the same length and the same present/missing pattern (unused
89    /// high bits are always clear, so a plain word comparison is exact).
90    fn eq(&self, other: &Self) -> bool {
91        self.len == other.len && self.words == other.words
92    }
93}
94
95/// An optional validity mask. `None` (the dense default) means every value is
96/// present with no allocation; `Some` carries a [`Bitmap`] with at least one
97/// missing value. The invariant "`Some` ⇒ has a missing value" is upheld by the
98/// constructors so equality and `has_nulls` stay cheap and canonical.
99#[derive(Clone, Debug, Default)]
100pub struct Validity(Option<Arc<Bitmap>>);
101
102impl Validity {
103    /// A fully-present (dense) validity — no allocation.
104    pub fn dense() -> Validity {
105        Validity(None)
106    }
107
108    /// Wrap a bitmap, collapsing an all-present one back to dense so the dense
109    /// fast path is always taken when there are no missing values.
110    pub fn from_bitmap(bm: Bitmap) -> Validity {
111        if bm.null_count() == 0 {
112            Validity(None)
113        } else {
114            Validity(Some(Arc::new(bm)))
115        }
116    }
117
118    /// Build from an iterator of `valid` flags; dense when all are present.
119    pub fn from_valid_iter(len: usize, valid: impl IntoIterator<Item = bool>) -> Validity {
120        Validity::from_bitmap(Bitmap::from_valid_iter(len, valid))
121    }
122
123    /// Whether element `i` is present. Dense ⇒ always true. `i` in bounds.
124    pub fn is_valid(&self, i: usize) -> bool {
125        match &self.0 {
126            None => true,
127            Some(bm) => bm.get(i),
128        }
129    }
130
131    /// Whether any value is missing (cheap: dense ⇒ false).
132    pub fn has_nulls(&self) -> bool {
133        self.0.is_some()
134    }
135
136    /// Number of missing values (dense ⇒ 0).
137    pub fn null_count(&self) -> usize {
138        self.0.as_ref().map_or(0, |bm| bm.null_count())
139    }
140
141    /// Borrow the backing bitmap, if any (`None` when dense).
142    pub fn bitmap(&self) -> Option<&Bitmap> {
143        self.0.as_deref()
144    }
145
146    /// Combined validity of two equal-length columns: present only where **both**
147    /// are present — the missing-value rule for a binary op (`x ∘ NA = NA`).
148    pub fn and(&self, other: &Validity) -> Validity {
149        match (&self.0, &other.0) {
150            (None, None) => Validity::dense(),
151            // One side dense ⇒ the other's holes win (a `Some` already has a hole).
152            (Some(a), None) | (None, Some(a)) => Validity(Some(Arc::clone(a))),
153            (Some(a), Some(b)) => {
154                let n = a.len();
155                Validity::from_valid_iter(n, (0..n).map(|i| a.get(i) && b.get(i)))
156            }
157        }
158    }
159
160    /// Gather present-flags at `idx` (fancy indexing / dropna). Dense stays dense.
161    pub fn take(&self, idx: &[usize]) -> Validity {
162        match &self.0 {
163            None => Validity::dense(),
164            Some(bm) => Validity::from_valid_iter(idx.len(), idx.iter().map(|&i| bm.get(i))),
165        }
166    }
167
168    /// Present-flags over `[start, end)` (slicing). Dense stays dense.
169    pub fn slice(&self, start: usize, end: usize) -> Validity {
170        match &self.0 {
171            None => Validity::dense(),
172            Some(bm) => Validity::from_valid_iter(end - start, (start..end).map(|i| bm.get(i))),
173        }
174    }
175}
176
177impl PartialEq for Validity {
178    /// Dense compares equal to an all-present bitmap (the constructors keep
179    /// `Some` non-trivial, so in practice this reduces to comparing patterns).
180    fn eq(&self, other: &Self) -> bool {
181        match (&self.0, &other.0) {
182            (None, None) => true,
183            (Some(a), Some(b)) => a == b,
184            // Constructors collapse all-present to `None`, so a `Some` here has a
185            // missing value and can never equal a dense `None`.
186            (Some(_), None) | (None, Some(_)) => false,
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn bitmap_get_set_and_null_count() {
197        let mut bm = Bitmap::all_valid(70); // spans two words
198        assert_eq!(bm.len(), 70);
199        assert!(!bm.is_empty());
200        assert_eq!(bm.null_count(), 0);
201        assert!(bm.get(0) && bm.get(69));
202        bm.set(3, false);
203        bm.set(64, false);
204        assert!(!bm.get(3) && !bm.get(64));
205        assert_eq!(bm.null_count(), 2);
206        bm.set(3, true);
207        assert!(bm.get(3));
208        assert_eq!(bm.null_count(), 1);
209    }
210
211    #[test]
212    fn bitmap_empty_and_full_word_boundary() {
213        assert!(Bitmap::all_valid(0).is_empty());
214        // exactly 64 bits: no partial-word masking, popcount == len
215        assert_eq!(Bitmap::all_valid(64).null_count(), 0);
216    }
217
218    #[test]
219    fn bitmap_from_valid_iter_and_eq() {
220        let a = Bitmap::from_valid_iter(3, [true, false, true]);
221        let b = Bitmap::from_valid_iter(3, [true, false, true]);
222        assert_eq!(a, b);
223        assert_eq!(a.null_count(), 1);
224        assert!(!a.get(1));
225        assert_ne!(a, Bitmap::from_valid_iter(3, [true, true, true]));
226        assert_ne!(a, Bitmap::from_valid_iter(2, [true, false])); // length differs
227    }
228
229    #[test]
230    fn validity_dense_is_all_valid() {
231        let v = Validity::dense();
232        assert!(!v.has_nulls());
233        assert_eq!(v.null_count(), 0);
234        assert!(v.is_valid(0) && v.is_valid(999));
235        assert!(v.bitmap().is_none());
236    }
237
238    #[test]
239    fn validity_collapses_all_present_to_dense() {
240        // an all-present bitmap becomes dense (None)
241        let v = Validity::from_valid_iter(3, [true, true, true]);
242        assert!(!v.has_nulls());
243        assert!(v.bitmap().is_none());
244        // a bitmap with a hole stays Some
245        let v = Validity::from_valid_iter(3, [true, false, true]);
246        assert!(v.has_nulls());
247        assert_eq!(v.null_count(), 1);
248        assert!(!v.is_valid(1) && v.is_valid(0));
249        assert!(v.bitmap().is_some());
250    }
251
252    #[test]
253    fn validity_from_bitmap_and_default() {
254        assert_eq!(Validity::default(), Validity::dense());
255        let v = Validity::from_bitmap(Bitmap::all_valid(4));
256        assert!(!v.has_nulls()); // collapsed to dense
257    }
258
259    #[test]
260    fn validity_eq() {
261        let dense = Validity::dense();
262        let holed = Validity::from_valid_iter(2, [true, false]);
263        assert_eq!(dense, Validity::dense());
264        assert_eq!(holed, Validity::from_valid_iter(2, [true, false]));
265        assert_ne!(dense, holed);
266        assert_ne!(holed, dense);
267        assert_ne!(holed, Validity::from_valid_iter(2, [false, true]));
268    }
269
270    #[test]
271    fn validity_and_combines_holes() {
272        let dense = Validity::dense();
273        let a = Validity::from_valid_iter(3, [true, false, true]);
274        let b = Validity::from_valid_iter(3, [true, true, false]);
275        assert_eq!(dense.and(&dense), Validity::dense()); // both dense
276        assert_eq!(dense.and(&a), a); // dense ∧ holed -> holed
277        assert_eq!(a.and(&dense), a); // holed ∧ dense -> holed
278        // hole from either side wins
279        assert_eq!(a.and(&b), Validity::from_valid_iter(3, [true, false, false]));
280    }
281}