Skip to main content

chronix_encoding/
delta.rs

1//! Delta-of-delta timestamp encoding.
2//!
3//! Exploits the regularity of time-series timestamps where successive deltas
4//! are often identical (constant sampling rate). The first value and first delta
5//! are stored raw; subsequent deltas-of-deltas are variable-length bit-packed.
6//!
7//! ## When to use delta-of-delta encoding
8//!
9//! **Ideal for:** Monotonically increasing or nearly-monotonic sequences
10//! with a regular cadence, such as timestamps at a fixed sampling rate.
11//! In this case, the delta-of-delta is frequently zero, yielding
12//! compression ratios of 30× or higher (≈1 bit per value).
13//!
14//! **Acceptable for:** Slowly changing integer sequences where consecutive
15//! deltas are small (e.g., incrementing counters). Compression will still
16//! be good as long as the delta-of-delta fits in the compact bit buckets.
17//!
18//! **Suboptimal for:** Non-monotonic or highly variable data such as
19//! measured float values, gauge metrics, or random integers. Large
20//! swings produce large deltas-of-deltas that fall into the 68-bit
21//! fallback bucket, resulting in *worse* compression than raw storage
22//! (68 bits vs. 64 bits per value, plus header overhead).
23//!
24//! **Prefer instead:**
25//! - [`Gorilla`](crate::gorilla) / [`Chimp`](crate::chimp) XOR encoding
26//!   for floating-point value columns — they exploit the bitwise
27//!   similarity of consecutive IEEE 754 values.
28//! - [`RLE`](crate::rle) for columns with many consecutive repeated
29//!   values (e.g., status codes, boolean flags).
30//! - [`IntegerEncoder`](crate::integer) (delta + ZigZag) for general
31//!   integer columns that may not be monotonic.
32//!
33//! The [`AdaptiveSelector`](crate::adaptive::AdaptiveSelector) in the
34//! unified encoder automatically samples data and picks the best
35//! encoding, so callers using [`ColumnEncoder`](crate::unified::ColumnEncoder)
36//! do not need to make this choice manually.
37//!
38//! ## Encoding scheme
39//!
40//! | Delta-of-delta value     | Prefix bits | Value bits | Total bits |
41//! |--------------------------|-------------|------------|------------|
42//! | 0                        | `0`         | 0          | 1          |
43//! | −064..=63                 | `10`        | 7          | 9          |
44//! | −256..=255               | `110`       | 9          | 12         |
45//! | −2048..=2047             | `1110`      | 12         | 16         |
46//! | anything else            | `1111`      | 64         | 68         |
47
48use crate::coding::{checked_count, checked_decode_count, zigzag_decode, zigzag_encode};
49use crate::error::{EncodingError, Result};
50
51/// Encodes a sorted sequence of `i64` timestamps using delta-of-delta encoding.
52#[derive(Debug, Clone, Copy)]
53pub struct DeltaOfDeltaEncoder;
54
55/// Decodes delta-of-delta encoded timestamps.
56#[derive(Debug, Clone, Copy)]
57pub struct DeltaOfDeltaDecoder;
58
59impl DeltaOfDeltaEncoder {
60    /// Encode a sequence of timestamps.
61    ///
62    /// # Overflow safety
63    ///
64    /// All delta and delta-of-delta computations use **wrapping arithmetic**
65    /// (`wrapping_sub`).  This is intentional: the encoding scheme stores
66    /// every delta-of-delta with a 64-bit raw fallback, so any `i64`
67    /// difference — including values that wrap — is captured losslessly
68    /// and decoded symmetrically via `wrapping_add`.  Plain `-` / `+`
69    /// operators are avoided to prevent panics in debug builds.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`EncodingError::EmptyInput`] if `timestamps` is empty.
74    pub fn encode(timestamps: &[i64]) -> Result<Vec<u8>> {
75        if timestamps.is_empty() {
76            return Err(EncodingError::EmptyInput {
77                context: "delta-of-delta encoder",
78            });
79        }
80
81        // Pre-allocate: worst case ~9 bytes/value, but typically ~1 bit/value
82        let mut writer = BitWriter::with_capacity(8 + timestamps.len());
83
84        // Store value count as u32
85        let count = checked_count(timestamps.len())?;
86        writer.write_bits(u64::from(count), 32);
87
88        // First value: raw i64
89        writer.write_bits(timestamps[0] as u64, 64);
90
91        if timestamps.len() == 1 {
92            return Ok(writer.finish());
93        }
94
95        // First delta: raw i64 (wrapping to handle extreme values)
96        let first_delta = timestamps[1].wrapping_sub(timestamps[0]);
97        writer.write_bits(first_delta as u64, 64);
98
99        let mut prev_delta = first_delta;
100
101        for i in 2..timestamps.len() {
102            // wrapping arithmetic: any i64 difference is valid because the
103            // 64-bit raw fallback bucket captures arbitrary wrapping results.
104            let delta = timestamps[i].wrapping_sub(timestamps[i - 1]);
105            let dod = delta.wrapping_sub(prev_delta);
106            encode_dod(&mut writer, dod);
107            prev_delta = delta;
108        }
109
110        Ok(writer.finish())
111    }
112}
113
114fn encode_dod(writer: &mut BitWriter, dod: i64) {
115    match dod {
116        0 => {
117            writer.write_bit(false);
118        }
119        -64..=63 => {
120            writer.write_bits(0b10, 2);
121            // ZigZag encode to fit in 7 bits unsigned
122            // zigzag(-64)=127, zigzag(63)=126 — both fit in 7 bits
123            writer.write_bits(zigzag_encode(dod), 7);
124        }
125        -256..=255 => {
126            writer.write_bits(0b110, 3);
127            // zigzag(-256)=511, zigzag(255)=510 — both fit in 9 bits
128            writer.write_bits(zigzag_encode(dod), 9);
129        }
130        -2048..=2047 => {
131            writer.write_bits(0b1110, 4);
132            // zigzag(-2048)=4095, zigzag(2047)=4094 — both fit in 12 bits
133            writer.write_bits(zigzag_encode(dod), 12);
134        }
135        _ => {
136            writer.write_bits(0b1111, 4);
137            writer.write_bits(dod as u64, 64);
138        }
139    }
140}
141
142impl DeltaOfDeltaDecoder {
143    /// Decode a delta-of-delta encoded byte buffer back to timestamps.
144    ///
145    /// # Overflow safety
146    ///
147    /// Reconstruction uses **wrapping arithmetic** (`wrapping_add`) to
148    /// mirror the `wrapping_sub` used during encoding.  This guarantees
149    /// correct round-trip for any `i64` sequence and provides robustness
150    /// against corrupt data — the decoder never panics on arithmetic.
151    ///
152    /// # Errors
153    ///
154    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
155    pub fn decode(data: &[u8]) -> Result<Vec<i64>> {
156        if data.len() < 4 {
157            return Err(EncodingError::CorruptData {
158                detail: "delta-of-delta data too short for header".to_string(),
159            });
160        }
161
162        let mut reader = BitReader::new(data);
163
164        let count = checked_decode_count(reader.read_bits(32)? as u32 as usize, "delta")?;
165        if count == 0 {
166            return Ok(Vec::new());
167        }
168
169        let mut result = Vec::with_capacity(count);
170
171        // First value
172        let first = reader.read_bits(64)? as i64;
173        result.push(first);
174
175        if count == 1 {
176            return Ok(result);
177        }
178
179        // First delta (wrapping to handle extreme values)
180        let first_delta = reader.read_bits(64)? as i64;
181        result.push(first.wrapping_add(first_delta));
182
183        let mut prev_delta = first_delta;
184
185        for _ in 2..count {
186            let dod = decode_dod(&mut reader)?;
187            // wrapping: mirrors wrapping_sub used during encoding
188            let delta = prev_delta.wrapping_add(dod);
189            let value = result[result.len() - 1].wrapping_add(delta);
190            result.push(value);
191            prev_delta = delta;
192        }
193
194        Ok(result)
195    }
196}
197
198fn decode_dod(reader: &mut BitReader) -> Result<i64> {
199    if !reader.read_bit()? {
200        // 0 prefix → dod = 0
201        return Ok(0);
202    }
203    if !reader.read_bit()? {
204        // 10 prefix → 7-bit zigzag
205        let z = reader.read_bits(7)?;
206        return Ok(zigzag_decode(z));
207    }
208    if !reader.read_bit()? {
209        // 110 prefix → 9-bit zigzag
210        let z = reader.read_bits(9)?;
211        return Ok(zigzag_decode(z));
212    }
213    if !reader.read_bit()? {
214        // 1110 prefix → 12-bit zigzag
215        let z = reader.read_bits(12)?;
216        return Ok(zigzag_decode(z));
217    }
218    // 1111 prefix → raw 64-bit
219    let v = reader.read_bits(64)? as i64;
220    Ok(v)
221}
222
223// ── Bit-level I/O ──────────────────────────────────────────────────────
224
225/// A bit-level writer that packs bits MSB-first into a byte buffer.
226///
227/// Uses a `u64` word accumulator to batch bit operations — `write_bits()`
228/// performs at most two word-level operations per call instead of per-bit
229/// function calls. This eliminates the primary encoding bottleneck for
230/// all variable-length encoders (delta-of-delta, Chimp, Gorilla).
231///
232/// Output is byte-identical to a naive per-bit writer (MSB-first within
233/// each byte, big-endian word flushing).
234pub(crate) struct BitWriter {
235    buf: Vec<u8>,
236    /// Accumulator where bits are left-aligned (MSB-first, starting at bit 63).
237    accumulator: u64,
238    /// Number of valid bits in the accumulator (0..64).
239    bits_in_acc: u8,
240}
241
242impl BitWriter {
243    /// Create a new `BitWriter` with the given byte capacity hint.
244    pub(crate) fn with_capacity(bytes: usize) -> Self {
245        Self {
246            buf: Vec::with_capacity(bytes),
247            accumulator: 0,
248            bits_in_acc: 0,
249        }
250    }
251
252    /// Flush the full 64-bit accumulator as 8 big-endian bytes.
253    #[inline]
254    fn flush_accumulator(&mut self) {
255        self.buf.extend_from_slice(&self.accumulator.to_be_bytes());
256        self.accumulator = 0;
257        self.bits_in_acc = 0;
258    }
259
260    /// Write a single bit (MSB-first ordering).
261    #[inline]
262    pub(crate) fn write_bit(&mut self, bit: bool) {
263        if bit {
264            self.accumulator |= 1u64 << (63 - self.bits_in_acc);
265        }
266        self.bits_in_acc += 1;
267        if self.bits_in_acc == 64 {
268            self.flush_accumulator();
269        }
270    }
271
272    /// Write `count` bits from the least-significant bits of `value`.
273    ///
274    /// Uses word-level operations to pack bits in at most two steps
275    /// (fill current word + start next word), regardless of `count`.
276    #[inline]
277    pub(crate) fn write_bits(&mut self, value: u64, count: u8) {
278        debug_assert!(count <= 64);
279        if count == 0 {
280            return;
281        }
282
283        // Mask to exactly `count` least-significant bits
284        let masked = if count >= 64 {
285            value
286        } else {
287            value & ((1u64 << count) - 1)
288        };
289
290        let space = 64 - self.bits_in_acc;
291        if count <= space {
292            // All bits fit in the current accumulator word
293            self.accumulator |= masked << (space - count);
294            self.bits_in_acc += count;
295            if self.bits_in_acc == 64 {
296                self.flush_accumulator();
297            }
298        } else {
299            // Split across word boundary: fill current word, start next
300            self.accumulator |= masked >> (count - space);
301            self.flush_accumulator();
302            let remaining = count - space;
303            self.accumulator = masked << (64 - remaining);
304            self.bits_in_acc = remaining;
305        }
306    }
307
308    /// Flush remaining bits and return the completed byte buffer.
309    pub(crate) fn finish(mut self) -> Vec<u8> {
310        if self.bits_in_acc > 0 {
311            let bytes_needed = self.bits_in_acc.div_ceil(8) as usize;
312            let word_bytes = self.accumulator.to_be_bytes();
313            self.buf.extend_from_slice(&word_bytes[..bytes_needed]);
314        }
315        self.buf
316    }
317}
318
319/// A bit-level reader that reads bits MSB-first from a byte slice.
320///
321/// `read_bits()` processes a byte-at-a-time instead of bit-at-a-time,
322/// reducing iterations from `count` to at most `ceil(count/8) + 1` per
323/// call. For a 64-bit read this means ≤ 9 iterations instead of 64.
324pub(crate) struct BitReader<'a> {
325    data: &'a [u8],
326    byte_pos: usize,
327    bit_pos: u8, // 0..8
328}
329
330impl<'a> BitReader<'a> {
331    /// Create a new `BitReader` over the given byte slice.
332    pub(crate) fn new(data: &'a [u8]) -> Self {
333        Self {
334            data,
335            byte_pos: 0,
336            bit_pos: 0,
337        }
338    }
339
340    /// Read a single bit.
341    #[inline]
342    pub(crate) fn read_bit(&mut self) -> Result<bool> {
343        if self.byte_pos >= self.data.len() {
344            return Err(EncodingError::CorruptData {
345                detail: "unexpected end of bit stream".to_string(),
346            });
347        }
348        let bit = (self.data[self.byte_pos] >> (7 - self.bit_pos)) & 1 == 1;
349        self.bit_pos += 1;
350        if self.bit_pos == 8 {
351            self.byte_pos += 1;
352            self.bit_pos = 0;
353        }
354        Ok(bit)
355    }
356
357    /// Read `count` bits into the least-significant bits of a `u64`.
358    ///
359    /// Processes a full byte chunk per iteration instead of one bit at a
360    /// time, yielding up to 8× fewer loop iterations for multi-bit reads.
361    #[inline]
362    pub(crate) fn read_bits(&mut self, count: u8) -> Result<u64> {
363        debug_assert!(count <= 64);
364        if count == 0 {
365            return Ok(0);
366        }
367
368        let mut value: u64 = 0;
369        let mut remaining = count;
370
371        while remaining > 0 {
372            if self.byte_pos >= self.data.len() {
373                return Err(EncodingError::CorruptData {
374                    detail: "unexpected end of bit stream".to_string(),
375                });
376            }
377
378            let bits_left_in_byte = 8 - self.bit_pos;
379            let to_read = remaining.min(bits_left_in_byte);
380
381            // Extract `to_read` bits from the current byte at bit_pos
382            let shift = bits_left_in_byte - to_read;
383            let mask: u8 = if to_read >= 8 {
384                0xFF
385            } else {
386                (1u8 << to_read) - 1
387            };
388            let bits = (self.data[self.byte_pos] >> shift) & mask;
389
390            value = (value << to_read) | u64::from(bits);
391            remaining -= to_read;
392            self.bit_pos += to_read;
393
394            if self.bit_pos >= 8 {
395                self.byte_pos += 1;
396                self.bit_pos = 0;
397            }
398        }
399
400        Ok(value)
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn roundtrip_monotonic_1s() {
410        let ts: Vec<i64> = (0..1000)
411            .map(|i| 1_000_000_000 + i * 1_000_000_000)
412            .collect();
413        let encoded = DeltaOfDeltaEncoder::encode(&ts).unwrap();
414        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
415        assert_eq!(ts, decoded);
416        // With constant delta, most dod=0 → ~1 bit each → very compact
417        assert!(encoded.len() < ts.len()); // much better than 8 bytes/value
418    }
419
420    #[test]
421    fn roundtrip_single_value() {
422        let ts = vec![42_i64];
423        let encoded = DeltaOfDeltaEncoder::encode(&ts).unwrap();
424        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
425        assert_eq!(ts, decoded);
426    }
427
428    #[test]
429    fn roundtrip_two_values() {
430        let ts = vec![100_i64, 200];
431        let encoded = DeltaOfDeltaEncoder::encode(&ts).unwrap();
432        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
433        assert_eq!(ts, decoded);
434    }
435
436    #[test]
437    fn roundtrip_constant_timestamps() {
438        let ts = vec![42_i64; 100];
439        let encoded = DeltaOfDeltaEncoder::encode(&ts).unwrap();
440        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
441        assert_eq!(ts, decoded);
442    }
443
444    #[test]
445    fn roundtrip_irregular_intervals() {
446        let ts = vec![0, 1, 5, 10, 50, 100, 500, 1000, 2000];
447        let encoded = DeltaOfDeltaEncoder::encode(&ts).unwrap();
448        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
449        assert_eq!(ts, decoded);
450    }
451
452    #[test]
453    fn roundtrip_negative_timestamps() {
454        let ts = vec![-1000, -500, -200, -100, 0, 100, 200];
455        let encoded = DeltaOfDeltaEncoder::encode(&ts).unwrap();
456        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
457        assert_eq!(ts, decoded);
458    }
459
460    #[test]
461    fn roundtrip_edge_i64() {
462        let ts = vec![i64::MIN, i64::MIN / 2, 0, i64::MAX / 2, i64::MAX];
463        let encoded = DeltaOfDeltaEncoder::encode(&ts).unwrap();
464        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
465        assert_eq!(ts, decoded);
466    }
467
468    #[test]
469    fn empty_input_error() {
470        let result = DeltaOfDeltaEncoder::encode(&[]);
471        assert!(result.is_err());
472    }
473
474    #[test]
475    fn compression_ratio_regular() {
476        // 10_000 timestamps at 1s intervals
477        let ts: Vec<i64> = (0..10_000).map(|i| i * 1_000_000_000).collect();
478        let raw_size = ts.len() * 8;
479        let encoded = DeltaOfDeltaEncoder::encode(&ts).unwrap();
480        #[allow(clippy::cast_precision_loss)]
481        let ratio = raw_size as f64 / encoded.len() as f64;
482        // Should achieve excellent compression on regular data
483        assert!(
484            ratio > 10.0,
485            "Expected ratio > 10x, got {ratio:.1}x ({} raw, {} encoded)",
486            raw_size,
487            encoded.len()
488        );
489    }
490
491    /// Regression test for the `DoD` boundary bug: values at the exact
492    /// `ZigZag` bit-width boundaries must roundtrip correctly.
493    /// Previously, ranges were `-63..=64`, `-255..=256`, `-2047..=2048`
494    /// which silently truncated the positive boundary values.
495    #[test]
496    fn roundtrip_dod_boundary_values() {
497        // DoD of exactly 64 (was corrupted to 0)
498        let ts_64 = vec![0_i64, 0, 64];
499        let encoded = DeltaOfDeltaEncoder::encode(&ts_64).unwrap();
500        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
501        assert_eq!(ts_64, decoded, "DoD=64 boundary failed");
502
503        // DoD of exactly -64
504        let ts_neg64 = vec![0_i64, 64, 0];
505        let encoded = DeltaOfDeltaEncoder::encode(&ts_neg64).unwrap();
506        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
507        assert_eq!(ts_neg64, decoded, "DoD=-64 boundary failed");
508
509        // DoD of exactly 63 (last value that fits in 7-bit zigzag)
510        let ts_63 = vec![0_i64, 0, 63];
511        let encoded = DeltaOfDeltaEncoder::encode(&ts_63).unwrap();
512        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
513        assert_eq!(ts_63, decoded, "DoD=63 boundary failed");
514
515        // DoD of exactly 256 (was corrupted to 0)
516        let ts_256 = vec![0_i64, 0, 256];
517        let encoded = DeltaOfDeltaEncoder::encode(&ts_256).unwrap();
518        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
519        assert_eq!(ts_256, decoded, "DoD=256 boundary failed");
520
521        // DoD of exactly -256
522        let ts_neg256 = vec![0_i64, 256, 0];
523        let encoded = DeltaOfDeltaEncoder::encode(&ts_neg256).unwrap();
524        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
525        assert_eq!(ts_neg256, decoded, "DoD=-256 boundary failed");
526
527        // DoD of exactly 255
528        let ts_255 = vec![0_i64, 0, 255];
529        let encoded = DeltaOfDeltaEncoder::encode(&ts_255).unwrap();
530        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
531        assert_eq!(ts_255, decoded, "DoD=255 boundary failed");
532
533        // DoD of exactly 2048 (was corrupted to 0)
534        let ts_2048 = vec![0_i64, 0, 2048];
535        let encoded = DeltaOfDeltaEncoder::encode(&ts_2048).unwrap();
536        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
537        assert_eq!(ts_2048, decoded, "DoD=2048 boundary failed");
538
539        // DoD of exactly -2048
540        let ts_neg2048 = vec![0_i64, 2048, 0];
541        let encoded = DeltaOfDeltaEncoder::encode(&ts_neg2048).unwrap();
542        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
543        assert_eq!(ts_neg2048, decoded, "DoD=-2048 boundary failed");
544
545        // DoD of exactly 2047
546        let ts_2047 = vec![0_i64, 0, 2047];
547        let encoded = DeltaOfDeltaEncoder::encode(&ts_2047).unwrap();
548        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
549        assert_eq!(ts_2047, decoded, "DoD=2047 boundary failed");
550    }
551
552    /// Verify that values just outside the compact encoding ranges also
553    /// roundtrip correctly (they use the next-wider encoding bucket).
554    #[test]
555    fn roundtrip_dod_just_outside_boundaries() {
556        // 65 → should use the 9-bit bucket
557        let ts = vec![0_i64, 0, 65];
558        let encoded = DeltaOfDeltaEncoder::encode(&ts).unwrap();
559        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
560        assert_eq!(ts, decoded);
561
562        // 257 → should use the 12-bit bucket
563        let ts = vec![0_i64, 0, 257];
564        let encoded = DeltaOfDeltaEncoder::encode(&ts).unwrap();
565        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
566        assert_eq!(ts, decoded);
567
568        // 2049 → should use the 64-bit raw bucket
569        let ts = vec![0_i64, 0, 2049];
570        let encoded = DeltaOfDeltaEncoder::encode(&ts).unwrap();
571        let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
572        assert_eq!(ts, decoded);
573    }
574}
575
576#[cfg(test)]
577mod proptests {
578    use super::*;
579    use proptest::prelude::*;
580
581    proptest! {
582        #[test]
583        fn roundtrip_arbitrary_sorted(mut values in proptest::collection::vec(any::<i64>(), 1..500)) {
584            values.sort_unstable();
585            let encoded = DeltaOfDeltaEncoder::encode(&values).unwrap();
586            let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
587            prop_assert_eq!(&values, &decoded);
588        }
589
590        #[test]
591        fn roundtrip_arbitrary_unsorted(values in proptest::collection::vec(any::<i64>(), 1..500)) {
592            // Works on unsorted data too (just less compressible)
593            let encoded = DeltaOfDeltaEncoder::encode(&values).unwrap();
594            let decoded = DeltaOfDeltaDecoder::decode(&encoded).unwrap();
595            prop_assert_eq!(&values, &decoded);
596        }
597    }
598}