Skip to main content

questdb/egress/
column.rs

1/*******************************************************************************
2 *     ___                  _   ____  ____
3 *    / _ \ _   _  ___  ___| |_|  _ \| __ )
4 *   | | | | | | |/ _ \/ __| __| | | |  _ \
5 *   | |_| | |_| |  __/\__ \ |_| |_| | |_) |
6 *    \__\_\\__,_|\___||___/\__|____/|____/
7 *
8 *  Copyright (c) 2014-2019 Appsicle
9 *  Copyright (c) 2019-2025 QuestDB
10 *
11 *  Licensed under the Apache License, Version 2.0 (the "License");
12 *  you may not use this file except in compliance with the License.
13 *  You may obtain a copy of the License at
14 *
15 *  http://www.apache.org/licenses/LICENSE-2.0
16 *
17 *  Unless required by applicable law or agreed to in writing, software
18 *  distributed under the License is distributed on an "AS IS" BASIS,
19 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 *  See the License for the specific language governing permissions and
21 *  limitations under the License.
22 *
23 ******************************************************************************/
24
25//! Layer 0 column views.
26//!
27//! Typed, borrowing views over the bytes a `RESULT_BATCH` decoder leaves in
28//! the batch's owned buffers. These types are deliberately QWP-shaped: they
29//! preserve symbol-as-id, decimal-as-(value,scale), and never materialize
30//! strings or perform conversions that would force a copy. Adapters
31//! (Arrow C ABI, numpy/pandas, polars) consume these on top.
32//!
33//! ## Validity
34//!
35//! Per QWP, the null bitmap is LSB-first within each byte and `1` means
36//! NULL. A column may carry no bitmap at all when no row is null;
37//! [`Validity::None`] expresses that compactly.
38//!
39//! ## What's modelled here
40//!
41//! Fixed-width numerics (Bool, Byte, Short, Int, Long, Float, Double, Ipv4),
42//! temporals (Timestamp µs / Date ms / TimestampNanos), 16-byte UUID,
43//! 32-byte Long256, 2-byte Char, Symbol (dense u32 codes + dict reference),
44//! Decimal64/128/256 (mantissa + scale), Geohash (variable byte width),
45//! Varchar / Binary (varlen with offset table), and DOUBLE_ARRAY /
46//! LONG_ARRAY (multi-dimensional array views).
47
48use std::marker::PhantomData;
49
50use crate::egress::column_kind::ColumnKind;
51use crate::egress::symbol_dict::SymbolDict;
52use crate::error::{Result, fmt};
53
54// ---------------------------------------------------------------------------
55// Validity bitmap
56// ---------------------------------------------------------------------------
57
58/// Per-row null information.
59///
60/// `Validity::None` means "no nulls for this column"; the column carries no
61/// bitmap on the wire and `is_null` always returns `false`.
62#[derive(Debug, Clone, Copy)]
63#[non_exhaustive]
64pub enum Validity<'a> {
65    /// No row in this column is null.
66    None,
67    /// LSB-first bitmap; bit `1` = null. `row_count` rows total.
68    Bitmap { bytes: &'a [u8], row_count: usize },
69}
70
71impl<'a> Validity<'a> {
72    /// Construct a bitmap-backed validity view.
73    ///
74    /// Returns `Err(InvalidApiCall)` if `bytes.len() < row_count.div_ceil(8)`
75    /// — a too-short bitmap would otherwise cause [`Self::is_null`] to silently
76    /// report null rows as non-null (the bytes beyond the buffer end are
77    /// indistinguishable from "this row's bit is 0"). The decoder always
78    /// sizes the bitmap exactly to `row_count.div_ceil(8)`
79    /// (see `decode_validity`), so the error is unreachable from
80    /// crate-internal callers; the check exists so external callers can't
81    /// build a corrupt view and have it silently mis-report NULL rows.
82    #[inline]
83    pub fn from_bitmap(bytes: &'a [u8], row_count: usize) -> Result<Self> {
84        let needed = row_count.div_ceil(8);
85        if bytes.len() < needed {
86            return Err(fmt!(
87                InvalidApiCall,
88                "Validity::from_bitmap: bitmap is {} bytes but row_count={} needs at least {}",
89                bytes.len(),
90                row_count,
91                needed
92            ));
93        }
94        Ok(Validity::Bitmap { bytes, row_count })
95    }
96
97    #[inline]
98    pub fn has_nulls(&self) -> bool {
99        matches!(self, Validity::Bitmap { .. })
100    }
101
102    /// `true` if `row` is null. Out-of-range rows return `false` (matches
103    /// the column accessors' "row was never written" treatment).
104    ///
105    /// Bounds-checked: a [`Validity::Bitmap`](Self::Bitmap) constructed
106    /// directly (bypassing [`from_bitmap`](Self::from_bitmap)) with a
107    /// too-short bitmap reports `false` for the missing tail rather
108    /// than panicking. Constructor-validated values never trip the
109    /// fallback.
110    #[inline]
111    pub fn is_null(&self, row: usize) -> bool {
112        match self {
113            Validity::None => false,
114            Validity::Bitmap { bytes, row_count } => {
115                if row >= *row_count {
116                    return false;
117                }
118                match bytes.get(row >> 3) {
119                    Some(byte) => (byte >> (row & 7)) & 1 != 0,
120                    None => false,
121                }
122            }
123        }
124    }
125
126    /// Raw bitmap, when present.
127    #[inline]
128    pub fn bytes(&self) -> Option<&'a [u8]> {
129        match self {
130            Validity::None => None,
131            Validity::Bitmap { bytes, .. } => Some(bytes),
132        }
133    }
134}
135
136// ---------------------------------------------------------------------------
137// Fixed-width primitives
138// ---------------------------------------------------------------------------
139
140/// Decode trait for fixed-width little-endian primitives.
141pub trait FixedWidth: Copy {
142    const SIZE: usize;
143    fn from_le(bytes: &[u8]) -> Self;
144}
145
146macro_rules! impl_fixed {
147    ($t:ty, $sz:expr) => {
148        impl FixedWidth for $t {
149            const SIZE: usize = $sz;
150            #[inline]
151            fn from_le(bytes: &[u8]) -> Self {
152                <$t>::from_le_bytes(bytes.try_into().expect("FixedWidth slice length"))
153            }
154        }
155    };
156}
157
158impl_fixed!(i16, 2);
159impl_fixed!(i32, 4);
160impl_fixed!(i64, 8);
161impl_fixed!(u16, 2);
162impl_fixed!(u32, 4);
163impl_fixed!(u64, 8);
164impl_fixed!(f32, 4);
165impl_fixed!(f64, 8);
166
167impl FixedWidth for i8 {
168    const SIZE: usize = 1;
169    #[inline]
170    fn from_le(bytes: &[u8]) -> Self {
171        bytes[0] as i8
172    }
173}
174
175impl FixedWidth for u8 {
176    const SIZE: usize = 1;
177    #[inline]
178    fn from_le(bytes: &[u8]) -> Self {
179        bytes[0]
180    }
181}
182
183/// Borrowed view over a packed little-endian array of `T`.
184#[derive(Debug, Clone, Copy)]
185pub struct FixedColumn<'a, T: FixedWidth> {
186    raw: &'a [u8],
187    validity: Validity<'a>,
188    _phantom: PhantomData<T>,
189}
190
191impl<'a, T: FixedWidth> FixedColumn<'a, T> {
192    /// Construct a borrowed view over decoder-produced bytes.
193    ///
194    /// `pub(crate)` because the constructor accepts raw wire-format
195    /// bytes whose length invariants (multiple of `T::SIZE`, validity
196    /// bitmap matching `len()`) are only `debug_assert!`-checked. In
197    /// release builds, an out-of-spec input causes silent garbage or
198    /// out-of-bounds panics from `value()` — exposing it as a safe
199    /// `pub fn` would let external callers trip both. The decoder
200    /// upholds the invariants by construction.
201    #[inline]
202    pub(crate) fn new(raw: &'a [u8], validity: Validity<'a>) -> Self {
203        debug_assert_eq!(
204            raw.len() % T::SIZE,
205            0,
206            "raw length must be multiple of element size"
207        );
208        Self {
209            raw,
210            validity,
211            _phantom: PhantomData,
212        }
213    }
214
215    #[inline]
216    pub fn len(&self) -> usize {
217        self.raw.len() / T::SIZE
218    }
219
220    #[inline]
221    pub fn is_empty(&self) -> bool {
222        self.raw.is_empty()
223    }
224
225    #[inline]
226    pub fn validity(&self) -> Validity<'a> {
227        self.validity
228    }
229
230    #[inline]
231    pub fn is_null(&self, row: usize) -> bool {
232        self.validity.is_null(row)
233    }
234
235    /// Raw little-endian bytes for the entire column. `len() * T::SIZE` long.
236    #[inline]
237    pub fn raw(&self) -> &'a [u8] {
238        self.raw
239    }
240
241    /// Decode the value at `row`. Caller should consult [`is_null`](Self::is_null)
242    /// separately; this returns the underlying bit-pattern regardless.
243    ///
244    /// # Panics
245    /// Panics if `row >= self.len()`. `#[track_caller]` makes the panic
246    /// point at the offending call site rather than into this accessor.
247    #[inline]
248    #[track_caller]
249    pub fn value(&self, row: usize) -> T {
250        let s = row * T::SIZE;
251        T::from_le(&self.raw[s..s + T::SIZE])
252    }
253
254    /// Iterator yielding `Option<T>` (None for null rows).
255    #[inline]
256    pub fn iter(&self) -> FixedIter<'_, 'a, T> {
257        FixedIter {
258            col: self,
259            row: 0,
260            len: self.len(),
261        }
262    }
263}
264
265pub struct FixedIter<'c, 'a, T: FixedWidth> {
266    col: &'c FixedColumn<'a, T>,
267    row: usize,
268    len: usize,
269}
270
271impl<'c, 'a, T: FixedWidth> Iterator for FixedIter<'c, 'a, T> {
272    type Item = Option<T>;
273    #[inline]
274    fn next(&mut self) -> Option<Self::Item> {
275        if self.row >= self.len {
276            return None;
277        }
278        let r = self.row;
279        self.row += 1;
280        if self.col.is_null(r) {
281            Some(None)
282        } else {
283            Some(Some(self.col.value(r)))
284        }
285    }
286}
287
288// ---------------------------------------------------------------------------
289// Fixed-size byte arrays (UUID, Long256)
290// ---------------------------------------------------------------------------
291
292/// Borrowed view over a packed array of fixed-size byte slices.
293#[derive(Debug, Clone, Copy)]
294pub struct FixedBytesColumn<'a, const N: usize> {
295    raw: &'a [u8],
296    validity: Validity<'a>,
297}
298
299impl<'a, const N: usize> FixedBytesColumn<'a, N> {
300    /// `pub(crate)` for the same reason as [`FixedColumn::new`]: the
301    /// `raw.len() % N == 0` invariant is `debug_assert!`-only.
302    #[inline]
303    pub(crate) fn new(raw: &'a [u8], validity: Validity<'a>) -> Self {
304        debug_assert_eq!(raw.len() % N, 0);
305        Self { raw, validity }
306    }
307
308    #[inline]
309    pub fn len(&self) -> usize {
310        self.raw.len() / N
311    }
312
313    #[inline]
314    pub fn is_empty(&self) -> bool {
315        self.raw.is_empty()
316    }
317
318    #[inline]
319    pub fn validity(&self) -> Validity<'a> {
320        self.validity
321    }
322
323    #[inline]
324    pub fn is_null(&self, row: usize) -> bool {
325        self.validity.is_null(row)
326    }
327
328    #[inline]
329    pub fn raw(&self) -> &'a [u8] {
330        self.raw
331    }
332
333    /// `&[u8; N]` for the requested row.
334    ///
335    /// # Panics
336    /// Panics if `row >= self.len()`.
337    #[inline]
338    #[track_caller]
339    pub fn value(&self, row: usize) -> &'a [u8; N] {
340        let s = row * N;
341        (&self.raw[s..s + N])
342            .try_into()
343            .expect("FixedBytesColumn slice length")
344    }
345}
346
347pub type UuidColumn<'a> = FixedBytesColumn<'a, 16>;
348pub type Long256Column<'a> = FixedBytesColumn<'a, 32>;
349
350// ---------------------------------------------------------------------------
351// Symbol column
352// ---------------------------------------------------------------------------
353
354/// SYMBOL column: dense per-row `u32` codes plus a borrowed reference to
355/// the connection-scoped dictionary.
356///
357/// The wire encodes codes as a compact varint stream over non-null rows;
358/// the decoder densifies that into a `row_count`-sized `u32` slice with
359/// `0` in null slots. The validity bitmap is the source of truth for
360/// null vs id-zero, so random access is O(1).
361#[derive(Debug, Clone, Copy)]
362pub struct SymbolColumn<'a> {
363    codes: &'a [u32],
364    validity: Validity<'a>,
365    dict: &'a SymbolDict,
366}
367
368impl<'a> SymbolColumn<'a> {
369    /// `pub(crate)`: callers must supply `codes` whose entries are all
370    /// less than `dict.len()` (decoder-enforced via the post-decode
371    /// dict-bounds check). A safe `pub fn` would let external callers
372    /// build a column where `resolve()` silently returns `None` for
373    /// non-null rows — masking wire corruption as SQL NULL.
374    #[inline]
375    pub(crate) fn new(codes: &'a [u32], validity: Validity<'a>, dict: &'a SymbolDict) -> Self {
376        Self {
377            codes,
378            validity,
379            dict,
380        }
381    }
382
383    #[inline]
384    pub fn len(&self) -> usize {
385        self.codes.len()
386    }
387
388    #[inline]
389    pub fn is_empty(&self) -> bool {
390        self.codes.is_empty()
391    }
392
393    #[inline]
394    pub fn validity(&self) -> Validity<'a> {
395        self.validity
396    }
397
398    #[inline]
399    pub fn is_null(&self, row: usize) -> bool {
400        self.validity.is_null(row)
401    }
402
403    /// Dense per-row codes (`0` in null slots — see [`is_null`](Self::is_null)).
404    #[inline]
405    pub fn codes(&self) -> &'a [u32] {
406        self.codes
407    }
408
409    #[inline]
410    pub fn dict(&self) -> &'a SymbolDict {
411        self.dict
412    }
413
414    /// Resolve `row` to its UTF-8 string. `None` for null rows or unknown ids.
415    #[inline]
416    pub fn resolve(&self, row: usize) -> Option<&'a str> {
417        if self.is_null(row) {
418            return None;
419        }
420        let code = *self.codes.get(row)?;
421        self.dict.get(code)
422    }
423}
424
425// ---------------------------------------------------------------------------
426// Decimal64
427// ---------------------------------------------------------------------------
428
429/// DECIMAL64 column: i64 mantissas + a per-batch scale prefix the decoder
430/// has already stripped from the data buffer.
431#[derive(Debug, Clone, Copy)]
432pub struct Decimal64Column<'a> {
433    values: FixedColumn<'a, i64>,
434    scale: i8,
435}
436
437impl<'a> Decimal64Column<'a> {
438    /// `pub(crate)`: wraps a `FixedColumn<i64>`; same wire-bytes
439    /// invariants apply.
440    #[inline]
441    pub(crate) fn new(raw: &'a [u8], validity: Validity<'a>, scale: i8) -> Self {
442        Self {
443            values: FixedColumn::new(raw, validity),
444            scale,
445        }
446    }
447
448    #[inline]
449    pub fn len(&self) -> usize {
450        self.values.len()
451    }
452
453    #[inline]
454    pub fn is_empty(&self) -> bool {
455        self.values.is_empty()
456    }
457
458    #[inline]
459    pub fn validity(&self) -> Validity<'a> {
460        self.values.validity()
461    }
462
463    #[inline]
464    pub fn is_null(&self, row: usize) -> bool {
465        self.values.is_null(row)
466    }
467
468    #[inline]
469    pub fn scale(&self) -> i8 {
470        self.scale
471    }
472
473    #[inline]
474    pub fn raw(&self) -> &'a [u8] {
475        self.values.raw()
476    }
477
478    /// Mantissa for `row`. Use `scale()` to interpret the decimal point.
479    ///
480    /// # Panics
481    /// Panics if `row >= self.len()`.
482    #[inline]
483    #[track_caller]
484    pub fn value(&self, row: usize) -> i64 {
485        self.values.value(row)
486    }
487}
488
489// ---------------------------------------------------------------------------
490// Variable-length columns (VARCHAR, BINARY)
491// ---------------------------------------------------------------------------
492
493/// Per-row offsets into a flat byte buffer.
494///
495/// `offsets` has `row_count + 1` entries; the bytes for row `i` live at
496/// `data[offsets[i]..offsets[i+1]]`. Null rows are represented as
497/// zero-length entries (`offsets[i] == offsets[i+1]`); the validity
498/// bitmap remains the source of truth for "null vs empty".
499///
500/// Used internally by [`VarcharColumn`] and [`BinaryColumn`] so they
501/// share offset semantics.
502#[derive(Debug, Clone, Copy)]
503struct VarlenLayout<'a> {
504    offsets: &'a [u32],
505    data: &'a [u8],
506    validity: Validity<'a>,
507}
508
509impl<'a> VarlenLayout<'a> {
510    #[inline]
511    fn len(&self) -> usize {
512        self.offsets.len().saturating_sub(1)
513    }
514
515    #[inline]
516    fn slice(&self, row: usize) -> Option<&'a [u8]> {
517        if self.validity.is_null(row) {
518            return None;
519        }
520        let s = *self.offsets.get(row)? as usize;
521        let e = *self.offsets.get(row + 1)? as usize;
522        self.data.get(s..e)
523    }
524}
525
526/// VARCHAR column.
527#[derive(Debug, Clone, Copy)]
528pub struct VarcharColumn<'a> {
529    inner: VarlenLayout<'a>,
530}
531
532impl<'a> VarcharColumn<'a> {
533    /// Construct from caller-validated buffers.
534    ///
535    /// # Safety
536    ///
537    /// The entire `data` byte range, from offset `0` up to the largest
538    /// value referenced by `offsets`, must be valid UTF-8 *and* every
539    /// `(offsets[i], offsets[i+1])` pair must lie on a UTF-8 character
540    /// boundary. [`value`](Self::value) reads each row through
541    /// `from_utf8_unchecked` for performance; violating this contract
542    /// produces an invalid `&str` and is undefined behavior.
543    ///
544    /// The decoder upholds this invariant by validating the concatenated
545    /// `data` buffer once at decode time and only emitting offsets at
546    /// codepoint boundaries.
547    pub(crate) unsafe fn new(offsets: &'a [u32], data: &'a [u8], validity: Validity<'a>) -> Self {
548        Self {
549            inner: VarlenLayout {
550                offsets,
551                data,
552                validity,
553            },
554        }
555    }
556
557    #[inline]
558    pub fn len(&self) -> usize {
559        self.inner.len()
560    }
561
562    #[inline]
563    pub fn is_empty(&self) -> bool {
564        self.inner.len() == 0
565    }
566
567    #[inline]
568    pub fn validity(&self) -> Validity<'a> {
569        self.inner.validity
570    }
571
572    #[inline]
573    pub fn is_null(&self, row: usize) -> bool {
574        self.inner.validity.is_null(row)
575    }
576
577    #[inline]
578    pub fn offsets(&self) -> &'a [u32] {
579        self.inner.offsets
580    }
581
582    #[inline]
583    pub fn data(&self) -> &'a [u8] {
584        self.inner.data
585    }
586
587    /// UTF-8 string for `row`. `None` for null rows.
588    ///
589    /// # Panics
590    /// Panics if `row >= self.len()`.
591    #[inline]
592    #[track_caller]
593    pub fn value(&self, row: usize) -> Option<&'a str> {
594        let bytes = self.inner.slice(row)?;
595        // Safety: `VarcharColumn::new` is an `unsafe fn` whose contract
596        // requires `data` to be valid UTF-8 across every offset boundary,
597        // so any sub-slice produced by `inner.slice` is also valid UTF-8.
598        Some(unsafe { std::str::from_utf8_unchecked(bytes) })
599    }
600}
601
602/// BINARY column. Same offset/data shape as [`VarcharColumn`] but bytes
603/// are opaque.
604#[derive(Debug, Clone, Copy)]
605pub struct BinaryColumn<'a> {
606    inner: VarlenLayout<'a>,
607}
608
609impl<'a> BinaryColumn<'a> {
610    /// `pub(crate)`: callers must supply monotonically-non-decreasing
611    /// `offsets` ending at `data.len()`, with `offsets.len() == row_count + 1`
612    /// (or matching the no-null fast path the decoder uses). Out-of-spec
613    /// inputs cause `value()` to read garbage or panic.
614    #[inline]
615    pub(crate) fn new(offsets: &'a [u32], data: &'a [u8], validity: Validity<'a>) -> Self {
616        Self {
617            inner: VarlenLayout {
618                offsets,
619                data,
620                validity,
621            },
622        }
623    }
624
625    #[inline]
626    pub fn len(&self) -> usize {
627        self.inner.len()
628    }
629
630    #[inline]
631    pub fn is_empty(&self) -> bool {
632        self.inner.len() == 0
633    }
634
635    #[inline]
636    pub fn validity(&self) -> Validity<'a> {
637        self.inner.validity
638    }
639
640    #[inline]
641    pub fn is_null(&self, row: usize) -> bool {
642        self.inner.validity.is_null(row)
643    }
644
645    #[inline]
646    pub fn offsets(&self) -> &'a [u32] {
647        self.inner.offsets
648    }
649
650    #[inline]
651    pub fn data(&self) -> &'a [u8] {
652        self.inner.data
653    }
654
655    /// Raw bytes for `row`. `None` for null rows.
656    ///
657    /// # Panics
658    /// Panics if `row >= self.len()`.
659    #[inline]
660    #[track_caller]
661    pub fn value(&self, row: usize) -> Option<&'a [u8]> {
662        self.inner.slice(row)
663    }
664}
665
666// ---------------------------------------------------------------------------
667// GEOHASH
668// ---------------------------------------------------------------------------
669
670/// GEOHASH column.
671///
672/// Wire carries a column-level `precision_bits` (1..60) and packs each row
673/// into `ceil(precision_bits / 8)` little-endian bytes. The decoder
674/// densifies into `row_count × byte_width`. Values can be inspected raw or
675/// zero-extended to `u64` via [`value`](Self::value).
676#[derive(Debug, Clone, Copy)]
677pub struct GeohashColumn<'a> {
678    raw: &'a [u8],
679    byte_width: u8,
680    precision_bits: u8,
681    validity: Validity<'a>,
682}
683
684impl<'a> GeohashColumn<'a> {
685    /// `pub(crate)`: the `byte_width` ∈ 1..=8 and `raw.len() % byte_width
686    /// == 0` invariants are `debug_assert!`-only.
687    #[inline]
688    pub(crate) fn new(
689        raw: &'a [u8],
690        byte_width: u8,
691        precision_bits: u8,
692        validity: Validity<'a>,
693    ) -> Self {
694        debug_assert!((1..=8).contains(&byte_width));
695        debug_assert_eq!(raw.len() % byte_width as usize, 0);
696        Self {
697            raw,
698            byte_width,
699            precision_bits,
700            validity,
701        }
702    }
703
704    #[inline]
705    pub fn precision_bits(&self) -> u8 {
706        self.precision_bits
707    }
708
709    #[inline]
710    pub fn byte_width(&self) -> u8 {
711        self.byte_width
712    }
713
714    #[inline]
715    pub fn len(&self) -> usize {
716        if self.byte_width == 0 {
717            0
718        } else {
719            self.raw.len() / self.byte_width as usize
720        }
721    }
722
723    #[inline]
724    pub fn is_empty(&self) -> bool {
725        self.raw.is_empty()
726    }
727
728    #[inline]
729    pub fn validity(&self) -> Validity<'a> {
730        self.validity
731    }
732
733    #[inline]
734    pub fn is_null(&self, row: usize) -> bool {
735        self.validity.is_null(row)
736    }
737
738    #[inline]
739    pub fn raw(&self) -> &'a [u8] {
740        self.raw
741    }
742
743    /// Zero-extend the row's `byte_width` LE bytes to a `u64`.
744    ///
745    /// # Panics
746    /// Panics if `row >= self.len()`.
747    #[track_caller]
748    #[inline]
749    pub fn value(&self, row: usize) -> u64 {
750        let bw = self.byte_width as usize;
751        let s = row * bw;
752        let mut buf = [0u8; 8];
753        buf[..bw].copy_from_slice(&self.raw[s..s + bw]);
754        u64::from_le_bytes(buf)
755    }
756}
757
758// ---------------------------------------------------------------------------
759// DECIMAL128 / DECIMAL256
760// ---------------------------------------------------------------------------
761
762/// DECIMAL128 column: 16-byte little-endian mantissa per row, single column-
763/// level scale.
764#[derive(Debug, Clone, Copy)]
765pub struct Decimal128Column<'a> {
766    raw: &'a [u8],
767    scale: i8,
768    validity: Validity<'a>,
769}
770
771impl<'a> Decimal128Column<'a> {
772    /// `pub(crate)`: `raw.len() % 16 == 0` invariant is
773    /// `debug_assert!`-only.
774    #[inline]
775    pub(crate) fn new(raw: &'a [u8], validity: Validity<'a>, scale: i8) -> Self {
776        debug_assert_eq!(raw.len() % 16, 0);
777        Self {
778            raw,
779            scale,
780            validity,
781        }
782    }
783
784    #[inline]
785    pub fn len(&self) -> usize {
786        self.raw.len() / 16
787    }
788
789    #[inline]
790    pub fn is_empty(&self) -> bool {
791        self.raw.is_empty()
792    }
793
794    #[inline]
795    pub fn scale(&self) -> i8 {
796        self.scale
797    }
798
799    #[inline]
800    pub fn validity(&self) -> Validity<'a> {
801        self.validity
802    }
803
804    #[inline]
805    pub fn is_null(&self, row: usize) -> bool {
806        self.validity.is_null(row)
807    }
808
809    #[inline]
810    pub fn raw(&self) -> &'a [u8] {
811        self.raw
812    }
813
814    /// Mantissa for `row` as `i128`. Use [`scale`](Self::scale) to
815    /// interpret the decimal point.
816    ///
817    /// # Panics
818    /// Panics if `row >= self.len()`.
819    #[inline]
820    #[track_caller]
821    pub fn value(&self, row: usize) -> i128 {
822        let s = row * 16;
823        i128::from_le_bytes(self.raw[s..s + 16].try_into().expect("16-byte row"))
824    }
825}
826
827/// DECIMAL256 column: 32-byte mantissa per row, single column-level scale.
828///
829/// Rust has no native 256-bit integer; the accessor returns the raw 32
830/// little-endian bytes and leaves higher-level decoding (e.g. via
831/// `bigdecimal`) to the consumer.
832#[derive(Debug, Clone, Copy)]
833pub struct Decimal256Column<'a> {
834    raw: &'a [u8],
835    scale: i8,
836    validity: Validity<'a>,
837}
838
839impl<'a> Decimal256Column<'a> {
840    /// `pub(crate)`: `raw.len() % 32 == 0` invariant is
841    /// `debug_assert!`-only.
842    #[inline]
843    pub(crate) fn new(raw: &'a [u8], validity: Validity<'a>, scale: i8) -> Self {
844        debug_assert_eq!(raw.len() % 32, 0);
845        Self {
846            raw,
847            scale,
848            validity,
849        }
850    }
851
852    #[inline]
853    pub fn len(&self) -> usize {
854        self.raw.len() / 32
855    }
856
857    #[inline]
858    pub fn is_empty(&self) -> bool {
859        self.raw.is_empty()
860    }
861
862    #[inline]
863    pub fn scale(&self) -> i8 {
864        self.scale
865    }
866
867    #[inline]
868    pub fn validity(&self) -> Validity<'a> {
869        self.validity
870    }
871
872    #[inline]
873    pub fn is_null(&self, row: usize) -> bool {
874        self.validity.is_null(row)
875    }
876
877    #[inline]
878    pub fn raw(&self) -> &'a [u8] {
879        self.raw
880    }
881
882    /// Raw 32 LE bytes for `row`. Apply scale via a wider decimal type.
883    ///
884    /// # Panics
885    /// Panics if `row >= self.len()`.
886    #[inline]
887    #[track_caller]
888    pub fn value(&self, row: usize) -> &'a [u8; 32] {
889        let s = row * 32;
890        (&self.raw[s..s + 32]).try_into().expect("32-byte row")
891    }
892}
893
894// ---------------------------------------------------------------------------
895// Width-agnostic decimal view
896// ---------------------------------------------------------------------------
897
898/// Borrowed view over a DECIMAL64, DECIMAL128, or DECIMAL256 column.
899///
900/// The mantissas remain in their decoded little-endian two's-complement
901/// representation. This view does not convert or copy them, and it preserves
902/// the physical QWP width exposed by [`kind`](Self::kind).
903#[derive(Debug, Clone, Copy)]
904pub struct DecimalColumn<'a> {
905    kind: ColumnKind,
906    raw: &'a [u8],
907    scale: i8,
908    validity: Validity<'a>,
909}
910
911impl<'a> DecimalColumn<'a> {
912    #[inline]
913    fn new(kind: ColumnKind, raw: &'a [u8], validity: Validity<'a>, scale: i8) -> Self {
914        debug_assert!(matches!(
915            kind,
916            ColumnKind::Decimal64 | ColumnKind::Decimal128 | ColumnKind::Decimal256
917        ));
918        let column = Self {
919            kind,
920            raw,
921            scale,
922            validity,
923        };
924        debug_assert_eq!(raw.len() % usize::from(column.byte_width()), 0);
925        column
926    }
927
928    /// Physical QWP decimal kind.
929    #[inline]
930    pub fn kind(&self) -> ColumnKind {
931        self.kind
932    }
933
934    /// Mantissa width in bytes: 8, 16, or 32.
935    #[inline]
936    pub fn byte_width(&self) -> u8 {
937        match self.kind {
938            ColumnKind::Decimal64 => 8,
939            ColumnKind::Decimal128 => 16,
940            ColumnKind::Decimal256 => 32,
941            _ => unreachable!("DecimalColumn contains a non-decimal kind"),
942        }
943    }
944
945    /// Maximum SQL precision represented by this physical width.
946    ///
947    /// This is 18, 38, or 76. It is not necessarily the result expression's
948    /// declared precision because QWP carries the physical kind, not the exact
949    /// SQL precision.
950    #[inline]
951    pub fn max_precision(&self) -> u8 {
952        match self.kind {
953            ColumnKind::Decimal64 => 18,
954            ColumnKind::Decimal128 => 38,
955            ColumnKind::Decimal256 => 76,
956            _ => unreachable!("DecimalColumn contains a non-decimal kind"),
957        }
958    }
959
960    #[inline]
961    pub fn scale(&self) -> i8 {
962        self.scale
963    }
964
965    #[inline]
966    pub fn len(&self) -> usize {
967        self.raw.len() / usize::from(self.byte_width())
968    }
969
970    #[inline]
971    pub fn is_empty(&self) -> bool {
972        self.raw.is_empty()
973    }
974
975    #[inline]
976    pub fn validity(&self) -> Validity<'a> {
977        self.validity
978    }
979
980    #[inline]
981    pub fn is_null(&self, row: usize) -> bool {
982        self.validity.is_null(row)
983    }
984
985    /// Dense mantissa bytes for the complete column, including NULL slots.
986    #[inline]
987    pub fn raw(&self) -> &'a [u8] {
988        self.raw
989    }
990
991    /// Little-endian two's-complement mantissa bytes for `row`.
992    ///
993    /// Check [`is_null`](Self::is_null) before interpreting the bytes. Like the
994    /// width-specific decimal accessors, this returns the decoded slot even
995    /// when the row is NULL.
996    ///
997    /// # Panics
998    /// Panics if `row >= self.len()`.
999    #[inline]
1000    #[track_caller]
1001    pub fn mantissa_le(&self, row: usize) -> &'a [u8] {
1002        let len = self.len();
1003        assert!(
1004            row < len,
1005            "DecimalColumn::mantissa_le: row {row} out of range (len={len})"
1006        );
1007        let width = usize::from(self.byte_width());
1008        let start = row * width;
1009        &self.raw[start..start + width]
1010    }
1011}
1012
1013// ---------------------------------------------------------------------------
1014// DOUBLE_ARRAY / LONG_ARRAY
1015// ---------------------------------------------------------------------------
1016
1017/// Borrowed view over per-row shape + flat element bytes for an array
1018/// column. Each row is independently shaped (n-D); null rows have
1019/// zero-length shape and zero-length data slices.
1020///
1021/// Used internally by [`DoubleArrayColumn`] and [`LongArrayColumn`].
1022#[derive(Debug, Clone, Copy)]
1023struct ArrayLayout<'a> {
1024    /// Byte offsets into `data` per row; length `row_count + 1`.
1025    data_offsets: &'a [u32],
1026    /// Concatenated little-endian element bytes for all non-null rows.
1027    data: &'a [u8],
1028    /// Concatenated per-row shape entries.
1029    shapes: &'a [u32],
1030    /// Offsets into `shapes` per row; length `row_count + 1`.
1031    shape_offsets: &'a [u32],
1032    validity: Validity<'a>,
1033}
1034
1035impl<'a> ArrayLayout<'a> {
1036    #[inline]
1037    fn len(&self) -> usize {
1038        self.data_offsets.len().saturating_sub(1)
1039    }
1040
1041    #[inline]
1042    fn shape(&self, row: usize) -> Option<&'a [u32]> {
1043        if self.validity.is_null(row) {
1044            return None;
1045        }
1046        let s = *self.shape_offsets.get(row)? as usize;
1047        let e = *self.shape_offsets.get(row + 1)? as usize;
1048        self.shapes.get(s..e)
1049    }
1050
1051    #[inline]
1052    fn raw(&self, row: usize) -> Option<&'a [u8]> {
1053        if self.validity.is_null(row) {
1054            return None;
1055        }
1056        let s = *self.data_offsets.get(row)? as usize;
1057        let e = *self.data_offsets.get(row + 1)? as usize;
1058        self.data.get(s..e)
1059    }
1060}
1061
1062/// `DOUBLE_ARRAY` column: per-row n-D shape and flat little-endian `f64`
1063/// elements.
1064#[derive(Debug, Clone, Copy)]
1065pub struct DoubleArrayColumn<'a> {
1066    inner: ArrayLayout<'a>,
1067}
1068
1069impl<'a> DoubleArrayColumn<'a> {
1070    /// `pub(crate)`: the four-buffer layout (data_offsets,
1071    /// shape_offsets, shapes, data) must be internally consistent —
1072    /// `data_offsets`/`shape_offsets` non-decreasing, terminating at
1073    /// `data.len()`/`shapes.len()`, and matching the validity bitmap's
1074    /// row count. Out-of-spec inputs cause `element()` / `shape()` to
1075    /// return garbage or panic.
1076    #[inline]
1077    pub(crate) fn new(
1078        data_offsets: &'a [u32],
1079        data: &'a [u8],
1080        shapes: &'a [u32],
1081        shape_offsets: &'a [u32],
1082        validity: Validity<'a>,
1083    ) -> Self {
1084        Self {
1085            inner: ArrayLayout {
1086                data_offsets,
1087                data,
1088                shapes,
1089                shape_offsets,
1090                validity,
1091            },
1092        }
1093    }
1094
1095    #[inline]
1096    pub fn len(&self) -> usize {
1097        self.inner.len()
1098    }
1099
1100    #[inline]
1101    pub fn is_empty(&self) -> bool {
1102        self.inner.len() == 0
1103    }
1104
1105    #[inline]
1106    pub fn validity(&self) -> Validity<'a> {
1107        self.inner.validity
1108    }
1109
1110    #[inline]
1111    pub fn is_null(&self, row: usize) -> bool {
1112        self.inner.validity.is_null(row)
1113    }
1114
1115    /// Per-row shape (`None` for null rows).
1116    #[inline]
1117    pub fn shape(&self, row: usize) -> Option<&'a [u32]> {
1118        self.inner.shape(row)
1119    }
1120
1121    /// Flat little-endian element bytes for `row` (`None` for null rows).
1122    /// Decode each 8-byte chunk as `f64::from_le_bytes`.
1123    #[inline]
1124    pub fn raw(&self, row: usize) -> Option<&'a [u8]> {
1125        self.inner.raw(row)
1126    }
1127
1128    /// Element count for `row` (product of shape; 0 for null rows).
1129    #[inline]
1130    pub fn element_count(&self, row: usize) -> usize {
1131        self.raw(row).map(|b| b.len() / 8).unwrap_or(0)
1132    }
1133
1134    /// Decode element at flat index `idx` of `row`. Caller must respect
1135    /// shape ordering; this is row-major flat indexing.
1136    #[inline]
1137    pub fn element(&self, row: usize, idx: usize) -> Option<f64> {
1138        let bytes = self.raw(row)?;
1139        let s = idx.checked_mul(8)?;
1140        let chunk = bytes.get(s..s + 8)?;
1141        Some(f64::from_le_bytes(chunk.try_into().expect("8 bytes")))
1142    }
1143
1144    /// Concatenated little-endian `f64` element bytes for every row,
1145    /// addressed per row by [`data_offsets`](Self::data_offsets).
1146    #[inline]
1147    pub fn data(&self) -> &'a [u8] {
1148        self.inner.data
1149    }
1150
1151    /// Per-row byte offsets into [`data`](Self::data); `len() + 1` entries,
1152    /// row `r` spanning `[data_offsets[r], data_offsets[r + 1])`.
1153    #[inline]
1154    pub fn data_offsets(&self) -> &'a [u32] {
1155        self.inner.data_offsets
1156    }
1157
1158    /// Concatenated per-row shapes (dimension lengths), addressed per row
1159    /// by [`shape_offsets`](Self::shape_offsets).
1160    #[inline]
1161    pub fn shapes(&self) -> &'a [u32] {
1162        self.inner.shapes
1163    }
1164
1165    /// Per-row offsets into [`shapes`](Self::shapes); `len() + 1` entries.
1166    #[inline]
1167    pub fn shape_offsets(&self) -> &'a [u32] {
1168        self.inner.shape_offsets
1169    }
1170}
1171
1172/// `LONG_ARRAY` column: per-row n-D shape and flat little-endian `i64`
1173/// elements.
1174#[derive(Debug, Clone, Copy)]
1175pub struct LongArrayColumn<'a> {
1176    inner: ArrayLayout<'a>,
1177}
1178
1179impl<'a> LongArrayColumn<'a> {
1180    /// `pub(crate)`: see [`DoubleArrayColumn::new`] — same four-buffer
1181    /// invariants apply.
1182    #[inline]
1183    pub(crate) fn new(
1184        data_offsets: &'a [u32],
1185        data: &'a [u8],
1186        shapes: &'a [u32],
1187        shape_offsets: &'a [u32],
1188        validity: Validity<'a>,
1189    ) -> Self {
1190        Self {
1191            inner: ArrayLayout {
1192                data_offsets,
1193                data,
1194                shapes,
1195                shape_offsets,
1196                validity,
1197            },
1198        }
1199    }
1200
1201    #[inline]
1202    pub fn len(&self) -> usize {
1203        self.inner.len()
1204    }
1205
1206    #[inline]
1207    pub fn is_empty(&self) -> bool {
1208        self.inner.len() == 0
1209    }
1210
1211    #[inline]
1212    pub fn validity(&self) -> Validity<'a> {
1213        self.inner.validity
1214    }
1215
1216    #[inline]
1217    pub fn is_null(&self, row: usize) -> bool {
1218        self.inner.validity.is_null(row)
1219    }
1220
1221    #[inline]
1222    pub fn shape(&self, row: usize) -> Option<&'a [u32]> {
1223        self.inner.shape(row)
1224    }
1225
1226    #[inline]
1227    pub fn raw(&self, row: usize) -> Option<&'a [u8]> {
1228        self.inner.raw(row)
1229    }
1230
1231    #[inline]
1232    pub fn element_count(&self, row: usize) -> usize {
1233        self.raw(row).map(|b| b.len() / 8).unwrap_or(0)
1234    }
1235
1236    #[inline]
1237    pub fn element(&self, row: usize, idx: usize) -> Option<i64> {
1238        let bytes = self.raw(row)?;
1239        let s = idx.checked_mul(8)?;
1240        let chunk = bytes.get(s..s + 8)?;
1241        Some(i64::from_le_bytes(chunk.try_into().expect("8 bytes")))
1242    }
1243
1244    /// Concatenated little-endian `i64` element bytes for every row,
1245    /// addressed per row by [`data_offsets`](Self::data_offsets).
1246    #[inline]
1247    pub fn data(&self) -> &'a [u8] {
1248        self.inner.data
1249    }
1250
1251    /// Per-row byte offsets into [`data`](Self::data); `len() + 1` entries,
1252    /// row `r` spanning `[data_offsets[r], data_offsets[r + 1])`.
1253    #[inline]
1254    pub fn data_offsets(&self) -> &'a [u32] {
1255        self.inner.data_offsets
1256    }
1257
1258    /// Concatenated per-row shapes (dimension lengths), addressed per row
1259    /// by [`shape_offsets`](Self::shape_offsets).
1260    #[inline]
1261    pub fn shapes(&self) -> &'a [u32] {
1262        self.inner.shapes
1263    }
1264
1265    /// Per-row offsets into [`shapes`](Self::shapes); `len() + 1` entries.
1266    #[inline]
1267    pub fn shape_offsets(&self) -> &'a [u32] {
1268        self.inner.shape_offsets
1269    }
1270}
1271
1272// ---------------------------------------------------------------------------
1273// ColumnView discriminated union
1274// ---------------------------------------------------------------------------
1275
1276/// Typed view over a single column in a `RESULT_BATCH`.
1277///
1278/// Covers every column kind the QWP egress decoder produces today:
1279/// fixed-width numerics and temporals, UUID, Long256, Char, Symbol,
1280/// Decimal64/128/256, Geohash, Varchar, Binary, and DOUBLE_ARRAY /
1281/// LONG_ARRAY.
1282#[derive(Debug, Clone, Copy)]
1283#[non_exhaustive]
1284pub enum ColumnView<'a> {
1285    Boolean(FixedColumn<'a, u8>),
1286    Byte(FixedColumn<'a, i8>),
1287    Short(FixedColumn<'a, i16>),
1288    Int(FixedColumn<'a, i32>),
1289    Long(FixedColumn<'a, i64>),
1290    Float(FixedColumn<'a, f32>),
1291    Double(FixedColumn<'a, f64>),
1292    Symbol(SymbolColumn<'a>),
1293    /// Microsecond-precision timestamp (i64 LE).
1294    Timestamp(FixedColumn<'a, i64>),
1295    /// Millisecond-precision date (i64 LE).
1296    Date(FixedColumn<'a, i64>),
1297    Uuid(UuidColumn<'a>),
1298    Long256(Long256Column<'a>),
1299    /// Nanosecond-precision timestamp (i64 LE).
1300    TimestampNanos(FixedColumn<'a, i64>),
1301    Decimal64(Decimal64Column<'a>),
1302    /// QuestDB CHAR is a 2-byte UTF-16 code unit.
1303    Char(FixedColumn<'a, u16>),
1304    /// IPv4 address as a host-order u32 (server emits LE).
1305    Ipv4(FixedColumn<'a, u32>),
1306    Varchar(VarcharColumn<'a>),
1307    Binary(BinaryColumn<'a>),
1308    Geohash(GeohashColumn<'a>),
1309    Decimal128(Decimal128Column<'a>),
1310    Decimal256(Decimal256Column<'a>),
1311    DoubleArray(DoubleArrayColumn<'a>),
1312    LongArray(LongArrayColumn<'a>),
1313}
1314
1315impl<'a> ColumnView<'a> {
1316    #[inline]
1317    pub fn kind(&self) -> ColumnKind {
1318        match self {
1319            ColumnView::Boolean(_) => ColumnKind::Boolean,
1320            ColumnView::Byte(_) => ColumnKind::Byte,
1321            ColumnView::Short(_) => ColumnKind::Short,
1322            ColumnView::Int(_) => ColumnKind::Int,
1323            ColumnView::Long(_) => ColumnKind::Long,
1324            ColumnView::Float(_) => ColumnKind::Float,
1325            ColumnView::Double(_) => ColumnKind::Double,
1326            ColumnView::Symbol(_) => ColumnKind::Symbol,
1327            ColumnView::Timestamp(_) => ColumnKind::Timestamp,
1328            ColumnView::Date(_) => ColumnKind::Date,
1329            ColumnView::Uuid(_) => ColumnKind::Uuid,
1330            ColumnView::Long256(_) => ColumnKind::Long256,
1331            ColumnView::TimestampNanos(_) => ColumnKind::TimestampNanos,
1332            ColumnView::Decimal64(_) => ColumnKind::Decimal64,
1333            ColumnView::Char(_) => ColumnKind::Char,
1334            ColumnView::Ipv4(_) => ColumnKind::Ipv4,
1335            ColumnView::Varchar(_) => ColumnKind::Varchar,
1336            ColumnView::Binary(_) => ColumnKind::Binary,
1337            ColumnView::Geohash(_) => ColumnKind::Geohash,
1338            ColumnView::Decimal128(_) => ColumnKind::Decimal128,
1339            ColumnView::Decimal256(_) => ColumnKind::Decimal256,
1340            ColumnView::DoubleArray(_) => ColumnKind::DoubleArray,
1341            ColumnView::LongArray(_) => ColumnKind::LongArray,
1342        }
1343    }
1344
1345    /// Project any DECIMAL64/128/256 variant to one width-agnostic view.
1346    ///
1347    /// The returned view borrows the same decoded buffers and performs no
1348    /// conversion or copy. Returns `None` for non-decimal columns.
1349    #[inline]
1350    pub fn as_decimal(&self) -> Option<DecimalColumn<'a>> {
1351        match self {
1352            ColumnView::Decimal64(c) => Some(DecimalColumn::new(
1353                ColumnKind::Decimal64,
1354                c.raw(),
1355                c.validity(),
1356                c.scale(),
1357            )),
1358            ColumnView::Decimal128(c) => Some(DecimalColumn::new(
1359                ColumnKind::Decimal128,
1360                c.raw(),
1361                c.validity(),
1362                c.scale(),
1363            )),
1364            ColumnView::Decimal256(c) => Some(DecimalColumn::new(
1365                ColumnKind::Decimal256,
1366                c.raw(),
1367                c.validity(),
1368                c.scale(),
1369            )),
1370            _ => None,
1371        }
1372    }
1373
1374    #[inline]
1375    pub fn len(&self) -> usize {
1376        match self {
1377            ColumnView::Boolean(c) => c.len(),
1378            ColumnView::Byte(c) => c.len(),
1379            ColumnView::Short(c) => c.len(),
1380            ColumnView::Int(c) => c.len(),
1381            ColumnView::Long(c) => c.len(),
1382            ColumnView::Float(c) => c.len(),
1383            ColumnView::Double(c) => c.len(),
1384            ColumnView::Symbol(c) => c.len(),
1385            ColumnView::Timestamp(c) => c.len(),
1386            ColumnView::Date(c) => c.len(),
1387            ColumnView::Uuid(c) => c.len(),
1388            ColumnView::Long256(c) => c.len(),
1389            ColumnView::TimestampNanos(c) => c.len(),
1390            ColumnView::Decimal64(c) => c.len(),
1391            ColumnView::Char(c) => c.len(),
1392            ColumnView::Ipv4(c) => c.len(),
1393            ColumnView::Varchar(c) => c.len(),
1394            ColumnView::Binary(c) => c.len(),
1395            ColumnView::Geohash(c) => c.len(),
1396            ColumnView::Decimal128(c) => c.len(),
1397            ColumnView::Decimal256(c) => c.len(),
1398            ColumnView::DoubleArray(c) => c.len(),
1399            ColumnView::LongArray(c) => c.len(),
1400        }
1401    }
1402
1403    #[inline]
1404    pub fn is_empty(&self) -> bool {
1405        self.len() == 0
1406    }
1407
1408    #[inline]
1409    pub fn is_null(&self, row: usize) -> bool {
1410        match self {
1411            ColumnView::Boolean(c) => c.is_null(row),
1412            ColumnView::Byte(c) => c.is_null(row),
1413            ColumnView::Short(c) => c.is_null(row),
1414            ColumnView::Int(c) => c.is_null(row),
1415            ColumnView::Long(c) => c.is_null(row),
1416            ColumnView::Float(c) => c.is_null(row),
1417            ColumnView::Double(c) => c.is_null(row),
1418            ColumnView::Symbol(c) => c.is_null(row),
1419            ColumnView::Timestamp(c) => c.is_null(row),
1420            ColumnView::Date(c) => c.is_null(row),
1421            ColumnView::Uuid(c) => c.is_null(row),
1422            ColumnView::Long256(c) => c.is_null(row),
1423            ColumnView::TimestampNanos(c) => c.is_null(row),
1424            ColumnView::Decimal64(c) => c.is_null(row),
1425            ColumnView::Char(c) => c.is_null(row),
1426            ColumnView::Ipv4(c) => c.is_null(row),
1427            ColumnView::Varchar(c) => c.is_null(row),
1428            ColumnView::Binary(c) => c.is_null(row),
1429            ColumnView::Geohash(c) => c.is_null(row),
1430            ColumnView::Decimal128(c) => c.is_null(row),
1431            ColumnView::Decimal256(c) => c.is_null(row),
1432            ColumnView::DoubleArray(c) => c.is_null(row),
1433            ColumnView::LongArray(c) => c.is_null(row),
1434        }
1435    }
1436
1437    #[inline]
1438    pub fn validity<'b>(&'b self) -> Validity<'b> {
1439        match self {
1440            ColumnView::Boolean(c) => c.validity(),
1441            ColumnView::Byte(c) => c.validity(),
1442            ColumnView::Short(c) => c.validity(),
1443            ColumnView::Int(c) => c.validity(),
1444            ColumnView::Long(c) => c.validity(),
1445            ColumnView::Float(c) => c.validity(),
1446            ColumnView::Double(c) => c.validity(),
1447            ColumnView::Symbol(c) => c.validity(),
1448            ColumnView::Timestamp(c) => c.validity(),
1449            ColumnView::Date(c) => c.validity(),
1450            ColumnView::Uuid(c) => c.validity(),
1451            ColumnView::Long256(c) => c.validity(),
1452            ColumnView::TimestampNanos(c) => c.validity(),
1453            ColumnView::Decimal64(c) => c.validity(),
1454            ColumnView::Char(c) => c.validity(),
1455            ColumnView::Ipv4(c) => c.validity(),
1456            ColumnView::Varchar(c) => c.validity(),
1457            ColumnView::Binary(c) => c.validity(),
1458            ColumnView::Geohash(c) => c.validity(),
1459            ColumnView::Decimal128(c) => c.validity(),
1460            ColumnView::Decimal256(c) => c.validity(),
1461            ColumnView::DoubleArray(c) => c.validity(),
1462            ColumnView::LongArray(c) => c.validity(),
1463        }
1464    }
1465}
1466
1467// ---------------------------------------------------------------------------
1468// Tests
1469// ---------------------------------------------------------------------------
1470
1471#[cfg(test)]
1472mod tests {
1473    use super::*;
1474
1475    fn le_i64s(values: &[i64]) -> Vec<u8> {
1476        let mut out = Vec::with_capacity(values.len() * 8);
1477        for v in values {
1478            out.extend_from_slice(&v.to_le_bytes());
1479        }
1480        out
1481    }
1482
1483    fn le_f64s(values: &[f64]) -> Vec<u8> {
1484        let mut out = Vec::with_capacity(values.len() * 8);
1485        for v in values {
1486            out.extend_from_slice(&v.to_le_bytes());
1487        }
1488        out
1489    }
1490
1491    #[test]
1492    fn validity_no_bitmap() {
1493        let v = Validity::None;
1494        assert!(!v.has_nulls());
1495        for r in 0..10 {
1496            assert!(!v.is_null(r));
1497        }
1498    }
1499
1500    #[test]
1501    fn validity_bitmap_lsb_first_one_is_null() {
1502        // 8 rows: row0=null, row1=valid, row2=null, row3..7=valid
1503        // bitmap byte: 0b0000_0101 = 0x05
1504        let bytes = [0x05];
1505        let v = Validity::from_bitmap(&bytes, 8).unwrap();
1506        assert!(v.is_null(0));
1507        assert!(!v.is_null(1));
1508        assert!(v.is_null(2));
1509        for r in 3..8 {
1510            assert!(!v.is_null(r));
1511        }
1512    }
1513
1514    #[test]
1515    fn validity_bitmap_spans_bytes() {
1516        // 10 rows, only row 9 is null → byte 0 = 0, byte 1 = 0b0000_0010 = 0x02
1517        let bytes = [0x00, 0x02];
1518        let v = Validity::from_bitmap(&bytes, 10).unwrap();
1519        for r in 0..9 {
1520            assert!(!v.is_null(r));
1521        }
1522        assert!(v.is_null(9));
1523    }
1524
1525    #[test]
1526    fn validity_bitmap_exact_length_accepted() {
1527        // The decoder always sizes the bitmap to ceil(row_count / 8).
1528        // `from_bitmap` must accept exact-length buffers.
1529        let bytes = [0x00u8; 13]; // ceil(100 / 8) = 13
1530        let v = Validity::from_bitmap(&bytes, 100).unwrap();
1531        for r in 0..100 {
1532            assert!(!v.is_null(r));
1533        }
1534    }
1535
1536    #[test]
1537    fn validity_bitmap_short_rejected_in_constructor() {
1538        // 100 rows need ceil(100 / 8) = 13 bytes; supplying 0 must
1539        // surface InvalidApiCall up front rather than silently treat
1540        // null rows as non-null.
1541        let bytes: [u8; 0] = [];
1542        let err = Validity::from_bitmap(&bytes, 100).unwrap_err();
1543        assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
1544        assert!(err.msg().contains("Validity::from_bitmap: bitmap is"));
1545    }
1546
1547    #[test]
1548    fn validity_bitmap_off_by_one_rejected() {
1549        // 9 rows need 2 bytes; supplying 1 must surface InvalidApiCall.
1550        let bytes = [0xFFu8];
1551        let err = Validity::from_bitmap(&bytes, 9).unwrap_err();
1552        assert_eq!(err.code(), crate::ErrorCode::InvalidApiCall);
1553    }
1554
1555    #[test]
1556    fn validity_bitmap_direct_construction_short_does_not_panic() {
1557        // External code that bypasses `from_bitmap` and builds the
1558        // variant literally with a too-short bitmap is technically
1559        // outside the type's contract. `is_null` MUST NOT panic for
1560        // this case; the missing tail is reported as "not null"
1561        // (matching the pre-validation behavior of `bytes.get(...)`).
1562        // Properly-constructed `Validity` values can never trip this
1563        // path because `from_bitmap` rejects short bitmaps up front.
1564        let bytes: [u8; 0] = [];
1565        let v = Validity::Bitmap {
1566            bytes: &bytes,
1567            row_count: 100,
1568        };
1569        assert!(!v.is_null(50));
1570    }
1571
1572    #[test]
1573    fn fixed_i64_value_and_iter() {
1574        let raw = le_i64s(&[1, -2, 0x0102_0304_0506_0708]);
1575        let col = FixedColumn::<i64>::new(&raw, Validity::None);
1576        assert_eq!(col.len(), 3);
1577        assert_eq!(col.value(0), 1);
1578        assert_eq!(col.value(1), -2);
1579        assert_eq!(col.value(2), 0x0102_0304_0506_0708);
1580        let collected: Vec<_> = col.iter().collect();
1581        assert_eq!(
1582            collected,
1583            vec![Some(1i64), Some(-2), Some(0x0102_0304_0506_0708)]
1584        );
1585    }
1586
1587    #[test]
1588    fn fixed_f64_with_nulls() {
1589        let raw = le_f64s(&[1.0, 2.0, 3.0, 4.0]);
1590        // row 1 null → bitmap 0b0000_0010 = 0x02
1591        let bm = [0x02];
1592        let col = FixedColumn::<f64>::new(&raw, Validity::from_bitmap(&bm, 4).unwrap());
1593        let collected: Vec<_> = col.iter().collect();
1594        assert_eq!(collected, vec![Some(1.0), None, Some(3.0), Some(4.0)]);
1595    }
1596
1597    #[test]
1598    fn fixed_i32_le() {
1599        let raw = vec![0x04u8, 0x03, 0x02, 0x01]; // 0x01020304 LE
1600        let col = FixedColumn::<i32>::new(&raw, Validity::None);
1601        assert_eq!(col.len(), 1);
1602        assert_eq!(col.value(0), 0x01020304);
1603    }
1604
1605    #[test]
1606    fn fixed_bool_via_u8() {
1607        let raw = vec![0x00u8, 0x01, 0x00];
1608        let col = FixedColumn::<u8>::new(&raw, Validity::None);
1609        assert_eq!(col.value(0), 0);
1610        assert_eq!(col.value(1), 1);
1611    }
1612
1613    #[test]
1614    fn uuid_value_returns_array() {
1615        let raw: Vec<u8> = (0..32u8).collect();
1616        let col = UuidColumn::new(&raw, Validity::None);
1617        assert_eq!(col.len(), 2);
1618        assert_eq!(col.value(0)[0], 0);
1619        assert_eq!(col.value(0)[15], 15);
1620        assert_eq!(col.value(1)[0], 16);
1621        assert_eq!(col.value(1)[15], 31);
1622    }
1623
1624    #[test]
1625    fn long256_value_returns_32_bytes() {
1626        let raw: Vec<u8> = (0..32u8).collect();
1627        let col = Long256Column::new(&raw, Validity::None);
1628        assert_eq!(col.len(), 1);
1629        assert_eq!(col.value(0).len(), 32);
1630        assert_eq!(col.value(0)[31], 31);
1631    }
1632
1633    #[test]
1634    fn symbol_resolves_codes_through_dict() {
1635        let mut dict = SymbolDict::new();
1636        dict.apply_delta(
1637            0,
1638            [b"AAPL".as_slice(), b"MSFT".as_slice(), b"GOOG".as_slice()],
1639        )
1640        .unwrap();
1641
1642        // 4 rows: AAPL, NULL, MSFT, GOOG. Bitmap row1 null → 0b0000_0010 = 0x02
1643        // Codes are dense per row, with `0` (garbage) in the null slot.
1644        let codes = [0u32, 0, 1, 2];
1645        let bm = [0x02u8];
1646        let col = SymbolColumn::new(&codes, Validity::from_bitmap(&bm, 4).unwrap(), &dict);
1647
1648        assert_eq!(col.len(), 4);
1649        assert_eq!(col.resolve(0), Some("AAPL"));
1650        assert_eq!(col.resolve(1), None);
1651        assert_eq!(col.resolve(2), Some("MSFT"));
1652        assert_eq!(col.resolve(3), Some("GOOG"));
1653    }
1654
1655    #[test]
1656    fn symbol_no_nulls_path() {
1657        let mut dict = SymbolDict::new();
1658        dict.apply_delta(0, [b"x".as_slice(), b"y".as_slice()])
1659            .unwrap();
1660        let codes = [1u32, 0, 1];
1661        let col = SymbolColumn::new(&codes, Validity::None, &dict);
1662        assert_eq!(col.resolve(0), Some("y"));
1663        assert_eq!(col.resolve(1), Some("x"));
1664        assert_eq!(col.resolve(2), Some("y"));
1665    }
1666
1667    #[test]
1668    fn decimal64_carries_scale() {
1669        let raw = le_i64s(&[12345, 6789]);
1670        let col = Decimal64Column::new(&raw, Validity::None, 2);
1671        assert_eq!(col.scale(), 2);
1672        assert_eq!(col.value(0), 12345);
1673        assert_eq!(col.value(1), 6789);
1674    }
1675
1676    #[test]
1677    fn column_view_as_decimal_unifies_widths() {
1678        let raw64 = le_i64s(&[12345, -678]);
1679        let bitmap = [0x02u8];
1680        let view64 = ColumnView::Decimal64(Decimal64Column::new(
1681            &raw64,
1682            Validity::from_bitmap(&bitmap, 2).unwrap(),
1683            2,
1684        ));
1685        let decimal64 = view64.as_decimal().unwrap();
1686        assert_eq!(decimal64.kind(), ColumnKind::Decimal64);
1687        assert_eq!(decimal64.byte_width(), 8);
1688        assert_eq!(decimal64.max_precision(), 18);
1689        assert_eq!(decimal64.scale(), 2);
1690        assert_eq!(decimal64.len(), 2);
1691        assert!(!decimal64.is_empty());
1692        assert_eq!(decimal64.raw(), raw64.as_slice());
1693        assert_eq!(decimal64.mantissa_le(0), &raw64[..8]);
1694        assert!(decimal64.is_null(1));
1695        assert_eq!(decimal64.mantissa_le(1), &raw64[8..16]);
1696        assert_eq!(decimal64.validity().bytes(), Some(bitmap.as_slice()));
1697
1698        let raw128 = (-1_i128).to_le_bytes();
1699        let view128 = ColumnView::Decimal128(Decimal128Column::new(&raw128, Validity::None, 4));
1700        let decimal128 = view128.as_decimal().unwrap();
1701        assert_eq!(decimal128.kind(), ColumnKind::Decimal128);
1702        assert_eq!(decimal128.byte_width(), 16);
1703        assert_eq!(decimal128.max_precision(), 38);
1704        assert_eq!(decimal128.scale(), 4);
1705        assert_eq!(decimal128.mantissa_le(0), raw128.as_slice());
1706
1707        let raw256 = [0xFFu8; 32];
1708        let view256 = ColumnView::Decimal256(Decimal256Column::new(&raw256, Validity::None, 6));
1709        let decimal256 = view256.as_decimal().unwrap();
1710        assert_eq!(decimal256.kind(), ColumnKind::Decimal256);
1711        assert_eq!(decimal256.byte_width(), 32);
1712        assert_eq!(decimal256.max_precision(), 76);
1713        assert_eq!(decimal256.scale(), 6);
1714        assert_eq!(decimal256.mantissa_le(0), raw256.as_slice());
1715
1716        let non_decimal = ColumnView::Long(FixedColumn::<i64>::new(&raw64, Validity::None));
1717        assert!(non_decimal.as_decimal().is_none());
1718    }
1719
1720    #[test]
1721    #[should_panic(expected = "DecimalColumn::mantissa_le: row 1 out of range (len=1)")]
1722    fn decimal_column_mantissa_le_panics_out_of_range() {
1723        let raw = 1_i64.to_le_bytes();
1724        let view = ColumnView::Decimal64(Decimal64Column::new(&raw, Validity::None, 0));
1725        view.as_decimal().unwrap().mantissa_le(1);
1726    }
1727
1728    #[test]
1729    #[should_panic(expected = "DecimalColumn::mantissa_le: row")]
1730    fn decimal_column_mantissa_le_panics_before_offset_wraps() {
1731        let raw = 1_i64.to_le_bytes();
1732        let view = ColumnView::Decimal64(Decimal64Column::new(&raw, Validity::None, 0));
1733        let wrapping_row = 1usize << (usize::BITS - 3);
1734        view.as_decimal().unwrap().mantissa_le(wrapping_row);
1735    }
1736
1737    #[test]
1738    fn column_view_kind_matches_inner() {
1739        let raw = le_i64s(&[1, 2]);
1740        let v = ColumnView::Long(FixedColumn::<i64>::new(&raw, Validity::None));
1741        assert_eq!(v.kind(), ColumnKind::Long);
1742        assert_eq!(v.len(), 2);
1743
1744        let v = ColumnView::TimestampNanos(FixedColumn::<i64>::new(&raw, Validity::None));
1745        assert_eq!(v.kind(), ColumnKind::TimestampNanos);
1746
1747        let v = ColumnView::Decimal64(Decimal64Column::new(&raw, Validity::None, 4));
1748        assert_eq!(v.kind(), ColumnKind::Decimal64);
1749    }
1750
1751    #[test]
1752    fn column_view_is_null_dispatches() {
1753        let raw = le_i64s(&[1, 2, 3]);
1754        let bm = [0x02u8]; // row 1 null
1755        let v = ColumnView::Long(FixedColumn::<i64>::new(
1756            &raw,
1757            Validity::from_bitmap(&bm, 3).unwrap(),
1758        ));
1759        assert!(!v.is_null(0));
1760        assert!(v.is_null(1));
1761        assert!(!v.is_null(2));
1762    }
1763}