Skip to main content

chronix_encoding/
for_encoding.rs

1//! Frame-of-Reference (FOR) encoding for integer columns.
2//!
3//! FOR encoding stores each value as a small, fixed-width offset from
4//! a per-block reference value (the minimum). This is optimal for columns
5//! where all values fall within a **narrow range** — e.g. HTTP status codes,
6//! sensor readings that hover around a baseline, or enum-style identifiers.
7//!
8//! Unlike delta encoding (which stores differences between *consecutive*
9//! values), FOR stores differences from a *global* reference (the block
10//! minimum). This makes FOR ideal for data with a tight value range but
11//! no predictable ordering, while delta encoding excels at monotonic or
12//! slowly-changing sequences.
13//!
14//! ## Wire format
15//!
16//! ```text
17//! [count: u32 LE][reference: i64 LE][bit_width: u8][packed offsets…]
18//! ```
19//!
20//! - **count** — number of values (max `u32::MAX`).
21//! - **reference** — the minimum value in the block.
22//! - **bit_width** — bits needed to represent the largest offset from
23//!   reference (0–64). When 0, all values equal the reference.
24//! - **packed offsets** — each offset packed at `bit_width` bits, MSB-first.
25
26use crate::coding::{bits_needed, checked_count, checked_decode_count, pack_bits, unpack_bits};
27use crate::error::{EncodingError, Result};
28
29/// Header size: `count(4) + reference(8) + bit_width(1)` = 13 bytes.
30const HEADER_SIZE: usize = 13;
31
32/// Frame-of-Reference encoder for integer columns.
33///
34/// Stores each value as `value - reference` using the minimum number of
35/// bits. Values must fit in `i64`.
36#[derive(Debug, Clone, Copy)]
37pub struct ForEncoder;
38
39/// Frame-of-Reference decoder.
40#[derive(Debug, Clone, Copy)]
41pub struct ForDecoder;
42
43impl ForEncoder {
44    /// Encode a sequence of `i64` values using FOR.
45    ///
46    /// # Errors
47    ///
48    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
49    pub fn encode_i64(values: &[i64]) -> Result<Vec<u8>> {
50        if values.is_empty() {
51            return Err(EncodingError::EmptyInput {
52                context: "FOR encoder (i64)",
53            });
54        }
55
56        let count = checked_count(values.len())?;
57
58        // Find the minimum value to use as reference.
59        let reference = values.iter().copied().min().unwrap_or(0);
60
61        // Compute offsets and max offset for bit-width calculation.
62        // Subtracting the minimum from any value in the range gives a
63        // non-negative result, but we use wrapping_sub to avoid panic
64        // and reinterpret as u64 (safe because min ≤ v for all v).
65        let offsets: Vec<u64> = values
66            .iter()
67            .map(|&v| v.wrapping_sub(reference) as u64)
68            .collect();
69        let max_offset = offsets.iter().copied().max().unwrap_or(0);
70        let bit_width = bits_needed(max_offset);
71
72        // Pre-allocate: header + packed bits
73        let data_bits = offsets.len() as u64 * u64::from(bit_width);
74        let data_bytes = data_bits.div_ceil(8) as usize;
75        let mut buf = Vec::with_capacity(HEADER_SIZE + data_bytes);
76
77        // Header
78        buf.extend_from_slice(&count.to_le_bytes());
79        buf.extend_from_slice(&reference.to_le_bytes());
80        buf.push(bit_width);
81
82        if bit_width == 0 {
83            // All values equal — no data section needed.
84            return Ok(buf);
85        }
86
87        // Pack offsets at bit_width bits each
88        pack_bits(&offsets, bit_width, &mut buf);
89
90        Ok(buf)
91    }
92
93    /// Encode a sequence of `u64` values using FOR.
94    ///
95    /// Uses native u64 arithmetic to avoid bit-width inflation at the
96    /// i64 sign boundary (values near `i64::MAX` / `i64::MIN`).
97    ///
98    /// # Errors
99    ///
100    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
101    pub fn encode_u64(values: &[u64]) -> Result<Vec<u8>> {
102        if values.is_empty() {
103            return Err(EncodingError::EmptyInput {
104                context: "FOR encoder (u64)",
105            });
106        }
107
108        let count = checked_count(values.len())?;
109
110        let reference = values.iter().copied().min().unwrap_or(0);
111
112        let offsets: Vec<u64> = values.iter().map(|&v| v - reference).collect();
113        let max_offset = offsets.iter().copied().max().unwrap_or(0);
114        let bit_width = bits_needed(max_offset);
115
116        let data_bits = offsets.len() as u64 * u64::from(bit_width);
117        let data_bytes = data_bits.div_ceil(8) as usize;
118        let mut buf = Vec::with_capacity(HEADER_SIZE + data_bytes);
119
120        // Store reference as i64 (reinterpret-cast preserves bits).
121        buf.extend_from_slice(&count.to_le_bytes());
122        buf.extend_from_slice(&(reference as i64).to_le_bytes());
123        buf.push(bit_width);
124
125        if bit_width == 0 {
126            return Ok(buf);
127        }
128
129        pack_bits(&offsets, bit_width, &mut buf);
130
131        Ok(buf)
132    }
133}
134
135impl ForDecoder {
136    /// Decode a FOR-encoded byte buffer back to `i64` values.
137    ///
138    /// # Errors
139    ///
140    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
141    pub fn decode_i64(data: &[u8]) -> Result<Vec<i64>> {
142        if data.len() < HEADER_SIZE {
143            return Err(EncodingError::CorruptData {
144                detail: "FOR data too short for header".to_string(),
145            });
146        }
147
148        let count = checked_decode_count(
149            u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
150            "FOR",
151        )?;
152        let reference = i64::from_le_bytes([
153            data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
154        ]);
155        let bit_width = data[12];
156
157        if bit_width > 64 {
158            return Err(EncodingError::CorruptData {
159                detail: format!("invalid FOR bit_width {bit_width}, max is 64"),
160            });
161        }
162
163        if count == 0 {
164            return Ok(Vec::new());
165        }
166
167        // bit_width == 0 → all values equal the reference
168        if bit_width == 0 {
169            return Ok(vec![reference; count]);
170        }
171
172        // Unpack offsets
173        let packed_data = &data[HEADER_SIZE..];
174        let offsets = unpack_bits(packed_data, count, bit_width)?;
175
176        // SIMD-friendly batch add scalar
177        let mut values = Vec::new();
178        crate::simd::batch_add_scalar_i64(reference, &offsets, &mut values);
179
180        Ok(values)
181    }
182
183    /// Decode a FOR-encoded byte buffer back to `u64` values.
184    ///
185    /// Uses native u64 arithmetic, matching the encoder's native u64 path.
186    ///
187    /// # Errors
188    ///
189    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
190    pub fn decode_u64(data: &[u8]) -> Result<Vec<u64>> {
191        if data.len() < HEADER_SIZE {
192            return Err(EncodingError::CorruptData {
193                detail: "FOR data too short for header".to_string(),
194            });
195        }
196
197        let count = checked_decode_count(
198            u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
199            "FOR",
200        )?;
201        let reference_i64 = i64::from_le_bytes([
202            data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
203        ]);
204        let reference = reference_i64 as u64;
205        let bit_width = data[12];
206
207        if bit_width > 64 {
208            return Err(EncodingError::CorruptData {
209                detail: format!("invalid FOR bit_width {bit_width}, max is 64"),
210            });
211        }
212
213        if count == 0 {
214            return Ok(Vec::new());
215        }
216
217        if bit_width == 0 {
218            return Ok(vec![reference; count]);
219        }
220
221        let packed_data = &data[HEADER_SIZE..];
222        let offsets = unpack_bits(packed_data, count, bit_width)?;
223
224        // SIMD-friendly batch add scalar
225        let mut values = Vec::new();
226        crate::simd::batch_add_scalar_u64(reference, &offsets, &mut values);
227
228        Ok(values)
229    }
230}
231
232// ---------------------------------------------------------------------------
233// Tests
234// ---------------------------------------------------------------------------
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn roundtrip_monotonic() {
242        let values: Vec<i64> = (100..200).collect();
243        let encoded = ForEncoder::encode_i64(&values).unwrap();
244        let decoded = ForDecoder::decode_i64(&encoded).unwrap();
245        assert_eq!(values, decoded);
246    }
247
248    #[test]
249    fn roundtrip_constant() {
250        let values = vec![42_i64; 1000];
251        let encoded = ForEncoder::encode_i64(&values).unwrap();
252        // Constant → bit_width == 0, so payload is just the header.
253        assert_eq!(encoded.len(), 13);
254        let decoded = ForDecoder::decode_i64(&encoded).unwrap();
255        assert_eq!(values, decoded);
256    }
257
258    #[test]
259    fn roundtrip_narrow_range() {
260        // HTTP status codes: 200, 201, 204, 301, 302, 400, 404, 500, 502, 503
261        let values = vec![200, 201, 204, 301, 302, 400, 404, 500, 502, 503];
262        let encoded = ForEncoder::encode_i64(&values).unwrap();
263        let decoded = ForDecoder::decode_i64(&encoded).unwrap();
264        assert_eq!(values, decoded);
265
266        // range = 503 - 200 = 303 → 9 bits needed
267        // 13 header + ceil(10 * 9 / 8) = 13 + 12 = 25 bytes
268        // vs plain: 80 bytes → 3.2× compression
269        assert!(encoded.len() < 80);
270    }
271
272    #[test]
273    fn roundtrip_single_value() {
274        let values = vec![i64::MAX];
275        let encoded = ForEncoder::encode_i64(&values).unwrap();
276        let decoded = ForDecoder::decode_i64(&encoded).unwrap();
277        assert_eq!(values, decoded);
278    }
279
280    #[test]
281    fn roundtrip_negative_values() {
282        let values = vec![-100, -50, -10, -5, -1, 0, 5, 10, 50, 100];
283        let encoded = ForEncoder::encode_i64(&values).unwrap();
284        let decoded = ForDecoder::decode_i64(&encoded).unwrap();
285        assert_eq!(values, decoded);
286    }
287
288    #[test]
289    fn roundtrip_u64() {
290        let values: Vec<u64> = (1000..1100).collect();
291        let encoded = ForEncoder::encode_u64(&values).unwrap();
292        let decoded = ForDecoder::decode_u64(&encoded).unwrap();
293        assert_eq!(values, decoded);
294    }
295
296    #[test]
297    fn empty_input_error() {
298        assert!(ForEncoder::encode_i64(&[]).is_err());
299        assert!(ForEncoder::encode_u64(&[]).is_err());
300    }
301
302    #[test]
303    fn corrupt_data_too_short() {
304        assert!(ForDecoder::decode_i64(&[0; 5]).is_err());
305    }
306
307    #[test]
308    fn corrupt_bit_width_too_large() {
309        let mut data = vec![0u8; 13];
310        // count = 1
311        data[0] = 1;
312        // bit_width = 65 (invalid)
313        data[12] = 65;
314        assert!(ForDecoder::decode_i64(&data).is_err());
315    }
316
317    #[test]
318    fn compression_vs_plain_narrow_range() {
319        // 1000 values in range [1000, 1015] → 4 bits each
320        let values: Vec<i64> = (0..1000).map(|i| 1000 + (i % 16)).collect();
321        let raw_size = values.len() * 8; // 8000 bytes
322        let encoded = ForEncoder::encode_i64(&values).unwrap();
323        // Expected: 13 + ceil(1000 * 4 / 8) = 13 + 500 = 513 bytes
324        assert!(
325            encoded.len() < raw_size / 10,
326            "FOR should achieve >10× for 4-bit range"
327        );
328        let decoded = ForDecoder::decode_i64(&encoded).unwrap();
329        assert_eq!(values, decoded);
330    }
331
332    #[test]
333    fn compression_worse_for_wide_range() {
334        // Values spanning the full i64 range — FOR cannot help
335        let values = vec![i64::MIN, 0, i64::MAX];
336        let encoded = ForEncoder::encode_i64(&values).unwrap();
337        // 3 values × 64 bits each = 24 bytes data + 13 header = 37 bytes
338        // vs plain: 24 bytes
339        // FOR is worse here, which is correct — the adaptive selector
340        // should not choose FOR for wide-range data.
341        let decoded = ForDecoder::decode_i64(&encoded).unwrap();
342        assert_eq!(values, decoded);
343    }
344
345    #[test]
346    fn roundtrip_two_values() {
347        let values = vec![10_i64, 20];
348        let encoded = ForEncoder::encode_i64(&values).unwrap();
349        let decoded = ForDecoder::decode_i64(&encoded).unwrap();
350        assert_eq!(values, decoded);
351    }
352
353    #[test]
354    fn roundtrip_all_same_negative() {
355        let values = vec![-999_i64; 500];
356        let encoded = ForEncoder::encode_i64(&values).unwrap();
357        assert_eq!(encoded.len(), 13); // header only, bit_width = 0
358        let decoded = ForDecoder::decode_i64(&encoded).unwrap();
359        assert_eq!(values, decoded);
360    }
361
362    #[test]
363    fn u64_sign_boundary_narrow_range() {
364        // Values near the i64 sign boundary — reinterpret-as-i64 would
365        // see min = i64::MAX and max = i64::MIN, inflating bit_width to 64.
366        // Native u64 FOR correctly computes range = 2 → 2 bits.
367        let mid = (1u64 << 63) - 1; // i64::MAX as u64
368        let values = vec![mid, mid + 1, mid + 2];
369        let encoded = ForEncoder::encode_u64(&values).unwrap();
370        let decoded = ForDecoder::decode_u64(&encoded).unwrap();
371        assert_eq!(values, decoded);
372
373        // Range is 2 → 2 bits per value → 13 + ceil(3*2/8) = 14 bytes.
374        assert!(
375            encoded.len() <= 14,
376            "expected ≤14 bytes, got {}",
377            encoded.len()
378        );
379    }
380
381    #[test]
382    fn u64_large_values() {
383        let values = vec![u64::MAX - 10, u64::MAX - 5, u64::MAX];
384        let encoded = ForEncoder::encode_u64(&values).unwrap();
385        let decoded = ForDecoder::decode_u64(&encoded).unwrap();
386        assert_eq!(values, decoded);
387    }
388}
389
390#[cfg(test)]
391mod proptests {
392    use super::*;
393    use proptest::prelude::*;
394
395    proptest! {
396        #[test]
397        fn roundtrip_i64(values in proptest::collection::vec(any::<i64>(), 1..500)) {
398            let encoded = ForEncoder::encode_i64(&values).unwrap();
399            let decoded = ForDecoder::decode_i64(&encoded).unwrap();
400            prop_assert_eq!(&values, &decoded);
401        }
402
403        #[test]
404        fn roundtrip_u64(values in proptest::collection::vec(any::<u64>(), 1..500)) {
405            let encoded = ForEncoder::encode_u64(&values).unwrap();
406            let decoded = ForDecoder::decode_u64(&encoded).unwrap();
407            prop_assert_eq!(&values, &decoded);
408        }
409    }
410}