Skip to main content

chronix_encoding/
decimal.rs

1//! Encoding for exact decimal columns — the `i128` mantissa stream.
2//!
3//! A decimal column stores `mantissa × 10⁻ˢᶜᵃˡᵉ` with the scale held once in
4//! the column's metadata, so what reaches this codec is a column of plain
5//! integers. That is the whole argument for the type: the README's
6//! compression section already observes that most float columns are decimals
7//! that went through an `f64`, and that ALP and pco spend their first stage
8//! *recovering* the integer. A decimal column hands that integer over
9//! directly — nothing to recover, nothing lost recovering it.
10//!
11//! # Two physical forms
12//!
13//! Almost every real decimal column is narrow. A quarter-hour meter register
14//! in watt-hours at four decimal places needs 19 digits to reach 10¹⁴ kWh, so
15//! its mantissas fit an `i64` with room to spare, and the interesting shapes
16//! — a monotone counter, a slowly varying price, a constant tariff — are
17//! exactly the shapes the `i64` stack is built for.
18//!
19//! ```text
20//! [form: u8 = 0][inner i64 block: tag + payload]   narrow
21//! [form: u8 = 1][count: u32][first: i128 LE][zigzag varint deltas…]  wide
22//! ```
23//!
24//! The narrow form is not a second codec: it delegates to
25//! [`ColumnEncoder::encode_i64`](crate::ColumnEncoder::encode_i64), so a
26//! decimal column gets RLE on a constant register, frame-of-reference on a
27//! narrow range, and pco on everything else — the same trial-and-keep-the-
28//! smallest selection an `i64` field gets, for free and for ever.
29//!
30//! The wide form exists for the columns that genuinely need more than 63
31//! bits of mantissa: 38-digit financial quantities, or a scale so large that
32//! ordinary values overflow. Delta + `ZigZag` + LEB128 keeps a
33//! slowly-varying wide column near the narrow one's size, and a random wide
34//! column costs at most 19 bytes per value against the 16 it would take raw.
35
36use crate::coding::{checked_count, checked_decode_count};
37use crate::error::{EncodingError, Result};
38use crate::unified::{ColumnDecoder, ColumnEncoder, DecodedColumn, EncodedBlock, EncodingType};
39
40/// Physical form tag: mantissas that fit `i64`, delegated to the `i64` stack.
41pub(crate) const FORM_NARROW: u8 = 0;
42/// Physical form tag: full-width `i128` mantissas, delta + `ZigZag` + varint.
43pub(crate) const FORM_WIDE: u8 = 1;
44
45/// `ZigZag` encode a signed 128-bit integer to unsigned.
46#[inline]
47fn zigzag_encode_i128(n: i128) -> u128 {
48    ((n << 1) ^ (n >> 127)) as u128
49}
50
51/// `ZigZag` decode an unsigned 128-bit integer back to signed.
52#[inline]
53fn zigzag_decode_i128(n: u128) -> i128 {
54    ((n >> 1) as i128) ^ -((n & 1) as i128)
55}
56
57/// Append a LEB128 varint for a `u128`.
58#[inline]
59fn varint_encode_u128(mut value: u128, out: &mut Vec<u8>) {
60    while value >= 0x80 {
61        out.push((value as u8) | 0x80);
62        value >>= 7;
63    }
64    out.push(value as u8);
65}
66
67/// Read one LEB128 varint, returning the value and the bytes consumed.
68fn varint_decode_u128(bytes: &[u8]) -> Result<(u128, usize)> {
69    let mut value: u128 = 0;
70    let mut shift = 0u32;
71    for (i, &byte) in bytes.iter().enumerate() {
72        // 128 bits at 7 bits per byte is 19 bytes; the last one carries the
73        // final bit. A longer run is corrupt, not a large number.
74        if shift > 126 {
75            return Err(EncodingError::CorruptData {
76                detail: "decimal varint exceeds 128 bits".to_string(),
77            });
78        }
79        value |= u128::from(byte & 0x7f) << shift;
80        if byte & 0x80 == 0 {
81            return Ok((value, i + 1));
82        }
83        shift += 7;
84    }
85    Err(EncodingError::CorruptData {
86        detail: "decimal varint truncated".to_string(),
87    })
88}
89
90/// Encoder for a column of `i128` decimal mantissas.
91#[derive(Debug, Clone, Copy)]
92pub struct DecimalEncoder;
93
94impl DecimalEncoder {
95    /// Encode a mantissa column, choosing the narrow or wide form.
96    ///
97    /// # Errors
98    ///
99    /// Returns [`EncodingError::EmptyInput`] if `values` is empty, and
100    /// propagates any error from the delegated `i64` encoder.
101    pub fn encode(values: &[i128]) -> Result<Vec<u8>> {
102        if values.is_empty() {
103            return Err(EncodingError::EmptyInput {
104                context: "decimal encoder",
105            });
106        }
107
108        // The narrow form is the common one, so the check that selects it is
109        // a single pass with no allocation on the wide path.
110        let narrow = values.iter().all(|&v| i64::try_from(v).is_ok());
111
112        if narrow {
113            let as_i64: Vec<i64> = values.iter().map(|&v| v as i64).collect();
114            let inner = ColumnEncoder::encode_i64(&as_i64)?;
115            let mut out = Vec::with_capacity(2 + inner.payload.len());
116            out.push(FORM_NARROW);
117            out.push(inner.encoding.tag());
118            out.extend_from_slice(&inner.payload);
119            return Ok(out);
120        }
121
122        let count = checked_count(values.len())?;
123        let mut out = Vec::with_capacity(1 + 4 + 16 + values.len() * 4);
124        out.push(FORM_WIDE);
125        out.extend_from_slice(&count.to_le_bytes());
126        out.extend_from_slice(&values[0].to_le_bytes());
127        for window in values.windows(2) {
128            // Wrapping: the difference of two 128-bit mantissas can exceed
129            // `i128`, and the decoder undoes it with the matching wrap.
130            let delta = window[1].wrapping_sub(window[0]);
131            varint_encode_u128(zigzag_encode_i128(delta), &mut out);
132        }
133        Ok(out)
134    }
135}
136
137/// Decoder for a column of `i128` decimal mantissas.
138#[derive(Debug, Clone, Copy)]
139pub struct DecimalDecoder;
140
141impl DecimalDecoder {
142    /// Decode a mantissa column.
143    ///
144    /// # Errors
145    ///
146    /// Returns [`EncodingError::CorruptData`] if the form byte is unknown,
147    /// the payload is truncated, or the narrow form's inner block does not
148    /// decode to `i64` values.
149    pub fn decode(payload: &[u8]) -> Result<Vec<i128>> {
150        let (&form, rest) = payload
151            .split_first()
152            .ok_or_else(|| EncodingError::CorruptData {
153                detail: "empty decimal block".to_string(),
154            })?;
155
156        match form {
157            FORM_NARROW => Self::decode_narrow(rest),
158            FORM_WIDE => Self::decode_wide(rest),
159            other => Err(EncodingError::CorruptData {
160                detail: format!("unknown decimal form: {other}"),
161            }),
162        }
163    }
164
165    /// Decode the narrow form by handing the inner block back to the `i64`
166    /// stack.
167    fn decode_narrow(rest: &[u8]) -> Result<Vec<i128>> {
168        let (&tag, inner) = rest
169            .split_first()
170            .ok_or_else(|| EncodingError::CorruptData {
171                detail: "narrow decimal block has no inner encoding tag".to_string(),
172            })?;
173        let encoding = EncodingType::from_tag(tag)?;
174        // A corrupt tag naming this codec again would recurse until the
175        // stack ran out, so the one encoding the inner block may never be is
176        // this one. Checked before the decode, not after.
177        if encoding == EncodingType::DecimalI128 {
178            return Err(EncodingError::CorruptData {
179                detail: "narrow decimal block nests a decimal block".to_string(),
180            });
181        }
182        let block = EncodedBlock {
183            encoding,
184            payload: inner.to_vec(),
185        };
186        match ColumnDecoder::decode(&block)? {
187            DecodedColumn::I64(values) => Ok(values.into_iter().map(i128::from).collect()),
188            other => Err(EncodingError::CorruptData {
189                detail: format!(
190                    "narrow decimal block decoded to {} values, expected i64",
191                    match other {
192                        DecodedColumn::F64(_) => "f64",
193                        DecodedColumn::U64(_) => "u64",
194                        DecodedColumn::Bool(_) => "bool",
195                        DecodedColumn::String(_) => "string",
196                        _ => "nullable or wide",
197                    }
198                ),
199            }),
200        }
201    }
202
203    /// Decode the wide form: first value plus zigzag varint deltas.
204    fn decode_wide(rest: &[u8]) -> Result<Vec<i128>> {
205        if rest.len() < 4 + 16 {
206            return Err(EncodingError::CorruptData {
207                detail: "wide decimal block truncated".to_string(),
208            });
209        }
210        let count = checked_decode_count(
211            u32::from_le_bytes([rest[0], rest[1], rest[2], rest[3]]) as usize,
212            "decimal",
213        )?;
214        if count == 0 {
215            return Err(EncodingError::CorruptData {
216                detail: "wide decimal block declares zero values".to_string(),
217            });
218        }
219        let mut first_bytes = [0u8; 16];
220        first_bytes.copy_from_slice(&rest[4..20]);
221        let mut current = i128::from_le_bytes(first_bytes);
222
223        let mut values = Vec::with_capacity(count);
224        values.push(current);
225        let mut cursor = 20;
226        for _ in 1..count {
227            let (zz, used) = varint_decode_u128(&rest[cursor..])?;
228            cursor += used;
229            current = current.wrapping_add(zigzag_decode_i128(zz));
230            values.push(current);
231        }
232        Ok(values)
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    fn round_trip(values: &[i128]) {
241        let encoded = DecimalEncoder::encode(values).unwrap();
242        let decoded = DecimalDecoder::decode(&encoded).unwrap();
243        assert_eq!(decoded, values, "round trip");
244    }
245
246    #[test]
247    fn narrow_round_trip() {
248        round_trip(&[23_145]);
249        round_trip(&[0, 1, 2, 3, 4, 5]);
250        round_trip(&[-5, -4, -3, 0, 3, 4, 5]);
251        round_trip(&[i64::MAX as i128, i64::MIN as i128]);
252    }
253
254    #[test]
255    fn wide_round_trip() {
256        round_trip(&[i128::from(i64::MAX) + 1]);
257        round_trip(&[
258            99_999_999_999_999_999_999_999_999_999_999_999_999,
259            -99_999_999_999_999_999_999_999_999_999_999_999_999,
260            0,
261        ]);
262    }
263
264    #[test]
265    fn form_is_narrow_when_every_mantissa_fits_i64() {
266        let encoded = DecimalEncoder::encode(&[1, 2, 3]).unwrap();
267        assert_eq!(encoded[0], FORM_NARROW);
268        let encoded = DecimalEncoder::encode(&[i128::from(i64::MAX) + 1]).unwrap();
269        assert_eq!(encoded[0], FORM_WIDE);
270    }
271
272    #[test]
273    fn a_monotone_register_compresses() {
274        // A quarter-hour meter register in Wh at scale 4: strictly rising by
275        // roughly the same amount every step, which is what the i64 stack is
276        // built for.
277        let values: Vec<i128> = (0..4096).map(|i| 10_000_000_000 + i * 2_500_000).collect();
278        let encoded = DecimalEncoder::encode(&values).unwrap();
279        let raw = values.len() * 16;
280        assert!(
281            encoded.len() * 8 < raw,
282            "expected >8x on a monotone register, got {} vs {raw}",
283            encoded.len()
284        );
285        assert_eq!(DecimalDecoder::decode(&encoded).unwrap(), values);
286    }
287
288    #[test]
289    fn a_constant_register_compresses_hard() {
290        let values = vec![42_0000i128; 4096];
291        let encoded = DecimalEncoder::encode(&values).unwrap();
292        assert!(
293            encoded.len() < 128,
294            "constant column: {} bytes",
295            encoded.len()
296        );
297        assert_eq!(DecimalDecoder::decode(&encoded).unwrap(), values);
298    }
299
300    #[test]
301    fn empty_input_is_an_error() {
302        assert!(matches!(
303            DecimalEncoder::encode(&[]),
304            Err(EncodingError::EmptyInput { .. })
305        ));
306    }
307
308    #[test]
309    fn corrupt_form_byte_is_an_error() {
310        assert!(DecimalDecoder::decode(&[]).is_err());
311        assert!(DecimalDecoder::decode(&[7]).is_err());
312    }
313
314    #[test]
315    fn a_nested_decimal_tag_does_not_recurse() {
316        // The inner tag naming this codec again is the one shape that would
317        // recurse until the stack ran out.
318        let nested = [FORM_NARROW, EncodingType::DecimalI128.tag(), 0, 0];
319        assert!(DecimalDecoder::decode(&nested).is_err());
320    }
321
322    #[test]
323    fn truncated_wide_block_is_an_error() {
324        let encoded = DecimalEncoder::encode(&[i128::from(i64::MAX) + 1, 5, 9]).unwrap();
325        for cut in 1..encoded.len() {
326            // Never a panic: every truncation is either an error or a
327            // shorter-but-valid prefix the length check rejects.
328            let _ = DecimalDecoder::decode(&encoded[..cut]);
329        }
330    }
331
332    #[test]
333    fn a_corrupt_count_cannot_allocate_the_machine_away() {
334        let mut encoded = DecimalEncoder::encode(&[i128::from(i64::MAX) + 1]).unwrap();
335        encoded[1..5].copy_from_slice(&u32::MAX.to_le_bytes());
336        assert!(DecimalDecoder::decode(&encoded).is_err());
337    }
338
339    #[test]
340    fn varint_round_trip_at_the_boundaries() {
341        for value in [0u128, 1, 127, 128, u128::from(u64::MAX), u128::MAX] {
342            let mut buf = Vec::new();
343            varint_encode_u128(value, &mut buf);
344            let (decoded, used) = varint_decode_u128(&buf).unwrap();
345            assert_eq!(decoded, value);
346            assert_eq!(used, buf.len());
347        }
348    }
349
350    #[test]
351    fn zigzag_round_trip_at_the_boundaries() {
352        for value in [0i128, 1, -1, i128::MAX, i128::MIN, i128::from(i64::MIN)] {
353            assert_eq!(zigzag_decode_i128(zigzag_encode_i128(value)), value);
354        }
355    }
356
357    // ── Properties ─────────────────────────────────────────────────────
358    //
359    // This crate's first stated principle is that **every encoder
360    // guarantees bitwise-exact roundtrip**, and every other codec module
361    // here backs that with a `proptest!`. `decimal` and `coding` were the
362    // two that did not, and this is the one whose wide form is a
363    // hand-written 128-bit LEB128 over `wrapping_sub`/`wrapping_add`
364    // deltas — the shape where a chosen example proves least.
365
366    use proptest::prelude::*;
367
368    proptest! {
369        /// The property the module promises, over arbitrary mantissas.
370        ///
371        /// `any::<i128>()` reaches the wide form; the narrow form is the
372        /// separate strategy below, because a random `i128` almost never
373        /// fits an `i64` and the common path would otherwise go untested.
374        #[test]
375        fn roundtrip_arbitrary_mantissas(
376            values in prop::collection::vec(any::<i128>(), 1..200),
377        ) {
378            let encoded = DecimalEncoder::encode(&values).unwrap();
379            let decoded = DecimalDecoder::decode(&encoded).unwrap();
380            prop_assert_eq!(decoded, values);
381        }
382
383        /// The narrow form, which delegates to the whole `i64` stack — so
384        /// this also asserts that RLE, frame-of-reference and pco each
385        /// return the mantissas they were handed.
386        #[test]
387        fn roundtrip_narrow_mantissas(
388            values in prop::collection::vec(any::<i64>(), 1..200),
389        ) {
390            let wide: Vec<i128> = values.iter().map(|&v| i128::from(v)).collect();
391            let encoded = DecimalEncoder::encode(&wide).unwrap();
392            prop_assert_eq!(encoded[0], FORM_NARROW);
393            let decoded = DecimalDecoder::decode(&encoded).unwrap();
394            prop_assert_eq!(decoded, wide);
395        }
396
397        /// A slowly varying wide column is the shape the wide form is for,
398        /// and the one where a delta that wraps `i128` is reachable.
399        #[test]
400        fn roundtrip_wide_deltas_that_may_wrap(
401            base in any::<i128>(),
402            steps in prop::collection::vec(any::<i64>(), 1..200),
403        ) {
404            let mut values = vec![base];
405            for step in steps {
406                let prev = *values.last().unwrap();
407                values.push(prev.wrapping_add(i128::from(step)));
408            }
409            let encoded = DecimalEncoder::encode(&values).unwrap();
410            let decoded = DecimalDecoder::decode(&encoded).unwrap();
411            prop_assert_eq!(decoded, values);
412        }
413
414        /// A decoder must never panic on adversarial bytes, only error.
415        #[test]
416        fn decode_never_panics(bytes in prop::collection::vec(any::<u8>(), 0..512)) {
417            let _ = DecimalDecoder::decode(&bytes);
418        }
419
420        /// The varint is the wide form's whole payload; it must round-trip
421        /// every `u128` and consume exactly what it produced.
422        #[test]
423        fn varint_roundtrips_every_u128(value in any::<u128>()) {
424            let mut buf = Vec::new();
425            varint_encode_u128(value, &mut buf);
426            let (decoded, used) = varint_decode_u128(&buf).unwrap();
427            prop_assert_eq!(decoded, value);
428            prop_assert_eq!(used, buf.len());
429        }
430
431        /// `ZigZag` over the full signed range.
432        #[test]
433        fn zigzag_roundtrips_every_i128(value in any::<i128>()) {
434            prop_assert_eq!(zigzag_decode_i128(zigzag_encode_i128(value)), value);
435        }
436    }
437}