Skip to main content

hermes_core/structures/fast_field/
codec.rs

1//! Fast-field compression codecs with auto-selection.
2//!
3//! Four codecs are available, and the writer picks the smallest at build time:
4//!
5//! | Codec            | ID | Description                                      |
6//! |------------------|----|--------------------------------------------------|
7//! | Constant         |  0 | No data bytes — all values identical              |
8//! | Bitpacked        |  1 | min-subtract + global bitpack                     |
9//! | Linear           |  2 | Regression line, bitpack residuals                |
10//! | BlockwiseLinear  |  3 | Per-512-block linear, bitpack residuals per block |
11
12use std::io::{self, Write};
13
14use byteorder::{LittleEndian, WriteBytesExt};
15
16use super::{bitpack_read, bitpack_write, bits_needed_u64};
17
18// ── Codec type tag ───────────────────────────────────────────────────────
19
20/// Codec identifier stored in the column data region (first byte).
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[repr(u8)]
23pub enum CodecType {
24    Constant = 0,
25    Bitpacked = 1,
26    Linear = 2,
27    BlockwiseLinear = 3,
28}
29
30impl CodecType {
31    pub fn from_u8(v: u8) -> Option<Self> {
32        match v {
33            0 => Some(Self::Constant),
34            1 => Some(Self::Bitpacked),
35            2 => Some(Self::Linear),
36            3 => Some(Self::BlockwiseLinear),
37            _ => None,
38        }
39    }
40}
41
42/// Block size for BlockwiseLinear codec (matching Tantivy).
43pub const BLOCKWISE_LINEAR_BLOCK_SIZE: usize = 512;
44
45// ── Estimator trait ──────────────────────────────────────────────────────
46
47/// Estimates serialized size for a given codec.
48///
49/// Usage: call `collect(val)` for every value, then `finalize()`,
50/// then `estimate()` returns the byte count.
51pub trait CodecEstimator {
52    fn collect(&mut self, value: u64);
53    fn finalize(&mut self) {}
54    fn estimate(&self) -> Option<u64>;
55    fn serialize(&self, values: &[u64], writer: &mut dyn Write) -> io::Result<u64>;
56}
57
58// ── Constant codec ───────────────────────────────────────────────────────
59
60/// All values are identical → zero data bytes. Value stored in the codec header.
61#[derive(Default)]
62pub struct ConstantEstimator {
63    first: Option<u64>,
64    all_same: bool,
65}
66
67impl CodecEstimator for ConstantEstimator {
68    fn collect(&mut self, value: u64) {
69        match self.first {
70            None => {
71                self.first = Some(value);
72                self.all_same = true;
73            }
74            Some(f) => {
75                if value != f {
76                    self.all_same = false;
77                }
78            }
79        }
80    }
81
82    fn estimate(&self) -> Option<u64> {
83        if self.all_same {
84            // codec_id(1) + value(8) = 9 bytes
85            Some(9)
86        } else {
87            None
88        }
89    }
90
91    fn serialize(&self, values: &[u64], writer: &mut dyn Write) -> io::Result<u64> {
92        let val = if values.is_empty() { 0 } else { values[0] };
93        writer.write_u8(CodecType::Constant as u8)?;
94        writer.write_u64::<LittleEndian>(val)?;
95        Ok(9)
96    }
97}
98
99// ── Bitpacked codec ──────────────────────────────────────────────────────
100
101/// Min-subtract + global bitpack. This is the existing codec, now behind a tag.
102#[derive(Default)]
103pub struct BitpackedEstimator {
104    min: u64,
105    max: u64,
106    count: usize,
107    initialized: bool,
108}
109
110impl CodecEstimator for BitpackedEstimator {
111    fn collect(&mut self, value: u64) {
112        if !self.initialized {
113            self.min = value;
114            self.max = value;
115            self.initialized = true;
116        } else {
117            self.min = self.min.min(value);
118            self.max = self.max.max(value);
119        }
120        self.count += 1;
121    }
122
123    fn estimate(&self) -> Option<u64> {
124        if self.count == 0 {
125            return Some(0);
126        }
127        let range = self.max - self.min;
128        let bpv = bits_needed_u64(range) as u64;
129        // codec_id(1) + min(8) + bpv(1) + packed data
130        let data_bits = self.count as u64 * bpv;
131        let data_bytes = data_bits.div_ceil(8);
132        Some(1 + 8 + 1 + data_bytes)
133    }
134
135    fn serialize(&self, values: &[u64], writer: &mut dyn Write) -> io::Result<u64> {
136        let (min_value, bpv) = if values.is_empty() {
137            (0u64, 0u8)
138        } else {
139            let min_val = values.iter().copied().min().unwrap();
140            let max_val = values.iter().copied().max().unwrap();
141            (min_val, bits_needed_u64(max_val - min_val))
142        };
143
144        writer.write_u8(CodecType::Bitpacked as u8)?;
145        writer.write_u64::<LittleEndian>(min_value)?;
146        writer.write_u8(bpv)?;
147        let mut bytes_written = 10u64; // 1 + 8 + 1
148
149        if bpv > 0 && !values.is_empty() {
150            let shifted: Vec<u64> = values.iter().map(|&v| v - min_value).collect();
151            let mut packed = Vec::new();
152            bitpack_write(&shifted, bpv, &mut packed);
153            writer.write_all(&packed)?;
154            bytes_written += packed.len() as u64;
155        }
156        Ok(bytes_written)
157    }
158}
159
160/// Read a single value from a bitpacked-codec column.
161///
162/// `data` starts right after the codec_id byte (i.e. at min_value).
163#[inline]
164pub fn bitpacked_read(data: &[u8], index: usize) -> u64 {
165    let min_value = u64::from_le_bytes(data[0..8].try_into().unwrap());
166    let bpv = data[8];
167    if bpv == 0 {
168        return min_value;
169    }
170    let packed = &data[9..];
171    bitpack_read(packed, bpv, index) + min_value
172}
173
174// ── Linear codec ─────────────────────────────────────────────────────────
175
176/// Fits y = slope * x + intercept across all values, stores residuals bitpacked.
177///
178/// Header: codec_id(1) + intercept(8) + slope_num(8) + slope_den(8) + bpv(1) + offset(8) = 34
179///
180/// Estimation uses O(1) memory by tracking value extremes during collection and
181/// computing worst-case residual bounds in `finalize()`.
182///
183/// **Limitation**: the per-column offset is stored as i64 (8 bytes). When values
184/// span nearly the full u64 range (e.g. `FAST_FIELD_MISSING` mixed with small
185/// values), residuals can exceed i64 bounds.  The estimator returns `None` in
186/// that case so the auto-selector falls back to bitpacked.
187#[derive(Default)]
188pub struct LinearEstimator {
189    count: usize,
190    first: u64,
191    last: u64,
192    min_val: u64,
193    max_val: u64,
194    min_residual: i64,
195    max_residual: i64,
196    values_collected: bool,
197    /// Set by `finalize()` when residuals exceed i64 range.
198    overflow: bool,
199}
200
201impl CodecEstimator for LinearEstimator {
202    fn collect(&mut self, value: u64) {
203        if !self.values_collected {
204            self.first = value;
205            self.min_val = value;
206            self.max_val = value;
207            self.values_collected = true;
208        } else {
209            self.min_val = self.min_val.min(value);
210            self.max_val = self.max_val.max(value);
211        }
212        self.last = value;
213        self.count += 1;
214    }
215
216    fn finalize(&mut self) {
217        if self.count < 2 {
218            return;
219        }
220        // Compute worst-case residual bounds from value extremes vs predicted line.
221        // The predicted line spans [first, last]. The worst-case residuals occur when
222        // the most extreme value is farthest from the nearest predicted value.
223        // Predicted values range from min(first,last) to max(first,last), so:
224        //   max_residual ≥ max_val - min(predicted) = max_val - min(first, last)
225        //   min_residual ≤ min_val - max(predicted) = min_val - max(first, last)
226        // This is a conservative bound (may slightly overestimate bpv vs exact).
227        let pred_min = self.first.min(self.last) as i128;
228        let pred_max = self.first.max(self.last) as i128;
229        let min_res = self.min_val as i128 - pred_max;
230        let max_res = self.max_val as i128 - pred_min;
231        // The offset is stored as i64 on disk.  If residuals exceed i64 range,
232        // this codec cannot represent the data — mark as overflow.
233        if min_res < i64::MIN as i128 || max_res > i64::MAX as i128 {
234            self.overflow = true;
235            return;
236        }
237        self.min_residual = min_res as i64;
238        self.max_residual = max_res as i64;
239    }
240
241    fn estimate(&self) -> Option<u64> {
242        if self.count < 2 || self.overflow {
243            return None;
244        }
245        // Check for overflow: if the range doesn't fit u64, this codec is not viable
246        let range = (self.max_residual as i128 - self.min_residual as i128) as u64;
247        let bpv = bits_needed_u64(range) as u64;
248        let data_bits = self.count as u64 * bpv;
249        let data_bytes = data_bits.div_ceil(8);
250        // codec_id(1) + first(8) + last(8) + num_values(4) + offset(8) + bpv(1) + packed
251        Some(1 + 8 + 8 + 4 + 8 + 1 + data_bytes)
252    }
253
254    fn serialize(&self, values: &[u64], writer: &mut dyn Write) -> io::Result<u64> {
255        let n = values.len();
256        if n < 2 {
257            return Err(io::Error::new(
258                io::ErrorKind::InvalidInput,
259                "linear needs ≥ 2 values",
260            ));
261        }
262        let first = values[0];
263        let last = values[n - 1];
264
265        // Compute residuals using i128 to avoid overflow
266        let mut min_residual = i128::MAX;
267        for (i, &val) in values.iter().enumerate() {
268            let predicted = interpolate(first, last, n, i);
269            let residual = val as i128 - predicted as i128;
270            min_residual = min_residual.min(residual);
271        }
272
273        // The offset field is i64 on disk — reject data that doesn't fit.
274        if min_residual < i64::MIN as i128 || min_residual > i64::MAX as i128 {
275            return Err(io::Error::new(
276                io::ErrorKind::InvalidInput,
277                "linear codec: residual offset exceeds i64 range",
278            ));
279        }
280        let min_residual_i64 = min_residual as i64;
281
282        // Shift residuals to non-negative
283        let shifted: Vec<u64> = values
284            .iter()
285            .enumerate()
286            .map(|(i, &val)| {
287                let predicted = interpolate(first, last, n, i);
288                let residual = val as i128 - predicted as i128;
289                (residual - min_residual) as u64
290            })
291            .collect();
292        let max_shifted = shifted.iter().copied().max().unwrap_or(0);
293        let bpv = bits_needed_u64(max_shifted);
294        writer.write_u8(CodecType::Linear as u8)?;
295        writer.write_u64::<LittleEndian>(first)?;
296        writer.write_u64::<LittleEndian>(last)?;
297        writer.write_u32::<LittleEndian>(n as u32)?;
298        writer.write_i64::<LittleEndian>(min_residual_i64)?;
299        writer.write_u8(bpv)?;
300        let mut bytes_written = 30u64; // 1+8+8+4+8+1
301
302        if bpv > 0 {
303            let mut packed = Vec::new();
304            bitpack_write(&shifted, bpv, &mut packed);
305            writer.write_all(&packed)?;
306            bytes_written += packed.len() as u64;
307        }
308
309        Ok(bytes_written)
310    }
311}
312
313/// Interpolate value at index `i` on the line from first to last over `n` values.
314#[inline]
315fn interpolate(first: u64, last: u64, n: usize, i: usize) -> u64 {
316    if n <= 1 {
317        return first;
318    }
319    // Use i128 to avoid overflow
320    let first = first as i128;
321    let last = last as i128;
322    let n = n as i128;
323    let i = i as i128;
324    let result = first + (last - first) * i / (n - 1);
325    result as u64
326}
327
328/// Read a single value from a linear-codec column.
329///
330/// `data` starts right after the codec_id byte.
331#[inline]
332pub fn linear_read(data: &[u8], index: usize) -> u64 {
333    let first = u64::from_le_bytes(data[0..8].try_into().unwrap());
334    let last = u64::from_le_bytes(data[8..16].try_into().unwrap());
335    let n = u32::from_le_bytes(data[16..20].try_into().unwrap()) as usize;
336    let offset = i64::from_le_bytes(data[20..28].try_into().unwrap());
337    let bpv = data[28];
338    let predicted = interpolate(first, last, n, index);
339    let residual = if bpv == 0 {
340        0u64
341    } else {
342        bitpack_read(&data[29..], bpv, index)
343    };
344    // Use i128 to avoid overflow with large values
345    (predicted as i128 + offset as i128 + residual as i128) as u64
346}
347
348// ── BlockwiseLinear codec ────────────────────────────────────────────────
349
350/// Per-512-element-block linear interpolation with per-block bitpacked residuals.
351///
352/// Header: codec_id(1) + num_values(4) + num_blocks(4)
353/// Per block: first(8) + last(8) + offset(8) + bpv(1) + packed_len(4) + packed_data
354#[derive(Default)]
355pub struct BlockwiseLinearEstimator {
356    values: Vec<u64>,
357}
358
359impl CodecEstimator for BlockwiseLinearEstimator {
360    fn collect(&mut self, value: u64) {
361        self.values.push(value);
362    }
363
364    fn estimate(&self) -> Option<u64> {
365        let n = self.values.len();
366        if n < 2 * BLOCKWISE_LINEAR_BLOCK_SIZE {
367            // Only useful when there are enough values to amortize the per-block headers
368            return None;
369        }
370
371        let num_blocks = n.div_ceil(BLOCKWISE_LINEAR_BLOCK_SIZE);
372        // Global header
373        let mut total = 9u64; // codec_id(1) + num_values(4) + num_blocks(4)
374
375        for b in 0..num_blocks {
376            let start = b * BLOCKWISE_LINEAR_BLOCK_SIZE;
377            let end = (start + BLOCKWISE_LINEAR_BLOCK_SIZE).min(n);
378            let block = &self.values[start..end];
379            let block_len = block.len();
380
381            if block_len < 2 {
382                // Block header + 0 data
383                total += 29; // first(8)+last(8)+offset(8)+bpv(1)+packed_len(4)
384                continue;
385            }
386
387            let first = block[0];
388            let last = block[block_len - 1];
389            let mut min_res = i128::MAX;
390            let mut max_res = i128::MIN;
391            for (i, &val) in block.iter().enumerate() {
392                let pred = interpolate(first, last, block_len, i);
393                let res = val as i128 - pred as i128;
394                min_res = min_res.min(res);
395                max_res = max_res.max(res);
396            }
397            // Per-block offset is stored as i64 — if any block's residuals
398            // exceed i64 range, this codec cannot represent the data.
399            if min_res < i64::MIN as i128 || max_res > i64::MAX as i128 {
400                return None;
401            }
402            let range = (max_res - min_res) as u64;
403            let bpv = bits_needed_u64(range) as u64;
404            let data_bits = block_len as u64 * bpv;
405            let data_bytes = data_bits.div_ceil(8);
406            total += 29 + data_bytes;
407        }
408
409        Some(total)
410    }
411
412    fn serialize(&self, values: &[u64], writer: &mut dyn Write) -> io::Result<u64> {
413        let n = values.len();
414        let num_blocks = n.div_ceil(BLOCKWISE_LINEAR_BLOCK_SIZE);
415
416        writer.write_u8(CodecType::BlockwiseLinear as u8)?;
417        writer.write_u32::<LittleEndian>(n as u32)?;
418        writer.write_u32::<LittleEndian>(num_blocks as u32)?;
419        let mut bytes_written = 9u64;
420
421        for b in 0..num_blocks {
422            let start = b * BLOCKWISE_LINEAR_BLOCK_SIZE;
423            let end = (start + BLOCKWISE_LINEAR_BLOCK_SIZE).min(n);
424            let block = &values[start..end];
425            let block_len = block.len();
426
427            let first = block[0];
428            let last = if block_len > 1 {
429                block[block_len - 1]
430            } else {
431                first
432            };
433
434            // Compute residuals using i128 to avoid overflow
435            let mut min_residual = i128::MAX;
436            if block_len >= 2 {
437                for (i, &val) in block.iter().enumerate() {
438                    let pred = interpolate(first, last, block_len, i);
439                    let res = val as i128 - pred as i128;
440                    min_residual = min_residual.min(res);
441                }
442            } else {
443                min_residual = 0;
444            }
445
446            let shifted: Vec<u64> = block
447                .iter()
448                .enumerate()
449                .map(|(i, &val)| {
450                    if block_len < 2 {
451                        return 0;
452                    }
453                    let pred = interpolate(first, last, block_len, i);
454                    let res = val as i128 - pred as i128;
455                    (res - min_residual) as u64
456                })
457                .collect();
458            let max_shifted = shifted.iter().copied().max().unwrap_or(0);
459            let bpv = bits_needed_u64(max_shifted);
460
461            // Per-block offset is stored as i64 — reject data that doesn't fit.
462            if min_residual < i64::MIN as i128 || min_residual > i64::MAX as i128 {
463                return Err(io::Error::new(
464                    io::ErrorKind::InvalidInput,
465                    "blockwise linear codec: per-block residual offset exceeds i64 range",
466                ));
467            }
468            let min_res_i64 = min_residual as i64;
469            writer.write_u64::<LittleEndian>(first)?;
470            writer.write_u64::<LittleEndian>(last)?;
471            writer.write_i64::<LittleEndian>(min_res_i64)?;
472            writer.write_u8(bpv)?;
473
474            let mut packed = Vec::new();
475            if bpv > 0 {
476                bitpack_write(&shifted, bpv, &mut packed);
477            }
478            writer.write_u32::<LittleEndian>(packed.len() as u32)?;
479            writer.write_all(&packed)?;
480            bytes_written += 29 + packed.len() as u64;
481        }
482
483        Ok(bytes_written)
484    }
485}
486
487/// Read a single value from a blockwise-linear-codec column.
488///
489/// `data` starts right after the codec_id byte.
490pub fn blockwise_linear_read(data: &[u8], index: usize) -> u64 {
491    let _num_values = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
492    let num_blocks = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize;
493
494    let target_block = index / BLOCKWISE_LINEAR_BLOCK_SIZE;
495    let index_in_block = index % BLOCKWISE_LINEAR_BLOCK_SIZE;
496
497    // Scan block headers to find the right one
498    let mut pos = 8usize;
499    for b in 0..num_blocks {
500        let first = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
501        let last = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
502        let offset = i64::from_le_bytes(data[pos + 16..pos + 24].try_into().unwrap());
503        let bpv = data[pos + 24];
504        let packed_len = u32::from_le_bytes(data[pos + 25..pos + 29].try_into().unwrap()) as usize;
505
506        if b == target_block {
507            let block_start = b * BLOCKWISE_LINEAR_BLOCK_SIZE;
508            let block_end = ((b + 1) * BLOCKWISE_LINEAR_BLOCK_SIZE).min(_num_values);
509            let block_len = block_end - block_start;
510
511            let predicted = interpolate(first, last, block_len, index_in_block);
512            let residual = if bpv == 0 {
513                0u64
514            } else {
515                bitpack_read(&data[pos + 29..], bpv, index_in_block)
516            };
517            return (predicted as i128 + offset as i128 + residual as i128) as u64;
518        }
519
520        pos += 29 + packed_len;
521    }
522
523    0 // Should not reach here
524}
525
526// ── Auto-selection ───────────────────────────────────────────────────────
527
528/// Serialize values using the codec that produces the smallest output.
529///
530/// Returns the number of bytes written.
531pub fn serialize_auto(values: &[u64], writer: &mut dyn Write) -> io::Result<u64> {
532    let mut constant = ConstantEstimator::default();
533    let mut bitpacked = BitpackedEstimator::default();
534    let mut linear = LinearEstimator::default();
535    let mut blockwise = BlockwiseLinearEstimator::default();
536
537    // Pass 1: collect
538    for &v in values {
539        constant.collect(v);
540        bitpacked.collect(v);
541        linear.collect(v);
542        blockwise.collect(v);
543    }
544
545    // Finalize
546    constant.finalize();
547    bitpacked.finalize();
548    linear.finalize();
549    blockwise.finalize();
550
551    // Pick smallest
552    let candidates: Vec<(&dyn CodecEstimator, &str)> = vec![
553        (&constant, "constant"),
554        (&bitpacked, "bitpacked"),
555        (&linear, "linear"),
556        (&blockwise, "blockwise_linear"),
557    ];
558
559    let (best, _name) = candidates
560        .into_iter()
561        .filter_map(|(est, name)| est.estimate().map(|size| (est, name, size)))
562        .min_by_key(|&(_, _, size)| size)
563        .map(|(est, name, _)| (est, name))
564        .unwrap_or((&bitpacked as &dyn CodecEstimator, "bitpacked"));
565
566    best.serialize(values, writer)
567}
568
569/// Batch-read `count` consecutive values starting at `start_index` from bitpacked data.
570///
571/// `data` starts right after the codec_id byte (at min_value).
572/// Uses direct array access for byte-aligned bpv (8, 16, 32, 64) which the
573/// compiler auto-vectorizes to SIMD on aarch64 (NEON) and x86_64 (SSE/AVX).
574/// For arbitrary bpv, uses a tight scalar loop with the u64 fast-path.
575pub fn bitpacked_read_batch(data: &[u8], start_index: usize, out: &mut [u64]) {
576    let min_value = u64::from_le_bytes(data[0..8].try_into().unwrap());
577    let bpv = data[8];
578
579    if bpv == 0 {
580        out.iter_mut().for_each(|v| *v = min_value);
581        return;
582    }
583
584    let packed = &data[9..];
585
586    match bpv {
587        // Byte-aligned fast paths — compiler auto-vectorizes these loops
588        8 => {
589            for (i, v) in out.iter_mut().enumerate() {
590                let idx = start_index + i;
591                *v = packed[idx] as u64 + min_value;
592            }
593        }
594        16 => {
595            for (i, v) in out.iter_mut().enumerate() {
596                let idx = start_index + i;
597                let byte_off = idx * 2;
598                let raw = u16::from_le_bytes([packed[byte_off], packed[byte_off + 1]]);
599                *v = raw as u64 + min_value;
600            }
601        }
602        32 => {
603            for (i, v) in out.iter_mut().enumerate() {
604                let idx = start_index + i;
605                let byte_off = idx * 4;
606                let raw = u32::from_le_bytes(packed[byte_off..byte_off + 4].try_into().unwrap());
607                *v = raw as u64 + min_value;
608            }
609        }
610        64 => {
611            for (i, v) in out.iter_mut().enumerate() {
612                let idx = start_index + i;
613                let byte_off = idx * 8;
614                let raw = u64::from_le_bytes(packed[byte_off..byte_off + 8].try_into().unwrap());
615                *v = raw.wrapping_add(min_value);
616            }
617        }
618        // Arbitrary bpv — tight scalar loop using u64 fast-path read
619        _ => {
620            for (i, v) in out.iter_mut().enumerate() {
621                *v = super::bitpack_read(packed, bpv, start_index + i) + min_value;
622            }
623        }
624    }
625}
626
627/// Batch-read `out.len()` consecutive values starting at `start_index` from auto-codec data.
628///
629/// Dispatches codec type once (vs. per-value in `auto_read`), enabling tight inner
630/// loops that the compiler auto-vectorizes for byte-aligned bitpacked columns.
631pub fn auto_read_batch(data: &[u8], start_index: usize, out: &mut [u64]) {
632    if data.is_empty() || out.is_empty() {
633        out.iter_mut().for_each(|v| *v = 0);
634        return;
635    }
636    let codec_id = data[0];
637    let rest = &data[1..];
638    match CodecType::from_u8(codec_id) {
639        Some(CodecType::Constant) => {
640            let val = u64::from_le_bytes(rest[0..8].try_into().unwrap());
641            out.iter_mut().for_each(|v| *v = val);
642        }
643        Some(CodecType::Bitpacked) => bitpacked_read_batch(rest, start_index, out),
644        Some(CodecType::Linear) => {
645            for (i, v) in out.iter_mut().enumerate() {
646                *v = linear_read(rest, start_index + i);
647            }
648        }
649        Some(CodecType::BlockwiseLinear) => {
650            for (i, v) in out.iter_mut().enumerate() {
651                *v = blockwise_linear_read(rest, start_index + i);
652            }
653        }
654        None => out.iter_mut().for_each(|v| *v = 0),
655    }
656}
657
658/// Read a single value from auto-codec encoded data.
659///
660/// The first byte identifies the codec.
661#[inline]
662pub fn auto_read(data: &[u8], index: usize) -> u64 {
663    if data.is_empty() {
664        return 0;
665    }
666    let codec_id = data[0];
667    let rest = &data[1..];
668    match CodecType::from_u8(codec_id) {
669        Some(CodecType::Constant) => {
670            // rest = value(8)
671            u64::from_le_bytes(rest[0..8].try_into().unwrap())
672        }
673        Some(CodecType::Bitpacked) => bitpacked_read(rest, index),
674        Some(CodecType::Linear) => linear_read(rest, index),
675        Some(CodecType::BlockwiseLinear) => blockwise_linear_read(rest, index),
676        None => 0,
677    }
678}
679
680// ── Tests ────────────────────────────────────────────────────────────────
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685
686    fn roundtrip(values: &[u64]) -> Vec<u64> {
687        let mut buf = Vec::new();
688        serialize_auto(values, &mut buf).unwrap();
689        (0..values.len()).map(|i| auto_read(&buf, i)).collect()
690    }
691
692    #[test]
693    fn test_constant_codec() {
694        let values: Vec<u64> = vec![42; 100];
695        let mut buf = Vec::new();
696        serialize_auto(&values, &mut buf).unwrap();
697        assert_eq!(buf[0], CodecType::Constant as u8);
698        assert_eq!(buf.len(), 9);
699        assert_eq!(roundtrip(&values), values);
700    }
701
702    #[test]
703    fn test_bitpacked_codec() {
704        let values: Vec<u64> = (0..50).map(|i| 1000 + (i % 7) * 13).collect();
705        let result = roundtrip(&values);
706        assert_eq!(result, values);
707    }
708
709    #[test]
710    fn test_linear_codec_sequential() {
711        // Perfectly linear → 0 bpv residuals
712        let values: Vec<u64> = (0..1000).map(|i| 100 + i * 3).collect();
713        let mut buf = Vec::new();
714        serialize_auto(&values, &mut buf).unwrap();
715        // Should pick linear (smaller than bitpacked for sequential data)
716        assert_eq!(roundtrip(&values), values);
717    }
718
719    #[test]
720    fn test_blockwise_linear_codec() {
721        // Two distinct linear segments
722        let mut values: Vec<u64> = Vec::new();
723        for i in 0..1500 {
724            if i < 750 {
725                values.push(100 + i * 2);
726            } else {
727                values.push(5000 + (i - 750) * 5);
728            }
729        }
730        let result = roundtrip(&values);
731        assert_eq!(result, values);
732    }
733
734    #[test]
735    fn test_empty() {
736        let values: Vec<u64> = vec![];
737        let mut buf = Vec::new();
738        serialize_auto(&values, &mut buf).unwrap();
739        assert!(buf.len() <= 10);
740    }
741
742    #[test]
743    fn test_single_value() {
744        let values = vec![999u64];
745        assert_eq!(roundtrip(&values), values);
746    }
747
748    #[test]
749    fn test_two_values() {
750        let values = vec![10u64, 20];
751        assert_eq!(roundtrip(&values), values);
752    }
753
754    #[test]
755    fn test_large_range() {
756        let values = vec![0u64, u64::MAX / 2, u64::MAX];
757        assert_eq!(roundtrip(&values), values);
758    }
759
760    #[test]
761    fn test_timestamps_pick_linear_or_blockwise() {
762        // Simulate timestamps (monotonically increasing with small jitter)
763        let mut values: Vec<u64> = Vec::new();
764        let mut ts = 1_700_000_000u64;
765        for _ in 0..2000 {
766            values.push(ts);
767            ts += 1000 + (ts % 7); // ~1000 with jitter
768        }
769        let result = roundtrip(&values);
770        assert_eq!(result, values);
771    }
772
773    /// Helper: roundtrip via auto_read_batch and compare with per-element auto_read.
774    fn roundtrip_batch(values: &[u64]) {
775        let mut buf = Vec::new();
776        serialize_auto(values, &mut buf).unwrap();
777
778        // Batch read all values
779        let mut batch_out = vec![0u64; values.len()];
780        auto_read_batch(&buf, 0, &mut batch_out);
781        assert_eq!(batch_out, values, "batch read mismatch");
782
783        // Batch read a sub-range
784        if values.len() >= 10 {
785            let start = 3;
786            let count = values.len() - 6;
787            let mut sub = vec![0u64; count];
788            auto_read_batch(&buf, start, &mut sub);
789            assert_eq!(
790                sub,
791                &values[start..start + count],
792                "sub-range batch mismatch"
793            );
794        }
795    }
796
797    #[test]
798    fn test_batch_read_constant() {
799        roundtrip_batch(&vec![42u64; 100]);
800    }
801
802    #[test]
803    fn test_batch_read_bitpacked_8bit() {
804        // Values with range < 256 → 8-bit bpv
805        let values: Vec<u64> = (0..200).map(|i| 1000 + (i % 200)).collect();
806        roundtrip_batch(&values);
807    }
808
809    #[test]
810    fn test_batch_read_bitpacked_16bit() {
811        // Values with range fitting 16 bits
812        let values: Vec<u64> = (0..200).map(|i| 50000 + i * 100).collect();
813        roundtrip_batch(&values);
814    }
815
816    #[test]
817    fn test_batch_read_bitpacked_arbitrary() {
818        // Arbitrary bpv (e.g. 13 bits)
819        let values: Vec<u64> = (0..100).map(|i| 999 + (i * 37) % 8000).collect();
820        roundtrip_batch(&values);
821    }
822
823    #[test]
824    fn test_batch_read_linear() {
825        let values: Vec<u64> = (0..500).map(|i| 100 + i * 3).collect();
826        roundtrip_batch(&values);
827    }
828
829    #[test]
830    fn test_batch_read_blockwise() {
831        let mut values = Vec::new();
832        for i in 0..1500u64 {
833            values.push(if i < 750 {
834                100 + i * 2
835            } else {
836                5000 + (i - 750) * 5
837            });
838        }
839        roundtrip_batch(&values);
840    }
841
842    /// Regression: zigzag-encoded i64 timestamps mixed with FAST_FIELD_MISSING (u64::MAX).
843    /// The linear codec's min_residual clamping to i64 corrupts data when values
844    /// span nearly the full u64 range.
845    #[test]
846    fn test_zigzag_timestamps_with_missing() {
847        use super::super::{FAST_FIELD_MISSING, zigzag_encode};
848
849        // Simulate issued_at column: most docs have timestamps, some are missing
850        let timestamps: Vec<i64> = vec![
851            1724630400, // 2024-08-26
852            1724716800, // 2024-08-27
853            1724803200, // 2024-08-28
854            1700000000, // 2023-11-14
855            1680000000, // 2023-03-28
856            1724630400, // duplicate
857        ];
858
859        // Build values array: zigzag-encoded timestamps + some FAST_FIELD_MISSING gaps
860        let mut values = Vec::new();
861        for (i, &ts) in timestamps.iter().enumerate() {
862            values.push(zigzag_encode(ts));
863            // Insert a missing value after every 2nd doc
864            if i % 2 == 1 {
865                values.push(FAST_FIELD_MISSING);
866            }
867        }
868
869        let result = roundtrip(&values);
870        assert_eq!(
871            result, values,
872            "zigzag timestamps + missing roundtrip failed"
873        );
874    }
875
876    /// Test each codec individually with zigzag-encoded values + FAST_FIELD_MISSING
877    #[test]
878    fn test_codecs_individually_with_zigzag_and_missing() {
879        use super::super::{FAST_FIELD_MISSING, zigzag_encode};
880
881        let values: Vec<u64> = vec![
882            zigzag_encode(1724630400), // 3449260800
883            zigzag_encode(1700000000), // 3400000000
884            FAST_FIELD_MISSING,
885            zigzag_encode(1680000000), // 3360000000
886            zigzag_encode(1724716800), // 3449433600
887            FAST_FIELD_MISSING,
888            zigzag_encode(1724630400), // 3449260800
889            zigzag_encode(0),          // 0
890        ];
891
892        // Test bitpacked directly
893        {
894            let mut est = BitpackedEstimator::default();
895            for &v in &values {
896                est.collect(v);
897            }
898            est.finalize();
899            if est.estimate().is_some() {
900                let mut buf = Vec::new();
901                est.serialize(&values, &mut buf).unwrap();
902                for (i, &expected) in values.iter().enumerate() {
903                    let got = auto_read(&buf, i);
904                    assert_eq!(
905                        got, expected,
906                        "bitpacked: index {} expected {} got {}",
907                        i, expected, got
908                    );
909                }
910            }
911        }
912
913        // Test linear directly (needs ≥ 2 values)
914        {
915            let mut est = LinearEstimator::default();
916            for &v in &values {
917                est.collect(v);
918            }
919            est.finalize();
920            if est.estimate().is_some() {
921                let mut buf = Vec::new();
922                est.serialize(&values, &mut buf).unwrap();
923                for (i, &expected) in values.iter().enumerate() {
924                    let got = auto_read(&buf, i);
925                    assert_eq!(
926                        got, expected,
927                        "linear: index {} expected {} got {}",
928                        i, expected, got
929                    );
930                }
931            }
932        }
933
934        // Test auto (whichever is selected)
935        let result = roundtrip(&values);
936        assert_eq!(result, values, "auto codec roundtrip failed");
937    }
938
939    /// Regression: value that the user observed corrupted in production
940    #[test]
941    fn test_specific_issued_at_roundtrip() {
942        use super::super::{FAST_FIELD_MISSING, zigzag_encode};
943
944        // Reproduce exact scenario: 100 docs, mix of timestamps and missing
945        let mut values = Vec::new();
946        let base_ts = 1724630400i64; // 2024-08-26 epoch
947        for i in 0..100u64 {
948            if i % 5 == 0 {
949                // Every 5th doc has no issued_at
950                values.push(FAST_FIELD_MISSING);
951            } else {
952                // Varying timestamps
953                let ts = base_ts - (i as i64 * 86400); // one day apart
954                values.push(zigzag_encode(ts));
955            }
956        }
957
958        let result = roundtrip(&values);
959        for (i, (&expected, &got)) in values.iter().zip(result.iter()).enumerate() {
960            assert_eq!(
961                got,
962                expected,
963                "doc {}: expected {} (zigzag of {}), got {}",
964                i,
965                expected,
966                if expected == FAST_FIELD_MISSING {
967                    -1 // placeholder
968                } else {
969                    super::super::zigzag_decode(expected)
970                },
971                got
972            );
973        }
974    }
975
976    /// Large-scale test: exercise blockwise linear codec with realistic timestamp data.
977    /// Tests 10K, 50K, 100K docs to catch codec edge cases.
978    #[test]
979    fn test_large_scale_timestamp_roundtrip() {
980        use super::super::{FAST_FIELD_MISSING, zigzag_encode};
981
982        for num_docs in [10_000, 50_000, 100_000] {
983            let mut values = Vec::with_capacity(num_docs);
984            let base_ts = 1724630400i64;
985
986            for i in 0..num_docs {
987                if i % 7 == 0 {
988                    values.push(FAST_FIELD_MISSING);
989                } else {
990                    // Timestamps spanning ~5 years, with some jitter
991                    let ts = base_ts - (i as i64 * 3600) + ((i as i64 * 37) % 1000);
992                    values.push(zigzag_encode(ts));
993                }
994            }
995
996            // Check which codec is selected
997            let mut buf = Vec::new();
998            serialize_auto(&values, &mut buf).unwrap();
999            let codec_id = buf[0];
1000            let codec_name = match CodecType::from_u8(codec_id) {
1001                Some(CodecType::Constant) => "constant",
1002                Some(CodecType::Bitpacked) => "bitpacked",
1003                Some(CodecType::Linear) => "linear",
1004                Some(CodecType::BlockwiseLinear) => "blockwise_linear",
1005                None => "unknown",
1006            };
1007
1008            // Verify roundtrip
1009            let mut failures = Vec::new();
1010            for (i, &expected) in values.iter().enumerate() {
1011                let got = auto_read(&buf, i);
1012                if got != expected {
1013                    failures.push((i, expected, got));
1014                    if failures.len() >= 5 {
1015                        break;
1016                    }
1017                }
1018            }
1019
1020            assert!(
1021                failures.is_empty(),
1022                "num_docs={}, codec={}: {} failures. First 5: {:?}",
1023                num_docs,
1024                codec_name,
1025                failures.len(),
1026                failures
1027            );
1028        }
1029    }
1030
1031    /// Regression: blockwise linear codec selected for column where most blocks
1032    /// are efficient (sorted timestamps only) but a few blocks contain
1033    /// FAST_FIELD_MISSING, causing min_residual clamping corruption.
1034    #[test]
1035    fn test_blockwise_linear_with_clustered_missing() {
1036        use super::super::{FAST_FIELD_MISSING, zigzag_encode};
1037
1038        // 3000 values: first 512 are all FAST_FIELD_MISSING,
1039        // remaining 2488 are sorted timestamps (efficient linear blocks).
1040        // This should trigger blockwise linear selection overall,
1041        // but the first block has a mix that triggers the bug.
1042        let mut values = Vec::new();
1043
1044        // Block 0 (indices 0-511): mix of MISSING and timestamps
1045        // — first 100 are MISSING, rest are timestamps
1046        for i in 0..512 {
1047            if i < 100 {
1048                values.push(FAST_FIELD_MISSING);
1049            } else {
1050                let ts = 1724630400i64 + (i as i64 * 100);
1051                values.push(zigzag_encode(ts));
1052            }
1053        }
1054
1055        // Blocks 1-5 (indices 512-3071): sorted timestamps only
1056        for i in 512..3072 {
1057            let ts = 1724630400i64 + (i as i64 * 100);
1058            values.push(zigzag_encode(ts));
1059        }
1060
1061        let result = roundtrip(&values);
1062        let mut failures = Vec::new();
1063        for (i, (&expected, &got)) in values.iter().zip(result.iter()).enumerate() {
1064            if got != expected {
1065                failures.push((i, expected, got));
1066            }
1067        }
1068        assert!(
1069            failures.is_empty(),
1070            "blockwise linear with clustered missing: {} failures. First 5: {:?}",
1071            failures.len(),
1072            &failures[..failures.len().min(5)]
1073        );
1074    }
1075
1076    /// Test each codec FORCED with zigzag timestamps + FAST_FIELD_MISSING.
1077    /// This catches bugs that only manifest when a specific codec is forced.
1078    #[test]
1079    fn test_forced_codecs_with_timestamps_and_missing() {
1080        use super::super::{FAST_FIELD_MISSING, zigzag_encode};
1081
1082        let mut values = Vec::new();
1083        let base_ts = 1724630400i64;
1084        for i in 0..200 {
1085            if i % 5 == 0 {
1086                values.push(FAST_FIELD_MISSING);
1087            } else {
1088                let ts = base_ts - (i as i64 * 86400);
1089                values.push(zigzag_encode(ts));
1090            }
1091        }
1092
1093        // Force bitpacked
1094        {
1095            let est = BitpackedEstimator::default();
1096            let mut buf = Vec::new();
1097            est.serialize(&values, &mut buf).unwrap();
1098            for (i, &expected) in values.iter().enumerate() {
1099                let got = bitpacked_read(&buf[1..], i); // skip codec_id byte
1100                assert_eq!(got, expected, "forced bitpacked: index {} failed", i);
1101            }
1102        }
1103
1104        // Force linear — should error because FAST_FIELD_MISSING + timestamps
1105        // produce residuals exceeding i64 range
1106        {
1107            let est = LinearEstimator::default();
1108            let mut buf = Vec::new();
1109            let result = est.serialize(&values, &mut buf);
1110            assert!(
1111                result.is_err(),
1112                "linear codec should reject data with residuals exceeding i64"
1113            );
1114        }
1115
1116        // Force linear with values that DO fit in i64 (no FAST_FIELD_MISSING)
1117        {
1118            let safe_values: Vec<u64> = values
1119                .iter()
1120                .filter(|&&v| v != FAST_FIELD_MISSING)
1121                .copied()
1122                .collect();
1123            let est = LinearEstimator::default();
1124            let mut buf = Vec::new();
1125            est.serialize(&safe_values, &mut buf).unwrap();
1126            for (i, &expected) in safe_values.iter().enumerate() {
1127                let got = linear_read(&buf[1..], i);
1128                assert_eq!(got, expected, "forced linear (safe): index {} failed", i);
1129            }
1130        }
1131
1132        // Blockwise linear estimator should return None for data with FAST_FIELD_MISSING
1133        {
1134            let mut large_values = Vec::new();
1135            for i in 0..2000 {
1136                if i % 5 == 0 {
1137                    large_values.push(FAST_FIELD_MISSING);
1138                } else {
1139                    let ts = base_ts - (i as i64 * 86400);
1140                    large_values.push(zigzag_encode(ts));
1141                }
1142            }
1143            let mut est = BlockwiseLinearEstimator::default();
1144            for &v in &large_values {
1145                est.collect(v);
1146            }
1147            assert!(
1148                est.estimate().is_none(),
1149                "blockwise linear should reject data with per-block residuals exceeding i64"
1150            );
1151        }
1152    }
1153}