Skip to main content

chronix_encoding/
bitmap.rs

1//! Bitmap encoding for boolean columns.
2//!
3//! Packs boolean values at 1 bit each, padded to byte boundaries. A separate
4//! null bitmap tracks null positions for nullable boolean columns.
5//!
6//! ## Format
7//!
8//! ```text
9//! [value_count: u32]
10//! [has_nulls: u8]  (0 = no null bitmap, 1 = null bitmap follows values)
11//! [value bits: ceil(count/8) bytes]
12//! [null bitmap: ceil(count/8) bytes]  (only if has_nulls == 1)
13//! ```
14//!
15//! In the null bitmap, a `1` bit means the value is valid (non-null), and a `0`
16//! bit means the value is null.
17
18use crate::coding::{checked_count, checked_decode_count};
19use crate::error::{EncodingError, Result};
20
21/// Bitmap boolean encoder.
22#[derive(Debug, Clone, Copy)]
23pub struct BitmapEncoder;
24
25/// Bitmap boolean decoder.
26#[derive(Debug, Clone, Copy)]
27pub struct BitmapDecoder;
28
29impl BitmapEncoder {
30    /// Encode a sequence of non-nullable boolean values.
31    ///
32    /// # Errors
33    ///
34    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
35    pub fn encode(values: &[bool]) -> Result<Vec<u8>> {
36        if values.is_empty() {
37            return Err(EncodingError::EmptyInput {
38                context: "bitmap encoder",
39            });
40        }
41
42        let count = checked_count(values.len())?;
43        let bitmap_bytes = values.len().div_ceil(8);
44        let mut buf = Vec::with_capacity(4 + 1 + bitmap_bytes);
45
46        // Count
47        buf.extend_from_slice(&count.to_le_bytes());
48        // No nulls
49        buf.push(0);
50        // Value bitmap
51        buf.resize(4 + 1 + bitmap_bytes, 0);
52
53        for (i, &val) in values.iter().enumerate() {
54            if val {
55                let byte_idx = i / 8;
56                let bit_idx = 7 - (i % 8); // MSB first
57                buf[5 + byte_idx] |= 1 << bit_idx;
58            }
59        }
60
61        Ok(buf)
62    }
63
64    /// Encode a sequence of nullable boolean values.
65    ///
66    /// `None` values are represented in a separate null bitmap.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
71    pub fn encode_nullable(values: &[Option<bool>]) -> Result<Vec<u8>> {
72        if values.is_empty() {
73            return Err(EncodingError::EmptyInput {
74                context: "bitmap encoder (nullable)",
75            });
76        }
77
78        let has_nulls = values.iter().any(Option::is_none);
79        let bitmap_bytes = values.len().div_ceil(8);
80
81        let total_size = 4 + 1 + bitmap_bytes + if has_nulls { bitmap_bytes } else { 0 };
82        let count = checked_count(values.len())?;
83        let mut buf = vec![0u8; total_size];
84
85        // Count
86        buf[..4].copy_from_slice(&count.to_le_bytes());
87        // Has nulls flag
88        buf[4] = u8::from(has_nulls);
89
90        // Value bitmap
91        for (i, val) in values.iter().enumerate() {
92            if val.unwrap_or(false) {
93                let byte_idx = i / 8;
94                let bit_idx = 7 - (i % 8);
95                buf[5 + byte_idx] |= 1 << bit_idx;
96            }
97        }
98
99        // Null bitmap (1 = valid, 0 = null)
100        if has_nulls {
101            let null_offset = 5 + bitmap_bytes;
102            for (i, val) in values.iter().enumerate() {
103                if val.is_some() {
104                    let byte_idx = i / 8;
105                    let bit_idx = 7 - (i % 8);
106                    buf[null_offset + byte_idx] |= 1 << bit_idx;
107                }
108            }
109        }
110
111        Ok(buf)
112    }
113}
114
115impl BitmapDecoder {
116    /// Decode a bitmap-encoded byte buffer back to boolean values.
117    ///
118    /// # Errors
119    ///
120    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
121    pub fn decode(data: &[u8]) -> Result<Vec<bool>> {
122        if data.len() < 5 {
123            return Err(EncodingError::CorruptData {
124                detail: "bitmap data too short for header".to_string(),
125            });
126        }
127
128        let count = checked_decode_count(
129            u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
130            "bitmap",
131        )?;
132        let has_nulls = data[4];
133        if has_nulls != 0 {
134            return Err(EncodingError::CorruptData {
135                detail: "non-nullable decode called on nullable bitmap data".to_string(),
136            });
137        }
138
139        let bitmap_bytes = count.div_ceil(8);
140        if data.len() < 5 + bitmap_bytes {
141            return Err(EncodingError::CorruptData {
142                detail: "bitmap data truncated".to_string(),
143            });
144        }
145
146        let mut result = Vec::with_capacity(count);
147        for i in 0..count {
148            let byte_idx = i / 8;
149            let bit_idx = 7 - (i % 8);
150            let val = (data[5 + byte_idx] >> bit_idx) & 1 == 1;
151            result.push(val);
152        }
153
154        Ok(result)
155    }
156
157    /// Decode a nullable bitmap-encoded byte buffer.
158    ///
159    /// # Errors
160    ///
161    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
162    pub fn decode_nullable(data: &[u8]) -> Result<Vec<Option<bool>>> {
163        if data.len() < 5 {
164            return Err(EncodingError::CorruptData {
165                detail: "bitmap data too short for header".to_string(),
166            });
167        }
168
169        let count = checked_decode_count(
170            u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
171            "bitmap",
172        )?;
173        let has_nulls = data[4] != 0;
174
175        let bitmap_bytes = count.div_ceil(8);
176        let expected_len = 5 + bitmap_bytes + if has_nulls { bitmap_bytes } else { 0 };
177        if data.len() < expected_len {
178            return Err(EncodingError::CorruptData {
179                detail: "nullable bitmap data truncated".to_string(),
180            });
181        }
182
183        let mut result = Vec::with_capacity(count);
184        let null_offset = 5 + bitmap_bytes;
185
186        for i in 0..count {
187            let byte_idx = i / 8;
188            let bit_idx = 7 - (i % 8);
189
190            let is_valid = if has_nulls {
191                (data[null_offset + byte_idx] >> bit_idx) & 1 == 1
192            } else {
193                true
194            };
195
196            if is_valid {
197                let val = (data[5 + byte_idx] >> bit_idx) & 1 == 1;
198                result.push(Some(val));
199            } else {
200                result.push(None);
201            }
202        }
203
204        Ok(result)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn roundtrip_all_true() {
214        let values = vec![true; 100];
215        let encoded = BitmapEncoder::encode(&values).unwrap();
216        let decoded = BitmapDecoder::decode(&encoded).unwrap();
217        assert_eq!(values, decoded);
218    }
219
220    #[test]
221    fn roundtrip_all_false() {
222        let values = vec![false; 100];
223        let encoded = BitmapEncoder::encode(&values).unwrap();
224        let decoded = BitmapDecoder::decode(&encoded).unwrap();
225        assert_eq!(values, decoded);
226    }
227
228    #[test]
229    fn roundtrip_alternating() {
230        let values: Vec<bool> = (0..100).map(|i| i % 2 == 0).collect();
231        let encoded = BitmapEncoder::encode(&values).unwrap();
232        let decoded = BitmapDecoder::decode(&encoded).unwrap();
233        assert_eq!(values, decoded);
234    }
235
236    #[test]
237    fn roundtrip_non_aligned() {
238        // 13 values — not a multiple of 8
239        let values = vec![
240            true, false, true, true, false, false, true, false, true, false, true, true, false,
241        ];
242        let encoded = BitmapEncoder::encode(&values).unwrap();
243        let decoded = BitmapDecoder::decode(&encoded).unwrap();
244        assert_eq!(values, decoded);
245    }
246
247    #[test]
248    fn roundtrip_nullable() {
249        let values = vec![Some(true), Some(false), None, Some(true), None, Some(false)];
250        let encoded = BitmapEncoder::encode_nullable(&values).unwrap();
251        let decoded = BitmapDecoder::decode_nullable(&encoded).unwrap();
252        assert_eq!(values, decoded);
253    }
254
255    #[test]
256    fn roundtrip_nullable_no_nulls() {
257        let values = vec![Some(true), Some(false), Some(true)];
258        let encoded = BitmapEncoder::encode_nullable(&values).unwrap();
259        let decoded = BitmapDecoder::decode_nullable(&encoded).unwrap();
260        assert_eq!(values, decoded);
261    }
262
263    #[test]
264    fn roundtrip_nullable_all_null() {
265        let values = vec![None, None, None];
266        let encoded = BitmapEncoder::encode_nullable(&values).unwrap();
267        let decoded = BitmapDecoder::decode_nullable(&encoded).unwrap();
268        assert_eq!(values, decoded);
269    }
270
271    #[test]
272    fn empty_error() {
273        assert!(BitmapEncoder::encode(&[]).is_err());
274        let vals: Vec<Option<bool>> = vec![];
275        assert!(BitmapEncoder::encode_nullable(&vals).is_err());
276    }
277
278    #[test]
279    fn compression_ratio() {
280        let values = vec![true; 10_000];
281        let raw_size = values.len(); // 1 byte each as Rust bool
282        let encoded = BitmapEncoder::encode(&values).unwrap();
283        #[allow(clippy::cast_precision_loss)]
284        let ratio = raw_size as f64 / encoded.len() as f64;
285        // Should achieve ~8x (1 bit vs 1 byte) minus header
286        assert!(ratio > 6.0, "Expected ratio > 6x, got {ratio:.1}x");
287    }
288
289    #[test]
290    fn single_value() {
291        let encoded = BitmapEncoder::encode(&[true]).unwrap();
292        let decoded = BitmapDecoder::decode(&encoded).unwrap();
293        assert_eq!(vec![true], decoded);
294
295        let encoded = BitmapEncoder::encode(&[false]).unwrap();
296        let decoded = BitmapDecoder::decode(&encoded).unwrap();
297        assert_eq!(vec![false], decoded);
298    }
299}
300
301#[cfg(test)]
302mod proptests {
303    use super::*;
304    use proptest::prelude::*;
305
306    proptest! {
307        #[test]
308        fn roundtrip_bool(values in proptest::collection::vec(any::<bool>(), 1..500)) {
309            let encoded = BitmapEncoder::encode(&values).unwrap();
310            let decoded = BitmapDecoder::decode(&encoded).unwrap();
311            prop_assert_eq!(&values, &decoded);
312        }
313
314        #[test]
315        fn roundtrip_nullable(values in proptest::collection::vec(any::<Option<bool>>(), 1..500)) {
316            let encoded = BitmapEncoder::encode_nullable(&values).unwrap();
317            let decoded = BitmapDecoder::decode_nullable(&encoded).unwrap();
318            prop_assert_eq!(&values, &decoded);
319        }
320    }
321}