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