Skip to main content

summa_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/// Conservative raw-value interval derived only from an admitted codec header.
46/// A wrapping interval or a codec without cheap bounds returns `None`.
47pub(super) fn value_bounds(data: &[u8]) -> Option<(u64, u64)> {
48    let (&tag, data) = data.split_first()?;
49    let width_max = |bits: u8| u64::MAX.checked_shr(u32::from(64 - bits)).unwrap_or(0);
50    match CodecType::from_u8(tag)? {
51        CodecType::Constant => {
52            let value = u64::from_le_bytes(data[..8].try_into().unwrap());
53            Some((value, value))
54        }
55        CodecType::Bitpacked => {
56            let min = u64::from_le_bytes(data[..8].try_into().unwrap());
57            Some((min, min.checked_add(width_max(data[8]))?))
58        }
59        CodecType::Linear => {
60            let first = u64::from_le_bytes(data[..8].try_into().unwrap());
61            let last = u64::from_le_bytes(data[8..16].try_into().unwrap());
62            let offset = i64::from_le_bytes(data[20..28].try_into().unwrap()) as i128;
63            let min = i128::from(first.min(last)) + offset;
64            let max = i128::from(first.max(last)) + offset + i128::from(width_max(data[28]));
65            Some((u64::try_from(min).ok()?, u64::try_from(max).ok()?))
66        }
67        CodecType::BlockwiseLinear => None,
68    }
69}
70
71/// Validate an auto-codec payload before exposing it through the infallible
72/// hot-path readers below. This keeps every bounds check out of per-document
73/// access while ensuring corrupt segment metadata cannot trigger slice panics.
74pub fn validate_auto(data: &[u8], expected_values: usize) -> io::Result<()> {
75    let (&codec_id, rest) = data.split_first().ok_or_else(|| {
76        io::Error::new(io::ErrorKind::UnexpectedEof, "fast field codec is missing")
77    })?;
78
79    let packed_len = |count: usize, bpv: u8| -> io::Result<usize> {
80        if bpv > 64 {
81            return Err(io::Error::new(
82                io::ErrorKind::InvalidData,
83                "fast field bit width exceeds 64",
84            ));
85        }
86        count
87            .checked_mul(bpv as usize)
88            .and_then(|bits| bits.checked_add(7))
89            .map(|bits| bits / 8)
90            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "fast field size overflow"))
91    };
92
93    match CodecType::from_u8(codec_id) {
94        Some(CodecType::Constant) => {
95            if rest.len() != 8 {
96                return Err(io::Error::new(
97                    io::ErrorKind::InvalidData,
98                    "invalid constant fast field length",
99                ));
100            }
101        }
102        Some(CodecType::Bitpacked) => {
103            if rest.len() < 9 {
104                return Err(io::Error::new(
105                    io::ErrorKind::UnexpectedEof,
106                    "bitpacked fast field header is truncated",
107                ));
108            }
109            let expected_len = 9usize
110                .checked_add(packed_len(expected_values, rest[8])?)
111                .ok_or_else(|| {
112                    io::Error::new(io::ErrorKind::InvalidData, "fast field size overflow")
113                })?;
114            if rest.len() != expected_len {
115                return Err(io::Error::new(
116                    io::ErrorKind::InvalidData,
117                    "bitpacked fast field length is inconsistent",
118                ));
119            }
120        }
121        Some(CodecType::Linear) => {
122            if rest.len() < 29 {
123                return Err(io::Error::new(
124                    io::ErrorKind::UnexpectedEof,
125                    "linear fast field header is truncated",
126                ));
127            }
128            let count = u32::from_le_bytes(rest[16..20].try_into().unwrap()) as usize;
129            if count != expected_values || count < 2 {
130                return Err(io::Error::new(
131                    io::ErrorKind::InvalidData,
132                    "linear fast field value count is inconsistent",
133                ));
134            }
135            let expected_len = 29usize
136                .checked_add(packed_len(count, rest[28])?)
137                .ok_or_else(|| {
138                    io::Error::new(io::ErrorKind::InvalidData, "fast field size overflow")
139                })?;
140            if rest.len() != expected_len {
141                return Err(io::Error::new(
142                    io::ErrorKind::InvalidData,
143                    "linear fast field length is inconsistent",
144                ));
145            }
146        }
147        Some(CodecType::BlockwiseLinear) => {
148            if rest.len() < 8 {
149                return Err(io::Error::new(
150                    io::ErrorKind::UnexpectedEof,
151                    "blockwise fast field header is truncated",
152                ));
153            }
154            let count = u32::from_le_bytes(rest[0..4].try_into().unwrap()) as usize;
155            let num_blocks = u32::from_le_bytes(rest[4..8].try_into().unwrap()) as usize;
156            if count != expected_values || num_blocks != count.div_ceil(BLOCKWISE_LINEAR_BLOCK_SIZE)
157            {
158                return Err(io::Error::new(
159                    io::ErrorKind::InvalidData,
160                    "blockwise fast field counts are inconsistent",
161                ));
162            }
163
164            let mut pos = 8usize;
165            for block_idx in 0..num_blocks {
166                let header_end = pos.checked_add(29).ok_or_else(|| {
167                    io::Error::new(io::ErrorKind::InvalidData, "fast field offset overflow")
168                })?;
169                if header_end > rest.len() {
170                    return Err(io::Error::new(
171                        io::ErrorKind::UnexpectedEof,
172                        "blockwise fast field block header is truncated",
173                    ));
174                }
175                let bpv = rest[pos + 24];
176                let declared =
177                    u32::from_le_bytes(rest[pos + 25..header_end].try_into().unwrap()) as usize;
178                let block_start = block_idx * BLOCKWISE_LINEAR_BLOCK_SIZE;
179                let block_count = (count - block_start).min(BLOCKWISE_LINEAR_BLOCK_SIZE);
180                let expected = packed_len(block_count, bpv)?;
181                if declared != expected {
182                    return Err(io::Error::new(
183                        io::ErrorKind::InvalidData,
184                        "blockwise fast field packed length is inconsistent",
185                    ));
186                }
187                pos = header_end.checked_add(declared).ok_or_else(|| {
188                    io::Error::new(io::ErrorKind::InvalidData, "fast field offset overflow")
189                })?;
190                if pos > rest.len() {
191                    return Err(io::Error::new(
192                        io::ErrorKind::UnexpectedEof,
193                        "blockwise fast field data is truncated",
194                    ));
195                }
196            }
197            if pos != rest.len() {
198                return Err(io::Error::new(
199                    io::ErrorKind::InvalidData,
200                    "blockwise fast field contains trailing data",
201                ));
202            }
203        }
204        None => {
205            return Err(io::Error::new(
206                io::ErrorKind::InvalidData,
207                "unknown fast field codec",
208            ));
209        }
210    }
211    Ok(())
212}
213
214// ── Estimator trait ──────────────────────────────────────────────────────
215
216/// Estimates serialized size for a given codec.
217///
218/// Usage: call `collect(val)` for every value, then `finalize()`,
219/// then `estimate()` returns the byte count.
220pub trait CodecEstimator {
221    fn collect(&mut self, value: u64);
222    fn finalize(&mut self) {}
223    fn estimate(&self) -> Option<u64>;
224    fn serialize(&self, values: &[u64], writer: &mut dyn Write) -> io::Result<u64>;
225}
226
227// ── Constant codec ───────────────────────────────────────────────────────
228
229/// All values are identical → zero data bytes. Value stored in the codec header.
230#[derive(Default)]
231pub struct ConstantEstimator {
232    first: Option<u64>,
233    all_same: bool,
234}
235
236impl CodecEstimator for ConstantEstimator {
237    fn collect(&mut self, value: u64) {
238        match self.first {
239            None => {
240                self.first = Some(value);
241                self.all_same = true;
242            }
243            Some(f) => {
244                if value != f {
245                    self.all_same = false;
246                }
247            }
248        }
249    }
250
251    fn estimate(&self) -> Option<u64> {
252        if self.all_same {
253            // codec_id(1) + value(8) = 9 bytes
254            Some(9)
255        } else {
256            None
257        }
258    }
259
260    fn serialize(&self, values: &[u64], writer: &mut dyn Write) -> io::Result<u64> {
261        let val = if values.is_empty() { 0 } else { values[0] };
262        writer.write_u8(CodecType::Constant as u8)?;
263        writer.write_u64::<LittleEndian>(val)?;
264        Ok(9)
265    }
266}
267
268// ── Bitpacked codec ──────────────────────────────────────────────────────
269
270/// Min-subtract + global bitpack. This is the existing codec, now behind a tag.
271#[derive(Default)]
272pub struct BitpackedEstimator {
273    min: u64,
274    max: u64,
275    count: usize,
276    initialized: bool,
277}
278
279impl CodecEstimator for BitpackedEstimator {
280    fn collect(&mut self, value: u64) {
281        if !self.initialized {
282            self.min = value;
283            self.max = value;
284            self.initialized = true;
285        } else {
286            self.min = self.min.min(value);
287            self.max = self.max.max(value);
288        }
289        self.count += 1;
290    }
291
292    fn estimate(&self) -> Option<u64> {
293        if self.count == 0 {
294            return Some(0);
295        }
296        let range = self.max - self.min;
297        let bpv = bits_needed_u64(range) as u64;
298        // codec_id(1) + min(8) + bpv(1) + packed data
299        let data_bits = self.count as u64 * bpv;
300        let data_bytes = data_bits.div_ceil(8);
301        Some(1 + 8 + 1 + data_bytes)
302    }
303
304    fn serialize(&self, values: &[u64], writer: &mut dyn Write) -> io::Result<u64> {
305        let (min_value, bpv) = if values.is_empty() {
306            (0u64, 0u8)
307        } else {
308            let min_val = values.iter().copied().min().unwrap();
309            let max_val = values.iter().copied().max().unwrap();
310            (min_val, bits_needed_u64(max_val - min_val))
311        };
312
313        writer.write_u8(CodecType::Bitpacked as u8)?;
314        writer.write_u64::<LittleEndian>(min_value)?;
315        writer.write_u8(bpv)?;
316        let mut bytes_written = 10u64; // 1 + 8 + 1
317
318        if bpv > 0 && !values.is_empty() {
319            let shifted: Vec<u64> = values.iter().map(|&v| v - min_value).collect();
320            let mut packed = Vec::new();
321            bitpack_write(&shifted, bpv, &mut packed);
322            writer.write_all(&packed)?;
323            bytes_written += packed.len() as u64;
324        }
325        Ok(bytes_written)
326    }
327}
328
329/// Read a single value from a bitpacked-codec column.
330///
331/// `data` starts right after the codec_id byte (i.e. at min_value).
332#[inline]
333pub fn bitpacked_read(data: &[u8], index: usize) -> u64 {
334    let min_value = u64::from_le_bytes(data[0..8].try_into().unwrap());
335    let bpv = data[8];
336    if bpv == 0 {
337        return min_value;
338    }
339    let packed = &data[9..];
340    bitpack_read(packed, bpv, index).wrapping_add(min_value)
341}
342
343// ── Linear codec ─────────────────────────────────────────────────────────
344
345/// Fits y = slope * x + intercept across all values, stores residuals bitpacked.
346///
347/// Header: codec_id(1) + intercept(8) + slope_num(8) + slope_den(8) + bpv(1) + offset(8) = 34
348///
349/// Estimation uses O(1) memory by tracking value extremes during collection and
350/// computing worst-case residual bounds in `finalize()`.
351///
352/// **Limitation**: the per-column offset is stored as i64 (8 bytes). When values
353/// span nearly the full u64 range (e.g. `FAST_FIELD_MISSING` mixed with small
354/// values), residuals can exceed i64 bounds.  The estimator returns `None` in
355/// that case so the auto-selector falls back to bitpacked.
356#[derive(Default)]
357pub struct LinearEstimator {
358    count: usize,
359    first: u64,
360    last: u64,
361    min_val: u64,
362    max_val: u64,
363    min_residual: i64,
364    max_residual: i64,
365    values_collected: bool,
366    /// Set by `finalize()` when residuals exceed i64 range.
367    overflow: bool,
368}
369
370impl CodecEstimator for LinearEstimator {
371    fn collect(&mut self, value: u64) {
372        if !self.values_collected {
373            self.first = value;
374            self.min_val = value;
375            self.max_val = value;
376            self.values_collected = true;
377        } else {
378            self.min_val = self.min_val.min(value);
379            self.max_val = self.max_val.max(value);
380        }
381        self.last = value;
382        self.count += 1;
383    }
384
385    fn finalize(&mut self) {
386        if self.count < 2 {
387            return;
388        }
389        // Compute worst-case residual bounds from value extremes vs predicted line.
390        // The predicted line spans [first, last]. The worst-case residuals occur when
391        // the most extreme value is farthest from the nearest predicted value.
392        // Predicted values range from min(first,last) to max(first,last), so:
393        //   max_residual ≥ max_val - min(predicted) = max_val - min(first, last)
394        //   min_residual ≤ min_val - max(predicted) = min_val - max(first, last)
395        // This is a conservative bound (may slightly overestimate bpv vs exact).
396        let pred_min = self.first.min(self.last) as i128;
397        let pred_max = self.first.max(self.last) as i128;
398        let min_res = self.min_val as i128 - pred_max;
399        let max_res = self.max_val as i128 - pred_min;
400        // The offset is stored as i64 on disk.  If residuals exceed i64 range,
401        // this codec cannot represent the data — mark as overflow.
402        if min_res < i64::MIN as i128 || max_res > i64::MAX as i128 {
403            self.overflow = true;
404            return;
405        }
406        self.min_residual = min_res as i64;
407        self.max_residual = max_res as i64;
408    }
409
410    fn estimate(&self) -> Option<u64> {
411        if self.count < 2 || self.overflow {
412            return None;
413        }
414        // Check for overflow: if the range doesn't fit u64, this codec is not viable
415        let range = (self.max_residual as i128 - self.min_residual as i128) as u64;
416        let bpv = bits_needed_u64(range) as u64;
417        let data_bits = self.count as u64 * bpv;
418        let data_bytes = data_bits.div_ceil(8);
419        // codec_id(1) + first(8) + last(8) + num_values(4) + offset(8) + bpv(1) + packed
420        Some(1 + 8 + 8 + 4 + 8 + 1 + data_bytes)
421    }
422
423    fn serialize(&self, values: &[u64], writer: &mut dyn Write) -> io::Result<u64> {
424        let n = values.len();
425        if n < 2 {
426            return Err(io::Error::new(
427                io::ErrorKind::InvalidInput,
428                "linear needs ≥ 2 values",
429            ));
430        }
431        let first = values[0];
432        let last = values[n - 1];
433
434        // Compute residuals using i128 to avoid overflow
435        let mut min_residual = i128::MAX;
436        for (i, &val) in values.iter().enumerate() {
437            let predicted = interpolate(first, last, n, i);
438            let residual = val as i128 - predicted as i128;
439            min_residual = min_residual.min(residual);
440        }
441
442        // The offset field is i64 on disk — reject data that doesn't fit.
443        if min_residual < i64::MIN as i128 || min_residual > i64::MAX as i128 {
444            return Err(io::Error::new(
445                io::ErrorKind::InvalidInput,
446                "linear codec: residual offset exceeds i64 range",
447            ));
448        }
449        let min_residual_i64 = min_residual as i64;
450
451        // Shift residuals to non-negative
452        let shifted: Vec<u64> = values
453            .iter()
454            .enumerate()
455            .map(|(i, &val)| {
456                let predicted = interpolate(first, last, n, i);
457                let residual = val as i128 - predicted as i128;
458                (residual - min_residual) as u64
459            })
460            .collect();
461        let max_shifted = shifted.iter().copied().max().unwrap_or(0);
462        let bpv = bits_needed_u64(max_shifted);
463        writer.write_u8(CodecType::Linear as u8)?;
464        writer.write_u64::<LittleEndian>(first)?;
465        writer.write_u64::<LittleEndian>(last)?;
466        writer.write_u32::<LittleEndian>(n as u32)?;
467        writer.write_i64::<LittleEndian>(min_residual_i64)?;
468        writer.write_u8(bpv)?;
469        let mut bytes_written = 30u64; // 1+8+8+4+8+1
470
471        if bpv > 0 {
472            let mut packed = Vec::new();
473            bitpack_write(&shifted, bpv, &mut packed);
474            writer.write_all(&packed)?;
475            bytes_written += packed.len() as u64;
476        }
477
478        Ok(bytes_written)
479    }
480}
481
482/// Interpolate value at index `i` on the line from first to last over `n` values.
483#[inline]
484fn interpolate(first: u64, last: u64, n: usize, i: usize) -> u64 {
485    if n <= 1 {
486        return first;
487    }
488    // Use i128 to avoid overflow
489    let first = first as i128;
490    let last = last as i128;
491    let n = n as i128;
492    let i = i as i128;
493    let result = first + (last - first) * i / (n - 1);
494    result as u64
495}
496
497/// Read a single value from a linear-codec column.
498///
499/// `data` starts right after the codec_id byte.
500#[inline]
501pub fn linear_read(data: &[u8], index: usize) -> u64 {
502    let first = u64::from_le_bytes(data[0..8].try_into().unwrap());
503    let last = u64::from_le_bytes(data[8..16].try_into().unwrap());
504    let n = u32::from_le_bytes(data[16..20].try_into().unwrap()) as usize;
505    let offset = i64::from_le_bytes(data[20..28].try_into().unwrap());
506    let bpv = data[28];
507    let predicted = interpolate(first, last, n, index);
508    let residual = if bpv == 0 {
509        0u64
510    } else {
511        bitpack_read(&data[29..], bpv, index)
512    };
513    // Use i128 to avoid overflow with large values
514    (predicted as i128 + offset as i128 + residual as i128) as u64
515}
516
517// ── BlockwiseLinear codec ────────────────────────────────────────────────
518
519/// Per-512-element-block linear interpolation with per-block bitpacked residuals.
520///
521/// Header: codec_id(1) + num_values(4) + num_blocks(4)
522/// Per block: first(8) + last(8) + offset(8) + bpv(1) + packed_len(4) + packed_data
523#[derive(Clone, Copy)]
524struct BlockwiseLinearBlockEstimate {
525    min_residual: i64,
526    bits_per_value: u8,
527    serialized_size: u64,
528}
529
530#[derive(Default)]
531pub struct BlockwiseLinearEstimator {
532    count: usize,
533    completed_size: u64,
534    current_block: Vec<u64>,
535    completed_blocks: Vec<BlockwiseLinearBlockEstimate>,
536    tail_estimate: Option<BlockwiseLinearBlockEstimate>,
537    overflow: bool,
538}
539
540impl BlockwiseLinearEstimator {
541    /// Collect a complete column without retaining it.
542    ///
543    /// `serialize_auto` already owns the input slice, so full blocks can be
544    /// estimated in place. Keeping this pass separate from the other
545    /// per-value estimators also leaves their hot loop branch-free.
546    fn collect_values(&mut self, values: &[u64]) {
547        debug_assert_eq!(self.count, 0);
548        debug_assert!(self.current_block.is_empty());
549
550        self.count = values.len();
551        let mut blocks = values.chunks_exact(BLOCKWISE_LINEAR_BLOCK_SIZE);
552        for block in &mut blocks {
553            match estimate_blockwise_linear_block(block) {
554                Some(estimate) => {
555                    self.completed_size += estimate.serialized_size;
556                    self.completed_blocks.push(estimate);
557                }
558                None => {
559                    self.overflow = true;
560                    return;
561                }
562            }
563        }
564        self.current_block.extend_from_slice(blocks.remainder());
565    }
566}
567
568/// Return the serialized size of one block, excluding the global header.
569///
570/// A block is buffered until its final value is known because that value is
571/// part of the interpolation line. Keeping only this bounded scratch block
572/// avoids retaining a second copy of the entire column during auto-selection.
573fn estimate_blockwise_linear_block(block: &[u64]) -> Option<BlockwiseLinearBlockEstimate> {
574    debug_assert!(!block.is_empty());
575    let block_len = block.len();
576    if block_len < 2 {
577        return Some(BlockwiseLinearBlockEstimate {
578            min_residual: 0,
579            bits_per_value: 0,
580            serialized_size: 29,
581        });
582    }
583
584    let first = block[0];
585    let last = block[block_len - 1];
586    let mut min_res = i128::MAX;
587    let mut max_res = i128::MIN;
588    for (i, &val) in block.iter().enumerate() {
589        let pred = interpolate(first, last, block_len, i);
590        let res = val as i128 - pred as i128;
591        min_res = min_res.min(res);
592        max_res = max_res.max(res);
593    }
594
595    // Per-block offset is stored as i64. If either residual bound cannot be
596    // represented, the codec cannot encode this column.
597    if min_res < i64::MIN as i128 || max_res > i64::MAX as i128 {
598        return None;
599    }
600
601    let bits_per_value = bits_needed_u64((max_res - min_res) as u64);
602    let data_bytes = (block_len as u64 * u64::from(bits_per_value)).div_ceil(8);
603    Some(BlockwiseLinearBlockEstimate {
604        min_residual: min_res as i64,
605        bits_per_value,
606        serialized_size: 29 + data_bytes,
607    })
608}
609
610impl CodecEstimator for BlockwiseLinearEstimator {
611    fn collect(&mut self, value: u64) {
612        self.count += 1;
613        self.tail_estimate = None;
614        if self.overflow {
615            return;
616        }
617
618        self.current_block.push(value);
619        if self.current_block.len() == BLOCKWISE_LINEAR_BLOCK_SIZE {
620            match estimate_blockwise_linear_block(&self.current_block) {
621                Some(estimate) => {
622                    self.completed_size += estimate.serialized_size;
623                    self.completed_blocks.push(estimate);
624                }
625                None => self.overflow = true,
626            }
627            self.current_block.clear();
628        }
629    }
630
631    fn finalize(&mut self) {
632        self.tail_estimate = if self.current_block.is_empty() || self.overflow {
633            None
634        } else {
635            estimate_blockwise_linear_block(&self.current_block)
636        };
637        if !self.current_block.is_empty() && self.tail_estimate.is_none() {
638            self.overflow = true;
639        }
640    }
641
642    fn estimate(&self) -> Option<u64> {
643        if self.count < 2 * BLOCKWISE_LINEAR_BLOCK_SIZE || self.overflow {
644            // Only useful when there are enough values to amortize the per-block headers
645            return None;
646        }
647
648        // codec_id(1) + num_values(4) + num_blocks(4)
649        let mut total = 9 + self.completed_size;
650        if !self.current_block.is_empty() {
651            total += self
652                .tail_estimate
653                .or_else(|| estimate_blockwise_linear_block(&self.current_block))?
654                .serialized_size;
655        }
656        Some(total)
657    }
658
659    fn serialize(&self, values: &[u64], writer: &mut dyn Write) -> io::Result<u64> {
660        let n = values.len();
661        let num_blocks = n.div_ceil(BLOCKWISE_LINEAR_BLOCK_SIZE);
662
663        writer.write_u8(CodecType::BlockwiseLinear as u8)?;
664        writer.write_u32::<LittleEndian>(n as u32)?;
665        writer.write_u32::<LittleEndian>(num_blocks as u32)?;
666        let mut bytes_written = 9u64;
667
668        // Both scratch buffers are bounded by one block and reused across all
669        // blocks, avoiding one allocation pair per block.
670        let mut shifted = Vec::new();
671        let mut packed = Vec::new();
672        let estimates_match = self.count == n
673            && self.completed_blocks.len() == n / BLOCKWISE_LINEAR_BLOCK_SIZE
674            && (n.is_multiple_of(BLOCKWISE_LINEAR_BLOCK_SIZE) || self.tail_estimate.is_some());
675
676        for b in 0..num_blocks {
677            let start = b * BLOCKWISE_LINEAR_BLOCK_SIZE;
678            let end = (start + BLOCKWISE_LINEAR_BLOCK_SIZE).min(n);
679            let block = &values[start..end];
680            let block_len = block.len();
681
682            let first = block[0];
683            let last = if block_len > 1 {
684                block[block_len - 1]
685            } else {
686                first
687            };
688
689            let estimate = if estimates_match {
690                self.completed_blocks.get(b).copied().or(self.tail_estimate)
691            } else {
692                None
693            };
694            let estimate = match estimate {
695                Some(estimate) => estimate,
696                None => estimate_blockwise_linear_block(block).ok_or_else(|| {
697                    io::Error::new(
698                        io::ErrorKind::InvalidInput,
699                        "blockwise linear codec: per-block residual offset exceeds i64 range",
700                    )
701                })?,
702            };
703            let min_residual = i128::from(estimate.min_residual);
704
705            shifted.clear();
706            shifted.extend(block.iter().enumerate().map(|(i, &val)| {
707                if block_len < 2 {
708                    0
709                } else {
710                    let pred = interpolate(first, last, block_len, i);
711                    let res = val as i128 - pred as i128;
712                    (res - min_residual) as u64
713                }
714            }));
715            writer.write_u64::<LittleEndian>(first)?;
716            writer.write_u64::<LittleEndian>(last)?;
717            writer.write_i64::<LittleEndian>(estimate.min_residual)?;
718            writer.write_u8(estimate.bits_per_value)?;
719
720            packed.clear();
721            if estimate.bits_per_value > 0 {
722                bitpack_write(&shifted, estimate.bits_per_value, &mut packed);
723            }
724            writer.write_u32::<LittleEndian>(packed.len() as u32)?;
725            writer.write_all(&packed)?;
726            bytes_written += 29 + packed.len() as u64;
727        }
728
729        Ok(bytes_written)
730    }
731}
732
733/// Read a single value from a blockwise-linear-codec column.
734///
735/// `data` starts right after the codec_id byte.
736pub fn blockwise_linear_read(data: &[u8], index: usize) -> u64 {
737    blockwise_linear_read_from(data, index, 0, 8)
738}
739
740/// Resume from a validated header checkpoint (offset excludes the codec byte).
741pub(super) fn blockwise_linear_read_from(
742    data: &[u8],
743    index: usize,
744    first_block: usize,
745    mut pos: usize,
746) -> u64 {
747    let _num_values = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
748    let num_blocks = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize;
749    let target_block = index / BLOCKWISE_LINEAR_BLOCK_SIZE;
750    let index_in_block = index % BLOCKWISE_LINEAR_BLOCK_SIZE;
751    for b in first_block..num_blocks {
752        let first = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
753        let last = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
754        let offset = i64::from_le_bytes(data[pos + 16..pos + 24].try_into().unwrap());
755        let bpv = data[pos + 24];
756        let packed_len = u32::from_le_bytes(data[pos + 25..pos + 29].try_into().unwrap()) as usize;
757
758        if b == target_block {
759            let block_start = b * BLOCKWISE_LINEAR_BLOCK_SIZE;
760            let block_end = ((b + 1) * BLOCKWISE_LINEAR_BLOCK_SIZE).min(_num_values);
761            let block_len = block_end - block_start;
762
763            let predicted = interpolate(first, last, block_len, index_in_block);
764            let residual = if bpv == 0 {
765                0u64
766            } else {
767                bitpack_read(&data[pos + 29..], bpv, index_in_block)
768            };
769            return (predicted as i128 + offset as i128 + residual as i128) as u64;
770        }
771
772        pos += 29 + packed_len;
773    }
774
775    0 // Should not reach here
776}
777
778/// Batch-read consecutive values from a blockwise-linear column.
779///
780/// Variable-length block records are scanned once to reach `start_index`, then
781/// consumed in order. This avoids re-scanning every preceding block header for
782/// each value in the batch.
783pub fn blockwise_linear_read_batch(data: &[u8], start_index: usize, out: &mut [u64]) {
784    blockwise_linear_read_batch_with_cursor(
785        data,
786        start_index,
787        out,
788        &mut BlockwiseLinearCursor::default(),
789    );
790}
791
792/// Position in one admitted column payload. Reuse only for the same payload;
793/// copied column blocks each start with a fresh cursor. No payload is retained.
794pub(super) struct BlockwiseLinearCursor {
795    block: usize,
796    offset: usize,
797}
798
799impl Default for BlockwiseLinearCursor {
800    fn default() -> Self {
801        Self {
802            block: 0,
803            offset: 8,
804        }
805    }
806}
807
808fn blockwise_linear_read_batch_with_cursor(
809    data: &[u8],
810    start_index: usize,
811    out: &mut [u64],
812    cursor: &mut BlockwiseLinearCursor,
813) {
814    if out.is_empty() {
815        return;
816    }
817
818    let num_values = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
819    let num_blocks = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize;
820    let valid_len = out.len().min(num_values.saturating_sub(start_index));
821    out[valid_len..].fill(0);
822    if valid_len == 0 {
823        return;
824    }
825
826    let target_block = start_index / BLOCKWISE_LINEAR_BLOCK_SIZE;
827    if target_block < cursor.block {
828        *cursor = BlockwiseLinearCursor::default();
829    }
830    let mut pos = cursor.offset;
831    let mut written = 0usize;
832
833    for block_idx in cursor.block..num_blocks {
834        let packed_len = u32::from_le_bytes(data[pos + 25..pos + 29].try_into().unwrap()) as usize;
835        if block_idx < target_block {
836            pos += 29 + packed_len;
837            continue;
838        }
839
840        let first = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
841        let last = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
842        let offset = i64::from_le_bytes(data[pos + 16..pos + 24].try_into().unwrap());
843        let bpv = data[pos + 24];
844        let packed = &data[pos + 29..pos + 29 + packed_len];
845
846        let block_start = block_idx * BLOCKWISE_LINEAR_BLOCK_SIZE;
847        let block_len = (num_values - block_start).min(BLOCKWISE_LINEAR_BLOCK_SIZE);
848        let index_in_block = if block_idx == target_block {
849            start_index - block_start
850        } else {
851            0
852        };
853        let take = (block_len - index_in_block).min(valid_len - written);
854
855        for (i, value) in out[written..written + take].iter_mut().enumerate() {
856            let block_index = index_in_block + i;
857            let predicted = interpolate(first, last, block_len, block_index);
858            let residual = if bpv == 0 {
859                0
860            } else {
861                bitpack_read(packed, bpv, block_index)
862            };
863            *value = (predicted as i128 + offset as i128 + residual as i128) as u64;
864        }
865
866        written += take;
867        if index_in_block + take == block_len {
868            cursor.block = block_idx + 1;
869            cursor.offset = pos + 29 + packed_len;
870        } else {
871            cursor.block = block_idx;
872            cursor.offset = pos;
873        }
874        if written == valid_len {
875            break;
876        }
877        pos += 29 + packed_len;
878    }
879}
880
881// ── Auto-selection ───────────────────────────────────────────────────────
882
883/// Serialize values using the codec that produces the smallest output.
884///
885/// Returns the number of bytes written.
886pub fn serialize_auto(values: &[u64], writer: &mut dyn Write) -> io::Result<u64> {
887    let mut constant = ConstantEstimator::default();
888    let mut bitpacked = BitpackedEstimator::default();
889    let mut linear = LinearEstimator::default();
890    let mut blockwise = BlockwiseLinearEstimator::default();
891
892    // Pass 1: collect
893    for &v in values {
894        constant.collect(v);
895        bitpacked.collect(v);
896        linear.collect(v);
897    }
898    blockwise.collect_values(values);
899
900    // Finalize
901    constant.finalize();
902    bitpacked.finalize();
903    linear.finalize();
904    blockwise.finalize();
905
906    // Pick smallest
907    let candidates: Vec<(&dyn CodecEstimator, &str)> = vec![
908        (&constant, "constant"),
909        (&bitpacked, "bitpacked"),
910        (&linear, "linear"),
911        (&blockwise, "blockwise_linear"),
912    ];
913
914    let (best, _name) = candidates
915        .into_iter()
916        .filter_map(|(est, name)| est.estimate().map(|size| (est, name, size)))
917        .min_by_key(|&(_, _, size)| size)
918        .map(|(est, name, _)| (est, name))
919        .unwrap_or((&bitpacked as &dyn CodecEstimator, "bitpacked"));
920
921    best.serialize(values, writer)
922}
923
924/// Batch-read `out.len()` consecutive values starting at `start_index` from bitpacked data.
925///
926/// `data` starts right after the codec_id byte (at min_value).
927/// Byte-aligned bpv (8, 16, 32, 64) use fixed-width chunks with upfront range
928/// checks to enable auto-vectorization; inspect the target/profile's codegen.
929/// For arbitrary bpv, uses a tight scalar loop with the u64 fast-path.
930pub fn bitpacked_read_batch(data: &[u8], start_index: usize, out: &mut [u64]) {
931    let min_value = u64::from_le_bytes(data[0..8].try_into().unwrap());
932    let bpv = data[8];
933
934    if bpv == 0 {
935        out.iter_mut().for_each(|v| *v = min_value);
936        return;
937    }
938
939    let packed = &data[9..];
940
941    match bpv {
942        // Prove the whole byte range once so the inner loop has no per-value
943        // input bounds checks. Generic decode closures inline into each width.
944        8 => {
945            decode_byte_aligned_batch::<1>(packed, start_index, out, min_value, |v| u64::from(v[0]))
946        }
947        16 => decode_byte_aligned_batch::<2>(packed, start_index, out, min_value, |v| {
948            u64::from(u16::from_le_bytes(v))
949        }),
950        32 => decode_byte_aligned_batch::<4>(packed, start_index, out, min_value, |v| {
951            u64::from(u32::from_le_bytes(v))
952        }),
953        64 => {
954            decode_byte_aligned_batch::<8>(packed, start_index, out, min_value, u64::from_le_bytes)
955        }
956        // Arbitrary bpv — tight scalar loop using u64 fast-path read
957        _ => {
958            for (i, v) in out.iter_mut().enumerate() {
959                *v = super::bitpack_read(packed, bpv, start_index + i).wrapping_add(min_value);
960            }
961        }
962    }
963}
964
965/// Select exactly one input chunk per output before entering the decode loop.
966/// The upfront slice checks also prevent zip from silently accepting short data.
967#[inline]
968fn decode_byte_aligned_batch<const WIDTH: usize>(
969    packed: &[u8],
970    start: usize,
971    out: &mut [u64],
972    min: u64,
973    decode: impl Fn([u8; WIDTH]) -> u64,
974) {
975    if out.is_empty() {
976        return;
977    }
978    let byte_start = start.checked_mul(WIDTH).expect("bitpacked start overflow");
979    let byte_len = out
980        .len()
981        .checked_mul(WIDTH)
982        .expect("bitpacked length overflow");
983    let bytes = &packed[byte_start..][..byte_len];
984    let (chunks, _) = bytes.as_chunks::<WIDTH>();
985    for (value, &chunk) in out.iter_mut().zip(chunks) {
986        *value = decode(chunk).wrapping_add(min);
987    }
988}
989
990/// Batch-read `out.len()` consecutive values starting at `start_index` from auto-codec data.
991///
992/// Dispatches codec type once (vs. per-value in `auto_read`), enabling tight inner
993/// loops that the compiler auto-vectorizes for byte-aligned bitpacked columns.
994pub fn auto_read_batch(data: &[u8], start_index: usize, out: &mut [u64]) {
995    auto_read_batch_with_cursor(
996        data,
997        start_index,
998        out,
999        &mut BlockwiseLinearCursor::default(),
1000    );
1001}
1002
1003/// Batch decode with an optional sequential advantage for BlockwiseLinear.
1004/// The caller must reset the cursor when switching column payloads.
1005pub(super) fn auto_read_batch_with_cursor(
1006    data: &[u8],
1007    start_index: usize,
1008    out: &mut [u64],
1009    cursor: &mut BlockwiseLinearCursor,
1010) {
1011    if data.is_empty() || out.is_empty() {
1012        out.iter_mut().for_each(|v| *v = 0);
1013        return;
1014    }
1015    let codec_id = data[0];
1016    let rest = &data[1..];
1017    match CodecType::from_u8(codec_id) {
1018        Some(CodecType::Constant) => {
1019            let val = u64::from_le_bytes(rest[0..8].try_into().unwrap());
1020            out.iter_mut().for_each(|v| *v = val);
1021        }
1022        Some(CodecType::Bitpacked) => bitpacked_read_batch(rest, start_index, out),
1023        Some(CodecType::Linear) => {
1024            for (i, v) in out.iter_mut().enumerate() {
1025                *v = linear_read(rest, start_index + i);
1026            }
1027        }
1028        Some(CodecType::BlockwiseLinear) => {
1029            blockwise_linear_read_batch_with_cursor(rest, start_index, out, cursor)
1030        }
1031        None => out.iter_mut().for_each(|v| *v = 0),
1032    }
1033}
1034
1035/// Read a single value from auto-codec encoded data.
1036///
1037/// The first byte identifies the codec.
1038#[inline]
1039pub fn auto_read(data: &[u8], index: usize) -> u64 {
1040    if data.is_empty() {
1041        return 0;
1042    }
1043    let codec_id = data[0];
1044    let rest = &data[1..];
1045    match CodecType::from_u8(codec_id) {
1046        Some(CodecType::Constant) => {
1047            // rest = value(8)
1048            u64::from_le_bytes(rest[0..8].try_into().unwrap())
1049        }
1050        Some(CodecType::Bitpacked) => bitpacked_read(rest, index),
1051        Some(CodecType::Linear) => linear_read(rest, index),
1052        Some(CodecType::BlockwiseLinear) => blockwise_linear_read(rest, index),
1053        None => 0,
1054    }
1055}
1056
1057// ── Tests ────────────────────────────────────────────────────────────────
1058
1059#[cfg(test)]
1060mod tests {
1061    use super::*;
1062
1063    #[test]
1064    fn cursor_batches_preserve_all_codecs_boundaries_and_backward_reads() {
1065        let values = blockwise_values(2053);
1066        let mut constant = Vec::new();
1067        ConstantEstimator::default()
1068            .serialize(&[42; 2053], &mut constant)
1069            .unwrap();
1070        let mut bitpacked = Vec::new();
1071        let mut estimator = BitpackedEstimator::default();
1072        for &value in &values {
1073            estimator.collect(value);
1074        }
1075        estimator.finalize();
1076        estimator.serialize(&values, &mut bitpacked).unwrap();
1077        let descending: Vec<_> = (0..2053).map(|i| u64::MAX - i * 7).collect();
1078        let mut linear = Vec::new();
1079        let mut estimator = LinearEstimator::default();
1080        for &value in &descending {
1081            estimator.collect(value);
1082        }
1083        estimator.finalize();
1084        estimator.serialize(&descending, &mut linear).unwrap();
1085        let mut blockwise = Vec::new();
1086        BlockwiseLinearEstimator::default()
1087            .serialize(&values, &mut blockwise)
1088            .unwrap();
1089        assert_eq!(blockwise, serialize_blockwise_reference(&values));
1090        for (tag, encoded) in [constant, bitpacked, linear, blockwise].iter().enumerate() {
1091            assert_eq!(usize::from(encoded[0]), tag);
1092            validate_auto(encoded, 2053).unwrap();
1093            let expected: Vec<_> = (0..2053).map(|i| auto_read(encoded, i)).collect();
1094            for size in [1, 255, 256, 257, 511, 512, 513, 1000, 2053] {
1095                let mut cursor = BlockwiseLinearCursor::default();
1096                let mut actual = vec![0; 2053];
1097                for (batch, out) in actual.chunks_mut(size).enumerate() {
1098                    auto_read_batch_with_cursor(encoded, batch * size, &mut [], &mut cursor);
1099                    auto_read_batch_with_cursor(encoded, batch * size, out, &mut cursor);
1100                }
1101                assert_eq!(actual, expected, "codec {tag}, batch {size}");
1102                for start in [1023, 511, 0, 2048] {
1103                    let mut out = [0; 5];
1104                    auto_read_batch_with_cursor(encoded, start, &mut out, &mut cursor);
1105                    assert_eq!(out, expected[start..start + 5]);
1106                }
1107                if tag == CodecType::BlockwiseLinear as usize {
1108                    let mut out = [u64::MAX; 17];
1109                    auto_read_batch_with_cursor(encoded, 2050, &mut out, &mut cursor);
1110                    assert_eq!(&out[..3], &expected[2050..]);
1111                    assert_eq!(&out[3..], &[0; 14]);
1112                    auto_read_batch_with_cursor(encoded, usize::MAX, &mut out, &mut cursor);
1113                    assert_eq!(out, [0; 17]);
1114                }
1115            }
1116        }
1117    }
1118
1119    #[test]
1120    fn header_bounds_contain_every_bitpacked_value_including_wrapping_payloads() {
1121        for bits in 0..=64 {
1122            let max = u64::MAX.checked_shr(64 - bits).unwrap_or(0);
1123            for min in [0u64, 17, u64::MAX - 17, u64::MAX] {
1124                let raw = [0, max / 2, max];
1125                let mut encoded = vec![CodecType::Bitpacked as u8];
1126                encoded.extend_from_slice(&min.to_le_bytes());
1127                encoded.push(bits as u8);
1128                bitpack_write(&raw, bits as u8, &mut encoded);
1129                validate_auto(&encoded, raw.len()).unwrap();
1130                let bounds = value_bounds(&encoded);
1131                if min.checked_add(max).is_none() {
1132                    assert_eq!(bounds, None, "wrapping bounds must not prune");
1133                } else {
1134                    assert_eq!(bounds, Some((min, min + max)));
1135                }
1136                for i in 0..raw.len() {
1137                    let value = auto_read(&encoded, i);
1138                    assert!(bounds.is_none_or(|(lo, hi)| value >= lo && value <= hi));
1139                }
1140            }
1141        }
1142    }
1143
1144    #[test]
1145    fn linear_header_bounds_preserve_descending_extreme_and_wrapping_values() {
1146        for first in [0u64, 100, u64::MAX - 100] {
1147            for last in [0u64, 100, u64::MAX - 100] {
1148                for offset in [i64::MIN, -17, 0, 17, i64::MAX] {
1149                    for bits in [0u8, 1, 4, 64] {
1150                        let max = u64::MAX.checked_shr(u32::from(64 - bits)).unwrap_or(0);
1151                        let raw: Vec<_> =
1152                            (0..17).map(|i| if i % 2 == 0 { max } else { 0 }).collect();
1153                        let mut encoded = vec![CodecType::Linear as u8];
1154                        encoded.extend_from_slice(&first.to_le_bytes());
1155                        encoded.extend_from_slice(&last.to_le_bytes());
1156                        encoded.extend_from_slice(&17u32.to_le_bytes());
1157                        encoded.extend_from_slice(&offset.to_le_bytes());
1158                        encoded.push(bits);
1159                        bitpack_write(&raw, bits, &mut encoded);
1160                        validate_auto(&encoded, raw.len()).unwrap();
1161                        let bounds = value_bounds(&encoded);
1162                        for i in 0..raw.len() {
1163                            let value = auto_read(&encoded, i);
1164                            assert!(bounds.is_none_or(|(lo, hi)| value >= lo && value <= hi));
1165                        }
1166                    }
1167                }
1168            }
1169        }
1170        let encoded = [CodecType::Constant as u8]
1171            .into_iter()
1172            .chain(u64::MAX.to_le_bytes())
1173            .collect::<Vec<_>>();
1174        assert_eq!(value_bounds(&encoded), Some((u64::MAX, u64::MAX)));
1175    }
1176
1177    fn roundtrip(values: &[u64]) -> Vec<u64> {
1178        let mut buf = Vec::new();
1179        serialize_auto(values, &mut buf).unwrap();
1180        (0..values.len()).map(|i| auto_read(&buf, i)).collect()
1181    }
1182
1183    fn blockwise_values(len: usize) -> Vec<u64> {
1184        (0..len)
1185            .map(|i| {
1186                let block = i / BLOCKWISE_LINEAR_BLOCK_SIZE;
1187                let index = i % BLOCKWISE_LINEAR_BLOCK_SIZE;
1188                block as u64 * 1_000_000
1189                    + index as u64 * (block as u64 + 3)
1190                    + ((i * 17 + block * 11) % 23) as u64
1191            })
1192            .collect()
1193    }
1194
1195    /// Reference implementation matching the original allocation-per-block
1196    /// serializer. This guards the on-disk representation while the production
1197    /// implementation reuses bounded scratch buffers.
1198    fn serialize_blockwise_reference(values: &[u64]) -> Vec<u8> {
1199        let n = values.len();
1200        let num_blocks = n.div_ceil(BLOCKWISE_LINEAR_BLOCK_SIZE);
1201        let mut encoded = Vec::new();
1202        encoded.push(CodecType::BlockwiseLinear as u8);
1203        encoded.extend_from_slice(&(n as u32).to_le_bytes());
1204        encoded.extend_from_slice(&(num_blocks as u32).to_le_bytes());
1205
1206        for block in values.chunks(BLOCKWISE_LINEAR_BLOCK_SIZE) {
1207            let block_len = block.len();
1208            let first = block[0];
1209            let last = if block_len > 1 {
1210                block[block_len - 1]
1211            } else {
1212                first
1213            };
1214            let min_residual = if block_len < 2 {
1215                0
1216            } else {
1217                block
1218                    .iter()
1219                    .enumerate()
1220                    .map(|(i, &value)| {
1221                        value as i128 - interpolate(first, last, block_len, i) as i128
1222                    })
1223                    .min()
1224                    .unwrap()
1225            };
1226            let shifted: Vec<u64> = block
1227                .iter()
1228                .enumerate()
1229                .map(|(i, &value)| {
1230                    if block_len < 2 {
1231                        0
1232                    } else {
1233                        let predicted = interpolate(first, last, block_len, i);
1234                        (value as i128 - predicted as i128 - min_residual) as u64
1235                    }
1236                })
1237                .collect();
1238            let bpv = bits_needed_u64(shifted.iter().copied().max().unwrap_or(0));
1239            let mut packed = Vec::new();
1240            bitpack_write(&shifted, bpv, &mut packed);
1241
1242            encoded.extend_from_slice(&first.to_le_bytes());
1243            encoded.extend_from_slice(&last.to_le_bytes());
1244            encoded.extend_from_slice(&(min_residual as i64).to_le_bytes());
1245            encoded.push(bpv);
1246            encoded.extend_from_slice(&(packed.len() as u32).to_le_bytes());
1247            encoded.extend_from_slice(&packed);
1248        }
1249
1250        encoded
1251    }
1252
1253    #[test]
1254    fn test_constant_codec() {
1255        let values: Vec<u64> = vec![42; 100];
1256        let mut buf = Vec::new();
1257        serialize_auto(&values, &mut buf).unwrap();
1258        assert_eq!(buf[0], CodecType::Constant as u8);
1259        assert_eq!(buf.len(), 9);
1260        assert_eq!(roundtrip(&values), values);
1261    }
1262
1263    #[test]
1264    fn test_bitpacked_codec() {
1265        let values: Vec<u64> = (0..50).map(|i| 1000 + (i % 7) * 13).collect();
1266        let result = roundtrip(&values);
1267        assert_eq!(result, values);
1268    }
1269
1270    #[test]
1271    fn test_linear_codec_sequential() {
1272        // Perfectly linear → 0 bpv residuals
1273        let values: Vec<u64> = (0..1000).map(|i| 100 + i * 3).collect();
1274        let mut buf = Vec::new();
1275        serialize_auto(&values, &mut buf).unwrap();
1276        // Should pick linear (smaller than bitpacked for sequential data)
1277        assert_eq!(roundtrip(&values), values);
1278    }
1279
1280    #[test]
1281    fn test_blockwise_linear_codec() {
1282        // Two distinct linear segments
1283        let mut values: Vec<u64> = Vec::new();
1284        for i in 0..1500 {
1285            if i < 750 {
1286                values.push(100 + i * 2);
1287            } else {
1288                values.push(5000 + (i - 750) * 5);
1289            }
1290        }
1291        let result = roundtrip(&values);
1292        assert_eq!(result, values);
1293    }
1294
1295    #[test]
1296    fn test_blockwise_estimate_is_bounded_and_serialization_is_byte_compatible() {
1297        for len in [1024, 1025, 1536, 1537, 4097] {
1298            let values = blockwise_values(len);
1299            let reference = serialize_blockwise_reference(&values);
1300            let mut estimator = BlockwiseLinearEstimator::default();
1301            for &value in &values {
1302                estimator.collect(value);
1303            }
1304            estimator.finalize();
1305
1306            assert_eq!(estimator.estimate(), Some(reference.len() as u64));
1307            assert!(
1308                estimator.current_block.len() < BLOCKWISE_LINEAR_BLOCK_SIZE,
1309                "estimator retained more than one partial block"
1310            );
1311            assert!(
1312                estimator.current_block.capacity() <= BLOCKWISE_LINEAR_BLOCK_SIZE,
1313                "estimator scratch grew beyond one block"
1314            );
1315
1316            let mut encoded = Vec::new();
1317            estimator.serialize(&values, &mut encoded).unwrap();
1318            assert_eq!(
1319                encoded, reference,
1320                "serialized bytes changed for {len} values"
1321            );
1322        }
1323    }
1324
1325    #[test]
1326    fn test_empty() {
1327        let values: Vec<u64> = vec![];
1328        let mut buf = Vec::new();
1329        serialize_auto(&values, &mut buf).unwrap();
1330        assert!(buf.len() <= 10);
1331    }
1332
1333    #[test]
1334    fn test_validate_rejects_truncated_and_inconsistent_payloads() {
1335        assert!(validate_auto(&[], 1).is_err());
1336        assert!(validate_auto(&[CodecType::Constant as u8], 1).is_err());
1337
1338        let mut bitpacked = vec![CodecType::Bitpacked as u8];
1339        bitpacked.extend_from_slice(&0u64.to_le_bytes());
1340        bitpacked.push(65);
1341        assert!(validate_auto(&bitpacked, 1).is_err());
1342
1343        let mut valid = Vec::new();
1344        serialize_auto(&[1, 2, 3, 4], &mut valid).unwrap();
1345        assert!(validate_auto(&valid, 4).is_ok());
1346        valid.pop();
1347        assert!(validate_auto(&valid, 4).is_err());
1348    }
1349
1350    #[test]
1351    fn test_single_value() {
1352        let values = vec![999u64];
1353        assert_eq!(roundtrip(&values), values);
1354    }
1355
1356    #[test]
1357    fn test_two_values() {
1358        let values = vec![10u64, 20];
1359        assert_eq!(roundtrip(&values), values);
1360    }
1361
1362    #[test]
1363    fn test_large_range() {
1364        let values = vec![0u64, u64::MAX / 2, u64::MAX];
1365        assert_eq!(roundtrip(&values), values);
1366    }
1367
1368    #[test]
1369    fn test_timestamps_pick_linear_or_blockwise() {
1370        // Simulate timestamps (monotonically increasing with small jitter)
1371        let mut values: Vec<u64> = Vec::new();
1372        let mut ts = 1_700_000_000u64;
1373        for _ in 0..2000 {
1374            values.push(ts);
1375            ts += 1000 + (ts % 7); // ~1000 with jitter
1376        }
1377        let result = roundtrip(&values);
1378        assert_eq!(result, values);
1379    }
1380
1381    /// Helper: roundtrip via auto_read_batch and compare with per-element auto_read.
1382    fn roundtrip_batch(values: &[u64]) {
1383        let mut buf = Vec::new();
1384        serialize_auto(values, &mut buf).unwrap();
1385
1386        // Batch read all values
1387        let mut batch_out = vec![0u64; values.len()];
1388        auto_read_batch(&buf, 0, &mut batch_out);
1389        assert_eq!(batch_out, values, "batch read mismatch");
1390
1391        // Batch read a sub-range
1392        if values.len() >= 10 {
1393            let start = 3;
1394            let count = values.len() - 6;
1395            let mut sub = vec![0u64; count];
1396            auto_read_batch(&buf, start, &mut sub);
1397            assert_eq!(
1398                sub,
1399                &values[start..start + count],
1400                "sub-range batch mismatch"
1401            );
1402        }
1403    }
1404
1405    #[test]
1406    fn test_batch_read_constant() {
1407        roundtrip_batch(&vec![42u64; 100]);
1408    }
1409
1410    #[test]
1411    fn test_batch_read_bitpacked_8bit() {
1412        // Values with range < 256 → 8-bit bpv
1413        let values: Vec<u64> = (0..200).map(|i| 1000 + (i % 200)).collect();
1414        roundtrip_batch(&values);
1415    }
1416
1417    #[test]
1418    fn test_batch_read_bitpacked_16bit() {
1419        // Values with range fitting 16 bits
1420        let values: Vec<u64> = (0..200).map(|i| 50000 + i * 100).collect();
1421        roundtrip_batch(&values);
1422    }
1423
1424    #[test]
1425    fn test_batch_read_bitpacked_arbitrary() {
1426        // Arbitrary bpv (e.g. 13 bits)
1427        let values: Vec<u64> = (0..100).map(|i| 999 + (i * 37) % 8000).collect();
1428        roundtrip_batch(&values);
1429    }
1430
1431    #[test]
1432    fn byte_aligned_batches_preserve_offsets_tails_and_wrapping_values() {
1433        for bpv in [8u8, 16, 32, 64] {
1434            let min = u64::MAX - 11;
1435            let mut data = min.to_le_bytes().to_vec();
1436            data.push(bpv);
1437            let mut expected = Vec::new();
1438            for i in 0..521u64 {
1439                let raw = i.wrapping_mul(0x9e37_79b9_7f4a_7c15) >> (64 - bpv);
1440                data.extend_from_slice(&raw.to_le_bytes()[..usize::from(bpv / 8)]);
1441                expected.push(raw.wrapping_add(min));
1442            }
1443            for start in [0, 1, 3, 255, 256, 519, 521] {
1444                for len in [0, 1, 2, 7, 16, 255, 256, 257] {
1445                    if start + len > expected.len() {
1446                        continue;
1447                    }
1448                    let mut out = vec![0; len];
1449                    bitpacked_read_batch(&data, start, &mut out);
1450                    assert_eq!(
1451                        out,
1452                        expected[start..start + len],
1453                        "bpv={bpv}, start={start}, len={len}"
1454                    );
1455                }
1456            }
1457            // Empty output performs no payload access, even with a large start.
1458            bitpacked_read_batch(&data, usize::MAX, &mut []);
1459        }
1460    }
1461
1462    #[test]
1463    fn byte_aligned_batches_reject_truncated_input_instead_of_short_decoding() {
1464        for bpv in [8u8, 16, 32, 64] {
1465            let mut data = 0u64.to_le_bytes().to_vec();
1466            data.push(bpv);
1467            data.resize(9 + usize::from(bpv / 8) * 3 - 1, 0);
1468            assert!(
1469                std::panic::catch_unwind(|| {
1470                    bitpacked_read_batch(&data, 0, &mut [0u64; 3]);
1471                })
1472                .is_err()
1473            );
1474        }
1475    }
1476
1477    #[test]
1478    fn test_batch_read_linear() {
1479        let values: Vec<u64> = (0..500).map(|i| 100 + i * 3).collect();
1480        roundtrip_batch(&values);
1481    }
1482
1483    #[test]
1484    fn test_batch_read_blockwise() {
1485        let mut values = Vec::new();
1486        for i in 0..1500u64 {
1487            values.push(if i < 750 {
1488                100 + i * 2
1489            } else {
1490                5000 + (i - 750) * 5
1491            });
1492        }
1493        roundtrip_batch(&values);
1494    }
1495
1496    #[test]
1497    fn test_blockwise_batch_read_across_block_boundaries() {
1498        let values = blockwise_values(2053);
1499        let estimator = BlockwiseLinearEstimator::default();
1500        let mut encoded = Vec::new();
1501        estimator.serialize(&values, &mut encoded).unwrap();
1502        assert_eq!(encoded[0], CodecType::BlockwiseLinear as u8);
1503
1504        for (start, len) in [
1505            (0, values.len()),
1506            (510, 5),
1507            (511, 3),
1508            (512, 513),
1509            (777, 900),
1510            (1023, 514),
1511            (1535, 518),
1512            (2048, 5),
1513        ] {
1514            let expected = &values[start..start + len];
1515
1516            let mut direct = vec![u64::MAX; len];
1517            blockwise_linear_read_batch(&encoded[1..], start, &mut direct);
1518            assert_eq!(direct, expected, "direct batch mismatch at {start}");
1519
1520            let mut auto = vec![u64::MAX; len];
1521            auto_read_batch(&encoded, start, &mut auto);
1522            assert_eq!(auto, expected, "auto batch mismatch at {start}");
1523        }
1524    }
1525
1526    /// Regression: zigzag-encoded i64 timestamps mixed with FAST_FIELD_MISSING (u64::MAX).
1527    /// The linear codec's min_residual clamping to i64 corrupts data when values
1528    /// span nearly the full u64 range.
1529    #[test]
1530    fn test_zigzag_timestamps_with_missing() {
1531        use super::super::{FAST_FIELD_MISSING, zigzag_encode};
1532
1533        // Simulate issued_at column: most docs have timestamps, some are missing
1534        let timestamps: Vec<i64> = vec![
1535            1724630400, // 2024-08-26
1536            1724716800, // 2024-08-27
1537            1724803200, // 2024-08-28
1538            1700000000, // 2023-11-14
1539            1680000000, // 2023-03-28
1540            1724630400, // duplicate
1541        ];
1542
1543        // Build values array: zigzag-encoded timestamps + some FAST_FIELD_MISSING gaps
1544        let mut values = Vec::new();
1545        for (i, &ts) in timestamps.iter().enumerate() {
1546            values.push(zigzag_encode(ts));
1547            // Insert a missing value after every 2nd doc
1548            if i % 2 == 1 {
1549                values.push(FAST_FIELD_MISSING);
1550            }
1551        }
1552
1553        let result = roundtrip(&values);
1554        assert_eq!(
1555            result, values,
1556            "zigzag timestamps + missing roundtrip failed"
1557        );
1558    }
1559
1560    /// Test each codec individually with zigzag-encoded values + FAST_FIELD_MISSING
1561    #[test]
1562    fn test_codecs_individually_with_zigzag_and_missing() {
1563        use super::super::{FAST_FIELD_MISSING, zigzag_encode};
1564
1565        let values: Vec<u64> = vec![
1566            zigzag_encode(1724630400), // 3449260800
1567            zigzag_encode(1700000000), // 3400000000
1568            FAST_FIELD_MISSING,
1569            zigzag_encode(1680000000), // 3360000000
1570            zigzag_encode(1724716800), // 3449433600
1571            FAST_FIELD_MISSING,
1572            zigzag_encode(1724630400), // 3449260800
1573            zigzag_encode(0),          // 0
1574        ];
1575
1576        // Test bitpacked directly
1577        {
1578            let mut est = BitpackedEstimator::default();
1579            for &v in &values {
1580                est.collect(v);
1581            }
1582            est.finalize();
1583            if est.estimate().is_some() {
1584                let mut buf = Vec::new();
1585                est.serialize(&values, &mut buf).unwrap();
1586                for (i, &expected) in values.iter().enumerate() {
1587                    let got = auto_read(&buf, i);
1588                    assert_eq!(
1589                        got, expected,
1590                        "bitpacked: index {} expected {} got {}",
1591                        i, expected, got
1592                    );
1593                }
1594            }
1595        }
1596
1597        // Test linear directly (needs ≥ 2 values)
1598        {
1599            let mut est = LinearEstimator::default();
1600            for &v in &values {
1601                est.collect(v);
1602            }
1603            est.finalize();
1604            if est.estimate().is_some() {
1605                let mut buf = Vec::new();
1606                est.serialize(&values, &mut buf).unwrap();
1607                for (i, &expected) in values.iter().enumerate() {
1608                    let got = auto_read(&buf, i);
1609                    assert_eq!(
1610                        got, expected,
1611                        "linear: index {} expected {} got {}",
1612                        i, expected, got
1613                    );
1614                }
1615            }
1616        }
1617
1618        // Test auto (whichever is selected)
1619        let result = roundtrip(&values);
1620        assert_eq!(result, values, "auto codec roundtrip failed");
1621    }
1622
1623    /// Regression: value that the user observed corrupted in production
1624    #[test]
1625    fn test_specific_issued_at_roundtrip() {
1626        use super::super::{FAST_FIELD_MISSING, zigzag_encode};
1627
1628        // Reproduce exact scenario: 100 docs, mix of timestamps and missing
1629        let mut values = Vec::new();
1630        let base_ts = 1724630400i64; // 2024-08-26 epoch
1631        for i in 0..100u64 {
1632            if i % 5 == 0 {
1633                // Every 5th doc has no issued_at
1634                values.push(FAST_FIELD_MISSING);
1635            } else {
1636                // Varying timestamps
1637                let ts = base_ts - (i as i64 * 86400); // one day apart
1638                values.push(zigzag_encode(ts));
1639            }
1640        }
1641
1642        let result = roundtrip(&values);
1643        for (i, (&expected, &got)) in values.iter().zip(result.iter()).enumerate() {
1644            assert_eq!(
1645                got,
1646                expected,
1647                "doc {}: expected {} (zigzag of {}), got {}",
1648                i,
1649                expected,
1650                if expected == FAST_FIELD_MISSING {
1651                    -1 // placeholder
1652                } else {
1653                    super::super::zigzag_decode(expected)
1654                },
1655                got
1656            );
1657        }
1658    }
1659
1660    /// Large-scale test: exercise blockwise linear codec with realistic timestamp data.
1661    /// Tests 10K, 50K, 100K docs to catch codec edge cases.
1662    #[test]
1663    fn test_large_scale_timestamp_roundtrip() {
1664        use super::super::{FAST_FIELD_MISSING, zigzag_encode};
1665
1666        for num_docs in [10_000, 50_000, 100_000] {
1667            let mut values = Vec::with_capacity(num_docs);
1668            let base_ts = 1724630400i64;
1669
1670            for i in 0..num_docs {
1671                if i % 7 == 0 {
1672                    values.push(FAST_FIELD_MISSING);
1673                } else {
1674                    // Timestamps spanning ~5 years, with some jitter
1675                    let ts = base_ts - (i as i64 * 3600) + ((i as i64 * 37) % 1000);
1676                    values.push(zigzag_encode(ts));
1677                }
1678            }
1679
1680            // Check which codec is selected
1681            let mut buf = Vec::new();
1682            serialize_auto(&values, &mut buf).unwrap();
1683            let codec_id = buf[0];
1684            let codec_name = match CodecType::from_u8(codec_id) {
1685                Some(CodecType::Constant) => "constant",
1686                Some(CodecType::Bitpacked) => "bitpacked",
1687                Some(CodecType::Linear) => "linear",
1688                Some(CodecType::BlockwiseLinear) => "blockwise_linear",
1689                None => "unknown",
1690            };
1691
1692            // Verify roundtrip
1693            let mut failures = Vec::new();
1694            for (i, &expected) in values.iter().enumerate() {
1695                let got = auto_read(&buf, i);
1696                if got != expected {
1697                    failures.push((i, expected, got));
1698                    if failures.len() >= 5 {
1699                        break;
1700                    }
1701                }
1702            }
1703
1704            assert!(
1705                failures.is_empty(),
1706                "num_docs={}, codec={}: {} failures. First 5: {:?}",
1707                num_docs,
1708                codec_name,
1709                failures.len(),
1710                failures
1711            );
1712        }
1713    }
1714
1715    /// Regression: blockwise linear codec selected for column where most blocks
1716    /// are efficient (sorted timestamps only) but a few blocks contain
1717    /// FAST_FIELD_MISSING, causing min_residual clamping corruption.
1718    #[test]
1719    fn test_blockwise_linear_with_clustered_missing() {
1720        use super::super::{FAST_FIELD_MISSING, zigzag_encode};
1721
1722        // 3000 values: first 512 are all FAST_FIELD_MISSING,
1723        // remaining 2488 are sorted timestamps (efficient linear blocks).
1724        // This should trigger blockwise linear selection overall,
1725        // but the first block has a mix that triggers the bug.
1726        let mut values = Vec::new();
1727
1728        // Block 0 (indices 0-511): mix of MISSING and timestamps
1729        // — first 100 are MISSING, rest are timestamps
1730        for i in 0..512 {
1731            if i < 100 {
1732                values.push(FAST_FIELD_MISSING);
1733            } else {
1734                let ts = 1724630400i64 + (i as i64 * 100);
1735                values.push(zigzag_encode(ts));
1736            }
1737        }
1738
1739        // Blocks 1-5 (indices 512-3071): sorted timestamps only
1740        for i in 512..3072 {
1741            let ts = 1724630400i64 + (i as i64 * 100);
1742            values.push(zigzag_encode(ts));
1743        }
1744
1745        let result = roundtrip(&values);
1746        let mut failures = Vec::new();
1747        for (i, (&expected, &got)) in values.iter().zip(result.iter()).enumerate() {
1748            if got != expected {
1749                failures.push((i, expected, got));
1750            }
1751        }
1752        assert!(
1753            failures.is_empty(),
1754            "blockwise linear with clustered missing: {} failures. First 5: {:?}",
1755            failures.len(),
1756            &failures[..failures.len().min(5)]
1757        );
1758    }
1759
1760    /// Test each codec FORCED with zigzag timestamps + FAST_FIELD_MISSING.
1761    /// This catches bugs that only manifest when a specific codec is forced.
1762    #[test]
1763    fn test_forced_codecs_with_timestamps_and_missing() {
1764        use super::super::{FAST_FIELD_MISSING, zigzag_encode};
1765
1766        let mut values = Vec::new();
1767        let base_ts = 1724630400i64;
1768        for i in 0..200 {
1769            if i % 5 == 0 {
1770                values.push(FAST_FIELD_MISSING);
1771            } else {
1772                let ts = base_ts - (i as i64 * 86400);
1773                values.push(zigzag_encode(ts));
1774            }
1775        }
1776
1777        // Force bitpacked
1778        {
1779            let est = BitpackedEstimator::default();
1780            let mut buf = Vec::new();
1781            est.serialize(&values, &mut buf).unwrap();
1782            for (i, &expected) in values.iter().enumerate() {
1783                let got = bitpacked_read(&buf[1..], i); // skip codec_id byte
1784                assert_eq!(got, expected, "forced bitpacked: index {} failed", i);
1785            }
1786        }
1787
1788        // Force linear — should error because FAST_FIELD_MISSING + timestamps
1789        // produce residuals exceeding i64 range
1790        {
1791            let est = LinearEstimator::default();
1792            let mut buf = Vec::new();
1793            let result = est.serialize(&values, &mut buf);
1794            assert!(
1795                result.is_err(),
1796                "linear codec should reject data with residuals exceeding i64"
1797            );
1798        }
1799
1800        // Force linear with values that DO fit in i64 (no FAST_FIELD_MISSING)
1801        {
1802            let safe_values: Vec<u64> = values
1803                .iter()
1804                .filter(|&&v| v != FAST_FIELD_MISSING)
1805                .copied()
1806                .collect();
1807            let est = LinearEstimator::default();
1808            let mut buf = Vec::new();
1809            est.serialize(&safe_values, &mut buf).unwrap();
1810            for (i, &expected) in safe_values.iter().enumerate() {
1811                let got = linear_read(&buf[1..], i);
1812                assert_eq!(got, expected, "forced linear (safe): index {} failed", i);
1813            }
1814        }
1815
1816        // Blockwise linear estimator should return None for data with FAST_FIELD_MISSING
1817        {
1818            let mut large_values = Vec::new();
1819            for i in 0..2000 {
1820                if i % 5 == 0 {
1821                    large_values.push(FAST_FIELD_MISSING);
1822                } else {
1823                    let ts = base_ts - (i as i64 * 86400);
1824                    large_values.push(zigzag_encode(ts));
1825                }
1826            }
1827            let mut est = BlockwiseLinearEstimator::default();
1828            for &v in &large_values {
1829                est.collect(v);
1830            }
1831            assert!(
1832                est.estimate().is_none(),
1833                "blockwise linear should reject data with per-block residuals exceeding i64"
1834            );
1835        }
1836    }
1837}