Skip to main content

cqlite_core/storage/sstable/bti/
node.rs

1//! BTI trie node implementations
2//!
3//! This module defines the trie node structures and operations for the BTI format.
4//! Each node type is optimized for different branching patterns and storage efficiency.
5
6use crate::error::{Error, Result};
7use std::fmt;
8
9/// BTI-specific result type
10pub type BtiResult<T> = Result<T>;
11
12/// BTI-specific error types
13#[derive(Debug, Clone)]
14pub enum BtiError {
15    /// Parse error with details
16    Parse(String),
17    /// Invalid node structure
18    InvalidNodeStructure(String),
19    /// Navigation error during trie traversal
20    NavigationError(String),
21    /// Invalid node type
22    InvalidNodeType(u8),
23    /// Maximum depth exceeded
24    MaxDepthExceeded(usize),
25    /// Invalid byte-comparable key
26    InvalidByteComparableKey(String),
27    /// Corrupted trie structure
28    CorruptedTrie(String),
29    /// Missing component file
30    MissingComponent(String),
31}
32
33impl fmt::Display for BtiError {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            BtiError::Parse(msg) => write!(f, "BTI parse error: {}", msg),
37            BtiError::InvalidNodeStructure(msg) => write!(f, "Invalid BTI node structure: {}", msg),
38            BtiError::NavigationError(msg) => write!(f, "BTI navigation error: {}", msg),
39            BtiError::InvalidNodeType(node_type) => {
40                write!(f, "Invalid BTI trie node type: 0x{:02X}", node_type)
41            }
42            BtiError::MaxDepthExceeded(depth) => {
43                write!(f, "BTI trie depth exceeded maximum: {}", depth)
44            }
45            BtiError::InvalidByteComparableKey(key) => {
46                write!(f, "Invalid byte-comparable key: {}", key)
47            }
48            BtiError::CorruptedTrie(msg) => {
49                write!(f, "Corrupted BTI trie structure: {}", msg)
50            }
51            BtiError::MissingComponent(component) => {
52                write!(f, "Missing BTI component: {}", component)
53            }
54        }
55    }
56}
57
58impl std::error::Error for BtiError {}
59
60impl From<BtiError> for Error {
61    fn from(err: BtiError) -> Self {
62        Error::Parse(format!("BTI error: {}", err))
63    }
64}
65
66/// BTI node types corresponding to the four trie node variants
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum BtiNodeType {
69    /// Payload-only node (leaf)
70    PayloadOnly,
71    /// Single child node
72    Single,
73    /// Sparse node with few children
74    Sparse,
75    /// Dense node with many consecutive children
76    Dense,
77}
78
79impl BtiNodeType {
80    /// Get expected children range for this node type
81    pub fn expected_children_range(&self) -> (usize, Option<usize>) {
82        match self {
83            BtiNodeType::PayloadOnly => (0, Some(0)),
84            BtiNodeType::Single => (1, Some(1)),
85            BtiNodeType::Sparse => (2, Some(256)), // Reasonable upper bound
86            BtiNodeType::Dense => (1, Some(256)),  // Full byte range
87        }
88    }
89}
90
91impl fmt::Display for BtiNodeType {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        match self {
94            BtiNodeType::PayloadOnly => write!(f, "PayloadOnly"),
95            BtiNodeType::Single => write!(f, "Single"),
96            BtiNodeType::Sparse => write!(f, "Sparse"),
97            BtiNodeType::Dense => write!(f, "Dense"),
98        }
99    }
100}
101
102/// Resolved child pointer: the ABSOLUTE trie offset of a child node.
103///
104/// The on-disk backward-delta is resolved to an absolute offset
105/// (`parent_offset - delta`) at decode time and stored in `distance`.
106///
107/// Issue #1650 (L3) removed the dead `size` field (the pointer-encoding width, in
108/// bytes) and its `to_bytes`/`from_bytes` helpers: nothing on the read path or the
109/// canonical BTI (`da`) write path ever consumed them — the width is derived from
110/// the node-type ordinal during (de)serialization, never from a per-pointer field.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct SizedPointer {
113    /// Absolute offset of the target child node (`parent_offset - backward_delta`).
114    pub distance: u64,
115}
116
117// Struct-size regression guard (issue #1616, Epic H/H3; see
118// docs/reports/parser-performance-audit-2026-07-01.md §Epic H (finding H3)). BTI trie
119// pointer decoded once per transition during partition lookup on the read hot
120// path. Measured 8 bytes today (a single u64) on 64-bit targets after issue #1650
121// (L3) dropped the dead 1-byte `size` field (was 16 padded). Update this pin
122// DELIBERATELY, never silently: any change — growth or shrink — must be a
123// reviewed edit here.
124#[cfg(target_pointer_width = "64")]
125const _: () = assert!(std::mem::size_of::<SizedPointer>() == 8);
126
127impl SizedPointer {
128    /// Create a pointer to the child at absolute offset `distance`.
129    pub fn new(distance: u64) -> Self {
130        Self { distance }
131    }
132}
133
134/// Trie node transition representing a path to a child node
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct Transition {
137    /// The byte value for this transition
138    pub byte: u8,
139    /// Pointer to the child node
140    pub child: SizedPointer,
141}
142
143// Struct-size regression guard (issue #1616, Epic H/H3; see
144// docs/reports/parser-performance-audit-2026-07-01.md §Epic H (finding H3)). One BTI
145// `Transition` is decoded per trie edge walked during partition lookup on the
146// read hot path. Measured 16 bytes today (u8 + `SizedPointer`{u64}, padded) on
147// 64-bit targets after issue #1650 (L3) shrank `SizedPointer` to a bare u64.
148// Update this pin DELIBERATELY, never silently: any change — growth or shrink —
149// must be a reviewed edit here.
150#[cfg(target_pointer_width = "64")]
151const _: () = assert!(std::mem::size_of::<Transition>() == 16);
152
153impl Transition {
154    pub fn new(byte: u8, child: SizedPointer) -> Self {
155        Self { byte, child }
156    }
157}
158
159/// Payload reference for leaf nodes
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct PayloadRef {
162    /// Offset to the payload data
163    pub offset: u64,
164    /// Length of the payload data
165    pub length: u32,
166    /// Optional checksum for validation
167    pub checksum: Option<u32>,
168}
169
170// Struct-size regression guard (issue #1616, Epic H/H3; see
171// docs/reports/parser-performance-audit-2026-07-01.md §Epic H (finding H3)). One BTI
172// `PayloadRef` is produced per matched leaf during partition lookup on the read
173// hot path. Measured 24 bytes today (u64 + u32 + Option<u32>, padded) on 64-bit
174// targets. Update this pin DELIBERATELY, never silently: any change — growth or
175// shrink — must be a reviewed edit here.
176#[cfg(target_pointer_width = "64")]
177const _: () = assert!(std::mem::size_of::<PayloadRef>() == 24);
178
179impl PayloadRef {
180    pub fn new(offset: u64, length: u32) -> Self {
181        Self {
182            offset,
183            length,
184            checksum: None,
185        }
186    }
187
188    pub fn with_checksum(mut self, checksum: u32) -> Self {
189        self.checksum = Some(checksum);
190        self
191    }
192}
193
194/// Base trie node structure
195#[derive(Debug, Clone)]
196pub struct BtiNode {
197    /// Type of this node
198    pub node_type: BtiNodeType,
199    /// Level in the trie (0 = leaf level)
200    pub level: u16,
201    /// Key prefix stored at this node (for optimization)
202    pub key_prefix: Vec<u8>,
203    /// Node-specific data
204    pub data: BtiNodeData,
205}
206
207/// Node-specific data based on node type
208#[derive(Debug, Clone)]
209pub enum BtiNodeData {
210    /// Payload-only node (leaf)
211    PayloadOnly { payload: PayloadRef },
212
213    /// Single child node
214    Single { transition: Transition },
215
216    /// Sparse node with few children
217    Sparse { transitions: Vec<Transition> },
218
219    /// Dense node with many consecutive children
220    Dense {
221        /// Starting byte value for the consecutive range
222        start_byte: u8,
223        /// Child pointers for the consecutive range.
224        ///
225        /// Each slot represents the transition `start_byte + index`.  `None`
226        /// means "no transition" (the raw Dense delta was `0`, the sentinel);
227        /// `Some(ptr)` is a real child.  Presence is tracked explicitly because
228        /// a REAL child can legitimately live at absolute trie offset `0` (the
229        /// first-written leaf in BTI's bottom-up layout): an offset of `0` is a
230        /// valid child and must NOT be confused with the absent-transition
231        /// sentinel.
232        children: Vec<Option<SizedPointer>>,
233    },
234}
235
236impl BtiNode {
237    /// Create a payload-only node
238    pub fn payload_only(level: u16, key_prefix: Vec<u8>, payload: PayloadRef) -> Self {
239        Self {
240            node_type: BtiNodeType::PayloadOnly,
241            level,
242            key_prefix,
243            data: BtiNodeData::PayloadOnly { payload },
244        }
245    }
246
247    /// Create a single child node
248    pub fn single(level: u16, key_prefix: Vec<u8>, transition: Transition) -> Self {
249        Self {
250            node_type: BtiNodeType::Single,
251            level,
252            key_prefix,
253            data: BtiNodeData::Single { transition },
254        }
255    }
256
257    /// Create a sparse node
258    pub fn sparse(level: u16, key_prefix: Vec<u8>, mut transitions: Vec<Transition>) -> Self {
259        // Ensure transitions are sorted by byte value for binary search
260        transitions.sort_by_key(|t| t.byte);
261
262        Self {
263            node_type: BtiNodeType::Sparse,
264            level,
265            key_prefix,
266            data: BtiNodeData::Sparse { transitions },
267        }
268    }
269
270    /// Create a dense node
271    ///
272    /// `children[i]` is the transition for byte `start_byte + i`: `None` for a
273    /// missing transition (raw delta `0`), `Some(ptr)` for a real child (which
274    /// may point at absolute offset `0`).
275    pub fn dense(
276        level: u16,
277        key_prefix: Vec<u8>,
278        start_byte: u8,
279        children: Vec<Option<SizedPointer>>,
280    ) -> Self {
281        Self {
282            node_type: BtiNodeType::Dense,
283            level,
284            key_prefix,
285            data: BtiNodeData::Dense {
286                start_byte,
287                children,
288            },
289        }
290    }
291
292    /// Find the child node pointer for a given byte
293    pub fn find_child(&self, byte: u8) -> Option<&SizedPointer> {
294        match &self.data {
295            BtiNodeData::PayloadOnly { .. } => None,
296
297            BtiNodeData::Single { transition } => {
298                if transition.byte == byte {
299                    Some(&transition.child)
300                } else {
301                    None
302                }
303            }
304
305            BtiNodeData::Sparse { transitions } => {
306                // Binary search on sorted transitions
307                transitions
308                    .binary_search_by_key(&byte, |t| t.byte)
309                    .ok()
310                    .map(|idx| &transitions[idx].child)
311            }
312
313            BtiNodeData::Dense {
314                start_byte,
315                children,
316            } => {
317                if byte >= *start_byte && (byte as usize) < (*start_byte as usize + children.len())
318                {
319                    let index = byte as usize - *start_byte as usize;
320                    // `None` means "no transition" for this byte; `Some(ptr)` is
321                    // a real child (possibly at absolute offset 0).
322                    children.get(index).and_then(|slot| slot.as_ref())
323                } else {
324                    None
325                }
326            }
327        }
328    }
329
330    /// Get the payload reference if this is a leaf node
331    pub fn get_payload(&self) -> Option<&PayloadRef> {
332        match &self.data {
333            BtiNodeData::PayloadOnly { payload } => Some(payload),
334            _ => None,
335        }
336    }
337
338    /// Check if this node is a leaf (has payload)
339    pub fn is_leaf(&self) -> bool {
340        matches!(self.data, BtiNodeData::PayloadOnly { .. })
341    }
342
343    /// Get the number of children
344    pub fn child_count(&self) -> usize {
345        match &self.data {
346            BtiNodeData::PayloadOnly { .. } => 0,
347            BtiNodeData::Single { .. } => 1,
348            BtiNodeData::Sparse { transitions } => transitions.len(),
349            BtiNodeData::Dense { children, .. } => children.len(),
350        }
351    }
352
353    /// Validate node structure consistency
354    pub fn validate(&self) -> BtiResult<()> {
355        let expected_range = self.node_type.expected_children_range();
356        let child_count = self.child_count();
357
358        // Check child count is within expected range
359        if child_count < expected_range.0 {
360            return Err(BtiError::InvalidNodeStructure(format!(
361                "Node type {} has {} children, expected at least {}",
362                self.node_type, child_count, expected_range.0
363            ))
364            .into());
365        }
366
367        if let Some(max) = expected_range.1 {
368            if child_count > max {
369                return Err(BtiError::InvalidNodeStructure(format!(
370                    "Node type {} has {} children, expected at most {}",
371                    self.node_type, child_count, max
372                ))
373                .into());
374            }
375        }
376
377        // Type-specific validation
378        match &self.data {
379            BtiNodeData::Sparse { transitions } => {
380                // Check that transitions are sorted
381                for window in transitions.windows(2) {
382                    if window[0].byte >= window[1].byte {
383                        return Err(BtiError::InvalidNodeStructure(
384                            "Sparse node transitions not sorted".to_string(),
385                        )
386                        .into());
387                    }
388                }
389            }
390
391            BtiNodeData::Dense {
392                start_byte,
393                children,
394            } => {
395                // Check that we don't overflow byte range
396                let end_byte = *start_byte as usize + children.len();
397                if end_byte > 256 {
398                    return Err(BtiError::InvalidNodeStructure(
399                        "Dense node range overflows byte values".to_string(),
400                    )
401                    .into());
402                }
403            }
404
405            _ => {} // Other types don't need special validation
406        }
407
408        Ok(())
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    #[test]
417    fn test_sized_pointer() {
418        // Issue #1650 (L3): `SizedPointer` is now a bare absolute-offset holder;
419        // the dead `size`/`to_bytes`/`from_bytes` encoding path was removed.
420        let small = SizedPointer::new(100);
421        assert_eq!(small.distance, 100);
422
423        let large = SizedPointer::new(0x10000);
424        assert_eq!(large.distance, 0x10000);
425    }
426
427    #[test]
428    fn test_node_creation() {
429        let payload = PayloadRef::new(1000, 50);
430        let node = BtiNode::payload_only(0, b"test".to_vec(), payload);
431
432        assert_eq!(node.node_type, BtiNodeType::PayloadOnly);
433        assert_eq!(node.level, 0);
434        assert_eq!(node.key_prefix, b"test");
435        assert!(node.is_leaf());
436        assert_eq!(node.child_count(), 0);
437    }
438
439    #[test]
440    fn test_sparse_node_search() {
441        let transitions = vec![
442            Transition::new(b'a', SizedPointer::new(100)),
443            Transition::new(b'm', SizedPointer::new(200)),
444            Transition::new(b'z', SizedPointer::new(300)),
445        ];
446
447        let node = BtiNode::sparse(1, Vec::new(), transitions);
448
449        assert!(node.find_child(b'a').is_some());
450        assert!(node.find_child(b'm').is_some());
451        assert!(node.find_child(b'z').is_some());
452        assert!(node.find_child(b'b').is_none());
453
454        assert_eq!(node.child_count(), 3);
455    }
456
457    #[test]
458    fn test_dense_node_lookup() {
459        let children = vec![
460            Some(SizedPointer::new(100)),
461            Some(SizedPointer::new(200)),
462            Some(SizedPointer::new(300)),
463        ];
464
465        let node = BtiNode::dense(1, Vec::new(), b'a', children);
466
467        assert!(node.find_child(b'a').is_some());
468        assert!(node.find_child(b'b').is_some());
469        assert!(node.find_child(b'c').is_some());
470        assert!(node.find_child(b'd').is_none());
471        assert!(node.find_child(b'@').is_none()); // Before range
472    }
473
474    /// Finding 1 (issue #832): a Dense node where the FIRST real child points at
475    /// absolute trie offset 0 (a legitimate position — the first-written leaf in
476    /// BTI's bottom-up layout) and a later slot is the "no transition" sentinel
477    /// (`None`).  `find_child` must return the offset-0 child for the real byte
478    /// and `None` for the gap byte — the offset-0 pointer must NOT be treated as
479    /// "no transition".
480    #[test]
481    fn test_dense_node_offset_zero_child_distinct_from_no_transition() {
482        // start_byte = b'a':
483        //   b'a' → real child at offset 0 (SizedPointer distance 0)
484        //   b'b' → no transition (None)
485        //   b'c' → real child at offset 300
486        let children = vec![
487            Some(SizedPointer::new(0)), // real child at absolute offset 0
488            None,                       // no transition
489            Some(SizedPointer::new(300)),
490        ];
491        let node = BtiNode::dense(1, Vec::new(), b'a', children);
492
493        let a = node.find_child(b'a');
494        assert!(a.is_some(), "offset-0 child must be found, not dropped");
495        assert_eq!(
496            a.unwrap().distance,
497            0,
498            "the real child at absolute offset 0 must be returned"
499        );
500        assert!(
501            node.find_child(b'b').is_none(),
502            "no-transition slot must return None"
503        );
504        assert!(node.find_child(b'c').is_some());
505        // child_count is the dense RANGE length (slots), independent of gaps.
506        assert_eq!(node.child_count(), 3);
507    }
508
509    #[test]
510    fn test_node_validation() {
511        // Valid payload-only node
512        let payload_node = BtiNode::payload_only(0, Vec::new(), PayloadRef::new(0, 10));
513        assert!(payload_node.validate().is_ok());
514
515        // Invalid sparse node (not enough children)
516        let _invalid_sparse = BtiNode::sparse(
517            1,
518            Vec::new(),
519            vec![Transition::new(b'a', SizedPointer::new(100))],
520        );
521        // Note: This would be invalid in practice but our implementation
522        // doesn't enforce minimum children for sparse nodes in this test
523    }
524}