Skip to main content

grafeo_core/codec/
bitpack.rs

1//! Bit-packing for small integers.
2//!
3//! If your largest value is 15, why use 64 bits per number? Bit-packing uses
4//! only the bits you need - 4 bits for values up to 15, giving you 16x compression.
5//!
6//! This works especially well after delta encoding sorted data, where the deltas
7//! are often tiny even when the original values are huge.
8//!
9//! # Example
10//!
11//! ```no_run
12//! # use grafeo_core::codec::bitpack::BitPackedInts;
13//! // Values [5, 2, 3, 5, 5, 8, 2] - max is 8, needs 4 bits
14//! // Without packing: 7 * 64 = 448 bits
15//! // With packing:    7 * 4  = 28 bits (16x smaller!)
16//!
17//! let values = vec![5u64, 2, 3, 5, 5, 8, 2];
18//! let packed = BitPackedInts::pack(&values);
19//! let unpacked = packed.unpack();
20//! assert_eq!(values, unpacked);
21//! ```
22
23use std::io;
24
25use bytes::{Bytes, BytesMut};
26
27/// Converts a slice of `u64` words to a refcounted `Bytes` buffer of
28/// little-endian bytes (8 bytes per word). Used by builders that produce
29/// `Vec<u64>` and need to expose it as `Bytes` storage.
30fn words_to_bytes(words: &[u64]) -> Bytes {
31    let mut buf = BytesMut::with_capacity(words.len() * 8);
32    for &w in words {
33        buf.extend_from_slice(&w.to_le_bytes());
34    }
35    buf.freeze()
36}
37
38/// Two-variant backing for the packed `u64` words.
39///
40/// `Inline` is used when the column is built in RAM (the common path for
41/// `pack`, `from_raw_parts`, and any in-memory constructor): scans can
42/// iterate the `&[u64]` slice directly with no per-element decode.
43///
44/// `Mapped` is used when the column is constructed from a slice of an
45/// mmap-backed file (`from_bytes_storage`). Per-element access pays an
46/// 8-byte little-endian decode but no copy occurs.
47#[derive(Debug, Clone)]
48enum WordStore {
49    Inline(Vec<u64>),
50    Mapped(Bytes),
51}
52
53impl WordStore {
54    #[inline]
55    fn byte_len(&self) -> usize {
56        match self {
57            Self::Inline(v) => v.len() * 8,
58            Self::Mapped(b) => b.len(),
59        }
60    }
61
62    #[inline]
63    fn word_count(&self) -> usize {
64        match self {
65            Self::Inline(v) => v.len(),
66            Self::Mapped(b) => b.len() / 8,
67        }
68    }
69
70    #[inline]
71    fn as_slice(&self) -> Option<&[u64]> {
72        match self {
73            Self::Inline(v) => Some(v.as_slice()),
74            Self::Mapped(_) => None,
75        }
76    }
77
78    #[inline]
79    fn word_at(&self, idx: usize) -> Option<u64> {
80        match self {
81            Self::Inline(v) => v.get(idx).copied(),
82            Self::Mapped(b) => {
83                let start = idx.checked_mul(8)?;
84                let end = start.checked_add(8)?;
85                let chunk: [u8; 8] = b.get(start..end)?.try_into().ok()?;
86                Some(u64::from_le_bytes(chunk))
87            }
88        }
89    }
90
91    /// Returns a `Bytes` view for serialization. `Inline` materialises a
92    /// fresh LE-encoded buffer; `Mapped` returns the existing refcount.
93    fn to_bytes(&self) -> Bytes {
94        match self {
95            Self::Inline(v) => words_to_bytes(v),
96            Self::Mapped(b) => b.clone(),
97        }
98    }
99}
100
101/// Stores integers using only as many bits as the largest value needs.
102///
103/// Pass your values to [`pack()`](Self::pack) and we'll figure out the optimal
104/// bit width automatically. Random access via [`get()`](Self::get) is O(1).
105///
106/// # Storage
107///
108/// Phase 3c: word storage has two shapes.
109/// `Inline(Vec<u64>)` is used for in-RAM builds (`pack`, `from_raw_parts`)
110/// so scans can iterate the `&[u64]` slice directly with no per-element
111/// decode. `Mapped(Bytes)` is used for mmap-backed loads
112/// (`from_bytes_storage`) so the column shares the underlying allocation
113/// without copying. Per-element access via [`word_at`](Self::word_at)
114/// works for both; callers that want zero-decode iteration should use
115/// [`as_words_slice`](Self::as_words_slice) and fall back to `word_at`
116/// when it returns `None`.
117#[derive(Debug, Clone)]
118pub struct BitPackedInts {
119    /// Packed `u64` words: `Inline(Vec<u64>)` for RAM builds, `Mapped(Bytes)`
120    /// for mmap-backed loads. Scans branch once at the call site to use the
121    /// slice fast path when available.
122    data: WordStore,
123    /// Number of bits per value.
124    bits_per_value: u8,
125    /// Number of values.
126    count: usize,
127}
128
129impl BitPackedInts {
130    /// Reconstructs from pre-packed raw parts.
131    ///
132    /// Used by section deserialization. The caller is responsible for ensuring
133    /// the data is consistent (correct word count for the given bits and count).
134    #[must_use]
135    pub fn from_raw_parts(data: Vec<u64>, bits_per_value: u8, count: usize) -> Self {
136        Self {
137            data: WordStore::Inline(data),
138            bits_per_value,
139            count,
140        }
141    }
142
143    /// Reconstructs from pre-encoded bytes (Phase 3c entry point).
144    ///
145    /// The byte slice must be `word_count * 8` bytes of little-endian
146    /// `u64` words. Used by the mmap path so a column can hold a slice
147    /// of mapped memory without copying.
148    #[must_use]
149    pub fn from_bytes_storage(data: Bytes, bits_per_value: u8, count: usize) -> Self {
150        Self {
151            data: WordStore::Mapped(data),
152            bits_per_value,
153            count,
154        }
155    }
156
157    /// Packs a slice of u64 values using the minimum bits needed.
158    #[must_use]
159    pub fn pack(values: &[u64]) -> Self {
160        if values.is_empty() {
161            return Self {
162                data: WordStore::Inline(Vec::new()),
163                bits_per_value: 0,
164                count: 0,
165            };
166        }
167
168        let max_value = values.iter().copied().max().unwrap_or(0);
169        let bits = Self::bits_needed(max_value);
170        Self::pack_with_bits(values, bits)
171    }
172
173    /// Packs values using a specified bit width.
174    ///
175    /// # Panics
176    ///
177    /// Panics if any value doesn't fit in the specified bit width.
178    #[must_use]
179    pub fn pack_with_bits(values: &[u64], bits_per_value: u8) -> Self {
180        if values.is_empty() {
181            return Self {
182                data: WordStore::Inline(Vec::new()),
183                bits_per_value,
184                count: 0,
185            };
186        }
187
188        if bits_per_value == 0 {
189            // All values must be 0
190            debug_assert!(values.iter().all(|&v| v == 0));
191            return Self {
192                data: WordStore::Inline(Vec::new()),
193                bits_per_value: 0,
194                count: values.len(),
195            };
196        }
197
198        let bits = bits_per_value as usize;
199        let values_per_word = 64 / bits;
200        let num_words = values.len().div_ceil(values_per_word);
201
202        let mut words = vec![0u64; num_words];
203        let mask = if bits >= 64 {
204            u64::MAX
205        } else {
206            (1u64 << bits) - 1
207        };
208
209        for (i, &value) in values.iter().enumerate() {
210            debug_assert!(
211                value <= mask,
212                "Value {} doesn't fit in {} bits",
213                value,
214                bits_per_value
215            );
216
217            let word_idx = i / values_per_word;
218            let bit_offset = (i % values_per_word) * bits;
219            words[word_idx] |= (value & mask) << bit_offset;
220        }
221
222        Self {
223            data: WordStore::Inline(words),
224            bits_per_value,
225            count: values.len(),
226        }
227    }
228
229    /// Unpacks all values back to u64.
230    #[must_use]
231    pub fn unpack(&self) -> Vec<u64> {
232        if self.count == 0 {
233            return Vec::new();
234        }
235
236        if self.bits_per_value == 0 {
237            return vec![0u64; self.count];
238        }
239
240        let bits = self.bits_per_value as usize;
241        let values_per_word = 64 / bits;
242        let mask = if bits >= 64 {
243            u64::MAX
244        } else {
245            (1u64 << bits) - 1
246        };
247
248        let mut result = Vec::with_capacity(self.count);
249
250        for i in 0..self.count {
251            let word_idx = i / values_per_word;
252            let bit_offset = (i % values_per_word) * bits;
253            let word = self.word_at(word_idx).unwrap_or(0);
254            let value = (word >> bit_offset) & mask;
255            result.push(value);
256        }
257
258        result
259    }
260
261    /// Gets a single value at the given index.
262    #[must_use]
263    #[inline]
264    pub fn get(&self, index: usize) -> Option<u64> {
265        if index >= self.count {
266            return None;
267        }
268
269        if self.bits_per_value == 0 {
270            return Some(0);
271        }
272
273        let bits = self.bits_per_value as usize;
274        let values_per_word = 64 / bits;
275        let word_idx = index / values_per_word;
276        let bit_offset = (index % values_per_word) * bits;
277        let mask = if bits >= 64 {
278            u64::MAX
279        } else {
280            (1u64 << bits) - 1
281        };
282
283        let word = match &self.data {
284            WordStore::Inline(v) => *v.get(word_idx)?,
285            // Duplicates WordStore::word_at's Mapped arm intentionally so LLVM sees
286            // the full hot path without a call boundary. Keep in sync.
287            WordStore::Mapped(b) => {
288                let start = word_idx.checked_mul(8)?;
289                let end = start.checked_add(8)?;
290                let chunk: [u8; 8] = b.get(start..end)?.try_into().ok()?;
291                u64::from_le_bytes(chunk)
292            }
293        };
294        Some((word >> bit_offset) & mask)
295    }
296
297    /// Returns the row offsets where the unpacked value equals `target`.
298    ///
299    /// Hoists the `WordStore` discriminant check, the `bits_per_value`
300    /// constants, and the `values_per_word` / `mask` computation out of the
301    /// per-row loop. CodSpeed callgrind counts every retired instruction, so
302    /// re-deriving these per row (as `(0..len).filter(|i| get(*i) == ...)` did)
303    /// inflated the instruction count materially even though wall-time on
304    /// real CPUs was unaffected.
305    #[must_use]
306    pub fn scan_eq(&self, target: u64) -> Vec<usize> {
307        if self.count == 0 {
308            return Vec::new();
309        }
310
311        if self.bits_per_value == 0 {
312            return if target == 0 {
313                (0..self.count).collect()
314            } else {
315                Vec::new()
316            };
317        }
318
319        let bits = self.bits_per_value as usize;
320        // bits is in 1..=64 here (constructor guarantees), so values_per_word is 1..=64.
321        let values_per_word = 64 / bits;
322        let mask = if bits >= 64 {
323            u64::MAX
324        } else {
325            (1u64 << bits) - 1
326        };
327
328        // Bail if the target can't be represented in `bits` bits — no row
329        // can possibly match. Saves the entire scan.
330        if mask != u64::MAX && target & !mask != 0 {
331            return Vec::new();
332        }
333
334        match &self.data {
335            WordStore::Inline(words) => {
336                let mut out = Vec::new();
337                for i in 0..self.count {
338                    let word_idx = i / values_per_word;
339                    let bit_offset = (i % values_per_word) * bits;
340                    // `word_idx < words.len()` is guaranteed by the
341                    // count/bits invariant set up at construction time;
342                    // use `get` defensively rather than panic-on-malformed.
343                    let Some(&word) = words.get(word_idx) else {
344                        break;
345                    };
346                    if (word >> bit_offset) & mask == target {
347                        out.push(i);
348                    }
349                }
350                out
351            }
352            WordStore::Mapped(b) => {
353                let mut out = Vec::new();
354                let bytes = b.as_ref();
355                for i in 0..self.count {
356                    let word_idx = i / values_per_word;
357                    let bit_offset = (i % values_per_word) * bits;
358                    let start = word_idx * 8;
359                    let Some(slice) = bytes.get(start..start + 8) else {
360                        break;
361                    };
362                    let Ok(chunk) = <[u8; 8]>::try_from(slice) else {
363                        break;
364                    };
365                    let word = u64::from_le_bytes(chunk);
366                    if (word >> bit_offset) & mask == target {
367                        out.push(i);
368                    }
369                }
370                out
371            }
372        }
373    }
374
375    /// Returns the number of values.
376    #[must_use]
377    pub fn len(&self) -> usize {
378        self.count
379    }
380
381    /// Returns whether the encoding is empty.
382    #[must_use]
383    pub fn is_empty(&self) -> bool {
384        self.count == 0
385    }
386
387    /// Returns the number of bits per value.
388    #[must_use]
389    pub fn bits_per_value(&self) -> u8 {
390        self.bits_per_value
391    }
392
393    /// Returns the raw packed bytes.
394    ///
395    /// Phase 3b/3c: when storage is `Inline`, we materialise an LE-encoded
396    /// view; when storage is `Mapped`, we return the existing refcount.
397    /// Callers that hold the result must accept either ownership shape.
398    #[must_use]
399    pub fn data_bytes(&self) -> Bytes {
400        self.data.to_bytes()
401    }
402
403    /// Returns the number of `u64` words backing this column.
404    #[must_use]
405    pub fn word_count(&self) -> usize {
406        self.data.word_count()
407    }
408
409    /// Returns a direct `&[u64]` view when the column lives in RAM.
410    ///
411    /// `Some(slice)` means the caller can iterate words without per-element
412    /// decode. `None` means the column is mmap-backed; callers should fall
413    /// back to [`Self::word_at`] (one safe LE decode per access).
414    #[must_use]
415    #[inline]
416    pub fn as_words_slice(&self) -> Option<&[u64]> {
417        self.data.as_slice()
418    }
419
420    /// Returns the word at `idx`, or `None` if out of range.
421    #[must_use]
422    #[inline]
423    pub fn word_at(&self, idx: usize) -> Option<u64> {
424        self.data.word_at(idx)
425    }
426
427    /// Returns the compression ratio compared to storing full u64s.
428    #[must_use]
429    pub fn compression_ratio(&self) -> f64 {
430        if self.count == 0 {
431            return 1.0;
432        }
433
434        let original_size = self.count * 8; // 8 bytes per u64
435        let packed_size = self.data.byte_len();
436
437        if packed_size == 0 {
438            return f64::INFINITY; // All zeros, perfect compression
439        }
440
441        original_size as f64 / packed_size as f64
442    }
443
444    /// Returns the number of bits needed to represent a value.
445    ///
446    /// The result is always in `1..=64`.
447    ///
448    /// # Panics
449    ///
450    /// Cannot panic: the result of `64 - leading_zeros()` is always in
451    /// `1..=64`, which fits `u8`.
452    #[must_use]
453    pub fn bits_needed(value: u64) -> u8 {
454        if value == 0 {
455            1 // Need at least 1 bit to represent 0
456        } else {
457            // leading_zeros() returns u32 in [0, 63] for non-zero u64;
458            // (64 - n) is in [1, 64], which always fits u8.
459            u8::try_from(64u32 - value.leading_zeros()).expect("bits_needed result is in 1..=64")
460        }
461    }
462
463    /// Serializes to bytes.
464    ///
465    /// # Errors
466    ///
467    /// Returns `Err` if the value count exceeds `u32::MAX`.
468    pub fn to_bytes(&self) -> io::Result<Vec<u8>> {
469        let count_u32 = u32::try_from(self.count).map_err(|_| {
470            io::Error::new(
471                io::ErrorKind::InvalidInput,
472                format!(
473                    "BitPackedInts count {} exceeds u32::MAX, cannot serialize",
474                    self.count
475                ),
476            )
477        })?;
478        let mut buf = Vec::with_capacity(1 + 4 + self.data.byte_len());
479        buf.push(self.bits_per_value);
480        buf.extend_from_slice(&count_u32.to_le_bytes());
481        // Storage is little-endian words: borrow from `Mapped`, or build a
482        // fresh LE buffer for `Inline`.
483        let body = self.data.to_bytes();
484        buf.extend_from_slice(&body);
485        Ok(buf)
486    }
487
488    /// Deserializes from bytes.
489    ///
490    /// # Errors
491    ///
492    /// Returns `Err` if the byte slice is too short or contains invalid data.
493    pub fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
494        if bytes.len() < 5 {
495            return Err(io::Error::new(
496                io::ErrorKind::InvalidData,
497                "BitPackedInts too short",
498            ));
499        }
500
501        let bits_per_value = bytes[0];
502        if bits_per_value > 64 {
503            return Err(io::Error::new(
504                io::ErrorKind::InvalidData,
505                format!("BitPackedInts bits_per_value {bits_per_value} exceeds 64"),
506            ));
507        }
508        let count = u32::from_le_bytes(
509            bytes[1..5]
510                .try_into()
511                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
512        ) as usize;
513
514        let num_words = if bits_per_value == 0 || count == 0 {
515            0
516        } else {
517            let values_per_word = 64 / bits_per_value as usize;
518            (count + values_per_word - 1) / values_per_word
519        };
520
521        let needed = 5 + num_words * 8;
522        if bytes.len() < needed {
523            return Err(io::Error::new(
524                io::ErrorKind::InvalidData,
525                "BitPackedInts truncated",
526            ));
527        }
528
529        // Note: this always produces Mapped storage. Callers performing
530        // in-memory deserialisation that have a Vec<u64> in hand should
531        // prefer from_raw_parts() to keep the slice fast path.
532        let data = WordStore::Mapped(Bytes::copy_from_slice(&bytes[5..needed]));
533
534        Ok(Self {
535            data,
536            bits_per_value,
537            count,
538        })
539    }
540}
541
542/// The best compression for sorted integers - delta encoding plus bit-packing.
543///
544/// Stores the first value, then packs the differences between consecutive values.
545/// For sequential IDs like [1000, 1001, 1002, ...], deltas are all 1, needing just
546/// 1 bit each - that's up to 64x compression!
547#[derive(Debug, Clone)]
548pub struct DeltaBitPacked {
549    /// Base value (first value in sequence).
550    base: u64,
551    /// Bit-packed deltas.
552    deltas: BitPackedInts,
553}
554
555impl DeltaBitPacked {
556    /// Encodes sorted values using delta + bit-packing.
557    #[must_use]
558    pub fn encode(values: &[u64]) -> Self {
559        if values.is_empty() {
560            return Self {
561                base: 0,
562                deltas: BitPackedInts::pack(&[]),
563            };
564        }
565
566        let base = values[0];
567        let delta_values: Vec<u64> = values
568            .windows(2)
569            .map(|w| w[1].saturating_sub(w[0]))
570            .collect();
571
572        let deltas = BitPackedInts::pack(&delta_values);
573
574        Self { base, deltas }
575    }
576
577    /// Decodes back to the original values.
578    #[must_use]
579    pub fn decode(&self) -> Vec<u64> {
580        if self.deltas.is_empty() && self.base == 0 {
581            return Vec::new();
582        }
583
584        let delta_values = self.deltas.unpack();
585        let mut result = Vec::with_capacity(delta_values.len() + 1);
586        let mut current = self.base;
587        result.push(current);
588
589        for delta in delta_values {
590            current = current.wrapping_add(delta);
591            result.push(current);
592        }
593
594        result
595    }
596
597    /// Returns the number of values.
598    #[must_use]
599    pub fn len(&self) -> usize {
600        if self.deltas.is_empty() && self.base == 0 {
601            0
602        } else {
603            self.deltas.len() + 1
604        }
605    }
606
607    /// Returns whether the encoding is empty.
608    #[must_use]
609    pub fn is_empty(&self) -> bool {
610        self.deltas.is_empty() && self.base == 0
611    }
612
613    /// Returns the base value.
614    #[must_use]
615    pub fn base(&self) -> u64 {
616        self.base
617    }
618
619    /// Returns the bits used per delta.
620    #[must_use]
621    pub fn bits_per_delta(&self) -> u8 {
622        self.deltas.bits_per_value()
623    }
624
625    /// Returns the compression ratio.
626    #[must_use]
627    pub fn compression_ratio(&self) -> f64 {
628        let count = self.len();
629        if count == 0 {
630            return 1.0;
631        }
632
633        let original_size = count * 8;
634        let packed_size = 8 + self.deltas.data_bytes().len(); // base + packed deltas
635
636        original_size as f64 / packed_size as f64
637    }
638
639    /// Serializes to bytes.
640    ///
641    /// # Errors
642    ///
643    /// Returns `Err` if the delta count exceeds `u32::MAX`.
644    pub fn to_bytes(&self) -> io::Result<Vec<u8>> {
645        let delta_bytes = self.deltas.to_bytes()?;
646        let mut buf = Vec::with_capacity(8 + delta_bytes.len());
647        buf.extend_from_slice(&self.base.to_le_bytes());
648        buf.extend_from_slice(&delta_bytes);
649        Ok(buf)
650    }
651
652    /// Deserializes from bytes.
653    ///
654    /// # Errors
655    ///
656    /// Returns `Err` if the byte slice is too short or contains invalid data.
657    pub fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
658        if bytes.len() < 8 {
659            return Err(io::Error::new(
660                io::ErrorKind::InvalidData,
661                "DeltaBitPacked too short",
662            ));
663        }
664
665        let base = u64::from_le_bytes(
666            bytes[0..8]
667                .try_into()
668                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
669        );
670        let deltas = BitPackedInts::from_bytes(&bytes[8..])?;
671
672        Ok(Self { base, deltas })
673    }
674}
675
676#[cfg(test)]
677mod tests {
678    use super::*;
679
680    #[test]
681    fn test_bitpack_basic() {
682        let values = vec![5u64, 2, 3, 5, 5, 8, 2];
683        let packed = BitPackedInts::pack(&values);
684        let unpacked = packed.unpack();
685        assert_eq!(values, unpacked);
686    }
687
688    #[test]
689    fn test_bitpack_empty() {
690        let values: Vec<u64> = vec![];
691        let packed = BitPackedInts::pack(&values);
692        assert!(packed.is_empty());
693        assert_eq!(packed.unpack(), values);
694    }
695
696    #[test]
697    fn test_bitpack_single() {
698        let values = vec![42u64];
699        let packed = BitPackedInts::pack(&values);
700        assert_eq!(packed.len(), 1);
701        assert_eq!(packed.unpack(), values);
702    }
703
704    #[test]
705    fn test_bitpack_all_zeros() {
706        let values = vec![0u64; 100];
707        let packed = BitPackedInts::pack(&values);
708        assert_eq!(packed.bits_per_value(), 1);
709        assert_eq!(packed.unpack(), values);
710    }
711
712    #[test]
713    fn test_bitpack_powers_of_two() {
714        for bits in 1..=64u8 {
715            let max_val = if bits == 64 {
716                u64::MAX
717            } else {
718                (1u64 << bits) - 1
719            };
720            let values = vec![0, max_val / 2, max_val];
721            let packed = BitPackedInts::pack(&values);
722            assert_eq!(packed.bits_per_value(), bits);
723            assert_eq!(packed.unpack(), values);
724        }
725    }
726
727    #[test]
728    fn test_bitpack_get() {
729        let values = vec![1u64, 2, 3, 4, 5];
730        let packed = BitPackedInts::pack(&values);
731
732        for (i, &expected) in values.iter().enumerate() {
733            assert_eq!(packed.get(i), Some(expected));
734        }
735        assert_eq!(packed.get(100), None);
736    }
737
738    #[test]
739    fn test_bitpack_compression() {
740        // 100 values all <= 15 (4 bits each)
741        let values: Vec<u64> = (0..100).map(|i| i % 16).collect();
742        let packed = BitPackedInts::pack(&values);
743        assert_eq!(packed.bits_per_value(), 4);
744        // 100 * 64 bits -> 100 * 4 bits = 16x compression
745        let ratio = packed.compression_ratio();
746        assert!(ratio > 10.0, "Expected ratio > 10, got {}", ratio);
747    }
748
749    #[test]
750    fn test_bitpack_serialization() {
751        let values = vec![1u64, 3, 7, 15, 31];
752        let packed = BitPackedInts::pack(&values);
753        let bytes = packed.to_bytes().unwrap();
754        let restored = BitPackedInts::from_bytes(&bytes).unwrap();
755        assert_eq!(packed.unpack(), restored.unpack());
756    }
757
758    #[test]
759    fn test_delta_bitpacked_basic() {
760        let values = vec![100u64, 105, 107, 110, 115, 120, 128, 130];
761        let encoded = DeltaBitPacked::encode(&values);
762        let decoded = encoded.decode();
763        assert_eq!(values, decoded);
764    }
765
766    #[test]
767    fn test_delta_bitpacked_sequential() {
768        // Sequential values: deltas are all 1, needs only 1 bit each
769        let values: Vec<u64> = (1000..1100).collect();
770        let encoded = DeltaBitPacked::encode(&values);
771        assert_eq!(encoded.bits_per_delta(), 1);
772        assert_eq!(encoded.decode(), values);
773
774        // Great compression: 100 * 64 bits -> 8 (base) + ~100 bits
775        let ratio = encoded.compression_ratio();
776        assert!(ratio > 5.0, "Expected ratio > 5, got {}", ratio);
777    }
778
779    #[test]
780    fn test_delta_bitpacked_empty() {
781        let values: Vec<u64> = vec![];
782        let encoded = DeltaBitPacked::encode(&values);
783        assert!(encoded.is_empty());
784        assert_eq!(encoded.decode(), values);
785    }
786
787    #[test]
788    fn test_delta_bitpacked_single() {
789        let values = vec![42u64];
790        let encoded = DeltaBitPacked::encode(&values);
791        assert_eq!(encoded.len(), 1);
792        assert_eq!(encoded.decode(), values);
793    }
794
795    #[test]
796    fn test_delta_bitpacked_serialization() {
797        let values = vec![100u64, 105, 107, 110, 115];
798        let encoded = DeltaBitPacked::encode(&values);
799        let bytes = encoded.to_bytes().unwrap();
800        let restored = DeltaBitPacked::from_bytes(&bytes).unwrap();
801        assert_eq!(encoded.decode(), restored.decode());
802    }
803
804    #[test]
805    fn test_bits_needed() {
806        assert_eq!(BitPackedInts::bits_needed(0), 1);
807        assert_eq!(BitPackedInts::bits_needed(1), 1);
808        assert_eq!(BitPackedInts::bits_needed(2), 2);
809        assert_eq!(BitPackedInts::bits_needed(3), 2);
810        assert_eq!(BitPackedInts::bits_needed(4), 3);
811        assert_eq!(BitPackedInts::bits_needed(7), 3);
812        assert_eq!(BitPackedInts::bits_needed(8), 4);
813        assert_eq!(BitPackedInts::bits_needed(255), 8);
814        assert_eq!(BitPackedInts::bits_needed(256), 9);
815        assert_eq!(BitPackedInts::bits_needed(u64::MAX), 64);
816    }
817
818    // ── Phase 3b: Bytes-backed storage ────────────────────────────────
819
820    #[test]
821    fn test_bitpack_word_at_returns_words_from_bytes() {
822        // Pack 5 values at 4 bits each → fits in one u64 word.
823        let packed = BitPackedInts::pack(&[1u64, 3, 7, 15, 4]);
824        assert_eq!(packed.word_count(), 1);
825        let word = packed.word_at(0).unwrap();
826        // 4-bit values packed LE: bit 0..3 = 1, 4..7 = 3, 8..11 = 7, 12..15 = 15, 16..19 = 4.
827        assert_eq!(word & 0xF, 1);
828        assert_eq!((word >> 4) & 0xF, 3);
829        assert_eq!((word >> 8) & 0xF, 7);
830        assert_eq!((word >> 12) & 0xF, 15);
831        assert_eq!((word >> 16) & 0xF, 4);
832    }
833
834    #[test]
835    fn test_bitpack_word_at_out_of_range_returns_none() {
836        let packed = BitPackedInts::pack(&[1u64, 2, 3]);
837        assert!(packed.word_at(packed.word_count()).is_none());
838        assert!(packed.word_at(usize::MAX).is_none());
839    }
840
841    #[test]
842    fn test_bitpack_data_bytes_length_matches_word_count() {
843        let packed = BitPackedInts::pack_with_bits(
844            &(0u64..200).collect::<Vec<_>>(),
845            8, // 200 values × 8 bits = 1600 bits = 25 u64 words = 200 bytes
846        );
847        assert_eq!(packed.data_bytes().len(), packed.word_count() * 8);
848        // Round-trip: read each word via word_at, recombine, unpack still works.
849        let unpacked = packed.unpack();
850        assert_eq!(unpacked, (0u64..200).collect::<Vec<_>>());
851    }
852
853    #[test]
854    fn test_bitpack_round_trip_through_to_bytes_and_from_bytes() {
855        let values: Vec<u64> = (0u64..50).map(|i| i * 7 % 1024).collect();
856        let packed = BitPackedInts::pack(&values);
857        let serialized = packed.to_bytes().unwrap();
858        let restored = BitPackedInts::from_bytes(&serialized).unwrap();
859        assert_eq!(packed.unpack(), restored.unpack());
860        assert_eq!(packed.bits_per_value(), restored.bits_per_value());
861        assert_eq!(packed.len(), restored.len());
862    }
863
864    // ── Inline/Mapped storage variants (Task 1: failing tests for as_words_slice) ──
865
866    #[test]
867    fn test_bitpack_inline_storage_word_slice_via_pack() {
868        let values: Vec<u64> = (0..1000).map(|i| i % 16).collect();
869        let packed = BitPackedInts::pack(&values);
870
871        let slice = packed.as_words_slice();
872        assert!(
873            slice.is_some(),
874            "BitPackedInts::pack must produce Inline storage so scans skip per-element decode"
875        );
876    }
877
878    #[test]
879    fn test_bitpack_inline_storage_word_slice_matches_input() {
880        let words = vec![0xDEAD_BEEFu64, 0xCAFE_BABE];
881        let packed = BitPackedInts::from_raw_parts(words.clone(), 8, 16);
882        let slice = packed.as_words_slice().expect("Vec input is Inline");
883        assert_eq!(slice, words.as_slice());
884    }
885
886    #[test]
887    fn test_bitpack_mapped_storage_no_word_slice() {
888        let words: Vec<u64> = vec![1, 2, 3];
889        let mut buf = bytes::BytesMut::with_capacity(words.len() * 8);
890        for &w in &words {
891            buf.extend_from_slice(&w.to_le_bytes());
892        }
893        let packed = BitPackedInts::from_bytes_storage(buf.freeze(), 8, 24);
894
895        assert!(
896            packed.as_words_slice().is_none(),
897            "Bytes-backed (mmap) storage cannot expose a u64 slice"
898        );
899        assert_eq!(packed.word_at(0), Some(1));
900        assert_eq!(packed.word_at(2), Some(3));
901    }
902
903    #[test]
904    fn test_bitpack_get_matches_between_inline_and_mapped_storage() {
905        let values: Vec<u64> = (0..2_000).map(|i| (i * 7) % 31).collect();
906        let inline = BitPackedInts::pack(&values);
907
908        // Build a Mapped equivalent by re-encoding through bytes.
909        let bytes = inline.data_bytes();
910        let mapped =
911            BitPackedInts::from_bytes_storage(bytes, inline.bits_per_value(), values.len());
912
913        for i in 0..values.len() {
914            assert_eq!(inline.get(i), Some(values[i]), "inline mismatch at {i}");
915            assert_eq!(mapped.get(i), Some(values[i]), "mapped mismatch at {i}");
916        }
917        assert_eq!(inline.get(values.len()), None);
918        assert_eq!(mapped.get(values.len()), None);
919    }
920
921    #[test]
922    fn test_scan_eq_matches_get_filter_inline() {
923        let values: Vec<u64> = (0..5_000).map(|i| (i * 13) % 97).collect();
924        let packed = BitPackedInts::pack(&values);
925
926        for target in [0u64, 1, 50, 96, 97, u64::MAX] {
927            let scan: Vec<usize> = packed.scan_eq(target);
928            let getf: Vec<usize> = (0..values.len())
929                .filter(|&i| packed.get(i) == Some(target))
930                .collect();
931            assert_eq!(scan, getf, "scan_eq vs get-filter for target {target}");
932        }
933    }
934
935    #[test]
936    fn test_scan_eq_matches_get_filter_mapped() {
937        let values: Vec<u64> = (0..5_000).map(|i| (i * 13) % 97).collect();
938        let inline = BitPackedInts::pack(&values);
939        let bytes = inline.data_bytes();
940        let mapped =
941            BitPackedInts::from_bytes_storage(bytes, inline.bits_per_value(), values.len());
942
943        for target in [0u64, 1, 50, 96, 97, u64::MAX] {
944            let scan: Vec<usize> = mapped.scan_eq(target);
945            let getf: Vec<usize> = (0..values.len())
946                .filter(|&i| mapped.get(i) == Some(target))
947                .collect();
948            assert_eq!(
949                scan, getf,
950                "mapped scan_eq vs get-filter for target {target}"
951            );
952        }
953    }
954
955    #[test]
956    fn test_scan_eq_all_zeros_short_circuit() {
957        // bits_per_value == 0 path: every row stores 0.
958        let packed = BitPackedInts::pack(&vec![0u64; 64]);
959        assert_eq!(packed.bits_per_value(), 1);
960        // Force the zero-bits path by constructing manually.
961        let zero_bits = BitPackedInts::from_raw_parts(Vec::new(), 0, 64);
962        assert_eq!(zero_bits.scan_eq(0), (0..64).collect::<Vec<_>>());
963        assert!(zero_bits.scan_eq(1).is_empty());
964    }
965
966    #[test]
967    fn test_scan_eq_target_exceeds_mask_returns_empty() {
968        // 4-bit packing can hold 0..=15. A target of 100 must be rejected
969        // up-front without scanning.
970        let values: Vec<u64> = (0..10).map(|i| i % 16).collect();
971        let packed = BitPackedInts::pack(&values);
972        assert!(packed.bits_per_value() <= 4);
973        assert!(packed.scan_eq(100).is_empty());
974    }
975
976    #[test]
977    fn test_scan_eq_empty_column() {
978        let packed = BitPackedInts::pack(&[]);
979        assert!(packed.scan_eq(0).is_empty());
980        assert!(packed.scan_eq(42).is_empty());
981    }
982}