Skip to main content

nodedb_codec/
double_delta.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! DoubleDelta codec for timestamp columns.
4//!
5//! Timestamps are monotonically increasing with near-constant intervals
6//! (e.g., every 10s). DoubleDelta encodes the difference-of-differences:
7//!
8//! ```text
9//! value[0]              → stored raw (8 bytes)
10//! delta[0] = v[1]-v[0]  → stored raw (8 bytes)
11//! dod[i]   = delta[i] - delta[i-1]  → bit-packed (usually 0 → 1 bit)
12//! ```
13//!
14//! For constant-rate timestamps, all delta-of-deltas are 0, achieving ~1 bit
15//! per sample after the header. 4x better than Gorilla for timestamp columns.
16//!
17//! Wire format:
18//! ```text
19//! [4 bytes] sample count (LE u32)
20//! [8 bytes] first value (LE i64)
21//! [8 bytes] first delta (LE i64)  — only present if count >= 2
22//! [N bytes] bitstream of delta-of-deltas — only present if count >= 3
23//! ```
24//!
25//! DoD bit-packing uses the same bucket scheme as Gorilla timestamps:
26//! - `0`              → dod == 0
27//! - `10` + 7 bits    → dod in [-63, 64]
28//! - `110` + 9 bits   → dod in [-255, 256]
29//! - `1110` + 12 bits → dod in [-2047, 2048]
30//! - `1111` + 64 bits → arbitrary dod
31
32use std::mem::size_of;
33
34use crate::bounds::{
35    checked_add, checked_capacity, checked_mul, decoded_len, encode_input_len, u32_to_usize,
36};
37use crate::error::CodecError;
38
39// ---------------------------------------------------------------------------
40// Bit I/O helpers
41// ---------------------------------------------------------------------------
42
43#[derive(Debug)]
44pub(crate) struct BitWriter {
45    buf: Vec<u8>,
46    bit_pos: usize,
47}
48
49impl BitWriter {
50    pub(crate) fn new() -> Self {
51        Self {
52            buf: Vec::with_capacity(1024),
53            bit_pos: 0,
54        }
55    }
56
57    pub(crate) fn write_bit(&mut self, bit: bool) {
58        let byte_idx = self.bit_pos / 8;
59        let bit_idx = 7 - (self.bit_pos % 8);
60        if byte_idx >= self.buf.len() {
61            self.buf.push(0);
62        }
63        if bit {
64            self.buf[byte_idx] |= 1 << bit_idx;
65        }
66        self.bit_pos += 1;
67    }
68
69    pub(crate) fn write_bits(&mut self, value: u64, num_bits: usize) {
70        for i in (0..num_bits).rev() {
71            self.write_bit((value >> i) & 1 == 1);
72        }
73    }
74
75    fn try_write_bit(&mut self, bit: bool) -> Result<(), CodecError> {
76        checked_add(self.bit_pos, 1, "DoubleDelta bit offset")?;
77        self.write_bit(bit);
78        Ok(())
79    }
80
81    fn try_write_bits(&mut self, value: u64, num_bits: usize) -> Result<(), CodecError> {
82        checked_add(self.bit_pos, num_bits, "DoubleDelta bit offset")?;
83        for i in (0..num_bits).rev() {
84            self.write_bit((value >> i) & 1 == 1);
85        }
86        Ok(())
87    }
88
89    pub(crate) fn as_bytes(&self) -> &[u8] {
90        &self.buf
91    }
92
93    pub(crate) fn bit_len(&self) -> usize {
94        self.bit_pos
95    }
96}
97
98pub(crate) struct BitReader<'a> {
99    buf: &'a [u8],
100    bit_pos: usize,
101}
102
103impl<'a> BitReader<'a> {
104    pub(crate) fn new(buf: &'a [u8]) -> Self {
105        Self { buf, bit_pos: 0 }
106    }
107
108    pub(crate) fn read_bit(&mut self) -> Result<bool, CodecError> {
109        let byte_idx = self.bit_pos / 8;
110        if byte_idx >= self.buf.len() {
111            return Err(CodecError::Truncated {
112                expected: checked_add(byte_idx, 1, "DoubleDelta bit byte index")?,
113                actual: self.buf.len(),
114            });
115        }
116        let bit_idx = 7 - (self.bit_pos % 8);
117        let bit = (self.buf[byte_idx] >> bit_idx) & 1 == 1;
118        self.bit_pos = checked_add(self.bit_pos, 1, "DoubleDelta bit offset")?;
119        Ok(bit)
120    }
121
122    pub(crate) fn read_bits(&mut self, num_bits: usize) -> Result<u64, CodecError> {
123        let mut value = 0u64;
124        for _ in 0..num_bits {
125            value = (value << 1) | u64::from(self.read_bit()?);
126        }
127        Ok(value)
128    }
129}
130
131// ---------------------------------------------------------------------------
132// Public encode / decode API
133// ---------------------------------------------------------------------------
134
135/// Encode a slice of i64 values using DoubleDelta compression.
136pub fn encode(values: &[i64]) -> Result<Vec<u8>, CodecError> {
137    let count = encode_value_count(values.len(), "DoubleDelta")?;
138    let estimated = checked_add(20, values.len() / 4, "DoubleDelta output estimate")?;
139    let mut out = Vec::with_capacity(estimated);
140
141    out.extend_from_slice(&count.to_le_bytes());
142
143    if values.is_empty() {
144        return Ok(out);
145    }
146
147    out.extend_from_slice(&values[0].to_le_bytes());
148
149    if values.len() == 1 {
150        return Ok(out);
151    }
152
153    let first_delta = values[1].wrapping_sub(values[0]);
154    out.extend_from_slice(&first_delta.to_le_bytes());
155
156    if values.len() == 2 {
157        return Ok(out);
158    }
159
160    let mut bs = BitWriter::new();
161    let mut prev_delta = first_delta;
162
163    for i in 2..values.len() {
164        let delta = values[i].wrapping_sub(values[i - 1]);
165        let dod = delta.wrapping_sub(prev_delta);
166        encode_dod(&mut bs, dod)?;
167        prev_delta = delta;
168    }
169
170    out.extend_from_slice(bs.as_bytes());
171    Ok(out)
172}
173
174/// Decode DoubleDelta-compressed bytes back to i64 values.
175pub fn decode(data: &[u8]) -> Result<Vec<i64>, CodecError> {
176    if data.len() < 4 {
177        return Err(CodecError::Truncated {
178            expected: 4,
179            actual: data.len(),
180        });
181    }
182
183    let count = u32_to_usize(
184        u32::from_le_bytes(data[0..4].try_into().map_err(|_| CodecError::Corrupt {
185            detail: "invalid header".into(),
186        })?),
187        "DoubleDelta value count",
188    )?;
189    validate_value_count(count, "DoubleDelta")?;
190
191    if count == 0 {
192        if data.len() != 4 {
193            return Err(CodecError::Corrupt {
194                detail: "trailing bytes after empty DoubleDelta frame".into(),
195            });
196        }
197        return Ok(Vec::new());
198    }
199
200    if data.len() < 12 {
201        return Err(CodecError::Truncated {
202            expected: 12,
203            actual: data.len(),
204        });
205    }
206
207    let first_value =
208        i64::from_le_bytes(data[4..12].try_into().map_err(|_| CodecError::Corrupt {
209            detail: "invalid first value".into(),
210        })?);
211
212    let value_capacity = checked_capacity(count, size_of::<i64>(), "DoubleDelta values")?;
213    let mut values = Vec::with_capacity(value_capacity);
214    values.push(first_value);
215
216    if count == 1 {
217        if data.len() != 12 {
218            return Err(CodecError::Corrupt {
219                detail: "trailing bytes after single-value DoubleDelta frame".into(),
220            });
221        }
222        return Ok(values);
223    }
224
225    if data.len() < 20 {
226        return Err(CodecError::Truncated {
227            expected: 20,
228            actual: data.len(),
229        });
230    }
231
232    let first_delta =
233        i64::from_le_bytes(data[12..20].try_into().map_err(|_| CodecError::Corrupt {
234            detail: "invalid first delta".into(),
235        })?);
236    values.push(first_value.wrapping_add(first_delta));
237
238    if count == 2 {
239        if data.len() != 20 {
240            return Err(CodecError::Corrupt {
241                detail: "trailing bytes after two-value DoubleDelta frame".into(),
242            });
243        }
244        return Ok(values);
245    }
246
247    let mut reader = BitReader::new(&data[20..]);
248    let mut prev_delta = first_delta;
249
250    for _ in 2..count {
251        let dod = decode_dod(&mut reader)?;
252        let delta = prev_delta.wrapping_add(dod);
253        let value = values[values.len() - 1].wrapping_add(delta);
254        values.push(value);
255        prev_delta = delta;
256    }
257    let used_bytes = reader.bit_pos.div_ceil(8);
258    if used_bytes != reader.buf.len() {
259        return Err(CodecError::Corrupt {
260            detail: "trailing bytes after DoubleDelta frame".into(),
261        });
262    }
263    if !reader.bit_pos.is_multiple_of(8) {
264        let unused_bits = 8 - reader.bit_pos % 8;
265        let padding_mask = (1u8 << unused_bits) - 1;
266        if reader.buf[used_bytes - 1] & padding_mask != 0 {
267            return Err(CodecError::Corrupt {
268                detail: "nonzero DoubleDelta padding bits".into(),
269            });
270        }
271    }
272
273    Ok(values)
274}
275
276// ---------------------------------------------------------------------------
277// DoD bit encoding / decoding
278// ---------------------------------------------------------------------------
279
280fn encode_dod(bs: &mut BitWriter, dod: i64) -> Result<(), CodecError> {
281    if dod == 0 {
282        bs.try_write_bit(false)
283    } else if (-64..=63).contains(&dod) {
284        bs.try_write_bits(0b10, 2)?;
285        bs.try_write_bits((dod as u64) & 0x7F, 7)
286    } else if (-256..=255).contains(&dod) {
287        bs.try_write_bits(0b110, 3)?;
288        bs.try_write_bits((dod as u64) & 0x1FF, 9)
289    } else if (-2048..=2047).contains(&dod) {
290        bs.try_write_bits(0b1110, 4)?;
291        bs.try_write_bits((dod as u64) & 0xFFF, 12)
292    } else {
293        bs.try_write_bits(0b1111, 4)?;
294        bs.try_write_bits(dod as u64, 64)
295    }
296}
297
298fn decode_dod(reader: &mut BitReader<'_>) -> Result<i64, CodecError> {
299    let bit = reader.read_bit()?;
300    if !bit {
301        return Ok(0);
302    }
303
304    let bit2 = reader.read_bit()?;
305    if !bit2 {
306        let raw = reader.read_bits(7)? as i64;
307        return Ok(sign_extend(raw, 7));
308    }
309
310    let bit3 = reader.read_bit()?;
311    if !bit3 {
312        let raw = reader.read_bits(9)? as i64;
313        return Ok(sign_extend(raw, 9));
314    }
315
316    let bit4 = reader.read_bit()?;
317    if !bit4 {
318        let raw = reader.read_bits(12)? as i64;
319        return Ok(sign_extend(raw, 12));
320    }
321
322    let raw = reader.read_bits(64)?;
323    Ok(raw as i64)
324}
325
326fn sign_extend(value: i64, bits: u32) -> i64 {
327    let shift = 64 - bits;
328    (value << shift) >> shift
329}
330
331fn encode_value_count(len: usize, codec: &str) -> Result<u32, CodecError> {
332    validate_value_count(len, codec)?;
333    encode_input_len(len, codec)
334}
335
336fn validate_value_count(count: usize, codec: &str) -> Result<(), CodecError> {
337    let bytes = checked_mul(count, size_of::<i64>(), "integer decoded bytes")?;
338    decoded_len(bytes, codec)?;
339    Ok(())
340}
341
342// ---------------------------------------------------------------------------
343// Re-export types for lib.rs consistency
344// ---------------------------------------------------------------------------
345
346/// Streaming DoubleDelta encoder. Accumulates values and produces
347/// compressed bytes on `finish()`.
348pub struct DoubleDeltaEncoder {
349    values: Vec<i64>,
350}
351
352impl DoubleDeltaEncoder {
353    pub fn new() -> Self {
354        Self {
355            values: Vec::with_capacity(4096),
356        }
357    }
358
359    /// Append a single i64 value.
360    pub fn push(&mut self, value: i64) -> Result<(), CodecError> {
361        let next = checked_add(self.values.len(), 1, "DoubleDelta streaming count")?;
362        validate_value_count(next, "DoubleDelta streaming input")?;
363        self.values.push(value);
364        Ok(())
365    }
366
367    /// Append a batch of i64 values.
368    pub fn push_batch(&mut self, values: &[i64]) -> Result<(), CodecError> {
369        let next = checked_add(
370            self.values.len(),
371            values.len(),
372            "DoubleDelta streaming count",
373        )?;
374        validate_value_count(next, "DoubleDelta streaming input")?;
375        self.values.extend_from_slice(values);
376        Ok(())
377    }
378
379    /// Number of values encoded so far.
380    pub fn count(&self) -> usize {
381        self.values.len()
382    }
383
384    /// Finish encoding and return compressed bytes.
385    pub fn finish(self) -> Result<Vec<u8>, CodecError> {
386        encode(&self.values)
387    }
388}
389
390impl Default for DoubleDeltaEncoder {
391    fn default() -> Self {
392        Self::new()
393    }
394}
395
396/// Streaming DoubleDelta decoder. Wraps the batch `decode()` function.
397pub struct DoubleDeltaDecoder {
398    values: Vec<i64>,
399    pos: usize,
400}
401
402impl DoubleDeltaDecoder {
403    /// Create a decoder from compressed bytes.
404    pub fn new(data: &[u8]) -> Result<Self, CodecError> {
405        let values = decode(data)?;
406        Ok(Self { values, pos: 0 })
407    }
408
409    /// Decode all values at once.
410    pub fn decode_all(data: &[u8]) -> Result<Vec<i64>, CodecError> {
411        decode(data)
412    }
413
414    /// Next value, or None if exhausted.
415    pub fn next_value(&mut self) -> Option<i64> {
416        if self.pos < self.values.len() {
417            let v = self.values[self.pos];
418            self.pos += 1;
419            Some(v)
420        } else {
421            None
422        }
423    }
424
425    /// Remaining value count.
426    pub fn remaining(&self) -> usize {
427        self.values.len() - self.pos
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    fn encode(values: &[i64]) -> Vec<u8> {
436        super::encode(values).expect("test DoubleDelta encode")
437    }
438
439    #[test]
440    fn empty_roundtrip() {
441        let encoded = encode(&[]);
442        let decoded = decode(&encoded).unwrap();
443        assert!(decoded.is_empty());
444    }
445
446    #[test]
447    fn single_value() {
448        let encoded = encode(&[1_700_000_000_000i64]);
449        let decoded = decode(&encoded).unwrap();
450        assert_eq!(decoded, vec![1_700_000_000_000i64]);
451        assert_eq!(encoded.len(), 12);
452    }
453
454    #[test]
455    fn two_values() {
456        let values = vec![1000i64, 2000];
457        let encoded = encode(&values);
458        let decoded = decode(&encoded).unwrap();
459        assert_eq!(decoded, values);
460        assert_eq!(encoded.len(), 20);
461    }
462
463    #[test]
464    fn constant_rate_timestamps() {
465        let values: Vec<i64> = (0..10_000)
466            .map(|i| 1_700_000_000_000 + i * 10_000)
467            .collect();
468        let encoded = encode(&values);
469        let decoded = decode(&encoded).unwrap();
470        assert_eq!(decoded, values);
471
472        let bits_per_sample = (encoded.len() as f64 * 8.0) / values.len() as f64;
473        assert!(
474            bits_per_sample < 2.0,
475            "constant-rate should compress to ~1 bit/sample, got {bits_per_sample:.1}"
476        );
477    }
478
479    #[test]
480    fn monotonic_with_jitter() {
481        let mut values = Vec::with_capacity(10_000);
482        let mut ts = 1_700_000_000_000i64;
483        let mut rng: u64 = 42;
484        for _ in 0..10_000 {
485            values.push(ts);
486            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
487            let jitter = ((rng >> 33) as i64 % 101) - 50;
488            ts += 10_000 + jitter;
489        }
490        let encoded = encode(&values);
491        let decoded = decode(&encoded).unwrap();
492        assert_eq!(decoded, values);
493
494        let bytes_per_sample = encoded.len() as f64 / values.len() as f64;
495        assert!(
496            bytes_per_sample < 2.0,
497            "jittered timestamps should compress to <2 bytes/sample, got {bytes_per_sample:.2}"
498        );
499    }
500
501    #[test]
502    fn non_monotonic_values() {
503        let values: Vec<i64> = vec![100, 50, 200, 10, 300, 5, 1000, -500, 0, 42];
504        let encoded = encode(&values);
505        let decoded = decode(&encoded).unwrap();
506        assert_eq!(decoded, values);
507    }
508
509    #[test]
510    fn negative_values() {
511        let values: Vec<i64> = vec![-1000, -999, -998, -997, -996];
512        let encoded = encode(&values);
513        let decoded = decode(&encoded).unwrap();
514        assert_eq!(decoded, values);
515    }
516
517    #[test]
518    fn large_deltas() {
519        let values: Vec<i64> = vec![0, i64::MAX / 2, i64::MIN / 2, i64::MAX / 4, 0];
520        let encoded = encode(&values);
521        let decoded = decode(&encoded).unwrap();
522        assert_eq!(decoded, values);
523    }
524
525    #[test]
526    fn boundary_values() {
527        let values: Vec<i64> = vec![i64::MIN, 0, i64::MAX, 0, i64::MIN];
528        let encoded = encode(&values);
529        let decoded = decode(&encoded).unwrap();
530        assert_eq!(decoded, values);
531    }
532
533    #[test]
534    fn compression_better_than_raw_for_constant_rate() {
535        let values: Vec<i64> = (0..100_000)
536            .map(|i| 1_700_000_000_000 + i * 10_000)
537            .collect();
538        let encoded = encode(&values);
539        let raw_size = values.len() * 8;
540        let ratio = raw_size as f64 / encoded.len() as f64;
541        assert!(
542            ratio > 5.0,
543            "expected >5x compression for constant-rate, got {ratio:.1}x"
544        );
545    }
546
547    #[test]
548    fn streaming_encoder_matches_batch() {
549        let values: Vec<i64> = (0..1000).map(|i| 1_000_000 + i * 100).collect();
550        let batch_encoded = encode(&values);
551
552        let mut enc = DoubleDeltaEncoder::new();
553        for &v in &values {
554            enc.push(v).expect("push");
555        }
556        let stream_encoded = enc.finish().expect("finish");
557
558        assert_eq!(batch_encoded, stream_encoded);
559    }
560
561    #[test]
562    fn streaming_decoder() {
563        let values: Vec<i64> = (0..100).map(|i| 5000 + i * 10).collect();
564        let encoded = encode(&values);
565        let mut dec = DoubleDeltaDecoder::new(&encoded).unwrap();
566
567        for &expected in &values {
568            assert_eq!(dec.next_value(), Some(expected));
569        }
570        assert_eq!(dec.next_value(), None);
571        assert_eq!(dec.remaining(), 0);
572    }
573
574    #[test]
575    fn huge_count_and_noncanonical_tail_are_rejected_before_decode_work() {
576        let mut huge = u32::MAX.to_le_bytes().to_vec();
577        huge.extend_from_slice(&[0; 16]);
578        assert!(matches!(
579            decode(&huge),
580            Err(CodecError::ResourceLimit { .. })
581        ));
582
583        let mut one = encode(&[7]);
584        one.push(1);
585        assert!(matches!(decode(&one), Err(CodecError::Corrupt { .. })));
586
587        let mut padded = encode(&[0, 1, 2]);
588        let last = padded.len() - 1;
589        padded[last] |= 1;
590        assert!(matches!(decode(&padded), Err(CodecError::Corrupt { .. })));
591    }
592
593    #[test]
594    fn truncated_input_errors() {
595        assert!(decode(&[]).is_err());
596        assert!(decode(&[1, 0, 0, 0]).is_err());
597        assert!(decode(&[2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).is_err());
598    }
599}