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