Skip to main content

summa_core/structures/
sstable_index.rs

1//! Memory-efficient SSTable index structures
2//!
3//! This module provides two approaches for memory-efficient block indexing:
4//!
5//! ## Option 1: FST-based Index (native feature)
6//! Uses a Finite State Transducer to map keys to block ordinals. The FST can be
7//! mmap'd directly without parsing into heap-allocated structures.
8//!
9//! ## Option 2: Mmap'd Raw Index
10//! Keeps the prefix-compressed block index as raw bytes and decodes entries
11//! on-demand during binary search. No heap allocation for the index.
12//!
13//! Both approaches use a compact BlockAddrStore with bitpacked offsets/lengths.
14
15use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
16use std::io;
17use std::ops::Range;
18
19use crate::directories::OwnedBytes;
20
21use super::vint::{read_vint, write_vint};
22
23/// Block address - offset and length in the data section
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct BlockAddr {
26    pub offset: u64,
27    pub length: u32,
28}
29
30impl BlockAddr {
31    pub fn byte_range(&self) -> Range<u64> {
32        self.offset..self.offset + self.length as u64
33    }
34}
35
36/// Compact storage for block addresses using delta + bitpacking
37///
38/// Memory layout:
39/// - Header: num_blocks (u32) + offset_bits (u8) + length_bits (u8)
40/// - Bitpacked data: offsets and lengths interleaved
41///
42/// Uses delta encoding for offsets (blocks are sequential) and
43/// stores lengths directly (typically similar sizes).
44#[derive(Debug)]
45pub struct BlockAddrStore {
46    num_blocks: u32,
47    offset_bits: u8,
48    length_bits: u8,
49    /// Eagerly decoded addresses for O(1) random access
50    addrs: Vec<BlockAddr>,
51}
52
53impl BlockAddrStore {
54    /// Build from a list of block addresses
55    pub fn build(addrs: &[BlockAddr]) -> io::Result<Vec<u8>> {
56        if addrs.is_empty() {
57            let mut buf = Vec::with_capacity(6);
58            buf.write_u32::<LittleEndian>(0)?;
59            buf.write_u8(0)?;
60            buf.write_u8(0)?;
61            return Ok(buf);
62        }
63
64        // Compute delta offsets and find max values for bit width
65        let mut deltas = Vec::with_capacity(addrs.len());
66        let mut prev_end: u64 = 0;
67        let mut max_delta: u64 = 0;
68        let mut max_length: u32 = 0;
69
70        for addr in addrs {
71            // Delta from end of previous block (handles gaps)
72            let delta = addr.offset.saturating_sub(prev_end);
73            deltas.push(delta);
74            max_delta = max_delta.max(delta);
75            max_length = max_length.max(addr.length);
76            prev_end = addr.offset.checked_add(addr.length as u64).ok_or_else(|| {
77                io::Error::new(io::ErrorKind::InvalidInput, "block address overflow")
78            })?;
79        }
80
81        // Compute bit widths
82        let offset_bits = if max_delta == 0 {
83            1
84        } else {
85            (64 - max_delta.leading_zeros()) as u8
86        };
87        let length_bits = if max_length == 0 {
88            1
89        } else {
90            (32 - max_length.leading_zeros()) as u8
91        };
92
93        // Calculate packed size
94        let bits_per_entry = offset_bits as usize + length_bits as usize;
95        let total_bits = bits_per_entry.checked_mul(addrs.len()).ok_or_else(|| {
96            io::Error::new(io::ErrorKind::InvalidInput, "block address table too large")
97        })?;
98        let packed_bytes = total_bits.div_ceil(8);
99
100        let mut buf = Vec::with_capacity(6 + packed_bytes);
101        buf.write_u32::<LittleEndian>(addrs.len() as u32)?;
102        buf.write_u8(offset_bits)?;
103        buf.write_u8(length_bits)?;
104
105        // Bitpack the data
106        let mut bit_writer = BitWriter::new(&mut buf);
107        for (i, addr) in addrs.iter().enumerate() {
108            bit_writer.write(deltas[i], offset_bits)?;
109            bit_writer.write(addr.length as u64, length_bits)?;
110        }
111        bit_writer.flush()?;
112
113        Ok(buf)
114    }
115
116    /// Load from raw bytes — eagerly decodes all addresses for O(1) access
117    pub fn load(data: OwnedBytes) -> io::Result<Self> {
118        if data.len() < 6 {
119            return Err(io::Error::new(
120                io::ErrorKind::InvalidData,
121                "BlockAddrStore data too short",
122            ));
123        }
124
125        let mut reader = data.as_slice();
126        let num_blocks = reader.read_u32::<LittleEndian>()?;
127        let offset_bits = reader.read_u8()?;
128        let length_bits = reader.read_u8()?;
129
130        if offset_bits > 64 || length_bits > 32 {
131            return Err(io::Error::new(
132                io::ErrorKind::InvalidData,
133                "invalid block address bit width",
134            ));
135        }
136        if num_blocks > 0 && (offset_bits == 0 || length_bits == 0) {
137            return Err(io::Error::new(
138                io::ErrorKind::InvalidData,
139                "non-empty block address table has a zero bit width",
140            ));
141        }
142
143        let bits_per_entry = offset_bits as usize + length_bits as usize;
144        let total_bits = bits_per_entry
145            .checked_mul(num_blocks as usize)
146            .ok_or_else(|| {
147                io::Error::new(io::ErrorKind::InvalidData, "block address table overflow")
148            })?;
149        let packed_len = total_bits.checked_add(7).ok_or_else(|| {
150            io::Error::new(io::ErrorKind::InvalidData, "block address table overflow")
151        })? / 8;
152        if packed_len > data.len() - 6 {
153            return Err(io::Error::new(
154                io::ErrorKind::UnexpectedEof,
155                "block address table truncated",
156            ));
157        }
158
159        // Eagerly decode all block addresses once at load time
160        let packed_data = &data.as_slice()[6..6 + packed_len];
161        let mut bit_reader = BitReader::new(packed_data);
162        let mut addrs = Vec::new();
163        addrs.try_reserve_exact(num_blocks as usize).map_err(|_| {
164            io::Error::new(io::ErrorKind::InvalidData, "block address table too large")
165        })?;
166        let mut current_offset: u64 = 0;
167
168        for _ in 0..num_blocks {
169            let delta = bit_reader.read(offset_bits)?;
170            let length = bit_reader.read(length_bits)?;
171            let offset = current_offset.checked_add(delta).ok_or_else(|| {
172                io::Error::new(io::ErrorKind::InvalidData, "block address offset overflow")
173            })?;
174            let length = u32::try_from(length).map_err(|_| {
175                io::Error::new(io::ErrorKind::InvalidData, "block length exceeds u32")
176            })?;
177            current_offset = offset.checked_add(length as u64).ok_or_else(|| {
178                io::Error::new(io::ErrorKind::InvalidData, "block address end overflow")
179            })?;
180            addrs.push(BlockAddr { offset, length });
181        }
182
183        Ok(Self {
184            num_blocks,
185            offset_bits,
186            length_bits,
187            addrs,
188        })
189    }
190
191    /// Number of blocks
192    pub fn len(&self) -> usize {
193        self.num_blocks as usize
194    }
195
196    /// Check if empty
197    pub fn is_empty(&self) -> bool {
198        self.num_blocks == 0
199    }
200
201    /// Get block address by index — O(1) from eagerly decoded array
202    #[inline]
203    pub fn get(&self, idx: usize) -> Option<BlockAddr> {
204        self.addrs.get(idx).copied()
205    }
206
207    /// Get all block addresses
208    pub fn all(&self) -> Vec<BlockAddr> {
209        self.addrs.clone()
210    }
211}
212
213/// FST-based block index (Option 1)
214///
215/// Maps keys to block ordinals using an FST. The FST bytes can be mmap'd
216/// directly without any parsing or heap allocation.
217#[cfg(feature = "fst-index")]
218pub struct FstBlockIndex {
219    fst: fst::Map<OwnedBytes>,
220    block_addrs: BlockAddrStore,
221}
222
223#[cfg(feature = "fst-index")]
224impl FstBlockIndex {
225    /// Build FST index from keys and block addresses
226    pub fn build(entries: &[(Vec<u8>, BlockAddr)]) -> io::Result<Vec<u8>> {
227        // Empty term dictionaries are common after compaction and in vector/
228        // fast-field-only segments. FST's registry setup dominates these small
229        // writes; retain just the canonical empty bytes, never its build scratch.
230        static EMPTY: std::sync::OnceLock<Vec<u8>> = std::sync::OnceLock::new();
231        if entries.is_empty() {
232            if let Some(bytes) = EMPTY.get() {
233                return Ok(bytes.clone());
234            }
235            let bytes = Self::build_uncached(entries)?;
236            // Concurrent first calls may both encode the same tiny artifact.
237            let _ = EMPTY.set(bytes.clone());
238            return Ok(bytes);
239        }
240        Self::build_uncached(entries)
241    }
242
243    fn build_uncached(entries: &[(Vec<u8>, BlockAddr)]) -> io::Result<Vec<u8>> {
244        use fst::MapBuilder;
245
246        // Build FST mapping keys to block ordinals
247        let mut fst_builder = MapBuilder::memory();
248        for (i, (key, _)) in entries.iter().enumerate() {
249            fst_builder
250                .insert(key, i as u64)
251                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
252        }
253        let fst_bytes = fst_builder
254            .into_inner()
255            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
256
257        // Build block address store
258        let addrs: Vec<BlockAddr> = entries.iter().map(|(_, addr)| *addr).collect();
259        let addr_bytes = BlockAddrStore::build(&addrs)?;
260
261        // Combine: fst_len (u32) + fst_bytes + addr_bytes
262        let mut result = Vec::with_capacity(4 + fst_bytes.len() + addr_bytes.len());
263        result.write_u32::<LittleEndian>(fst_bytes.len() as u32)?;
264        result.extend_from_slice(&fst_bytes);
265        result.extend_from_slice(&addr_bytes);
266
267        Ok(result)
268    }
269
270    /// Load from raw bytes
271    pub fn load(data: OwnedBytes) -> io::Result<Self> {
272        if data.len() < 4 {
273            return Err(io::Error::new(
274                io::ErrorKind::InvalidData,
275                "FstBlockIndex data too short",
276            ));
277        }
278
279        let fst_len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
280
281        let fst_end = 4usize.checked_add(fst_len).ok_or_else(|| {
282            io::Error::new(io::ErrorKind::InvalidData, "FstBlockIndex length overflow")
283        })?;
284        if data.len() < fst_end {
285            return Err(io::Error::new(
286                io::ErrorKind::InvalidData,
287                "FstBlockIndex FST data truncated",
288            ));
289        }
290
291        let fst_data = data.slice(4..fst_end);
292        let addr_data = data.slice(fst_end..data.len());
293
294        let fst =
295            fst::Map::new(fst_data).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
296        let block_addrs = BlockAddrStore::load(addr_data)?;
297
298        if fst.len() != block_addrs.len() {
299            return Err(io::Error::new(
300                io::ErrorKind::InvalidData,
301                "FST key count does not match block address count",
302            ));
303        }
304        use fst::Streamer;
305        let mut entries = fst.stream();
306        let mut expected_ordinal = 0u64;
307        while let Some((_key, ordinal)) = entries.next() {
308            if ordinal != expected_ordinal {
309                return Err(io::Error::new(
310                    io::ErrorKind::InvalidData,
311                    "FST block ordinals are not contiguous",
312                ));
313            }
314            expected_ordinal += 1;
315        }
316
317        Ok(Self { fst, block_addrs })
318    }
319
320    /// Look up the block index for a key
321    /// Returns the block ordinal that could contain this key.
322    /// O(key_len) via FST exact lookup + single stream step.
323    pub fn locate(&self, key: &[u8]) -> Option<usize> {
324        // Fast exact match — O(key_len), no stream allocation
325        if let Some(ordinal) = self.fst.get(key) {
326            return Some(ordinal as usize);
327        }
328
329        // Find the first block whose first_key > target (single stream step)
330        use fst::{IntoStreamer, Streamer};
331        let mut stream = self.fst.range().gt(key).into_stream();
332        match stream.next() {
333            Some((_, ordinal)) if ordinal > 0 => Some(ordinal as usize - 1),
334            Some(_) => None, // key < first block's first key
335            None => {
336                // No key > target → target is after all keys; use last block
337                let len = self.fst.len();
338                if len > 0 { Some(len - 1) } else { None }
339            }
340        }
341    }
342
343    /// Get block address by ordinal
344    pub fn get_addr(&self, ordinal: usize) -> Option<BlockAddr> {
345        self.block_addrs.get(ordinal)
346    }
347
348    /// Number of blocks
349    pub fn len(&self) -> usize {
350        self.block_addrs.len()
351    }
352
353    /// Check if empty
354    pub fn is_empty(&self) -> bool {
355        self.block_addrs.is_empty()
356    }
357
358    /// Get all block addresses
359    pub fn all_addrs(&self) -> Vec<BlockAddr> {
360        self.block_addrs.all()
361    }
362}
363
364/// Mmap'd raw block index (Option 2)
365///
366/// Keeps the prefix-compressed block index as raw bytes and decodes
367/// entries on-demand. Uses restart points every R entries for O(log N)
368/// lookup via binary search instead of O(N) linear scan.
369pub struct MmapBlockIndex {
370    data: OwnedBytes,
371    num_blocks: u32,
372    block_addrs: BlockAddrStore,
373    /// Offset where the prefix-compressed keys start
374    keys_offset: usize,
375    /// Offset where the keys section ends (restart array begins)
376    keys_end: usize,
377    /// Byte offset in data where the restart offsets array starts
378    restart_array_offset: usize,
379    /// Number of restart points
380    restart_count: usize,
381    /// Restart interval (R) — a restart point every R entries
382    restart_interval: usize,
383}
384
385/// Restart interval: store full (uncompressed) key every R entries
386const RESTART_INTERVAL: usize = 16;
387
388impl MmapBlockIndex {
389    /// Build mmap-friendly index from entries.
390    ///
391    /// Format: `num_blocks (u32) | BlockAddrStore | prefix-compressed keys
392    /// (with restart points) | restart_offsets[..] | restart_count (u32) | restart_interval (u16)`
393    pub fn build(entries: &[(Vec<u8>, BlockAddr)]) -> io::Result<Vec<u8>> {
394        if entries.is_empty() {
395            let mut buf = Vec::with_capacity(16);
396            buf.write_u32::<LittleEndian>(0)?; // num_blocks
397            buf.extend_from_slice(&BlockAddrStore::build(&[])?);
398            // Empty restart array + footer
399            buf.write_u32::<LittleEndian>(0)?; // restart_count
400            buf.write_u16::<LittleEndian>(RESTART_INTERVAL as u16)?;
401            return Ok(buf);
402        }
403
404        // Build block address store
405        let addrs: Vec<BlockAddr> = entries.iter().map(|(_, addr)| *addr).collect();
406        let addr_bytes = BlockAddrStore::build(&addrs)?;
407
408        // Build prefix-compressed keys with restart points
409        let mut keys_buf = Vec::new();
410        let mut prev_key: Vec<u8> = Vec::new();
411        let mut restart_offsets: Vec<u32> = Vec::new();
412
413        for (i, (key, _)) in entries.iter().enumerate() {
414            let is_restart = i % RESTART_INTERVAL == 0;
415
416            if is_restart {
417                restart_offsets.push(keys_buf.len() as u32);
418                // Store full key (no prefix compression)
419                write_vint(&mut keys_buf, 0)?;
420                write_vint(&mut keys_buf, key.len() as u64)?;
421                keys_buf.extend_from_slice(key);
422            } else {
423                let prefix_len = common_prefix_len(&prev_key, key);
424                let suffix = &key[prefix_len..];
425                write_vint(&mut keys_buf, prefix_len as u64)?;
426                write_vint(&mut keys_buf, suffix.len() as u64)?;
427                keys_buf.extend_from_slice(suffix);
428            }
429
430            prev_key.clear();
431            prev_key.extend_from_slice(key);
432        }
433
434        // Combine: num_blocks + addr_bytes + keys + restart_offsets + footer
435        let restart_count = restart_offsets.len();
436        let mut result =
437            Vec::with_capacity(4 + addr_bytes.len() + keys_buf.len() + restart_count * 4 + 6);
438        result.write_u32::<LittleEndian>(entries.len() as u32)?;
439        result.extend_from_slice(&addr_bytes);
440        result.extend_from_slice(&keys_buf);
441
442        // Write restart offsets array
443        for &off in &restart_offsets {
444            result.write_u32::<LittleEndian>(off)?;
445        }
446
447        // Write footer
448        result.write_u32::<LittleEndian>(restart_count as u32)?;
449        result.write_u16::<LittleEndian>(RESTART_INTERVAL as u16)?;
450
451        Ok(result)
452    }
453
454    /// Load from raw bytes
455    pub fn load(data: OwnedBytes) -> io::Result<Self> {
456        if data.len() < 16 {
457            return Err(io::Error::new(
458                io::ErrorKind::InvalidData,
459                "MmapBlockIndex data too short",
460            ));
461        }
462
463        let num_blocks = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
464
465        // Load block addresses
466        let addr_data_start = 4;
467        let remaining = data.slice(addr_data_start..data.len());
468        let block_addrs = BlockAddrStore::load(remaining.clone())?;
469
470        if block_addrs.len() != num_blocks as usize {
471            return Err(io::Error::new(
472                io::ErrorKind::InvalidData,
473                "block address count does not match key count",
474            ));
475        }
476
477        // Calculate where keys start
478        let bits_per_entry = block_addrs.offset_bits as usize + block_addrs.length_bits as usize;
479        let total_bits = bits_per_entry
480            .checked_mul(num_blocks as usize)
481            .ok_or_else(|| {
482                io::Error::new(io::ErrorKind::InvalidData, "block index size overflow")
483            })?;
484        let addr_packed_size = total_bits.checked_add(7).ok_or_else(|| {
485            io::Error::new(io::ErrorKind::InvalidData, "block index size overflow")
486        })? / 8;
487        let keys_offset = addr_data_start
488            .checked_add(6)
489            .and_then(|v| v.checked_add(addr_packed_size))
490            .ok_or_else(|| {
491                io::Error::new(io::ErrorKind::InvalidData, "block index offset overflow")
492            })?; // 6 = header of BlockAddrStore
493
494        // Read footer (last 6 bytes: restart_count u32 + restart_interval u16)
495        if data.len() < keys_offset + 6 {
496            return Err(io::Error::new(
497                io::ErrorKind::InvalidData,
498                "MmapBlockIndex missing restart footer",
499            ));
500        }
501        let footer_start = data.len() - 6;
502        let restart_count = u32::from_le_bytes([
503            data[footer_start],
504            data[footer_start + 1],
505            data[footer_start + 2],
506            data[footer_start + 3],
507        ]) as usize;
508        let restart_interval =
509            u16::from_le_bytes([data[footer_start + 4], data[footer_start + 5]]) as usize;
510
511        if restart_interval == 0 {
512            return Err(io::Error::new(
513                io::ErrorKind::InvalidData,
514                "block index restart interval is zero",
515            ));
516        }
517
518        let expected_restart_count = (num_blocks as usize).div_ceil(restart_interval);
519        if restart_count != expected_restart_count {
520            return Err(io::Error::new(
521                io::ErrorKind::InvalidData,
522                "block index restart count is inconsistent",
523            ));
524        }
525
526        // Restart offsets array: restart_count × 4 bytes, just before footer
527        let restart_bytes = restart_count.checked_mul(4).ok_or_else(|| {
528            io::Error::new(io::ErrorKind::InvalidData, "restart table size overflow")
529        })?;
530        let restart_array_offset = footer_start.checked_sub(restart_bytes).ok_or_else(|| {
531            io::Error::new(io::ErrorKind::InvalidData, "restart table out of bounds")
532        })?;
533        if restart_array_offset < keys_offset {
534            return Err(io::Error::new(
535                io::ErrorKind::InvalidData,
536                "restart table overlaps block keys",
537            ));
538        }
539
540        // Keys section spans from keys_offset to restart_array_offset
541        let keys_end = restart_array_offset;
542
543        // Validate the complete prefix-compressed key stream and all restart
544        // offsets once so the hot lookup path can remain allocation-light and
545        // infallible without trusting corrupt on-disk lengths.
546        let keys_data = &data.as_slice()[keys_offset..keys_end];
547        let mut reader = keys_data;
548        let mut current_key = Vec::new();
549        let mut previous_key: Option<Vec<u8>> = None;
550        for ordinal in 0..num_blocks as usize {
551            let entry_offset = keys_data.len() - reader.len();
552            if ordinal % restart_interval == 0 {
553                let restart_idx = ordinal / restart_interval;
554                let pos = restart_array_offset + restart_idx * 4;
555                let recorded =
556                    u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
557                        as usize;
558                if recorded != entry_offset {
559                    return Err(io::Error::new(
560                        io::ErrorKind::InvalidData,
561                        "block index restart offset is inconsistent",
562                    ));
563                }
564            }
565
566            let prefix_len = usize::try_from(read_vint(&mut reader)?).map_err(|_| {
567                io::Error::new(io::ErrorKind::InvalidData, "block key prefix is too large")
568            })?;
569            let suffix_len = usize::try_from(read_vint(&mut reader)?).map_err(|_| {
570                io::Error::new(io::ErrorKind::InvalidData, "block key suffix is too large")
571            })?;
572            if ordinal % restart_interval == 0 && prefix_len != 0 {
573                return Err(io::Error::new(
574                    io::ErrorKind::InvalidData,
575                    "block index restart key uses a prefix",
576                ));
577            }
578            if prefix_len > current_key.len() || suffix_len > reader.len() {
579                return Err(io::Error::new(
580                    io::ErrorKind::UnexpectedEof,
581                    "block index key is truncated",
582                ));
583            }
584            current_key.truncate(prefix_len);
585            current_key.extend_from_slice(&reader[..suffix_len]);
586            reader = &reader[suffix_len..];
587
588            if previous_key
589                .as_ref()
590                .is_some_and(|previous| previous.as_slice() >= current_key.as_slice())
591            {
592                return Err(io::Error::new(
593                    io::ErrorKind::InvalidData,
594                    "block index keys are not strictly increasing",
595                ));
596            }
597            previous_key = Some(current_key.clone());
598        }
599        if !reader.is_empty() {
600            return Err(io::Error::new(
601                io::ErrorKind::InvalidData,
602                "block index contains trailing key data",
603            ));
604        }
605
606        Ok(Self {
607            data,
608            num_blocks,
609            block_addrs,
610            keys_offset,
611            keys_end,
612            restart_array_offset,
613            restart_count,
614            restart_interval,
615        })
616    }
617
618    /// Read restart offset at given index directly from mmap'd data
619    #[inline]
620    fn restart_offset(&self, idx: usize) -> u32 {
621        let pos = self.restart_array_offset + idx * 4;
622        u32::from_le_bytes([
623            self.data[pos],
624            self.data[pos + 1],
625            self.data[pos + 2],
626            self.data[pos + 3],
627        ])
628    }
629
630    /// Decode the full key at a restart point (prefix_len is always 0)
631    fn decode_restart_key<'a>(&self, keys_data: &'a [u8], restart_idx: usize) -> &'a [u8] {
632        let offset = self.restart_offset(restart_idx) as usize;
633        let mut reader = &keys_data[offset..];
634
635        let prefix_len = read_vint(&mut reader).unwrap_or(0) as usize;
636        debug_assert_eq!(prefix_len, 0, "restart point should have prefix_len=0");
637        let suffix_len = read_vint(&mut reader).unwrap_or(0) as usize;
638
639        // reader now points to the suffix bytes
640        &reader[..suffix_len]
641    }
642
643    /// O(log(N/R) + R) lookup using binary search on restart points, then
644    /// linear scan with prefix decompression within the interval.
645    pub fn locate(&self, target: &[u8]) -> Option<usize> {
646        if self.num_blocks == 0 {
647            return None;
648        }
649
650        let keys_data = &self.data.as_slice()[self.keys_offset..self.keys_end];
651
652        // Binary search on restart points to find the interval
653        let mut lo = 0usize;
654        let mut hi = self.restart_count;
655
656        while lo < hi {
657            let mid = lo + (hi - lo) / 2;
658            let key = self.decode_restart_key(keys_data, mid);
659            match key.cmp(target) {
660                std::cmp::Ordering::Equal => {
661                    return Some(mid * self.restart_interval);
662                }
663                std::cmp::Ordering::Less => lo = mid + 1,
664                std::cmp::Ordering::Greater => hi = mid,
665            }
666        }
667
668        // lo is the first restart point whose key > target (or restart_count)
669        // Search in the interval starting at restart (lo - 1), or 0 if lo == 0
670        if lo == 0 {
671            // target < first restart key — might be before all keys
672            // but we still need to scan from the beginning
673        }
674
675        let restart_idx = if lo > 0 { lo - 1 } else { 0 };
676        let start_ordinal = restart_idx * self.restart_interval;
677        let end_ordinal = if restart_idx + 1 < self.restart_count {
678            (restart_idx + 1) * self.restart_interval
679        } else {
680            self.num_blocks as usize
681        };
682
683        // Linear scan from restart point through at most R entries
684        let scan_offset = self.restart_offset(restart_idx) as usize;
685        let mut reader = &keys_data[scan_offset..];
686        let mut current_key = Vec::new();
687        let mut last_le_block: Option<usize> = None;
688
689        for i in start_ordinal..end_ordinal {
690            let prefix_len = match read_vint(&mut reader) {
691                Ok(v) => v as usize,
692                Err(_) => break,
693            };
694            let suffix_len = match read_vint(&mut reader) {
695                Ok(v) => v as usize,
696                Err(_) => break,
697            };
698
699            current_key.truncate(prefix_len);
700            if suffix_len > reader.len() {
701                break;
702            }
703            current_key.extend_from_slice(&reader[..suffix_len]);
704            reader = &reader[suffix_len..];
705
706            match current_key.as_slice().cmp(target) {
707                std::cmp::Ordering::Equal => return Some(i),
708                std::cmp::Ordering::Less => last_le_block = Some(i),
709                std::cmp::Ordering::Greater => return last_le_block,
710            }
711        }
712
713        last_le_block
714    }
715
716    /// Get block address by ordinal
717    pub fn get_addr(&self, ordinal: usize) -> Option<BlockAddr> {
718        self.block_addrs.get(ordinal)
719    }
720
721    /// Number of blocks
722    pub fn len(&self) -> usize {
723        self.num_blocks as usize
724    }
725
726    /// Check if empty
727    pub fn is_empty(&self) -> bool {
728        self.num_blocks == 0
729    }
730
731    /// Get all block addresses
732    pub fn all_addrs(&self) -> Vec<BlockAddr> {
733        self.block_addrs.all()
734    }
735
736    /// Decode all keys (for debugging/merging)
737    pub fn all_keys(&self) -> Vec<Vec<u8>> {
738        let mut result = Vec::with_capacity(self.num_blocks as usize);
739        let keys_data = &self.data.as_slice()[self.keys_offset..self.keys_end];
740        let mut reader = keys_data;
741        let mut current_key = Vec::new();
742
743        for _ in 0..self.num_blocks {
744            let prefix_len = match read_vint(&mut reader) {
745                Ok(v) => v as usize,
746                Err(_) => break,
747            };
748            let suffix_len = match read_vint(&mut reader) {
749                Ok(v) => v as usize,
750                Err(_) => break,
751            };
752
753            current_key.truncate(prefix_len);
754            if suffix_len > reader.len() {
755                break;
756            }
757            current_key.extend_from_slice(&reader[..suffix_len]);
758            reader = &reader[suffix_len..];
759
760            result.push(current_key.clone());
761        }
762
763        result
764    }
765}
766
767/// Unified block index that can use either FST or mmap'd raw index
768pub enum BlockIndex {
769    #[cfg(feature = "fst-index")]
770    Fst(FstBlockIndex),
771    Mmap(MmapBlockIndex),
772}
773
774impl BlockIndex {
775    /// Locate the block that could contain the key
776    pub fn locate(&self, key: &[u8]) -> Option<usize> {
777        match self {
778            #[cfg(feature = "fst-index")]
779            BlockIndex::Fst(idx) => idx.locate(key),
780            BlockIndex::Mmap(idx) => idx.locate(key),
781        }
782    }
783
784    /// Get block address by ordinal
785    pub fn get_addr(&self, ordinal: usize) -> Option<BlockAddr> {
786        match self {
787            #[cfg(feature = "fst-index")]
788            BlockIndex::Fst(idx) => idx.get_addr(ordinal),
789            BlockIndex::Mmap(idx) => idx.get_addr(ordinal),
790        }
791    }
792
793    /// Number of blocks
794    pub fn len(&self) -> usize {
795        match self {
796            #[cfg(feature = "fst-index")]
797            BlockIndex::Fst(idx) => idx.len(),
798            BlockIndex::Mmap(idx) => idx.len(),
799        }
800    }
801
802    /// Check if empty
803    pub fn is_empty(&self) -> bool {
804        self.len() == 0
805    }
806
807    /// Get all block addresses
808    pub fn all_addrs(&self) -> Vec<BlockAddr> {
809        match self {
810            #[cfg(feature = "fst-index")]
811            BlockIndex::Fst(idx) => idx.all_addrs(),
812            BlockIndex::Mmap(idx) => idx.all_addrs(),
813        }
814    }
815}
816
817// ============================================================================
818// Helper functions
819// ============================================================================
820
821fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
822    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
823}
824
825/// Simple bit writer for packing
826struct BitWriter<'a> {
827    output: &'a mut Vec<u8>,
828    buffer: u64,
829    bits_in_buffer: u8,
830}
831
832impl<'a> BitWriter<'a> {
833    fn new(output: &'a mut Vec<u8>) -> Self {
834        Self {
835            output,
836            buffer: 0,
837            bits_in_buffer: 0,
838        }
839    }
840
841    fn write(&mut self, value: u64, num_bits: u8) -> io::Result<()> {
842        debug_assert!(num_bits <= 64);
843
844        self.buffer |= value << self.bits_in_buffer;
845        self.bits_in_buffer += num_bits;
846
847        while self.bits_in_buffer >= 8 {
848            self.output.push(self.buffer as u8);
849            self.buffer >>= 8;
850            self.bits_in_buffer -= 8;
851        }
852
853        Ok(())
854    }
855
856    fn flush(&mut self) -> io::Result<()> {
857        if self.bits_in_buffer > 0 {
858            self.output.push(self.buffer as u8);
859            self.buffer = 0;
860            self.bits_in_buffer = 0;
861        }
862        Ok(())
863    }
864}
865
866/// Simple bit reader for unpacking
867struct BitReader<'a> {
868    data: &'a [u8],
869    byte_pos: usize,
870    bit_pos: u8,
871}
872
873impl<'a> BitReader<'a> {
874    fn new(data: &'a [u8]) -> Self {
875        Self {
876            data,
877            byte_pos: 0,
878            bit_pos: 0,
879        }
880    }
881
882    fn read(&mut self, num_bits: u8) -> io::Result<u64> {
883        if num_bits == 0 {
884            return Ok(0);
885        }
886
887        let mut result: u64 = 0;
888        let mut bits_read: u8 = 0;
889
890        while bits_read < num_bits {
891            if self.byte_pos >= self.data.len() {
892                return Err(io::Error::new(
893                    io::ErrorKind::UnexpectedEof,
894                    "Not enough bits",
895                ));
896            }
897
898            let bits_available = 8 - self.bit_pos;
899            let bits_to_read = (num_bits - bits_read).min(bits_available);
900            // Handle edge case where bits_to_read == 8 to avoid overflow
901            let mask = if bits_to_read >= 8 {
902                0xFF
903            } else {
904                (1u8 << bits_to_read) - 1
905            };
906            let bits = (self.data[self.byte_pos] >> self.bit_pos) & mask;
907
908            result |= (bits as u64) << bits_read;
909            bits_read += bits_to_read;
910            self.bit_pos += bits_to_read;
911
912            if self.bit_pos >= 8 {
913                self.byte_pos += 1;
914                self.bit_pos = 0;
915            }
916        }
917
918        Ok(result)
919    }
920}
921
922#[cfg(test)]
923mod tests {
924    use super::*;
925
926    #[test]
927    fn test_block_addr_store_roundtrip() {
928        let addrs = vec![
929            BlockAddr {
930                offset: 0,
931                length: 1000,
932            },
933            BlockAddr {
934                offset: 1000,
935                length: 1500,
936            },
937            BlockAddr {
938                offset: 2500,
939                length: 800,
940            },
941            BlockAddr {
942                offset: 3300,
943                length: 2000,
944            },
945        ];
946
947        let bytes = BlockAddrStore::build(&addrs).unwrap();
948        let store = BlockAddrStore::load(OwnedBytes::new(bytes)).unwrap();
949
950        assert_eq!(store.len(), 4);
951        for (i, expected) in addrs.iter().enumerate() {
952            let actual = store.get(i).unwrap();
953            assert_eq!(actual.offset, expected.offset, "offset mismatch at {}", i);
954            assert_eq!(actual.length, expected.length, "length mismatch at {}", i);
955        }
956    }
957
958    #[test]
959    fn test_block_addr_store_empty() {
960        let bytes = BlockAddrStore::build(&[]).unwrap();
961        let store = BlockAddrStore::load(OwnedBytes::new(bytes)).unwrap();
962        assert_eq!(store.len(), 0);
963        assert!(store.get(0).is_none());
964    }
965
966    #[test]
967    fn test_block_addr_store_rejects_truncated_packed_data() {
968        let bytes = vec![1, 0, 0, 0, 1, 1];
969        assert!(BlockAddrStore::load(OwnedBytes::new(bytes)).is_err());
970    }
971
972    #[test]
973    fn test_mmap_index_rejects_restart_table_underflow() {
974        let entries = vec![(
975            b"key".to_vec(),
976            BlockAddr {
977                offset: 0,
978                length: 1,
979            },
980        )];
981        let mut bytes = MmapBlockIndex::build(&entries).unwrap();
982        let footer = bytes.len() - 6;
983        bytes[footer..footer + 4].copy_from_slice(&u32::MAX.to_le_bytes());
984        assert!(MmapBlockIndex::load(OwnedBytes::new(bytes)).is_err());
985    }
986
987    #[test]
988    fn test_mmap_block_index_roundtrip() {
989        let entries = vec![
990            (
991                b"aaa".to_vec(),
992                BlockAddr {
993                    offset: 0,
994                    length: 100,
995                },
996            ),
997            (
998                b"bbb".to_vec(),
999                BlockAddr {
1000                    offset: 100,
1001                    length: 150,
1002                },
1003            ),
1004            (
1005                b"ccc".to_vec(),
1006                BlockAddr {
1007                    offset: 250,
1008                    length: 200,
1009                },
1010            ),
1011        ];
1012
1013        let bytes = MmapBlockIndex::build(&entries).unwrap();
1014        let index = MmapBlockIndex::load(OwnedBytes::new(bytes)).unwrap();
1015
1016        assert_eq!(index.len(), 3);
1017
1018        // Test locate
1019        assert_eq!(index.locate(b"aaa"), Some(0));
1020        assert_eq!(index.locate(b"bbb"), Some(1));
1021        assert_eq!(index.locate(b"ccc"), Some(2));
1022        assert_eq!(index.locate(b"aab"), Some(0)); // Between aaa and bbb
1023        assert_eq!(index.locate(b"ddd"), Some(2)); // After all keys
1024        assert_eq!(index.locate(b"000"), None); // Before all keys
1025    }
1026
1027    #[cfg(feature = "fst-index")]
1028    #[test]
1029    fn cached_empty_fst_preserves_canonical_bytes_and_empty_lookup() {
1030        let expected = FstBlockIndex::build_uncached(&[]).unwrap();
1031        assert!(expected.len() <= 128, "empty-index cache must remain tiny");
1032        let mut first = FstBlockIndex::build(&[]).unwrap();
1033        assert_eq!(first, expected);
1034        first.clear();
1035        let next = FstBlockIndex::build(&[]).unwrap();
1036        assert_eq!(next, expected, "callers cannot mutate the cached artifact");
1037        let index = FstBlockIndex::load(OwnedBytes::new(next)).unwrap();
1038        assert_eq!(index.len(), 0);
1039        assert_eq!(index.locate(b"anything"), None);
1040    }
1041
1042    #[cfg(feature = "fst-index")]
1043    #[test]
1044    fn test_fst_block_index_roundtrip() {
1045        let entries = vec![
1046            (
1047                b"aaa".to_vec(),
1048                BlockAddr {
1049                    offset: 0,
1050                    length: 100,
1051                },
1052            ),
1053            (
1054                b"bbb".to_vec(),
1055                BlockAddr {
1056                    offset: 100,
1057                    length: 150,
1058                },
1059            ),
1060            (
1061                b"ccc".to_vec(),
1062                BlockAddr {
1063                    offset: 250,
1064                    length: 200,
1065                },
1066            ),
1067        ];
1068
1069        let bytes = FstBlockIndex::build(&entries).unwrap();
1070        let index = FstBlockIndex::load(OwnedBytes::new(bytes)).unwrap();
1071
1072        assert_eq!(index.len(), 3);
1073
1074        // Test locate
1075        assert_eq!(index.locate(b"aaa"), Some(0));
1076        assert_eq!(index.locate(b"bbb"), Some(1));
1077        assert_eq!(index.locate(b"ccc"), Some(2));
1078        assert_eq!(index.locate(b"aab"), Some(0)); // Between aaa and bbb
1079        assert_eq!(index.locate(b"ddd"), Some(2)); // After all keys
1080    }
1081
1082    #[test]
1083    fn test_bit_writer_reader() {
1084        let mut buf = Vec::new();
1085        let mut writer = BitWriter::new(&mut buf);
1086
1087        writer.write(5, 3).unwrap(); // 101
1088        writer.write(3, 2).unwrap(); // 11
1089        writer.write(15, 4).unwrap(); // 1111
1090        writer.flush().unwrap();
1091
1092        let mut reader = BitReader::new(&buf);
1093        assert_eq!(reader.read(3).unwrap(), 5);
1094        assert_eq!(reader.read(2).unwrap(), 3);
1095        assert_eq!(reader.read(4).unwrap(), 15);
1096    }
1097}