Skip to main content

chronix_encoding/
unified.rs

1//! Unified column encoding API with automatic encoder selection.
2//!
3//! [`ColumnEncoder`] selects the best encoding for each column type and
4//! transparently falls back to [`PlainEncoder`] when the
5//! specialised encoder does not achieve at least 2× compression.
6//!
7//! [`ColumnDecoder`] reads the [`EncodingType`] tag from an [`EncodedBlock`]
8//! and dispatches to the correct decoder.
9//!
10//! ## Wire format
11//!
12//! ```text
13//! [encoding_type: u8]
14//! [payload …]
15//! ```
16
17use chronix_core::config::FloatEncoding;
18
19use crate::alp::AlpDecoder;
20use crate::bitmap::{BitmapDecoder, BitmapEncoder};
21use crate::chimp::{Chimp128Decoder, ChimpDecoder};
22use crate::delta::{DeltaOfDeltaDecoder, DeltaOfDeltaEncoder};
23use crate::dictionary::{DictionaryDecoder, DictionaryEncoder};
24use crate::error::{EncodingError, Result};
25use crate::for_encoding::{ForDecoder, ForEncoder};
26use crate::gorilla::{GorillaDecoder, GorillaEncoder};
27use crate::integer::{IntegerDecoder, IntegerEncoder, VarintDecoder, VarintEncoder};
28use crate::patas::PatasDecoder;
29use crate::plain::{PlainDecoder, PlainEncoder};
30use crate::rle::{RleDecoder, RleEncoder};
31
32// ---------------------------------------------------------------------------
33// EncodingType
34// ---------------------------------------------------------------------------
35
36/// Declare [`EncodingType`] once and derive everything that must stay in
37/// step with it.
38///
39/// Three things have to agree for the format to be safe: the variant list,
40/// the `u8` tag the reader maps back to it, and the codec list the fuzzer
41/// walks. Written out three times, a new codec can be added to one and
42/// forgotten in the others — and forgetting the fuzz list fails silently
43/// (the decoder is simply never fuzzed) rather than at compile time. So the
44/// enum, `from_tag`, `tag`, `Display` and `ALL` are all generated from this
45/// single list, and `ALL` is what the fuzz target iterates. Adding a variant
46/// here is the only edit a new codec needs.
47macro_rules! encoding_types {
48    ($(
49        $(#[$meta:meta])*
50        $variant:ident = $tag:literal, $name:literal;
51    )+) => {
52        /// Discriminant tag stored as the first byte of every [`EncodedBlock`].
53        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
54        #[repr(u8)]
55        pub enum EncodingType {
56            $(
57                $(#[$meta])*
58                $variant = $tag,
59            )+
60        }
61
62        impl EncodingType {
63            /// Every encoding type, in tag order.
64            ///
65            /// Complete by construction — generated from the same list as
66            /// the enum itself, so a codec cannot be added without appearing
67            /// here, and therefore in the fuzz corpus.
68            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
69
70            /// Convert a `u8` tag to an `EncodingType`.
71            ///
72            /// # Errors
73            ///
74            /// Returns [`EncodingError::UnsupportedEncoding`] for unknown tags.
75            pub fn from_tag(tag: u8) -> Result<Self> {
76                match tag {
77                    $($tag => Ok(Self::$variant),)+
78                    other => Err(EncodingError::UnsupportedEncoding { tag: other }),
79                }
80            }
81
82            /// Return the `u8` discriminant.
83            #[must_use]
84            pub fn tag(self) -> u8 {
85                self as u8
86            }
87        }
88
89        impl std::fmt::Display for EncodingType {
90            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91                f.write_str(match self {
92                    $(Self::$variant => $name,)+
93                })
94            }
95        }
96    };
97}
98
99encoding_types! {
100    /// Delta-of-delta for timestamps.
101    ///
102    /// Kept as a selectable codec — it still wins on a perfect metronome —
103    /// but no longer the default: see [`ColumnEncoder::encode_timestamps`].
104    DeltaOfDelta = 0, "delta-of-delta";
105    /// Chimp XOR for floats.
106    Chimp = 1, "chimp";
107    /// Gorilla XOR for floats.
108    Gorilla = 2, "gorilla";
109    /// Delta + `ZigZag` for i64.
110    IntegerI64 = 3, "integer-i64";
111    /// Delta + `ZigZag` for u64.
112    IntegerU64 = 4, "integer-u64";
113    /// Dictionary encoding for strings.
114    Dictionary = 5, "dictionary";
115    /// Bitmap for booleans.
116    Bitmap = 6, "bitmap";
117    /// Plain f64.
118    PlainF64 = 7, "plain-f64";
119    /// Plain i64.
120    PlainI64 = 8, "plain-i64";
121    /// Plain u64.
122    PlainU64 = 9, "plain-u64";
123    /// Plain bool.
124    PlainBool = 10, "plain-bool";
125    /// Plain string.
126    PlainString = 11, "plain-string";
127    /// Nullable wrapper — validity bitmap + inner encoded payload.
128    ///
129    /// Payload format:
130    /// ```text
131    /// [inner_encoding: u8][total_count: u32 LE][validity_bitmap: ceil(count/8) bytes][inner_payload…]
132    /// ```
133    /// Bit `1` in the validity bitmap means the value is present; `0` means null.
134    /// Only non-null values are encoded in `inner_payload`.
135    Nullable = 12, "nullable";
136    /// Run-length encoding for columns with repeated consecutive values.
137    Rle = 13, "rle";
138    /// Delta + ZigZag + LEB128 varint for i64.
139    ///
140    /// Better than [`IntegerI64`](Self::IntegerI64) for sparse or highly
141    /// variable data where most deltas are small but occasional outliers
142    /// would inflate the fixed bit width.
143    VarintI64 = 14, "varint-i64";
144    /// Delta + ZigZag + LEB128 varint for u64.
145    VarintU64 = 15, "varint-u64";
146    /// Chimp128 ring-buffer XOR for floats.
147    Chimp128 = 16, "chimp128";
148    /// Frame-of-Reference for narrow-range integers.
149    ForI64 = 17, "for-i64";
150    /// Frame-of-Reference for narrow-range unsigned integers.
151    ForU64 = 18, "for-u64";
152    /// Patas byte-aligned XOR for floats (VLDB 2023).
153    Patas = 19, "patas";
154    /// ALP adaptive lossless floating-point compression (SIGMOD 2024).
155    ///
156    /// A second opinion behind [`Pco`](Self::Pco) for `f64` columns whose
157    /// values originated as decimals — which most sensor, meter and price
158    /// data does. See [`crate::alp`].
159    Alp = 20, "alp";
160    /// Pcodec for floats (Loncaric 2025): mode recovery, delta, binned ANS.
161    /// Leads every float candidate list — see [`crate::pco`].
162    Pco = 21, "pco";
163    /// Pcodec for signed integers, and the default timestamp codec.
164    PcoI64 = 22, "pco_i64";
165    /// Pcodec for unsigned integers.
166    PcoU64 = 23, "pco_u64";
167}
168
169// ---------------------------------------------------------------------------
170// EncodedBlock
171// ---------------------------------------------------------------------------
172
173/// An encoded column block ready to be written to a segment.
174///
175/// Contains the encoding type discriminant and the raw payload bytes.
176#[derive(Debug, Clone)]
177pub struct EncodedBlock {
178    /// Which encoder produced this block.
179    pub encoding: EncodingType,
180    /// Raw encoded payload (without the type tag).
181    pub payload: Vec<u8>,
182}
183
184impl EncodedBlock {
185    /// Serialize to a byte vector: `[tag: u8][payload…]`.
186    #[must_use]
187    pub fn to_bytes(&self) -> Vec<u8> {
188        let mut out = Vec::with_capacity(1 + self.payload.len());
189        out.push(self.encoding.tag());
190        out.extend_from_slice(&self.payload);
191        out
192    }
193
194    /// Parse an `EncodedBlock` from raw bytes.
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if the tag byte is unknown or the data is empty.
199    pub fn from_bytes(data: &[u8]) -> Result<Self> {
200        if data.is_empty() {
201            return Err(EncodingError::CorruptData {
202                detail: "empty encoded block".to_string(),
203            });
204        }
205        let encoding = EncodingType::from_tag(data[0])?;
206        let payload = data[1..].to_vec();
207        Ok(Self { encoding, payload })
208    }
209}
210
211// ---------------------------------------------------------------------------
212// Minimum compression ratio for specialized encoder selection
213// ---------------------------------------------------------------------------
214
215/// Specialized encoders must achieve at least this ratio (encoded < raw / RATIO)
216/// or the plain encoder is used instead.
217const MIN_COMPRESSION_RATIO: f64 = 1.5;
218
219// ---------------------------------------------------------------------------
220// TimestampEncoding
221// ---------------------------------------------------------------------------
222
223/// Which codec [`ColumnEncoder::encode_timestamps_with`] should use.
224///
225/// The float equivalent lives in `chronix_core::config::FloatEncoding`
226/// because it is a user-facing knob; this one is not (yet) reachable from
227/// configuration, and exists so the codec that lost the default can still be
228/// produced on demand — by the tests that measure it, by a tool that has to
229/// write a segment in the old shape, and by anyone re-running the comparison
230/// when pco next changes.
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
232pub enum TimestampEncoding {
233    /// Trial-encode pco and delta-of-delta, keep the smaller, ties to pco.
234    #[default]
235    Auto,
236    /// Always Pcodec ([`EncodingType::PcoI64`]).
237    Pco,
238    /// Always delta-of-delta ([`EncodingType::DeltaOfDelta`]).
239    DeltaOfDelta,
240    /// No compression ([`EncodingType::PlainI64`]).
241    Plain,
242}
243
244// ---------------------------------------------------------------------------
245// ColumnEncoder
246// ---------------------------------------------------------------------------
247
248/// Unified column encoder that auto-selects the best encoding per column type.
249///
250/// Each `encode_*` method tries the specialised encoder for the column type
251/// (delta-of-delta for timestamps, Chimp/Gorilla for floats, delta+ZigZag
252/// for integers, dictionary for strings, bitmap for booleans) and
253/// transparently falls back to [`PlainEncoder`] when
254/// compression does not reach the minimum ratio threshold.
255///
256/// All methods return an [`EncodedBlock`] containing the encoding tag and
257/// the raw payload.  Nullable variants wrap a validity bitmap around the
258/// inner encoded payload.
259#[derive(Debug, Clone, Copy)]
260pub struct ColumnEncoder;
261
262impl ColumnEncoder {
263    /// Encode a timestamp column (sorted i64 nanoseconds).
264    ///
265    /// Defaults to [`TimestampEncoding::Auto`]: pco leads, delta-of-delta is
266    /// tried as a second opinion, and the smaller block wins. See
267    /// [`encode_timestamps_with`](Self::encode_timestamps_with) for why.
268    ///
269    /// # Errors
270    ///
271    /// Returns an error if the input is empty.
272    pub fn encode_timestamps(values: &[i64]) -> Result<EncodedBlock> {
273        Self::encode_timestamps_with(values, TimestampEncoding::Auto)
274    }
275
276    /// Encode a timestamp column with an explicit codec choice.
277    ///
278    /// # Why pco leads
279    ///
280    /// Delta-of-delta was the default here for as long as this crate has
281    /// existed, on the strength of its behaviour on a metronome: a perfectly
282    /// regular cadence makes every second difference zero and DoD reaches
283    /// ~63× (and the raw codec, without the block tag, over 1000×). Real
284    /// gateways do not produce metronomes. A userspace sampler on a 1 s
285    /// cadence jitters by milliseconds, and DoD costs a varint for every
286    /// non-zero second difference — on ±5 ms jitter it reaches **0.9×**,
287    /// i.e. it makes the column *larger* than storing the raw `i64`s.
288    /// pco entropy-codes the deltas against learned bins and reaches 2.7×
289    /// on the same data, which is within 2% of the Shannon bound for that
290    /// jitter (uniform over 10 ms of nanoseconds is 23.25 bits of genuine
291    /// information per sample; pco spends 23.7).
292    ///
293    /// pco is not merely better on the ragged shapes — it wins on all four
294    /// measured shapes, including the metronome (1092× against DoD's 63×),
295    /// so there is no shape for which DoD is kept as the default. It stays
296    /// *selectable* because a decoder for it must exist for as long as any
297    /// segment on disk contains a DoD block, and because a codec that can
298    /// be re-selected is a codec whose replacement can be measured.
299    /// `tests/timestamp_codec_check.rs` is the table.
300    ///
301    /// [`Auto`](TimestampEncoding::Auto) still trial-encodes both and keeps
302    /// the smaller: pco winning every shape we thought to measure is not the
303    /// same as pco winning every shape, and one extra encode of a column we
304    /// are already encoding is cheap insurance against a shape we did not
305    /// think of.
306    ///
307    /// # Errors
308    ///
309    /// Returns an error if the input is empty.
310    pub fn encode_timestamps_with(
311        values: &[i64],
312        strategy: TimestampEncoding,
313    ) -> Result<EncodedBlock> {
314        let raw_size = values.len() * 8;
315
316        let (encoding, payload) = match strategy {
317            TimestampEncoding::Plain => {
318                return Ok(EncodedBlock {
319                    encoding: EncodingType::PlainI64,
320                    payload: PlainEncoder::encode_i64(values)?,
321                })
322            }
323            TimestampEncoding::DeltaOfDelta => (
324                EncodingType::DeltaOfDelta,
325                DeltaOfDeltaEncoder::encode(values)?,
326            ),
327            TimestampEncoding::Pco => (
328                EncodingType::PcoI64,
329                crate::pco::PcoEncoder::encode_timestamps(values)?,
330            ),
331            TimestampEncoding::Auto => {
332                let pco = crate::pco::PcoEncoder::encode_timestamps(values)?;
333                let dod = DeltaOfDeltaEncoder::encode(values)?;
334                // Ties go to pco: it is the default, and a tie on size is a
335                // win on decode speed.
336                if dod.len() < pco.len() {
337                    (EncodingType::DeltaOfDelta, dod)
338                } else {
339                    (EncodingType::PcoI64, pco)
340                }
341            }
342        };
343
344        if should_use_specialized(&payload, raw_size) {
345            Ok(EncodedBlock { encoding, payload })
346        } else {
347            Ok(EncodedBlock {
348                encoding: EncodingType::PlainI64,
349                payload: PlainEncoder::encode_i64(values)?,
350            })
351        }
352    }
353
354    /// Encode a float column using the specified encoding strategy.
355    ///
356    /// Uses sample-based adaptive selection to avoid wasting CPU on
357    /// triple-encoding: analyzes the first 1024 values to pick the best
358    /// encoder, then encodes the full column once.
359    ///
360    /// # Errors
361    ///
362    /// Returns an error if the input is empty.
363    pub fn encode_f64(values: &[f64], strategy: FloatEncoding) -> Result<EncodedBlock> {
364        match strategy {
365            FloatEncoding::Chimp => {
366                // Sample-based selection: analyze first 1024 values to
367                // pick the optimal encoder, then encode the full column
368                // exactly once — avoids the previous triple-encode waste.
369                let selector = crate::adaptive::AdaptiveSelector::new();
370                selector.encode_f64_adaptive(values)
371            }
372            FloatEncoding::Gorilla => {
373                let raw_size = values.len() * 8;
374                let gorilla_payload = GorillaEncoder::encode(values)?;
375                if should_use_specialized(&gorilla_payload, raw_size) {
376                    return Ok(EncodedBlock {
377                        encoding: EncodingType::Gorilla,
378                        payload: gorilla_payload,
379                    });
380                }
381                Ok(EncodedBlock {
382                    encoding: EncodingType::PlainF64,
383                    payload: PlainEncoder::encode_f64(values)?,
384                })
385            }
386            FloatEncoding::Plain => Ok(EncodedBlock {
387                encoding: EncodingType::PlainF64,
388                payload: PlainEncoder::encode_f64(values)?,
389            }),
390        }
391    }
392
393    /// Encode a signed integer column.
394    ///
395    /// Tries RLE (constant), then both fixed-bit-width and varint delta
396    /// encoding, picking the smallest result.  Falls back to plain if
397    /// neither specialised encoding achieves sufficient compression.
398    ///
399    /// # Errors
400    ///
401    /// Returns an error if the input is empty.
402    pub fn encode_i64(values: &[i64]) -> Result<EncodedBlock> {
403        let selector = crate::adaptive::AdaptiveSelector::new();
404        let pattern = selector.analyze_integers(values);
405
406        // Try RLE for constant / repetitive data
407        if pattern == crate::adaptive::IntegerPattern::Constant {
408            let raw_size = values.len() * 8;
409            let rle_payload = RleEncoder::encode_i64(values)?;
410            if should_use_specialized(&rle_payload, raw_size) {
411                return Ok(EncodedBlock {
412                    encoding: EncodingType::Rle,
413                    payload: rle_payload,
414                });
415            }
416        }
417
418        let raw_size = values.len() * 8;
419
420        // Trial-encode with every integer encoder and keep the smallest.
421        let fixed_payload = IntegerEncoder::encode_i64(values)?;
422        let varint_payload = VarintEncoder::encode_i64(values)?;
423        let for_payload = ForEncoder::encode_i64(values)?;
424        let pco_payload = crate::pco::PcoEncoder::encode_i64(values)?;
425
426        let mut best_enc = EncodingType::IntegerI64;
427        let mut best_payload = fixed_payload;
428
429        if varint_payload.len() < best_payload.len() {
430            best_enc = EncodingType::VarintI64;
431            best_payload = varint_payload;
432        }
433        if for_payload.len() < best_payload.len() {
434            best_enc = EncodingType::ForI64;
435            best_payload = for_payload;
436        }
437        if pco_payload.len() < best_payload.len() {
438            best_enc = EncodingType::PcoI64;
439            best_payload = pco_payload;
440        }
441
442        if should_use_specialized(&best_payload, raw_size) {
443            Ok(EncodedBlock {
444                encoding: best_enc,
445                payload: best_payload,
446            })
447        } else {
448            Ok(EncodedBlock {
449                encoding: EncodingType::PlainI64,
450                payload: PlainEncoder::encode_i64(values)?,
451            })
452        }
453    }
454
455    /// Encode an unsigned integer column.
456    ///
457    /// Tries RLE (constant), then both fixed-bit-width and varint delta
458    /// encoding, picking the smallest result.  Falls back to plain if
459    /// neither specialised encoding achieves sufficient compression.
460    ///
461    /// # Errors
462    ///
463    /// Returns an error if the input is empty.
464    pub fn encode_u64(values: &[u64]) -> Result<EncodedBlock> {
465        // Try RLE for constant / repetitive data — reinterpret as i64 for analysis
466        let as_i64: Vec<i64> = values.iter().map(|&v| v as i64).collect();
467        let selector = crate::adaptive::AdaptiveSelector::new();
468        if selector.analyze_integers(&as_i64) == crate::adaptive::IntegerPattern::Constant {
469            let raw_size = values.len() * 8;
470            let rle_payload = RleEncoder::encode_u64(values)?;
471            if should_use_specialized(&rle_payload, raw_size) {
472                return Ok(EncodedBlock {
473                    encoding: EncodingType::Rle,
474                    payload: rle_payload,
475                });
476            }
477        }
478
479        let raw_size = values.len() * 8;
480        let fixed_payload = IntegerEncoder::encode_u64(values)?;
481        let varint_payload = VarintEncoder::encode_u64(values)?;
482        let for_payload = ForEncoder::encode_u64(values)?;
483        let pco_payload = crate::pco::PcoEncoder::encode_u64(values)?;
484
485        // Pick the smallest.
486        let mut best_enc = EncodingType::IntegerU64;
487        let mut best_payload = fixed_payload;
488
489        if varint_payload.len() < best_payload.len() {
490            best_enc = EncodingType::VarintU64;
491            best_payload = varint_payload;
492        }
493        if for_payload.len() < best_payload.len() {
494            best_enc = EncodingType::ForU64;
495            best_payload = for_payload;
496        }
497        if pco_payload.len() < best_payload.len() {
498            best_enc = EncodingType::PcoU64;
499            best_payload = pco_payload;
500        }
501
502        if should_use_specialized(&best_payload, raw_size) {
503            Ok(EncodedBlock {
504                encoding: best_enc,
505                payload: best_payload,
506            })
507        } else {
508            Ok(EncodedBlock {
509                encoding: EncodingType::PlainU64,
510                payload: PlainEncoder::encode_u64(values)?,
511            })
512        }
513    }
514
515    /// Encode a string/tag column.
516    ///
517    /// Uses dictionary encoding, falls back to plain if compression is poor
518    /// or the dictionary overflows.
519    ///
520    /// # Errors
521    ///
522    /// Returns an error if the input is empty.
523    pub fn encode_string(values: &[&str]) -> Result<EncodedBlock> {
524        let raw_size: usize = values.iter().map(|s| 4 + s.len()).sum();
525
526        match DictionaryEncoder::encode(values) {
527            Ok(payload) if should_use_specialized(&payload, raw_size) => Ok(EncodedBlock {
528                encoding: EncodingType::Dictionary,
529                payload,
530            }),
531            // Dictionary didn't compress enough, or overflowed — fall back to plain
532            Ok(_) | Err(EncodingError::DictionaryOverflow { .. }) => Ok(EncodedBlock {
533                encoding: EncodingType::PlainString,
534                payload: PlainEncoder::encode_string(values)?,
535            }),
536            // Propagate other errors (e.g. EmptyInput)
537            Err(e) => Err(e),
538        }
539    }
540
541    /// Encode a boolean column using 1-bit bitmap encoding.
542    ///
543    /// Always uses bitmap encoding (1 bit per value is optimal).
544    ///
545    /// # Errors
546    ///
547    /// Returns an error if the input is empty.
548    pub fn encode_bool(values: &[bool]) -> Result<EncodedBlock> {
549        // Try RLE for constant boolean columns
550        if !values.is_empty() {
551            let first = values[0];
552            if values.iter().all(|&v| v == first) {
553                let raw_size = values.len(); // 1 byte per bool raw
554                let rle_payload = RleEncoder::encode_bool(values)?;
555                if should_use_specialized(&rle_payload, raw_size) {
556                    return Ok(EncodedBlock {
557                        encoding: EncodingType::Rle,
558                        payload: rle_payload,
559                    });
560                }
561            }
562        }
563        Ok(EncodedBlock {
564            encoding: EncodingType::Bitmap,
565            payload: BitmapEncoder::encode(values)?,
566        })
567    }
568
569    // ── Nullable encoding methods ────────────────────────────────────
570
571    /// Encode a nullable float column.
572    ///
573    /// Separates non-null values, encodes them normally, then wraps with
574    /// a validity bitmap so null positions are preserved on decode.
575    ///
576    /// # Errors
577    ///
578    /// Returns an error if all values are null (no data to encode) or
579    /// the input is empty.
580    pub fn encode_f64_nullable(
581        values: &[Option<f64>],
582        strategy: FloatEncoding,
583    ) -> Result<EncodedBlock> {
584        if values.is_empty() {
585            return Err(EncodingError::EmptyInput {
586                context: "nullable f64 encoder",
587            });
588        }
589        let (non_null, validity) = extract_non_null_f64(values);
590        if non_null.is_empty() {
591            // All nulls — encode as nullable with an empty inner block
592            return Ok(wrap_nullable(
593                EncodingType::PlainF64,
594                values.len(),
595                &validity,
596                &[],
597            ));
598        }
599        let inner = Self::encode_f64(&non_null, strategy)?;
600        Ok(wrap_nullable(
601            inner.encoding,
602            values.len(),
603            &validity,
604            &inner.payload,
605        ))
606    }
607
608    /// Encode a nullable signed integer column.
609    ///
610    /// # Errors
611    ///
612    /// Returns an error if the input is empty.
613    pub fn encode_i64_nullable(values: &[Option<i64>]) -> Result<EncodedBlock> {
614        if values.is_empty() {
615            return Err(EncodingError::EmptyInput {
616                context: "nullable i64 encoder",
617            });
618        }
619        let (non_null, validity) = extract_non_null_i64(values);
620        if non_null.is_empty() {
621            return Ok(wrap_nullable(
622                EncodingType::PlainI64,
623                values.len(),
624                &validity,
625                &[],
626            ));
627        }
628        let inner = Self::encode_i64(&non_null)?;
629        Ok(wrap_nullable(
630            inner.encoding,
631            values.len(),
632            &validity,
633            &inner.payload,
634        ))
635    }
636
637    /// Encode a nullable unsigned integer column.
638    ///
639    /// # Errors
640    ///
641    /// Returns an error if the input is empty.
642    pub fn encode_u64_nullable(values: &[Option<u64>]) -> Result<EncodedBlock> {
643        if values.is_empty() {
644            return Err(EncodingError::EmptyInput {
645                context: "nullable u64 encoder",
646            });
647        }
648        let (non_null, validity) = extract_non_null_u64(values);
649        if non_null.is_empty() {
650            return Ok(wrap_nullable(
651                EncodingType::PlainU64,
652                values.len(),
653                &validity,
654                &[],
655            ));
656        }
657        let inner = Self::encode_u64(&non_null)?;
658        Ok(wrap_nullable(
659            inner.encoding,
660            values.len(),
661            &validity,
662            &inner.payload,
663        ))
664    }
665
666    /// Encode a nullable string column.
667    ///
668    /// # Errors
669    ///
670    /// Returns an error if the input is empty.
671    pub fn encode_string_nullable(values: &[Option<&str>]) -> Result<EncodedBlock> {
672        if values.is_empty() {
673            return Err(EncodingError::EmptyInput {
674                context: "nullable string encoder",
675            });
676        }
677        let (non_null, validity): (Vec<&str>, Vec<bool>) = values
678            .iter()
679            .map(|v| match v {
680                Some(s) => (*s, true),
681                None => ("", false),
682            })
683            .unzip();
684        let non_null_only: Vec<&str> = non_null
685            .into_iter()
686            .zip(validity.iter())
687            .filter(|(_, v)| **v)
688            .map(|(s, _)| s)
689            .collect();
690        if non_null_only.is_empty() {
691            return Ok(wrap_nullable(
692                EncodingType::PlainString,
693                values.len(),
694                &validity,
695                &[],
696            ));
697        }
698        let inner = Self::encode_string(&non_null_only)?;
699        Ok(wrap_nullable(
700            inner.encoding,
701            values.len(),
702            &validity,
703            &inner.payload,
704        ))
705    }
706
707    /// Encode a nullable boolean column.
708    ///
709    /// # Errors
710    ///
711    /// Returns an error if the input is empty.
712    pub fn encode_bool_nullable(values: &[Option<bool>]) -> Result<EncodedBlock> {
713        if values.is_empty() {
714            return Err(EncodingError::EmptyInput {
715                context: "nullable bool encoder",
716            });
717        }
718        let validity: Vec<bool> = values.iter().map(std::option::Option::is_some).collect();
719        let non_null: Vec<bool> = values.iter().filter_map(|v| *v).collect();
720        if non_null.is_empty() {
721            return Ok(wrap_nullable(
722                EncodingType::Bitmap,
723                values.len(),
724                &validity,
725                &[],
726            ));
727        }
728        let inner = Self::encode_bool(&non_null)?;
729        Ok(wrap_nullable(
730            inner.encoding,
731            values.len(),
732            &validity,
733            &inner.payload,
734        ))
735    }
736}
737
738// ---------------------------------------------------------------------------
739// ColumnDecoder
740// ---------------------------------------------------------------------------
741
742/// Typed column values returned by [`ColumnDecoder`].
743///
744/// Each variant wraps a `Vec` of the corresponding Rust type.  Nullable
745/// variants use `Option<T>` where `None` represents a null value.
746///
747/// Has a custom [`PartialEq`] implementation that uses bitwise comparison
748/// for `f64` values so that `NaN == NaN` holds.
749#[derive(Debug, Clone)]
750pub enum DecodedColumn {
751    /// Signed 64-bit integers (timestamps or i64 fields).
752    I64(Vec<i64>),
753    /// Unsigned 64-bit integers.
754    U64(Vec<u64>),
755    /// 64-bit floating point values.
756    F64(Vec<f64>),
757    /// Boolean values.
758    Bool(Vec<bool>),
759    /// UTF-8 string values.
760    String(Vec<String>),
761    /// Nullable signed 64-bit integers.
762    NullableI64(Vec<Option<i64>>),
763    /// Nullable unsigned 64-bit integers.
764    NullableU64(Vec<Option<u64>>),
765    /// Nullable 64-bit floating point values.
766    NullableF64(Vec<Option<f64>>),
767    /// Nullable boolean values.
768    NullableBool(Vec<Option<bool>>),
769    /// Nullable UTF-8 string values.
770    NullableString(Vec<Option<String>>),
771}
772
773/// Bitwise f64 comparison helper (NaN-safe: NaN == NaN).
774fn f64_slice_eq(a: &[f64], b: &[f64]) -> bool {
775    a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits())
776}
777
778/// Bitwise optional f64 comparison helper (NaN-safe).
779fn opt_f64_slice_eq(a: &[Option<f64>], b: &[Option<f64>]) -> bool {
780    a.len() == b.len()
781        && a.iter().zip(b).all(|(x, y)| match (x, y) {
782            (Some(x), Some(y)) => x.to_bits() == y.to_bits(),
783            (None, None) => true,
784            _ => false,
785        })
786}
787
788impl PartialEq for DecodedColumn {
789    fn eq(&self, other: &Self) -> bool {
790        match (self, other) {
791            (Self::I64(a), Self::I64(b)) => a == b,
792            (Self::U64(a), Self::U64(b)) => a == b,
793            (Self::F64(a), Self::F64(b)) => f64_slice_eq(a, b),
794            (Self::Bool(a), Self::Bool(b)) => a == b,
795            (Self::String(a), Self::String(b)) => a == b,
796            (Self::NullableI64(a), Self::NullableI64(b)) => a == b,
797            (Self::NullableU64(a), Self::NullableU64(b)) => a == b,
798            (Self::NullableF64(a), Self::NullableF64(b)) => opt_f64_slice_eq(a, b),
799            (Self::NullableBool(a), Self::NullableBool(b)) => a == b,
800            (Self::NullableString(a), Self::NullableString(b)) => a == b,
801            _ => false,
802        }
803    }
804}
805
806impl DecodedColumn {
807    /// Returns the number of values in this decoded column.
808    #[must_use]
809    pub fn len(&self) -> usize {
810        match self {
811            Self::I64(v) => v.len(),
812            Self::U64(v) => v.len(),
813            Self::F64(v) => v.len(),
814            Self::Bool(v) => v.len(),
815            Self::String(v) => v.len(),
816            Self::NullableI64(v) => v.len(),
817            Self::NullableU64(v) => v.len(),
818            Self::NullableF64(v) => v.len(),
819            Self::NullableBool(v) => v.len(),
820            Self::NullableString(v) => v.len(),
821        }
822    }
823
824    /// Returns `true` if this decoded column contains no values.
825    #[must_use]
826    pub fn is_empty(&self) -> bool {
827        self.len() == 0
828    }
829}
830
831/// Unified column decoder that dispatches based on the [`EncodingType`] tag.
832///
833/// Reads the encoding discriminant from an [`EncodedBlock`] and delegates
834/// to the corresponding specialised decoder (delta-of-delta, Chimp,
835/// Gorilla, integer, dictionary, bitmap, plain, RLE, or nullable wrapper).
836///
837/// Returns a [`DecodedColumn`] that preserves the original column type.
838#[derive(Debug, Clone, Copy)]
839pub struct ColumnDecoder;
840
841impl ColumnDecoder {
842    /// Decode an [`EncodedBlock`] back to typed column values.
843    ///
844    /// # Errors
845    ///
846    /// Returns an error if the payload is corrupt or the encoding type
847    /// is unsupported.
848    pub fn decode(block: &EncodedBlock) -> Result<DecodedColumn> {
849        match block.encoding {
850            EncodingType::DeltaOfDelta => {
851                let values = DeltaOfDeltaDecoder::decode(&block.payload)?;
852                Ok(DecodedColumn::I64(values))
853            }
854            EncodingType::Chimp => {
855                let values = ChimpDecoder::decode(&block.payload)?;
856                Ok(DecodedColumn::F64(values))
857            }
858            EncodingType::Chimp128 => {
859                let values = Chimp128Decoder::decode(&block.payload)?;
860                Ok(DecodedColumn::F64(values))
861            }
862            EncodingType::Patas => {
863                let values = PatasDecoder::decode(&block.payload)?;
864                Ok(DecodedColumn::F64(values))
865            }
866            EncodingType::Alp => {
867                let values = AlpDecoder::decode(&block.payload)?;
868                Ok(DecodedColumn::F64(values))
869            }
870            EncodingType::Pco => {
871                let values = crate::pco::PcoDecoder::decode_f64(&block.payload)?;
872                Ok(DecodedColumn::F64(values))
873            }
874            EncodingType::PcoI64 => {
875                let values = crate::pco::PcoDecoder::decode_i64(&block.payload)?;
876                Ok(DecodedColumn::I64(values))
877            }
878            EncodingType::PcoU64 => {
879                let values = crate::pco::PcoDecoder::decode_u64(&block.payload)?;
880                Ok(DecodedColumn::U64(values))
881            }
882            EncodingType::Gorilla => {
883                let values = GorillaDecoder::decode(&block.payload)?;
884                Ok(DecodedColumn::F64(values))
885            }
886            EncodingType::IntegerI64 => {
887                let values = IntegerDecoder::decode_i64(&block.payload)?;
888                Ok(DecodedColumn::I64(values))
889            }
890            EncodingType::IntegerU64 => {
891                let values = IntegerDecoder::decode_u64(&block.payload)?;
892                Ok(DecodedColumn::U64(values))
893            }
894            EncodingType::Dictionary => {
895                let values = DictionaryDecoder::decode(&block.payload)?;
896                Ok(DecodedColumn::String(values))
897            }
898            EncodingType::Bitmap => {
899                let values = BitmapDecoder::decode(&block.payload)?;
900                Ok(DecodedColumn::Bool(values))
901            }
902            EncodingType::PlainF64 => {
903                let values = PlainDecoder::decode_f64(&block.payload)?;
904                Ok(DecodedColumn::F64(values))
905            }
906            EncodingType::PlainI64 => {
907                let values = PlainDecoder::decode_i64(&block.payload)?;
908                Ok(DecodedColumn::I64(values))
909            }
910            EncodingType::PlainU64 => {
911                let values = PlainDecoder::decode_u64(&block.payload)?;
912                Ok(DecodedColumn::U64(values))
913            }
914            EncodingType::PlainBool => {
915                let values = PlainDecoder::decode_bool(&block.payload)?;
916                Ok(DecodedColumn::Bool(values))
917            }
918            EncodingType::PlainString => {
919                let values = PlainDecoder::decode_string(&block.payload)?;
920                Ok(DecodedColumn::String(values))
921            }
922            EncodingType::Nullable => Self::decode_nullable(&block.payload),
923            EncodingType::Rle => Self::decode_rle(&block.payload),
924            EncodingType::VarintI64 => {
925                let values = VarintDecoder::decode_i64(&block.payload)?;
926                Ok(DecodedColumn::I64(values))
927            }
928            EncodingType::VarintU64 => {
929                let values = VarintDecoder::decode_u64(&block.payload)?;
930                Ok(DecodedColumn::U64(values))
931            }
932            EncodingType::ForI64 => {
933                let values = ForDecoder::decode_i64(&block.payload)?;
934                Ok(DecodedColumn::I64(values))
935            }
936            EncodingType::ForU64 => {
937                let values = ForDecoder::decode_u64(&block.payload)?;
938                Ok(DecodedColumn::U64(values))
939            }
940        }
941    }
942
943    /// Decode a nullable-wrapped payload.
944    ///
945    /// # Wire format
946    ///
947    /// ```text
948    /// [inner_encoding: u8][total_count: u32 LE][validity_bitmap: ceil(count/8) bytes][inner_payload…]
949    /// ```
950    fn decode_nullable(payload: &[u8]) -> Result<DecodedColumn> {
951        if payload.len() < 5 {
952            return Err(EncodingError::CorruptData {
953                detail: "nullable payload too short".to_string(),
954            });
955        }
956
957        let inner_enc = EncodingType::from_tag(payload[0])?;
958        // The bitmap length bounds this to 8× the payload, but an 8× blow-up
959        // from a corrupt header is still worth refusing at the same ceiling
960        // every other decoder uses.
961        let total_count = crate::coding::checked_decode_count(
962            u32::from_le_bytes([payload[1], payload[2], payload[3], payload[4]]) as usize,
963            "nullable",
964        )?;
965        let bitmap_bytes = total_count.div_ceil(8);
966        let header_len = 5 + bitmap_bytes;
967
968        if payload.len() < header_len {
969            return Err(EncodingError::CorruptData {
970                detail: "nullable bitmap truncated".to_string(),
971            });
972        }
973
974        let bitmap = &payload[5..header_len];
975        let inner_payload = &payload[header_len..];
976
977        // Build validity vector
978        let validity: Vec<bool> = (0..total_count)
979            .map(|i| bitmap[i / 8] & (1 << (i % 8)) != 0)
980            .collect();
981
982        let non_null_count = validity.iter().filter(|v| **v).count();
983
984        // All nulls — no inner data
985        if non_null_count == 0 {
986            return match inner_enc {
987                EncodingType::PlainF64
988                | EncodingType::Chimp
989                | EncodingType::Chimp128
990                | EncodingType::Gorilla
991                | EncodingType::Patas
992                | EncodingType::Alp
993                | EncodingType::Pco => Ok(DecodedColumn::NullableF64(vec![None; total_count])),
994                EncodingType::PlainI64
995                | EncodingType::IntegerI64
996                | EncodingType::VarintI64
997                | EncodingType::ForI64
998                | EncodingType::PcoI64
999                | EncodingType::DeltaOfDelta => {
1000                    Ok(DecodedColumn::NullableI64(vec![None; total_count]))
1001                }
1002                EncodingType::PlainU64
1003                | EncodingType::IntegerU64
1004                | EncodingType::VarintU64
1005                | EncodingType::ForU64
1006                | EncodingType::PcoU64 => Ok(DecodedColumn::NullableU64(vec![None; total_count])),
1007                EncodingType::PlainString | EncodingType::Dictionary => {
1008                    Ok(DecodedColumn::NullableString(vec![None; total_count]))
1009                }
1010                EncodingType::Bitmap | EncodingType::PlainBool => {
1011                    Ok(DecodedColumn::NullableBool(vec![None; total_count]))
1012                }
1013                _ => Err(EncodingError::CorruptData {
1014                    detail: format!("unexpected inner encoding in nullable: {inner_enc}"),
1015                }),
1016            };
1017        }
1018
1019        // Decode non-null values
1020        let inner_block = EncodedBlock {
1021            encoding: inner_enc,
1022            payload: inner_payload.to_vec(),
1023        };
1024        let decoded = Self::decode(&inner_block)?;
1025
1026        // Scatter non-null values back into positions using validity bitmap
1027        match decoded {
1028            DecodedColumn::F64(vals) => Ok(DecodedColumn::NullableF64(scatter(vals, &validity))),
1029            DecodedColumn::I64(vals) => Ok(DecodedColumn::NullableI64(scatter(vals, &validity))),
1030            DecodedColumn::U64(vals) => Ok(DecodedColumn::NullableU64(scatter(vals, &validity))),
1031            DecodedColumn::Bool(vals) => Ok(DecodedColumn::NullableBool(scatter(vals, &validity))),
1032            DecodedColumn::String(vals) => {
1033                Ok(DecodedColumn::NullableString(scatter(vals, &validity)))
1034            }
1035            _ => Err(EncodingError::CorruptData {
1036                detail: "unexpected decoded type in nullable wrapper".to_string(),
1037            }),
1038        }
1039    }
1040
1041    /// Decode an RLE-encoded payload, dispatching by the value-type byte.
1042    fn decode_rle(payload: &[u8]) -> Result<DecodedColumn> {
1043        let vtype = RleDecoder::value_type(payload)?;
1044        match vtype {
1045            0 => Ok(DecodedColumn::I64(RleDecoder::decode_i64(payload)?)),
1046            1 => Ok(DecodedColumn::U64(RleDecoder::decode_u64(payload)?)),
1047            2 => Ok(DecodedColumn::F64(RleDecoder::decode_f64(payload)?)),
1048            3 => Ok(DecodedColumn::Bool(RleDecoder::decode_bool(payload)?)),
1049            4 => Ok(DecodedColumn::String(RleDecoder::decode_string(payload)?)),
1050            other => Err(EncodingError::CorruptData {
1051                detail: format!("unknown RLE value type: {other}"),
1052            }),
1053        }
1054    }
1055
1056    /// Convenience: decode from raw bytes (tag + payload).
1057    ///
1058    /// # Errors
1059    ///
1060    /// Returns an error if the data is empty, the tag is unknown, or
1061    /// the payload is corrupt.
1062    pub fn decode_bytes(data: &[u8]) -> Result<DecodedColumn> {
1063        let block = EncodedBlock::from_bytes(data)?;
1064        Self::decode(&block)
1065    }
1066}
1067
1068// ---------------------------------------------------------------------------
1069// Helpers
1070// ---------------------------------------------------------------------------
1071
1072/// Returns `true` if the specialized encoder achieved at least
1073/// [`MIN_COMPRESSION_RATIO`].
1074pub(crate) fn should_use_specialized(encoded: &[u8], raw_size: usize) -> bool {
1075    if raw_size == 0 {
1076        return false;
1077    }
1078    #[allow(clippy::cast_precision_loss)]
1079    let ratio = raw_size as f64 / encoded.len() as f64;
1080    ratio >= MIN_COMPRESSION_RATIO
1081}
1082
1083// ---------------------------------------------------------------------------
1084// Nullable encoding helpers
1085// ---------------------------------------------------------------------------
1086
1087/// Build a validity bitmap from a boolean slice.
1088///
1089/// Bit `1` at position `i` means value `i` is present (non-null).
1090fn encode_validity_bitmap(validity: &[bool]) -> Vec<u8> {
1091    let byte_count = validity.len().div_ceil(8);
1092    let mut bitmap = vec![0u8; byte_count];
1093    for (i, &valid) in validity.iter().enumerate() {
1094        if valid {
1095            bitmap[i / 8] |= 1 << (i % 8);
1096        }
1097    }
1098    bitmap
1099}
1100
1101/// Wrap an encoded inner block with a nullable header.
1102fn wrap_nullable(
1103    inner_enc: EncodingType,
1104    total_count: usize,
1105    validity: &[bool],
1106    inner_payload: &[u8],
1107) -> EncodedBlock {
1108    let bitmap = encode_validity_bitmap(validity);
1109    let mut payload = Vec::with_capacity(1 + 4 + bitmap.len() + inner_payload.len());
1110    payload.push(inner_enc.tag());
1111    payload.extend_from_slice(&(total_count as u32).to_le_bytes());
1112    payload.extend_from_slice(&bitmap);
1113    payload.extend_from_slice(inner_payload);
1114    EncodedBlock {
1115        encoding: EncodingType::Nullable,
1116        payload,
1117    }
1118}
1119
1120/// Extract non-null `f64` values and a validity bitmap.
1121fn extract_non_null_f64(values: &[Option<f64>]) -> (Vec<f64>, Vec<bool>) {
1122    let validity: Vec<bool> = values.iter().map(std::option::Option::is_some).collect();
1123    let non_null: Vec<f64> = values.iter().filter_map(|v| *v).collect();
1124    (non_null, validity)
1125}
1126
1127/// Extract non-null `i64` values and a validity bitmap.
1128fn extract_non_null_i64(values: &[Option<i64>]) -> (Vec<i64>, Vec<bool>) {
1129    let validity: Vec<bool> = values.iter().map(std::option::Option::is_some).collect();
1130    let non_null: Vec<i64> = values.iter().filter_map(|v| *v).collect();
1131    (non_null, validity)
1132}
1133
1134/// Extract non-null `u64` values and a validity bitmap.
1135fn extract_non_null_u64(values: &[Option<u64>]) -> (Vec<u64>, Vec<bool>) {
1136    let validity: Vec<bool> = values.iter().map(std::option::Option::is_some).collect();
1137    let non_null: Vec<u64> = values.iter().filter_map(|v| *v).collect();
1138    (non_null, validity)
1139}
1140
1141/// Scatter non-null values back into their positions using a validity bitmap.
1142///
1143/// `values` contains only the non-null elements in order.
1144/// `validity[i]` is `true` for positions that have a value.
1145fn scatter<T: Default + Clone>(values: Vec<T>, validity: &[bool]) -> Vec<Option<T>> {
1146    let mut result = Vec::with_capacity(validity.len());
1147    let mut val_iter = values.into_iter();
1148    for &valid in validity {
1149        if valid {
1150            result.push(val_iter.next().map(Some).unwrap_or(None));
1151        } else {
1152            result.push(None);
1153        }
1154    }
1155    result
1156}
1157
1158// ---------------------------------------------------------------------------
1159// Tests
1160// ---------------------------------------------------------------------------
1161
1162#[cfg(test)]
1163mod tests {
1164    use super::*;
1165
1166    // -- EncodingType tag roundtrip ----------------------------------------
1167
1168    #[test]
1169    fn encoding_type_tag_roundtrip() {
1170        let all = [
1171            EncodingType::DeltaOfDelta,
1172            EncodingType::Chimp,
1173            EncodingType::Gorilla,
1174            EncodingType::IntegerI64,
1175            EncodingType::IntegerU64,
1176            EncodingType::Dictionary,
1177            EncodingType::Bitmap,
1178            EncodingType::PlainF64,
1179            EncodingType::PlainI64,
1180            EncodingType::PlainU64,
1181            EncodingType::PlainBool,
1182            EncodingType::PlainString,
1183            EncodingType::Nullable,
1184            EncodingType::Rle,
1185        ];
1186        for enc in &all {
1187            let tag = enc.tag();
1188            let recovered = EncodingType::from_tag(tag).unwrap();
1189            assert_eq!(*enc, recovered);
1190        }
1191    }
1192
1193    #[test]
1194    fn encoding_type_unknown_tag() {
1195        assert!(EncodingType::from_tag(255).is_err());
1196    }
1197
1198    /// `EncodingType::ALL` is what the fuzz target iterates, so a variant it
1199    /// misses is a decoder that is never fuzzed. It is generated by the same
1200    /// macro that declares the enum, so it is complete by construction; this
1201    /// pins that `from_tag` agrees with it in both directions.
1202    #[test]
1203    fn all_lists_every_variant_and_agrees_with_from_tag() {
1204        let scanned: Vec<EncodingType> = (0..=u8::MAX)
1205            .filter_map(|t| EncodingType::from_tag(t).ok())
1206            .collect();
1207        assert_eq!(EncodingType::ALL, scanned.as_slice());
1208        for enc in EncodingType::ALL {
1209            assert_eq!(EncodingType::from_tag(enc.tag()).unwrap(), *enc);
1210        }
1211        // Tags are dense from 0: a gap would be a tag the reader accepts
1212        // for a variant that no longer exists.
1213        for (i, enc) in EncodingType::ALL.iter().enumerate() {
1214            assert_eq!(usize::from(enc.tag()), i, "{enc} has a non-dense tag");
1215        }
1216    }
1217
1218    // -- EncodedBlock serialization ---------------------------------------
1219
1220    #[test]
1221    fn encoded_block_roundtrip() {
1222        let block = EncodedBlock {
1223            encoding: EncodingType::Chimp,
1224            payload: vec![1, 2, 3, 4],
1225        };
1226        let bytes = block.to_bytes();
1227        assert_eq!(bytes[0], EncodingType::Chimp.tag());
1228        let recovered = EncodedBlock::from_bytes(&bytes).unwrap();
1229        assert_eq!(recovered.encoding, block.encoding);
1230        assert_eq!(recovered.payload, block.payload);
1231    }
1232
1233    #[test]
1234    fn encoded_block_empty_error() {
1235        assert!(EncodedBlock::from_bytes(&[]).is_err());
1236    }
1237
1238    // -- Timestamp encoding -----------------------------------------------
1239
1240    #[test]
1241    fn encode_timestamps_monotonic() {
1242        let ts: Vec<i64> = (0..1000).map(|i| 1_000_000 + i * 10_000).collect();
1243        let block = ColumnEncoder::encode_timestamps(&ts).unwrap();
1244        // pco is the default even on a perfect metronome, where DoD used to
1245        // be chosen: pco beats it there too (see
1246        // `tests/timestamp_codec_check.rs`).
1247        assert_eq!(block.encoding, EncodingType::PcoI64);
1248        let decoded = ColumnDecoder::decode(&block).unwrap();
1249        assert_eq!(decoded, DecodedColumn::I64(ts));
1250    }
1251
1252    #[test]
1253    fn encode_timestamps_honours_an_explicit_codec() {
1254        let ts: Vec<i64> = (0..1000).map(|i| 1_000_000 + i * 10_000).collect();
1255        for (strategy, expected) in [
1256            (TimestampEncoding::Pco, EncodingType::PcoI64),
1257            (TimestampEncoding::DeltaOfDelta, EncodingType::DeltaOfDelta),
1258            (TimestampEncoding::Plain, EncodingType::PlainI64),
1259            (TimestampEncoding::Auto, EncodingType::PcoI64),
1260        ] {
1261            let block = ColumnEncoder::encode_timestamps_with(&ts, strategy).unwrap();
1262            assert_eq!(block.encoding, expected, "{strategy:?}");
1263            assert_eq!(
1264                ColumnDecoder::decode(&block).unwrap(),
1265                DecodedColumn::I64(ts.clone())
1266            );
1267        }
1268    }
1269
1270    /// `Auto` must never be worse than the codec it could have picked.
1271    #[test]
1272    fn auto_timestamps_never_loses_to_delta_of_delta() {
1273        // Random-ish timestamps: neither codec compresses, and Auto must
1274        // still not be larger than the better of the two.
1275        let mut seed = 7u64;
1276        let ts: Vec<i64> = (0..2000)
1277            .map(|_| {
1278                seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1279                (seed >> 20) as i64
1280            })
1281            .collect();
1282        let auto = ColumnEncoder::encode_timestamps_with(&ts, TimestampEncoding::Auto).unwrap();
1283        let dod =
1284            ColumnEncoder::encode_timestamps_with(&ts, TimestampEncoding::DeltaOfDelta).unwrap();
1285        let pco = ColumnEncoder::encode_timestamps_with(&ts, TimestampEncoding::Pco).unwrap();
1286        assert!(auto.payload.len() <= dod.payload.len().min(pco.payload.len()));
1287        assert_eq!(
1288            ColumnDecoder::decode(&auto).unwrap(),
1289            DecodedColumn::I64(ts)
1290        );
1291    }
1292
1293    #[test]
1294    fn encode_timestamps_bytes_roundtrip() {
1295        let ts: Vec<i64> = (0..100).map(|i| 1_000_000 + i * 10_000).collect();
1296        let block = ColumnEncoder::encode_timestamps(&ts).unwrap();
1297        let raw = block.to_bytes();
1298        let decoded = ColumnDecoder::decode_bytes(&raw).unwrap();
1299        assert_eq!(decoded, DecodedColumn::I64(ts));
1300    }
1301
1302    // -- Float encoding ---------------------------------------------------
1303
1304    #[test]
1305    fn encode_f64_adaptive_strategy() {
1306        // `FloatEncoding::Chimp` runs the adaptive selector; on two-decimal
1307        // values that means ALP.
1308        let values: Vec<f64> = (0..500).map(|i| 20.0 + (i as f64) * 0.01).collect();
1309        let block = ColumnEncoder::encode_f64(&values, FloatEncoding::Chimp).unwrap();
1310        assert_eq!(block.encoding, EncodingType::Pco);
1311        let decoded = ColumnDecoder::decode(&block).unwrap();
1312        assert_eq!(decoded, DecodedColumn::F64(values));
1313    }
1314
1315    #[test]
1316    fn encode_f64_gorilla_strategy() {
1317        let values: Vec<f64> = (0..500).map(|i| 20.0 + (i as f64) * 0.01).collect();
1318        let block = ColumnEncoder::encode_f64(&values, FloatEncoding::Gorilla).unwrap();
1319        assert!(
1320            block.encoding == EncodingType::Gorilla || block.encoding == EncodingType::PlainF64
1321        );
1322        let decoded = ColumnDecoder::decode(&block).unwrap();
1323        assert_eq!(decoded, DecodedColumn::F64(values));
1324    }
1325
1326    #[test]
1327    fn encode_f64_plain_strategy() {
1328        let values = vec![1.0, 2.0, 3.0];
1329        let block = ColumnEncoder::encode_f64(&values, FloatEncoding::Plain).unwrap();
1330        assert_eq!(block.encoding, EncodingType::PlainF64);
1331        let decoded = ColumnDecoder::decode(&block).unwrap();
1332        assert_eq!(decoded, DecodedColumn::F64(values));
1333    }
1334
1335    // -- Integer encoding -------------------------------------------------
1336
1337    #[test]
1338    fn encode_i64_with_fallback() {
1339        let values: Vec<i64> = (0..1000).collect();
1340        let block = ColumnEncoder::encode_i64(&values).unwrap();
1341        assert!(
1342            block.encoding == EncodingType::PcoI64
1343                || block.encoding == EncodingType::IntegerI64
1344                || block.encoding == EncodingType::PlainI64
1345        );
1346        let decoded = ColumnDecoder::decode(&block).unwrap();
1347        assert_eq!(decoded, DecodedColumn::I64(values));
1348    }
1349
1350    #[test]
1351    fn encode_u64_with_fallback() {
1352        let values: Vec<u64> = (0..1000).collect();
1353        let block = ColumnEncoder::encode_u64(&values).unwrap();
1354        assert!(
1355            block.encoding == EncodingType::PcoU64
1356                || block.encoding == EncodingType::IntegerU64
1357                || block.encoding == EncodingType::PlainU64
1358        );
1359        let decoded = ColumnDecoder::decode(&block).unwrap();
1360        assert_eq!(decoded, DecodedColumn::U64(values));
1361    }
1362
1363    // -- String encoding --------------------------------------------------
1364
1365    #[test]
1366    fn encode_string_low_cardinality() {
1367        let raw = vec!["us-east-1"; 500];
1368        let block = ColumnEncoder::encode_string(&raw).unwrap();
1369        assert_eq!(block.encoding, EncodingType::Dictionary);
1370        let decoded = ColumnDecoder::decode(&block).unwrap();
1371        let expected: Vec<String> = raw.iter().map(|s| (*s).to_owned()).collect();
1372        assert_eq!(decoded, DecodedColumn::String(expected));
1373    }
1374
1375    #[test]
1376    fn encode_string_high_cardinality_fallback() {
1377        // All unique — dictionary may still compress, or fall back to plain
1378        let raw: Vec<String> = (0..200).map(|i| format!("uuid-{i}")).collect();
1379        let refs: Vec<&str> = raw.iter().map(String::as_str).collect();
1380        let block = ColumnEncoder::encode_string(&refs).unwrap();
1381        let decoded = ColumnDecoder::decode(&block).unwrap();
1382        assert_eq!(decoded, DecodedColumn::String(raw));
1383    }
1384
1385    // -- Boolean encoding -------------------------------------------------
1386
1387    #[test]
1388    fn encode_bool_bitmap() {
1389        let values: Vec<bool> = (0..100).map(|i| i % 3 == 0).collect();
1390        let block = ColumnEncoder::encode_bool(&values).unwrap();
1391        assert_eq!(block.encoding, EncodingType::Bitmap);
1392        let decoded = ColumnDecoder::decode(&block).unwrap();
1393        assert_eq!(decoded, DecodedColumn::Bool(values));
1394    }
1395
1396    // -- Empty input errors -----------------------------------------------
1397
1398    #[test]
1399    fn empty_inputs_error() {
1400        assert!(ColumnEncoder::encode_timestamps(&[]).is_err());
1401        assert!(ColumnEncoder::encode_f64(&[], FloatEncoding::Chimp).is_err());
1402        assert!(ColumnEncoder::encode_i64(&[]).is_err());
1403        assert!(ColumnEncoder::encode_u64(&[]).is_err());
1404        let empty: &[&str] = &[];
1405        assert!(ColumnEncoder::encode_string(empty).is_err());
1406        assert!(ColumnEncoder::encode_bool(&[]).is_err());
1407    }
1408
1409    // -- MIN_COMPRESSION_RATIO helper -------------------------------------
1410
1411    #[test]
1412    fn should_use_specialized_logic() {
1413        // 100 bytes raw, 50 bytes encoded → 2× → should use
1414        assert!(should_use_specialized(&[0u8; 50], 100));
1415        // 100 bytes raw, 80 bytes encoded → 1.25× → should NOT use
1416        assert!(!should_use_specialized(&[0u8; 80], 100));
1417        // 0 raw → false
1418        assert!(!should_use_specialized(&[0u8; 10], 0));
1419    }
1420
1421    // -- Nullable encoding roundtrips -------------------------------------
1422
1423    #[test]
1424    fn nullable_f64_mixed() {
1425        let values = vec![Some(1.0), None, Some(3.0), None, Some(5.0)];
1426        let block = ColumnEncoder::encode_f64_nullable(&values, FloatEncoding::Plain).unwrap();
1427        assert_eq!(block.encoding, EncodingType::Nullable);
1428        let decoded = ColumnDecoder::decode(&block).unwrap();
1429        assert_eq!(decoded, DecodedColumn::NullableF64(values));
1430    }
1431
1432    #[test]
1433    fn nullable_f64_all_present() {
1434        let values = vec![Some(1.0), Some(2.0), Some(3.0)];
1435        let block = ColumnEncoder::encode_f64_nullable(&values, FloatEncoding::Plain).unwrap();
1436        let decoded = ColumnDecoder::decode(&block).unwrap();
1437        assert_eq!(decoded, DecodedColumn::NullableF64(values));
1438    }
1439
1440    #[test]
1441    fn nullable_f64_all_null() {
1442        let values: Vec<Option<f64>> = vec![None, None, None];
1443        let block = ColumnEncoder::encode_f64_nullable(&values, FloatEncoding::Plain).unwrap();
1444        let decoded = ColumnDecoder::decode(&block).unwrap();
1445        assert_eq!(decoded, DecodedColumn::NullableF64(values));
1446    }
1447
1448    #[test]
1449    fn nullable_i64_mixed() {
1450        let values = vec![Some(10), None, Some(30), Some(40), None];
1451        let block = ColumnEncoder::encode_i64_nullable(&values).unwrap();
1452        assert_eq!(block.encoding, EncodingType::Nullable);
1453        let decoded = ColumnDecoder::decode(&block).unwrap();
1454        assert_eq!(decoded, DecodedColumn::NullableI64(values));
1455    }
1456
1457    #[test]
1458    fn nullable_u64_mixed() {
1459        let values = vec![None, Some(100u64), Some(200), None];
1460        let block = ColumnEncoder::encode_u64_nullable(&values).unwrap();
1461        let decoded = ColumnDecoder::decode(&block).unwrap();
1462        assert_eq!(decoded, DecodedColumn::NullableU64(values));
1463    }
1464
1465    #[test]
1466    fn nullable_string_mixed() {
1467        let values = vec![Some("hello"), None, Some("world"), None];
1468        let block = ColumnEncoder::encode_string_nullable(&values).unwrap();
1469        let decoded = ColumnDecoder::decode(&block).unwrap();
1470        let expected: Vec<Option<String>> =
1471            values.iter().map(|v| v.map(ToString::to_string)).collect();
1472        assert_eq!(decoded, DecodedColumn::NullableString(expected));
1473    }
1474
1475    #[test]
1476    fn nullable_f64_bytes_roundtrip() {
1477        let values = vec![Some(1.0), None, Some(3.0)];
1478        let block = ColumnEncoder::encode_f64_nullable(&values, FloatEncoding::Plain).unwrap();
1479        let raw = block.to_bytes();
1480        let decoded = ColumnDecoder::decode_bytes(&raw).unwrap();
1481        assert_eq!(decoded, DecodedColumn::NullableF64(values));
1482    }
1483
1484    #[test]
1485    fn nullable_empty_errors() {
1486        let empty_f64: &[Option<f64>] = &[];
1487        assert!(ColumnEncoder::encode_f64_nullable(empty_f64, FloatEncoding::Plain).is_err());
1488        let empty_i64: &[Option<i64>] = &[];
1489        assert!(ColumnEncoder::encode_i64_nullable(empty_i64).is_err());
1490        let empty_u64: &[Option<u64>] = &[];
1491        assert!(ColumnEncoder::encode_u64_nullable(empty_u64).is_err());
1492        let empty_str: &[Option<&str>] = &[];
1493        assert!(ColumnEncoder::encode_string_nullable(empty_str).is_err());
1494        let empty_bool: &[Option<bool>] = &[];
1495        assert!(ColumnEncoder::encode_bool_nullable(empty_bool).is_err());
1496    }
1497
1498    #[test]
1499    fn nullable_distinguishes_zero_from_null() {
1500        // The critical test: 0.0 is encoded as 0.0, null is encoded as None.
1501        // Without it, zeroes are indistinguishable from nulls.
1502        let values = vec![Some(0.0), None, Some(0.0), None];
1503        let block = ColumnEncoder::encode_f64_nullable(&values, FloatEncoding::Plain).unwrap();
1504        let decoded = ColumnDecoder::decode(&block).unwrap();
1505        match &decoded {
1506            DecodedColumn::NullableF64(vals) => {
1507                assert_eq!(vals[0], Some(0.0));
1508                assert_eq!(vals[1], None); // null, NOT 0.0
1509                assert_eq!(vals[2], Some(0.0));
1510                assert_eq!(vals[3], None); // null, NOT 0.0
1511            }
1512            other => panic!("Expected NullableF64, got {other:?}"),
1513        }
1514    }
1515
1516    #[test]
1517    fn nullable_i64_distinguishes_zero_from_null() {
1518        let values = vec![Some(0i64), None, Some(0), None, Some(42)];
1519        let block = ColumnEncoder::encode_i64_nullable(&values).unwrap();
1520        let decoded = ColumnDecoder::decode(&block).unwrap();
1521        match &decoded {
1522            DecodedColumn::NullableI64(vals) => {
1523                assert_eq!(vals[0], Some(0));
1524                assert_eq!(vals[1], None);
1525                assert_eq!(vals[2], Some(0));
1526                assert_eq!(vals[3], None);
1527                assert_eq!(vals[4], Some(42));
1528            }
1529            other => panic!("Expected NullableI64, got {other:?}"),
1530        }
1531    }
1532}
1533
1534#[cfg(test)]
1535mod proptests {
1536    use super::*;
1537    use proptest::prelude::*;
1538
1539    proptest! {
1540        #[test]
1541        fn roundtrip_timestamps(mut values in proptest::collection::vec(any::<i64>(), 1..300)) {
1542            values.sort_unstable();
1543            let block = ColumnEncoder::encode_timestamps(&values).unwrap();
1544            let decoded = ColumnDecoder::decode(&block).unwrap();
1545            prop_assert_eq!(DecodedColumn::I64(values), decoded);
1546        }
1547
1548        #[test]
1549        fn roundtrip_i64(values in proptest::collection::vec(any::<i64>(), 1..300)) {
1550            let block = ColumnEncoder::encode_i64(&values).unwrap();
1551            let decoded = ColumnDecoder::decode(&block).unwrap();
1552            prop_assert_eq!(DecodedColumn::I64(values), decoded);
1553        }
1554
1555        #[test]
1556        fn roundtrip_u64(values in proptest::collection::vec(any::<u64>(), 1..300)) {
1557            let block = ColumnEncoder::encode_u64(&values).unwrap();
1558            let decoded = ColumnDecoder::decode(&block).unwrap();
1559            prop_assert_eq!(DecodedColumn::U64(values), decoded);
1560        }
1561
1562        #[test]
1563        fn roundtrip_f64(values in proptest::collection::vec(any::<f64>(), 1..300)) {
1564            let block = ColumnEncoder::encode_f64(&values, FloatEncoding::Gorilla).unwrap();
1565            let decoded = ColumnDecoder::decode(&block).unwrap();
1566            prop_assert_eq!(DecodedColumn::F64(values), decoded);
1567        }
1568
1569        #[test]
1570        fn roundtrip_bool(values in proptest::collection::vec(any::<bool>(), 1..300)) {
1571            let block = ColumnEncoder::encode_bool(&values).unwrap();
1572            let decoded = ColumnDecoder::decode(&block).unwrap();
1573            prop_assert_eq!(DecodedColumn::Bool(values), decoded);
1574        }
1575
1576        #[test]
1577        fn roundtrip_string(values in proptest::collection::vec("[a-z]{0,8}", 1..100)) {
1578            let refs: Vec<&str> = values.iter().map(String::as_str).collect();
1579            let block = ColumnEncoder::encode_string(&refs).unwrap();
1580            let decoded = ColumnDecoder::decode(&block).unwrap();
1581            prop_assert_eq!(DecodedColumn::String(values), decoded);
1582        }
1583
1584        #[test]
1585        fn roundtrip_nullable_i64(values in proptest::collection::vec(any::<Option<i64>>(), 1..300)) {
1586            let block = ColumnEncoder::encode_i64_nullable(&values).unwrap();
1587            let decoded = ColumnDecoder::decode(&block).unwrap();
1588            prop_assert_eq!(DecodedColumn::NullableI64(values), decoded);
1589        }
1590
1591        #[test]
1592        fn roundtrip_nullable_u64(values in proptest::collection::vec(any::<Option<u64>>(), 1..300)) {
1593            let block = ColumnEncoder::encode_u64_nullable(&values).unwrap();
1594            let decoded = ColumnDecoder::decode(&block).unwrap();
1595            prop_assert_eq!(DecodedColumn::NullableU64(values), decoded);
1596        }
1597
1598        #[test]
1599        fn roundtrip_nullable_f64(values in proptest::collection::vec(any::<Option<f64>>(), 1..300)) {
1600            let block = ColumnEncoder::encode_f64_nullable(&values, FloatEncoding::Gorilla).unwrap();
1601            let decoded = ColumnDecoder::decode(&block).unwrap();
1602            prop_assert_eq!(DecodedColumn::NullableF64(values), decoded);
1603        }
1604
1605        #[test]
1606        fn roundtrip_nullable_bool(values in proptest::collection::vec(any::<Option<bool>>(), 1..300)) {
1607            let block = ColumnEncoder::encode_bool_nullable(&values).unwrap();
1608            let decoded = ColumnDecoder::decode(&block).unwrap();
1609            prop_assert_eq!(DecodedColumn::NullableBool(values), decoded);
1610        }
1611
1612        #[test]
1613        fn roundtrip_nullable_string(values in proptest::collection::vec(
1614            proptest::option::of("[a-z]{0,8}"), 1..100
1615        )) {
1616            let refs: Vec<Option<&str>> = values.iter().map(|o| o.as_deref()).collect();
1617            let block = ColumnEncoder::encode_string_nullable(&refs).unwrap();
1618            let decoded = ColumnDecoder::decode(&block).unwrap();
1619            let expected: Vec<Option<String>> = values.clone();
1620            prop_assert_eq!(DecodedColumn::NullableString(expected), decoded);
1621        }
1622    }
1623}