Skip to main content

chronix_encoding/
integer.rs

1//! Integer encoding using Delta + `ZigZag` + variable-length bit-packing.
2//!
3//! ## Encoders
4//!
5//! - [`IntegerEncoder`]: Delta + ZigZag + **fixed-width** bit-packing.
6//!   Optimal for monotonic or slowly-changing sequences where all deltas
7//!   have a similar magnitude.
8//!
9//! - [`VarintEncoder`]: Delta + ZigZag + **LEB128 varint** per delta.
10//!   Optimal for sparse or highly variable data where most deltas are
11//!   small but occasional outliers would inflate the fixed bit width.
12//!
13//! Both encoders compute deltas between consecutive values and ZigZag-encode
14//! them, but differ in how the unsigned deltas are serialised.
15
16use crate::coding::{
17    bits_needed, checked_count, checked_decode_count, pack_bits, unpack_bits, varint_decode,
18    varint_encode, zigzag_decode, zigzag_encode,
19};
20use crate::error::{EncodingError, Result};
21
22/// Integer column encoder (Delta + `ZigZag` + bit-packing).
23#[derive(Debug, Clone, Copy)]
24pub struct IntegerEncoder;
25
26/// Integer column decoder.
27#[derive(Debug, Clone, Copy)]
28pub struct IntegerDecoder;
29
30impl IntegerEncoder {
31    /// Encode a sequence of `i64` values.
32    ///
33    /// # Errors
34    ///
35    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
36    pub fn encode_i64(values: &[i64]) -> Result<Vec<u8>> {
37        if values.is_empty() {
38            return Err(EncodingError::EmptyInput {
39                context: "integer encoder (i64)",
40            });
41        }
42
43        // Compute deltas and zigzag-encode them
44        let mut zigzag_deltas = Vec::with_capacity(values.len().saturating_sub(1));
45        let mut max_zz: u64 = 0;
46
47        for i in 1..values.len() {
48            let delta = values[i].wrapping_sub(values[i - 1]);
49            let zz = zigzag_encode(delta);
50            max_zz = max_zz.max(zz);
51            zigzag_deltas.push(zz);
52        }
53
54        let bit_width = bits_needed(max_zz);
55
56        // Header: [count: u32][first_value: i64][bit_width: u8]
57        let count = checked_count(values.len())?;
58        let header_size = 4 + 8 + 1;
59        let data_bits = zigzag_deltas.len() as u64 * u64::from(bit_width);
60        let data_bytes = data_bits.div_ceil(8) as usize;
61        let mut buf = Vec::with_capacity(header_size + data_bytes);
62
63        // Count
64        buf.extend_from_slice(&count.to_le_bytes());
65        // First value
66        buf.extend_from_slice(&values[0].to_le_bytes());
67        // Bit width
68        buf.push(bit_width);
69
70        if bit_width == 0 {
71            // All deltas are 0 — constant sequence, no data needed
72            return Ok(buf);
73        }
74
75        // Pack deltas at bit_width bits each
76        pack_bits(&zigzag_deltas, bit_width, &mut buf);
77
78        Ok(buf)
79    }
80
81    /// Encode a sequence of `u64` values.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
86    pub fn encode_u64(values: &[u64]) -> Result<Vec<u8>> {
87        if values.is_empty() {
88            return Err(EncodingError::EmptyInput {
89                context: "integer encoder (u64)",
90            });
91        }
92
93        // Treat u64 as i64 for delta encoding (wrapping arithmetic works)
94        let i64_values: Vec<i64> = values.iter().map(|&v| v as i64).collect();
95        Self::encode_i64(&i64_values)
96    }
97}
98
99impl IntegerDecoder {
100    /// Decode an integer-encoded byte buffer back to `i64` values.
101    ///
102    /// # Errors
103    ///
104    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
105    pub fn decode_i64(data: &[u8]) -> Result<Vec<i64>> {
106        if data.len() < 13 {
107            return Err(EncodingError::CorruptData {
108                detail: "integer data too short for header".to_string(),
109            });
110        }
111
112        let count = checked_decode_count(
113            u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
114            "integer",
115        )?;
116        let first_value = i64::from_le_bytes([
117            data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
118        ]);
119        let bit_width = data[12];
120
121        if bit_width > 64 {
122            return Err(EncodingError::CorruptData {
123                detail: format!("invalid bit_width {bit_width}, max is 64"),
124            });
125        }
126
127        if count == 0 {
128            return Ok(Vec::new());
129        }
130
131        let mut result = Vec::with_capacity(count);
132        result.push(first_value);
133
134        if count == 1 || bit_width == 0 {
135            // Constant sequence
136            for _ in 1..count {
137                result.push(first_value);
138            }
139            return Ok(result);
140        }
141
142        // Unpack deltas
143        let packed_data = &data[13..];
144        let deltas = unpack_bits(packed_data, count - 1, bit_width)?;
145
146        // SIMD-friendly batch zigzag decode + prefix sum
147        let mut decoded_deltas = Vec::new();
148        crate::simd::batch_zigzag_decode(&deltas, &mut decoded_deltas);
149        crate::simd::batch_prefix_sum_i64(first_value, &decoded_deltas, &mut result);
150
151        Ok(result)
152    }
153
154    /// Decode an integer-encoded byte buffer back to `u64` values.
155    ///
156    /// # Errors
157    ///
158    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
159    pub fn decode_u64(data: &[u8]) -> Result<Vec<u64>> {
160        let i64_values = Self::decode_i64(data)?;
161        Ok(i64_values.into_iter().map(|v| v as u64).collect())
162    }
163}
164
165// ── Varint integer encoding ────────────────────────────────────────────
166
167/// Integer encoder using Delta + ZigZag + LEB128 varint per delta.
168///
169/// Unlike [`IntegerEncoder`] which uses a uniform bit width for all deltas
170/// (determined by the largest delta), this encoder uses LEB128 varint
171/// encoding so each delta occupies only the bytes it needs.
172///
173/// This is more efficient for **sparse or highly variable** data where
174/// most deltas are small but occasional large outliers would inflate
175/// the fixed bit width.
176///
177/// ## Wire format
178///
179/// ```text
180/// [count: u32 LE][first_value: i64 LE][zigzag(delta₀) as varint]…
181/// ```
182#[derive(Debug, Clone, Copy)]
183pub struct VarintEncoder;
184
185/// Decoder for [`VarintEncoder`]-encoded data.
186#[derive(Debug, Clone, Copy)]
187pub struct VarintDecoder;
188
189impl VarintEncoder {
190    /// Encode a sequence of `i64` values using delta + ZigZag + LEB128 varint.
191    ///
192    /// # Errors
193    ///
194    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
195    pub fn encode_i64(values: &[i64]) -> Result<Vec<u8>> {
196        if values.is_empty() {
197            return Err(EncodingError::EmptyInput {
198                context: "varint encoder (i64)",
199            });
200        }
201
202        let count = checked_count(values.len())?;
203
204        // Header: [count: u32][first_value: i64]
205        let mut buf = Vec::with_capacity(12 + values.len() * 2);
206        buf.extend_from_slice(&count.to_le_bytes());
207        buf.extend_from_slice(&values[0].to_le_bytes());
208
209        for i in 1..values.len() {
210            let delta = values[i].wrapping_sub(values[i - 1]);
211            let zz = zigzag_encode(delta);
212            varint_encode(zz, &mut buf);
213        }
214
215        Ok(buf)
216    }
217
218    /// Encode a sequence of `u64` values.
219    ///
220    /// # Errors
221    ///
222    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
223    pub fn encode_u64(values: &[u64]) -> Result<Vec<u8>> {
224        if values.is_empty() {
225            return Err(EncodingError::EmptyInput {
226                context: "varint encoder (u64)",
227            });
228        }
229        let i64_values: Vec<i64> = values.iter().map(|&v| v as i64).collect();
230        Self::encode_i64(&i64_values)
231    }
232}
233
234impl VarintDecoder {
235    /// Decode a varint-encoded byte buffer back to `i64` values.
236    ///
237    /// # Errors
238    ///
239    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
240    pub fn decode_i64(data: &[u8]) -> Result<Vec<i64>> {
241        if data.len() < 12 {
242            return Err(EncodingError::CorruptData {
243                detail: "varint data too short for header".to_string(),
244            });
245        }
246
247        let count = checked_decode_count(
248            u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
249            "integer",
250        )?;
251        let first_value = i64::from_le_bytes([
252            data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
253        ]);
254
255        if count == 0 {
256            return Ok(Vec::new());
257        }
258
259        let mut result = Vec::with_capacity(count);
260        result.push(first_value);
261
262        let mut pos = 12;
263        for _ in 1..count {
264            if pos >= data.len() {
265                return Err(EncodingError::CorruptData {
266                    detail: "unexpected end of varint-encoded data".to_string(),
267                });
268            }
269            let (zz, consumed) = varint_decode(&data[pos..])?;
270            pos += consumed;
271            let delta = zigzag_decode(zz);
272            let prev = result[result.len() - 1];
273            result.push(prev.wrapping_add(delta));
274        }
275
276        Ok(result)
277    }
278
279    /// Decode a varint-encoded byte buffer back to `u64` values.
280    ///
281    /// # Errors
282    ///
283    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
284    pub fn decode_u64(data: &[u8]) -> Result<Vec<u64>> {
285        let i64_values = Self::decode_i64(data)?;
286        Ok(i64_values.into_iter().map(|v| v as u64).collect())
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn roundtrip_monotonic_i64() {
296        let values: Vec<i64> = (0..1000).collect();
297        let encoded = IntegerEncoder::encode_i64(&values).unwrap();
298        let decoded = IntegerDecoder::decode_i64(&encoded).unwrap();
299        assert_eq!(values, decoded);
300    }
301
302    #[test]
303    fn roundtrip_constant_i64() {
304        let values = vec![42_i64; 100];
305        let encoded = IntegerEncoder::encode_i64(&values).unwrap();
306        let decoded = IntegerDecoder::decode_i64(&encoded).unwrap();
307        assert_eq!(values, decoded);
308        // Constant → bit_width=0 → header only
309        assert_eq!(encoded.len(), 13);
310    }
311
312    #[test]
313    fn roundtrip_negative_i64() {
314        let values = vec![-100, -50, -10, 0, 10, 50, 100];
315        let encoded = IntegerEncoder::encode_i64(&values).unwrap();
316        let decoded = IntegerDecoder::decode_i64(&encoded).unwrap();
317        assert_eq!(values, decoded);
318    }
319
320    #[test]
321    fn roundtrip_edge_i64() {
322        let values = vec![i64::MIN, 0, i64::MAX];
323        let encoded = IntegerEncoder::encode_i64(&values).unwrap();
324        let decoded = IntegerDecoder::decode_i64(&encoded).unwrap();
325        assert_eq!(values, decoded);
326    }
327
328    #[test]
329    fn roundtrip_single_i64() {
330        let values = vec![42_i64];
331        let encoded = IntegerEncoder::encode_i64(&values).unwrap();
332        let decoded = IntegerDecoder::decode_i64(&encoded).unwrap();
333        assert_eq!(values, decoded);
334    }
335
336    #[test]
337    fn roundtrip_u64() {
338        let values: Vec<u64> = vec![0, 1, 100, u64::MAX / 2, u64::MAX];
339        let encoded = IntegerEncoder::encode_u64(&values).unwrap();
340        let decoded = IntegerDecoder::decode_u64(&encoded).unwrap();
341        assert_eq!(values, decoded);
342    }
343
344    #[test]
345    fn empty_error() {
346        assert!(IntegerEncoder::encode_i64(&[]).is_err());
347        assert!(IntegerEncoder::encode_u64(&[]).is_err());
348    }
349
350    #[test]
351    fn compression_ratio_monotonic() {
352        let values: Vec<i64> = (0..10_000).collect();
353        let raw_size = values.len() * 8;
354        let encoded = IntegerEncoder::encode_i64(&values).unwrap();
355        #[allow(clippy::cast_precision_loss)]
356        let ratio = raw_size as f64 / encoded.len() as f64;
357        assert!(
358            ratio > 5.0,
359            "Expected ratio > 5x on monotonic data, got {ratio:.1}x"
360        );
361    }
362
363    #[test]
364    fn corrupt_bit_width() {
365        // Build a valid header with invalid bit_width = 255
366        let mut data = vec![0u8; 13];
367        data[0..4].copy_from_slice(&2u32.to_le_bytes()); // count=2
368        data[4..12].copy_from_slice(&42i64.to_le_bytes()); // first_value=42
369        data[12] = 255; // invalid bit_width
370        assert!(IntegerDecoder::decode_i64(&data).is_err());
371    }
372
373    // ── VarintEncoder / VarintDecoder tests ────────────────────────────
374
375    #[test]
376    fn varint_roundtrip_monotonic_i64() {
377        let values: Vec<i64> = (0..1000).collect();
378        let encoded = VarintEncoder::encode_i64(&values).unwrap();
379        let decoded = VarintDecoder::decode_i64(&encoded).unwrap();
380        assert_eq!(values, decoded);
381    }
382
383    #[test]
384    fn varint_roundtrip_constant_i64() {
385        let values = vec![42_i64; 100];
386        let encoded = VarintEncoder::encode_i64(&values).unwrap();
387        let decoded = VarintDecoder::decode_i64(&encoded).unwrap();
388        assert_eq!(values, decoded);
389        // Constant → all deltas are 0 → 1 byte (varint 0) each
390        // Header (12) + 99 * 1 byte = 111
391        assert_eq!(encoded.len(), 12 + 99);
392    }
393
394    #[test]
395    fn varint_roundtrip_negative_i64() {
396        let values = vec![-100, -50, -10, 0, 10, 50, 100];
397        let encoded = VarintEncoder::encode_i64(&values).unwrap();
398        let decoded = VarintDecoder::decode_i64(&encoded).unwrap();
399        assert_eq!(values, decoded);
400    }
401
402    #[test]
403    fn varint_roundtrip_edge_i64() {
404        let values = vec![i64::MIN, 0, i64::MAX];
405        let encoded = VarintEncoder::encode_i64(&values).unwrap();
406        let decoded = VarintDecoder::decode_i64(&encoded).unwrap();
407        assert_eq!(values, decoded);
408    }
409
410    #[test]
411    fn varint_roundtrip_single_i64() {
412        let values = vec![42_i64];
413        let encoded = VarintEncoder::encode_i64(&values).unwrap();
414        let decoded = VarintDecoder::decode_i64(&encoded).unwrap();
415        assert_eq!(values, decoded);
416    }
417
418    #[test]
419    fn varint_roundtrip_u64() {
420        let values: Vec<u64> = vec![0, 1, 100, u64::MAX / 2, u64::MAX];
421        let encoded = VarintEncoder::encode_u64(&values).unwrap();
422        let decoded = VarintDecoder::decode_u64(&encoded).unwrap();
423        assert_eq!(values, decoded);
424    }
425
426    #[test]
427    fn varint_empty_error() {
428        assert!(VarintEncoder::encode_i64(&[]).is_err());
429        assert!(VarintEncoder::encode_u64(&[]).is_err());
430    }
431
432    #[test]
433    fn varint_better_for_sparse_data() {
434        // Sparse data: mostly small deltas, some large outliers.
435        // Varint should beat fixed-bit-width here because the fixed width
436        // is driven by the max delta, wasting space on the small ones.
437        let mut values = Vec::new();
438        for i in 0..1000 {
439            if i % 100 == 0 {
440                values.push(i * 1_000_000); // large jump
441            } else {
442                values.push(values.last().copied().unwrap_or(0) + 1); // tiny delta
443            }
444        }
445        let fixed = IntegerEncoder::encode_i64(&values).unwrap();
446        let varint = VarintEncoder::encode_i64(&values).unwrap();
447        assert!(
448            varint.len() <= fixed.len(),
449            "varint ({}) should be <= fixed-width ({}) for sparse data",
450            varint.len(),
451            fixed.len(),
452        );
453    }
454}
455
456#[cfg(test)]
457mod proptests {
458    use super::*;
459    use proptest::prelude::*;
460
461    proptest! {
462        #[test]
463        fn roundtrip_arbitrary_i64(values in proptest::collection::vec(any::<i64>(), 1..500)) {
464            let encoded = IntegerEncoder::encode_i64(&values).unwrap();
465            let decoded = IntegerDecoder::decode_i64(&encoded).unwrap();
466            prop_assert_eq!(&values, &decoded);
467        }
468
469        #[test]
470        fn roundtrip_arbitrary_u64(values in proptest::collection::vec(any::<u64>(), 1..500)) {
471            let encoded = IntegerEncoder::encode_u64(&values).unwrap();
472            let decoded = IntegerDecoder::decode_u64(&encoded).unwrap();
473            prop_assert_eq!(&values, &decoded);
474        }
475
476        #[test]
477        fn varint_roundtrip_arbitrary_i64(values in proptest::collection::vec(any::<i64>(), 1..500)) {
478            let encoded = VarintEncoder::encode_i64(&values).unwrap();
479            let decoded = VarintDecoder::decode_i64(&encoded).unwrap();
480            prop_assert_eq!(&values, &decoded);
481        }
482
483        #[test]
484        fn varint_roundtrip_arbitrary_u64(values in proptest::collection::vec(any::<u64>(), 1..500)) {
485            let encoded = VarintEncoder::encode_u64(&values).unwrap();
486            let decoded = VarintDecoder::decode_u64(&encoded).unwrap();
487            prop_assert_eq!(&values, &decoded);
488        }
489    }
490}