Skip to main content

nodedb_codec/
pipeline.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Cascading codec pipeline: chains type-aware encoding → terminal compressor.
4//!
5//! Each `ColumnCodec` variant maps to a fixed pipeline. The `encode_pipeline()`
6//! and `decode_pipeline()` functions dispatch to the appropriate chain.
7//!
8//! Cascading chains:
9//! - `AlpFastLanesLz4`:    f64 → ALP → FastLanes bit-pack → lz4
10//! - `DeltaFastLanesLz4`:  i64 → Delta → FastLanes bit-pack → lz4
11//! - `FastLanesLz4`:       i64 → FastLanes bit-pack → lz4
12//!
13//! The pipeline writes a 1-byte codec ID header so the decoder knows which
14//! chain to reverse. This header is read by `decode_pipeline()`.
15
16use crate::ColumnCodec;
17use crate::error::CodecError;
18
19// ---------------------------------------------------------------------------
20// Pipeline encode
21// ---------------------------------------------------------------------------
22
23/// Encode i64 values through a cascading pipeline.
24///
25/// For cascading codecs, chains the appropriate stages. For legacy codecs,
26/// delegates to the single-step encoder.
27pub fn encode_i64_pipeline(values: &[i64], codec: ColumnCodec) -> Result<Vec<u8>, CodecError> {
28    match codec {
29        // Cascading chains — lz4 terminal.
30        ColumnCodec::DeltaFastLanesLz4 => encode_delta_fastlanes_lz4(values),
31        ColumnCodec::FastLanesLz4 => encode_fastlanes_lz4_i64(values),
32        ColumnCodec::PcodecLz4 => {
33            let pco_bytes = crate::pcodec::encode_i64(values)?;
34            crate::lz4::encode(&pco_bytes)
35        }
36        // Cascading chains — rANS terminal (cold tier).
37        ColumnCodec::DeltaFastLanesRans => encode_delta_fastlanes_rans(values),
38        // Single-step codecs (small partitions / non-ALP-encodable data).
39        ColumnCodec::DoubleDelta => crate::double_delta::encode(values),
40        ColumnCodec::Delta => crate::delta::encode(values),
41        ColumnCodec::Gorilla => Ok(crate::gorilla::encode_timestamps(values)),
42        ColumnCodec::Raw => {
43            let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
44            Ok(crate::raw::encode(&raw))
45        }
46        ColumnCodec::Lz4 => {
47            let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
48            crate::lz4::encode(&raw)
49        }
50        ColumnCodec::Zstd => {
51            let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
52            crate::zstd_codec::encode(&raw)
53        }
54        _ => crate::delta::encode(values),
55    }
56}
57
58/// Encode f64 values through a cascading pipeline.
59pub fn encode_f64_pipeline(values: &[f64], codec: ColumnCodec) -> Result<Vec<u8>, CodecError> {
60    match codec {
61        // Cascading chains — lz4 terminal.
62        ColumnCodec::AlpFastLanesLz4 => encode_alp_fastlanes_lz4(values),
63        ColumnCodec::AlpRdLz4 => {
64            let alp_rd_bytes = crate::alp_rd::encode(values)?;
65            crate::lz4::encode(&alp_rd_bytes)
66        }
67        ColumnCodec::PcodecLz4 => {
68            let pco_bytes = crate::pcodec::encode_f64(values)?;
69            crate::lz4::encode(&pco_bytes)
70        }
71        // Cascading chains — rANS terminal (cold tier).
72        ColumnCodec::AlpFastLanesRans => encode_alp_fastlanes_rans(values),
73        // Single-step codecs (small partitions / non-ALP-encodable data).
74        ColumnCodec::Gorilla => Ok(crate::gorilla::encode_f64(values)),
75        ColumnCodec::Raw => {
76            let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
77            Ok(crate::raw::encode(&raw))
78        }
79        ColumnCodec::Lz4 => {
80            let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
81            crate::lz4::encode(&raw)
82        }
83        ColumnCodec::Zstd => {
84            let raw: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
85            crate::zstd_codec::encode(&raw)
86        }
87        _ => Ok(crate::gorilla::encode_f64(values)),
88    }
89}
90
91/// Encode raw bytes (symbol columns or string data) through a pipeline.
92pub fn encode_bytes_pipeline(raw: &[u8], codec: ColumnCodec) -> Result<Vec<u8>, CodecError> {
93    match codec {
94        ColumnCodec::FsstLz4 => {
95            // FSST expects an array of strings. Treat raw as a single blob.
96            let fsst_bytes = crate::fsst::encode(&[raw])?;
97            crate::lz4::encode(&fsst_bytes)
98        }
99        ColumnCodec::FsstRans => {
100            let fsst_bytes = crate::fsst::encode(&[raw])?;
101            crate::rans::encode(&fsst_bytes)
102        }
103        ColumnCodec::Raw => Ok(crate::raw::encode(raw)),
104        ColumnCodec::Lz4 => crate::lz4::encode(raw),
105        ColumnCodec::Zstd => crate::zstd_codec::encode(raw),
106        ColumnCodec::FastLanesLz4 => {
107            // Symbol IDs are u32 — convert to i64, FastLanes pack, lz4.
108            if raw.len().is_multiple_of(4) {
109                let i64_vals: Vec<i64> = raw
110                    .as_chunks::<4>()
111                    .0
112                    .iter()
113                    .map(|c| u32::from_le_bytes(*c) as i64)
114                    .collect();
115                encode_fastlanes_lz4_i64(&i64_vals)
116            } else {
117                Ok(crate::raw::encode(raw))
118            }
119        }
120        _ => Ok(crate::raw::encode(raw)),
121    }
122}
123
124// ---------------------------------------------------------------------------
125// Pipeline decode
126// ---------------------------------------------------------------------------
127
128/// Decode i64 values from a cascading pipeline.
129pub fn decode_i64_pipeline(data: &[u8], codec: ColumnCodec) -> Result<Vec<i64>, CodecError> {
130    match codec {
131        // Cascading — lz4 terminal.
132        ColumnCodec::DeltaFastLanesLz4 => decode_delta_fastlanes_lz4(data),
133        ColumnCodec::FastLanesLz4 => decode_fastlanes_lz4_i64(data),
134        ColumnCodec::PcodecLz4 => {
135            let pco_bytes = crate::lz4::decode(data)?;
136            crate::pcodec::decode_i64(&pco_bytes)
137        }
138        // Cascading — rANS terminal.
139        ColumnCodec::DeltaFastLanesRans => decode_delta_fastlanes_rans(data),
140        // Single-step codecs (small partitions / non-ALP-encodable data).
141        ColumnCodec::DoubleDelta => crate::double_delta::decode(data),
142        ColumnCodec::Delta => crate::delta::decode(data),
143        ColumnCodec::Gorilla => crate::gorilla::decode_timestamps(data),
144        ColumnCodec::Raw => {
145            let raw = crate::raw::decode(data)?;
146            raw_to_i64(&raw)
147        }
148        ColumnCodec::Lz4 => {
149            let raw = crate::lz4::decode(data)?;
150            raw_to_i64(&raw)
151        }
152        ColumnCodec::Zstd => {
153            let raw = crate::zstd_codec::decode(data)?;
154            raw_to_i64(&raw)
155        }
156        _ => crate::delta::decode(data),
157    }
158}
159
160/// Decode f64 values from a cascading pipeline.
161pub fn decode_f64_pipeline(data: &[u8], codec: ColumnCodec) -> Result<Vec<f64>, CodecError> {
162    match codec {
163        // Cascading — lz4 terminal.
164        ColumnCodec::AlpFastLanesLz4 => decode_alp_fastlanes_lz4(data),
165        ColumnCodec::AlpRdLz4 => {
166            let alp_rd_bytes = crate::lz4::decode(data)?;
167            crate::alp_rd::decode(&alp_rd_bytes)
168        }
169        ColumnCodec::PcodecLz4 => {
170            let pco_bytes = crate::lz4::decode(data)?;
171            crate::pcodec::decode_f64(&pco_bytes)
172        }
173        // Cascading — rANS terminal.
174        ColumnCodec::AlpFastLanesRans => decode_alp_fastlanes_rans(data),
175        // Single-step codecs (small partitions / non-ALP-encodable data).
176        ColumnCodec::Gorilla => crate::gorilla::decode_f64(data),
177        ColumnCodec::Raw => {
178            let raw = crate::raw::decode(data)?;
179            raw_to_f64(&raw)
180        }
181        ColumnCodec::Lz4 => {
182            let raw = crate::lz4::decode(data)?;
183            raw_to_f64(&raw)
184        }
185        ColumnCodec::Zstd => {
186            let raw = crate::zstd_codec::decode(data)?;
187            raw_to_f64(&raw)
188        }
189        _ => crate::gorilla::decode_f64(data),
190    }
191}
192
193/// Decode raw bytes (symbol columns or string data) from a pipeline.
194pub fn decode_bytes_pipeline(data: &[u8], codec: ColumnCodec) -> Result<Vec<u8>, CodecError> {
195    match codec {
196        ColumnCodec::FsstLz4 => {
197            let fsst_bytes = crate::lz4::decode(data)?;
198            let strings = crate::fsst::decode(&fsst_bytes)?;
199            strings.into_iter().next().ok_or(CodecError::Corrupt {
200                detail: "FSST decode produced no output".into(),
201            })
202        }
203        ColumnCodec::FsstRans => {
204            let fsst_bytes = crate::rans::decode(data)?;
205            let strings = crate::fsst::decode(&fsst_bytes)?;
206            strings.into_iter().next().ok_or(CodecError::Corrupt {
207                detail: "FSST decode produced no output".into(),
208            })
209        }
210        ColumnCodec::Raw => crate::raw::decode(data),
211        ColumnCodec::Lz4 => crate::lz4::decode(data),
212        ColumnCodec::Zstd => crate::zstd_codec::decode(data),
213        ColumnCodec::FastLanesLz4 => {
214            let i64_vals = decode_fastlanes_lz4_i64(data)?;
215            Ok(i64_vals
216                .iter()
217                .flat_map(|&v| (v as u32).to_le_bytes())
218                .collect())
219        }
220        _ => crate::raw::decode(data).or_else(|_| Ok(data.to_vec())),
221    }
222}
223
224// ---------------------------------------------------------------------------
225// Cascading chain implementations
226// ---------------------------------------------------------------------------
227
228/// ALP → FastLanes → LZ4: f64 metrics (the big win).
229fn encode_alp_fastlanes_lz4(values: &[f64]) -> Result<Vec<u8>, CodecError> {
230    // Stage 1: ALP encodes f64 → FastLanes-packed i64 bytes.
231    let alp_encoded = crate::alp::encode(values)?;
232    // Stage 2: LZ4 terminal compression on the ALP output.
233    crate::lz4::encode(&alp_encoded)
234}
235
236fn decode_alp_fastlanes_lz4(data: &[u8]) -> Result<Vec<f64>, CodecError> {
237    // Reverse: LZ4 decompress → ALP decode (which internally FastLanes-decodes).
238    let alp_bytes = crate::lz4::decode(data)?;
239    crate::alp::decode(&alp_bytes)
240}
241
242/// Delta → FastLanes → LZ4: i64 timestamps and counters.
243fn encode_delta_fastlanes_lz4(values: &[i64]) -> Result<Vec<u8>, CodecError> {
244    if values.is_empty() {
245        return crate::lz4::encode(&crate::fastlanes::encode(&[])?);
246    }
247
248    // Stage 1a: Compute deltas.
249    let mut deltas = Vec::with_capacity(values.len());
250    deltas.push(values[0]); // First value stored raw.
251    for i in 1..values.len() {
252        deltas.push(values[i].wrapping_sub(values[i - 1]));
253    }
254
255    // Stage 1b: FastLanes bit-pack the deltas.
256    let packed = crate::fastlanes::encode(&deltas)?;
257
258    // Stage 2: LZ4 terminal.
259    crate::lz4::encode(&packed)
260}
261
262fn decode_delta_fastlanes_lz4(data: &[u8]) -> Result<Vec<i64>, CodecError> {
263    // Reverse: LZ4 → FastLanes unpack → reconstruct from deltas.
264    let packed = crate::lz4::decode(data)?;
265    let deltas = crate::fastlanes::decode(&packed)?;
266
267    if deltas.is_empty() {
268        return Ok(Vec::new());
269    }
270
271    // Reconstruct values from deltas.
272    let mut values = Vec::with_capacity(deltas.len());
273    values.push(deltas[0]); // First value is raw.
274    for &d in &deltas[1..] {
275        let prev = values[values.len() - 1];
276        values.push(prev.wrapping_add(d));
277    }
278
279    Ok(values)
280}
281
282/// FastLanes → LZ4: raw integers (symbol IDs, non-delta columns).
283fn encode_fastlanes_lz4_i64(values: &[i64]) -> Result<Vec<u8>, CodecError> {
284    let packed = crate::fastlanes::encode(values)?;
285    crate::lz4::encode(&packed)
286}
287
288fn decode_fastlanes_lz4_i64(data: &[u8]) -> Result<Vec<i64>, CodecError> {
289    let packed = crate::lz4::decode(data)?;
290    crate::fastlanes::decode(&packed)
291}
292
293/// ALP → FastLanes → rANS: f64 metrics cold tier.
294fn encode_alp_fastlanes_rans(values: &[f64]) -> Result<Vec<u8>, CodecError> {
295    let alp_encoded = crate::alp::encode(values)?;
296    crate::rans::encode(&alp_encoded)
297}
298
299fn decode_alp_fastlanes_rans(data: &[u8]) -> Result<Vec<f64>, CodecError> {
300    let alp_bytes = crate::rans::decode(data)?;
301    crate::alp::decode(&alp_bytes)
302}
303
304/// Delta → FastLanes → rANS: i64 cold tier.
305fn encode_delta_fastlanes_rans(values: &[i64]) -> Result<Vec<u8>, CodecError> {
306    if values.is_empty() {
307        return crate::rans::encode(&crate::fastlanes::encode(&[])?);
308    }
309
310    let mut deltas = Vec::with_capacity(values.len());
311    deltas.push(values[0]);
312    for i in 1..values.len() {
313        deltas.push(values[i].wrapping_sub(values[i - 1]));
314    }
315
316    let packed = crate::fastlanes::encode(&deltas)?;
317    crate::rans::encode(&packed)
318}
319
320fn decode_delta_fastlanes_rans(data: &[u8]) -> Result<Vec<i64>, CodecError> {
321    let packed = crate::rans::decode(data)?;
322    let deltas = crate::fastlanes::decode(&packed)?;
323
324    if deltas.is_empty() {
325        return Ok(Vec::new());
326    }
327
328    let mut values = Vec::with_capacity(deltas.len());
329    values.push(deltas[0]);
330    for &d in &deltas[1..] {
331        let prev = values[values.len() - 1];
332        values.push(prev.wrapping_add(d));
333    }
334
335    Ok(values)
336}
337
338// ---------------------------------------------------------------------------
339// Helpers
340// ---------------------------------------------------------------------------
341
342fn raw_to_i64(data: &[u8]) -> Result<Vec<i64>, CodecError> {
343    if !data.len().is_multiple_of(8) {
344        return Err(CodecError::Corrupt {
345            detail: "i64 data not aligned to 8 bytes".into(),
346        });
347    }
348    Ok(data
349        .as_chunks::<8>()
350        .0
351        .iter()
352        .map(|c| i64::from_le_bytes(*c))
353        .collect())
354}
355
356fn raw_to_f64(data: &[u8]) -> Result<Vec<f64>, CodecError> {
357    if !data.len().is_multiple_of(8) {
358        return Err(CodecError::Corrupt {
359            detail: "f64 data not aligned to 8 bytes".into(),
360        });
361    }
362    Ok(data
363        .as_chunks::<8>()
364        .0
365        .iter()
366        .map(|c| f64::from_le_bytes(*c))
367        .collect())
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    #[test]
375    fn alp_fastlanes_lz4_decimal_metrics() {
376        let values: Vec<f64> = (0..10_000).map(|i| i as f64 * 0.1).collect();
377        let encoded = encode_f64_pipeline(&values, ColumnCodec::AlpFastLanesLz4).unwrap();
378        let decoded = decode_f64_pipeline(&encoded, ColumnCodec::AlpFastLanesLz4).unwrap();
379
380        for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
381            assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}");
382        }
383
384        let raw_size = values.len() * 8;
385        let ratio = raw_size as f64 / encoded.len() as f64;
386        assert!(
387            ratio > 3.0,
388            "ALP+FL+LZ4 should compress decimals >3x, got {ratio:.1}x"
389        );
390    }
391
392    #[test]
393    fn alp_beats_gorilla_on_decimals() {
394        let mut values = Vec::with_capacity(10_000);
395        let mut rng: u64 = 42;
396        for _ in 0..10_000 {
397            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
398            let cpu = ((rng >> 33) as f64 / (u32::MAX as f64)) * 100.0;
399            values.push((cpu * 10.0).round() / 10.0);
400        }
401
402        let alp_size = encode_f64_pipeline(&values, ColumnCodec::AlpFastLanesLz4)
403            .unwrap()
404            .len();
405        let gorilla_size = encode_f64_pipeline(&values, ColumnCodec::Gorilla)
406            .unwrap()
407            .len();
408
409        assert!(
410            alp_size < gorilla_size,
411            "ALP ({alp_size}) should beat Gorilla ({gorilla_size}) on decimal metrics"
412        );
413    }
414
415    #[test]
416    fn delta_fastlanes_lz4_timestamps() {
417        let values: Vec<i64> = (0..10_000)
418            .map(|i| 1_700_000_000_000 + i * 10_000)
419            .collect();
420        let encoded = encode_i64_pipeline(&values, ColumnCodec::DeltaFastLanesLz4).unwrap();
421        let decoded = decode_i64_pipeline(&encoded, ColumnCodec::DeltaFastLanesLz4).unwrap();
422        assert_eq!(decoded, values);
423
424        let raw_size = values.len() * 8;
425        let ratio = raw_size as f64 / encoded.len() as f64;
426        assert!(
427            ratio > 5.0,
428            "Delta+FL+LZ4 should compress timestamps >5x, got {ratio:.1}x"
429        );
430    }
431
432    #[test]
433    fn delta_fastlanes_lz4_jittered_timestamps() {
434        let mut values = Vec::with_capacity(10_000);
435        let mut ts = 1_700_000_000_000i64;
436        let mut rng: u64 = 42;
437        for _ in 0..10_000 {
438            values.push(ts);
439            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
440            let jitter = ((rng >> 33) as i64 % 101) - 50;
441            ts += 10_000 + jitter;
442        }
443        let encoded = encode_i64_pipeline(&values, ColumnCodec::DeltaFastLanesLz4).unwrap();
444        let decoded = decode_i64_pipeline(&encoded, ColumnCodec::DeltaFastLanesLz4).unwrap();
445        assert_eq!(decoded, values);
446    }
447
448    #[test]
449    fn delta_fastlanes_lz4_counters() {
450        let values: Vec<i64> = (0..10_000).map(|i| i * 1000).collect();
451        let encoded = encode_i64_pipeline(&values, ColumnCodec::DeltaFastLanesLz4).unwrap();
452        let decoded = decode_i64_pipeline(&encoded, ColumnCodec::DeltaFastLanesLz4).unwrap();
453        assert_eq!(decoded, values);
454    }
455
456    #[test]
457    fn fastlanes_lz4_symbol_ids() {
458        let values: Vec<i64> = (0..5000).map(|i| i % 150).collect();
459        let encoded = encode_i64_pipeline(&values, ColumnCodec::FastLanesLz4).unwrap();
460        let decoded = decode_i64_pipeline(&encoded, ColumnCodec::FastLanesLz4).unwrap();
461        assert_eq!(decoded, values);
462    }
463
464    #[test]
465    fn legacy_codecs_still_work() {
466        let i64_vals: Vec<i64> = (0..1000).collect();
467        for codec in [
468            ColumnCodec::DoubleDelta,
469            ColumnCodec::Delta,
470            ColumnCodec::Gorilla,
471        ] {
472            let encoded = encode_i64_pipeline(&i64_vals, codec).unwrap();
473            let decoded = decode_i64_pipeline(&encoded, codec).unwrap();
474            assert_eq!(decoded, i64_vals, "legacy i64 codec {codec} failed");
475        }
476
477        let f64_vals: Vec<f64> = (0..1000).map(|i| i as f64 * 0.5).collect();
478        let encoded = encode_f64_pipeline(&f64_vals, ColumnCodec::Gorilla).unwrap();
479        let decoded = decode_f64_pipeline(&encoded, ColumnCodec::Gorilla).unwrap();
480        for (a, b) in f64_vals.iter().zip(decoded.iter()) {
481            assert_eq!(a.to_bits(), b.to_bits());
482        }
483    }
484
485    #[test]
486    fn empty_values() {
487        let empty_i64: Vec<i64> = vec![];
488        let empty_f64: Vec<f64> = vec![];
489
490        for codec in [
491            ColumnCodec::DeltaFastLanesLz4,
492            ColumnCodec::FastLanesLz4,
493            ColumnCodec::AlpFastLanesLz4,
494        ] {
495            if matches!(codec, ColumnCodec::AlpFastLanesLz4) {
496                let enc = encode_f64_pipeline(&empty_f64, codec).unwrap();
497                let dec = decode_f64_pipeline(&enc, codec).unwrap();
498                assert!(dec.is_empty());
499            } else {
500                let enc = encode_i64_pipeline(&empty_i64, codec).unwrap();
501                let dec = decode_i64_pipeline(&enc, codec).unwrap();
502                assert!(dec.is_empty());
503            }
504        }
505    }
506
507    #[test]
508    fn bytes_pipeline_roundtrip() {
509        let raw: Vec<u8> = (0..1000u32).flat_map(|i| i.to_le_bytes()).collect();
510        for codec in [ColumnCodec::Raw, ColumnCodec::Lz4] {
511            let encoded = encode_bytes_pipeline(&raw, codec).unwrap();
512            let decoded = decode_bytes_pipeline(&encoded, codec).unwrap();
513            assert_eq!(decoded, raw, "bytes pipeline {codec} failed");
514        }
515    }
516}