Skip to main content

hermes_core/structures/
sstable.rs

1//! Async SSTable with lazy loading via FileSlice
2//!
3//! Memory-efficient design - only loads minimal metadata into memory,
4//! blocks are loaded on-demand.
5//!
6//! ## Key Features
7//!
8//! 1. **FST-based Block Index**: Uses Finite State Transducer for key lookup
9//!    - Can be mmap'd directly without parsing into heap-allocated structures
10//!    - ~90% memory reduction compared to Vec<BlockIndexEntry>
11//!
12//! 2. **Bitpacked Block Addresses**: Offsets and lengths stored with delta encoding
13//!    - Minimal memory footprint for block metadata
14//!
15//! 3. **Dictionary Compression**: Zstd dictionary for 15-30% better compression
16//!
17//! 4. **Configurable Compression Level**: Levels 1-22 for space/speed tradeoff
18//!
19//! 5. **Bloom Filter**: Fast negative lookups to skip unnecessary I/O
20
21use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
22use parking_lot::RwLock;
23use rustc_hash::FxHashMap;
24use std::io::{self, Read, Write};
25use std::sync::Arc;
26
27#[cfg(feature = "fst-index")]
28use super::sstable_index::FstBlockIndex;
29use super::sstable_index::{BlockAddr, BlockIndex, MmapBlockIndex};
30use crate::compression::{CompressionDict, CompressionLevel};
31use crate::directories::{FileHandle, OwnedBytes};
32
33/// SSTable magic number - version 4 with memory-efficient index
34/// Uses FST-based or mmap'd block index to avoid heap allocation
35pub const SSTABLE_MAGIC: u32 = 0x53544234; // "STB4"
36
37/// Block size for SSTable (16KB default)
38pub const BLOCK_SIZE: usize = 16 * 1024;
39
40/// Default dictionary size (64KB)
41pub const DEFAULT_DICT_SIZE: usize = 64 * 1024;
42
43/// Bloom filter bits per key (10 bits ≈ 1% false positive rate)
44pub const BLOOM_BITS_PER_KEY: usize = 10;
45
46/// Bloom filter hash count (optimal for 10 bits/key)
47pub const BLOOM_HASH_COUNT: usize = 7;
48
49const MAX_SSTABLE_BLOCK_BYTES: usize = 64 * 1024 * 1024;
50const MAX_SSTABLE_DICTIONARY_BYTES: u64 = 16 * 1024 * 1024;
51
52/// Results and truncation flag returned by a budgeted prefix scan.
53pub type PrefixScanResult<V> = (Vec<(Vec<u8>, V)>, bool);
54
55// ============================================================================
56// Bloom Filter Implementation
57// ============================================================================
58
59/// Simple bloom filter for key existence checks
60#[derive(Debug, Clone)]
61pub struct BloomFilter {
62    bits: BloomBits,
63    num_bits: usize,
64    num_hashes: usize,
65}
66
67/// Bloom filter storage — Vec for write path, OwnedBytes for zero-copy read path.
68#[derive(Debug, Clone)]
69enum BloomBits {
70    /// Mutable storage for building (SSTable writer)
71    Vec(Vec<u64>),
72    /// Zero-copy mmap reference for reading (raw LE u64 words, no header)
73    Bytes(OwnedBytes),
74}
75
76impl BloomBits {
77    #[inline]
78    fn len(&self) -> usize {
79        match self {
80            BloomBits::Vec(v) => v.len(),
81            BloomBits::Bytes(b) => b.len() / 8,
82        }
83    }
84
85    #[inline]
86    fn get(&self, word_idx: usize) -> u64 {
87        match self {
88            BloomBits::Vec(v) => v[word_idx],
89            BloomBits::Bytes(b) => {
90                let off = word_idx * 8;
91                u64::from_le_bytes([
92                    b[off],
93                    b[off + 1],
94                    b[off + 2],
95                    b[off + 3],
96                    b[off + 4],
97                    b[off + 5],
98                    b[off + 6],
99                    b[off + 7],
100                ])
101            }
102        }
103    }
104
105    #[inline]
106    fn set_bit(&mut self, word_idx: usize, bit_idx: usize) {
107        match self {
108            BloomBits::Vec(v) => v[word_idx] |= 1u64 << bit_idx,
109            BloomBits::Bytes(_) => panic!("cannot mutate read-only bloom filter"),
110        }
111    }
112
113    fn size_bytes(&self) -> usize {
114        match self {
115            BloomBits::Vec(v) => v.len() * 8,
116            BloomBits::Bytes(b) => b.len(),
117        }
118    }
119}
120
121impl BloomFilter {
122    /// Create a new bloom filter sized for expected number of keys
123    pub fn new(expected_keys: usize, bits_per_key: usize) -> Self {
124        let num_bits = expected_keys.saturating_mul(bits_per_key).max(64);
125        let num_words = num_bits.div_ceil(64);
126        Self {
127            bits: BloomBits::Vec(vec![0u64; num_words]),
128            num_bits,
129            num_hashes: BLOOM_HASH_COUNT,
130        }
131    }
132
133    /// Create from serialized bytes into a mutable Vec (for building/mutation).
134    /// Unlike `from_owned_bytes`, this copies data into a `Vec<u64>` so that
135    /// `insert()` works. Used by the primary-key bloom cache.
136    pub fn from_bytes_mutable(data: &[u8]) -> io::Result<Self> {
137        if data.len() < 12 {
138            return Err(io::Error::new(
139                io::ErrorKind::InvalidData,
140                "Bloom filter data too short",
141            ));
142        }
143        let num_bits = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
144        let num_hashes = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
145        let num_words = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
146
147        validate_bloom_header(data.len(), num_bits, num_hashes, num_words)?;
148        let expected_len = 12 + num_words * 8;
149        if data.len() != expected_len {
150            return Err(io::Error::new(
151                io::ErrorKind::InvalidData,
152                "Bloom filter data truncated",
153            ));
154        }
155
156        let mut vec = vec![0u64; num_words];
157        for (i, v) in vec.iter_mut().enumerate() {
158            let off = 12 + i * 8;
159            *v = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
160        }
161
162        Ok(Self {
163            bits: BloomBits::Vec(vec),
164            num_bits,
165            num_hashes,
166        })
167    }
168
169    /// Create from serialized OwnedBytes (zero-copy for mmap)
170    pub fn from_owned_bytes(data: OwnedBytes) -> io::Result<Self> {
171        if data.len() < 12 {
172            return Err(io::Error::new(
173                io::ErrorKind::InvalidData,
174                "Bloom filter data too short",
175            ));
176        }
177        let d = data.as_slice();
178        let num_bits = u32::from_le_bytes([d[0], d[1], d[2], d[3]]) as usize;
179        let num_hashes = u32::from_le_bytes([d[4], d[5], d[6], d[7]]) as usize;
180        let num_words = u32::from_le_bytes([d[8], d[9], d[10], d[11]]) as usize;
181
182        validate_bloom_header(d.len(), num_bits, num_hashes, num_words)?;
183        let expected_len = 12 + num_words * 8;
184        if d.len() != expected_len {
185            return Err(io::Error::new(
186                io::ErrorKind::InvalidData,
187                "Bloom filter data truncated",
188            ));
189        }
190
191        // Slice past the 12-byte header to get raw u64 LE words (zero-copy)
192        let bits_bytes = data.slice(12..12 + num_words * 8);
193
194        Ok(Self {
195            bits: BloomBits::Bytes(bits_bytes),
196            num_bits,
197            num_hashes,
198        })
199    }
200
201    /// Serialize to bytes
202    pub fn to_bytes(&self) -> Vec<u8> {
203        let num_words = self.bits.len();
204        let mut data = Vec::with_capacity(12 + num_words * 8);
205        data.write_u32::<LittleEndian>(self.num_bits as u32)
206            .unwrap();
207        data.write_u32::<LittleEndian>(self.num_hashes as u32)
208            .unwrap();
209        data.write_u32::<LittleEndian>(num_words as u32).unwrap();
210        for i in 0..num_words {
211            data.write_u64::<LittleEndian>(self.bits.get(i)).unwrap();
212        }
213        data
214    }
215
216    /// Add a key to the filter
217    pub fn insert(&mut self, key: &[u8]) {
218        let (h1, h2) = self.hash_pair(key);
219        for i in 0..self.num_hashes {
220            let bit_pos = self.get_bit_pos(h1, h2, i);
221            let word_idx = bit_pos / 64;
222            let bit_idx = bit_pos % 64;
223            if word_idx < self.bits.len() {
224                self.bits.set_bit(word_idx, bit_idx);
225            }
226        }
227    }
228
229    /// Check if a key might be in the filter
230    /// Returns false if definitely not present, true if possibly present
231    pub fn may_contain(&self, key: &[u8]) -> bool {
232        let (h1, h2) = self.hash_pair(key);
233        for i in 0..self.num_hashes {
234            let bit_pos = self.get_bit_pos(h1, h2, i);
235            let word_idx = bit_pos / 64;
236            let bit_idx = bit_pos % 64;
237            if word_idx >= self.bits.len() || (self.bits.get(word_idx) & (1u64 << bit_idx)) == 0 {
238                return false;
239            }
240        }
241        true
242    }
243
244    /// Size in bytes
245    pub fn size_bytes(&self) -> usize {
246        12 + self.bits.size_bytes()
247    }
248
249    /// Insert a pre-computed hash pair into the filter
250    pub fn insert_hashed(&mut self, h1: u64, h2: u64) {
251        for i in 0..self.num_hashes {
252            let bit_pos = self.get_bit_pos(h1, h2, i);
253            let word_idx = bit_pos / 64;
254            let bit_idx = bit_pos % 64;
255            if word_idx < self.bits.len() {
256                self.bits.set_bit(word_idx, bit_idx);
257            }
258        }
259    }
260
261    /// Compute two hash values using FNV-1a variant (single pass over key bytes)
262    #[inline]
263    fn hash_pair(&self, key: &[u8]) -> (u64, u64) {
264        let mut h1: u64 = 0xcbf29ce484222325;
265        let mut h2: u64 = 0x84222325cbf29ce4;
266        for &byte in key {
267            h1 ^= byte as u64;
268            h1 = h1.wrapping_mul(0x100000001b3);
269            h2 = h2.wrapping_mul(0x100000001b3);
270            h2 ^= byte as u64;
271        }
272        (h1, h2)
273    }
274
275    /// Get bit position for hash iteration i using double hashing
276    #[inline]
277    fn get_bit_pos(&self, h1: u64, h2: u64, i: usize) -> usize {
278        (h1.wrapping_add((i as u64).wrapping_mul(h2)) % (self.num_bits as u64)) as usize
279    }
280}
281
282fn validate_bloom_header(
283    data_len: usize,
284    num_bits: usize,
285    num_hashes: usize,
286    num_words: usize,
287) -> io::Result<()> {
288    if num_bits == 0 || num_hashes == 0 || num_hashes > 32 || num_words == 0 {
289        return Err(io::Error::new(
290            io::ErrorKind::InvalidData,
291            "invalid bloom filter parameters",
292        ));
293    }
294    let word_bytes = num_words
295        .checked_mul(8)
296        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "bloom filter size overflow"))?;
297    let expected_len = 12usize
298        .checked_add(word_bytes)
299        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "bloom filter size overflow"))?;
300    let capacity_bits = num_words
301        .checked_mul(64)
302        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "bloom filter size overflow"))?;
303    if expected_len > data_len
304        || num_bits > capacity_bits
305        || num_bits <= capacity_bits.saturating_sub(64)
306    {
307        return Err(io::Error::new(
308            io::ErrorKind::InvalidData,
309            "inconsistent bloom filter dimensions",
310        ));
311    }
312    Ok(())
313}
314
315/// Compute bloom filter hash pair for a key (standalone, no BloomFilter needed).
316/// Uses the same FNV-1a double-hashing as BloomFilter::hash_pair (single pass).
317#[inline]
318fn bloom_hash_pair(key: &[u8]) -> (u64, u64) {
319    let mut h1: u64 = 0xcbf29ce484222325;
320    let mut h2: u64 = 0x84222325cbf29ce4;
321    for &byte in key {
322        h1 ^= byte as u64;
323        h1 = h1.wrapping_mul(0x100000001b3);
324        h2 = h2.wrapping_mul(0x100000001b3);
325        h2 ^= byte as u64;
326    }
327    (h1, h2)
328}
329
330/// SSTable value trait
331pub trait SSTableValue: Clone + Send + Sync {
332    fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()>;
333    fn deserialize<R: Read>(reader: &mut R) -> io::Result<Self>;
334}
335
336/// u64 value implementation
337impl SSTableValue for u64 {
338    fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
339        write_vint(writer, *self)
340    }
341
342    fn deserialize<R: Read>(reader: &mut R) -> io::Result<Self> {
343        read_vint(reader)
344    }
345}
346
347/// Vec<u8> value implementation
348impl SSTableValue for Vec<u8> {
349    fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
350        write_vint(writer, self.len() as u64)?;
351        writer.write_all(self)
352    }
353
354    fn deserialize<R: Read>(reader: &mut R) -> io::Result<Self> {
355        let len = usize::try_from(read_vint(reader)?).map_err(|_| {
356            io::Error::new(io::ErrorKind::InvalidData, "SSTable value length too large")
357        })?;
358        if len > MAX_SSTABLE_BLOCK_BYTES {
359            return Err(io::Error::new(
360                io::ErrorKind::InvalidData,
361                "SSTable value exceeds block safety limit",
362            ));
363        }
364        let mut data = vec![0u8; len];
365        reader.read_exact(&mut data)?;
366        Ok(data)
367    }
368}
369
370/// Sparse dimension info for SSTable-based sparse index
371/// Stores offset and length for posting list lookup
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373pub struct SparseDimInfo {
374    /// Offset in sparse file where posting list starts
375    pub offset: u64,
376    /// Length of serialized posting list
377    pub length: u32,
378}
379
380impl SparseDimInfo {
381    pub fn new(offset: u64, length: u32) -> Self {
382        Self { offset, length }
383    }
384}
385
386impl SSTableValue for SparseDimInfo {
387    fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
388        write_vint(writer, self.offset)?;
389        write_vint(writer, self.length as u64)
390    }
391
392    fn deserialize<R: Read>(reader: &mut R) -> io::Result<Self> {
393        let offset = read_vint(reader)?;
394        let length = u32::try_from(read_vint(reader)?).map_err(|_| {
395            io::Error::new(
396                io::ErrorKind::InvalidData,
397                "sparse posting length exceeds u32",
398            )
399        })?;
400        Ok(Self { offset, length })
401    }
402}
403
404/// Maximum number of postings that can be inlined in TermInfo
405pub const MAX_INLINE_POSTINGS: usize = 3;
406
407/// Term info for posting list references
408///
409/// Supports two modes:
410/// - **Inline**: Small posting lists (1-3 docs) stored directly in TermInfo
411/// - **External**: Larger posting lists stored in separate .post file
412///
413/// This eliminates a separate I/O read for rare/unique terms.
414#[derive(Debug, Clone, PartialEq, Eq)]
415pub enum TermInfo {
416    /// Small posting list inlined directly (up to MAX_INLINE_POSTINGS entries)
417    /// Each entry is (doc_id, term_freq) delta-encoded
418    Inline {
419        /// Number of postings (1-3)
420        doc_freq: u8,
421        /// Inline data: delta-encoded (doc_id, term_freq) pairs
422        /// Format: [delta_doc_id, term_freq, delta_doc_id, term_freq, ...]
423        data: [u8; 16],
424        /// Actual length of data used
425        data_len: u8,
426    },
427    /// Reference to external posting list in .post file
428    External {
429        posting_offset: u64,
430        posting_len: u64,
431        doc_freq: u32,
432        /// Position data offset (0 if no positions)
433        position_offset: u64,
434        /// Position data length (0 if no positions)
435        position_len: u64,
436    },
437}
438
439impl TermInfo {
440    /// Create an external reference
441    pub fn external(posting_offset: u64, posting_len: u64, doc_freq: u32) -> Self {
442        TermInfo::External {
443            posting_offset,
444            posting_len,
445            doc_freq,
446            position_offset: 0,
447            position_len: 0,
448        }
449    }
450
451    /// Create an external reference with position info
452    pub fn external_with_positions(
453        posting_offset: u64,
454        posting_len: u64,
455        doc_freq: u32,
456        position_offset: u64,
457        position_len: u64,
458    ) -> Self {
459        TermInfo::External {
460            posting_offset,
461            posting_len,
462            doc_freq,
463            position_offset,
464            position_len,
465        }
466    }
467
468    /// Try to create an inline TermInfo from posting data
469    /// Returns None if posting list is too large to inline
470    pub fn try_inline(doc_ids: &[u32], term_freqs: &[u32]) -> Option<Self> {
471        if doc_ids.len() > MAX_INLINE_POSTINGS
472            || doc_ids.is_empty()
473            || doc_ids.len() != term_freqs.len()
474        {
475            return None;
476        }
477
478        let mut data = [0u8; 16];
479        let mut cursor = std::io::Cursor::new(&mut data[..]);
480        let mut prev_doc_id = 0u32;
481
482        for (i, &doc_id) in doc_ids.iter().enumerate() {
483            let delta = doc_id.checked_sub(prev_doc_id)?;
484            if write_vint(&mut cursor, delta as u64).is_err() {
485                return None;
486            }
487            if write_vint(&mut cursor, term_freqs[i] as u64).is_err() {
488                return None;
489            }
490            prev_doc_id = doc_id;
491        }
492
493        let data_len = cursor.position() as u8;
494        if data_len > 16 {
495            return None;
496        }
497
498        Some(TermInfo::Inline {
499            doc_freq: doc_ids.len() as u8,
500            data,
501            data_len,
502        })
503    }
504
505    /// Try to create an inline TermInfo from an iterator of (doc_id, term_freq) pairs.
506    /// Zero-allocation alternative to `try_inline` — avoids collecting into Vec<u32>.
507    /// `count` is the number of postings (must match iterator length).
508    pub fn try_inline_iter(count: usize, iter: impl Iterator<Item = (u32, u32)>) -> Option<Self> {
509        if count > MAX_INLINE_POSTINGS || count == 0 {
510            return None;
511        }
512
513        let mut data = [0u8; 16];
514        let mut cursor = std::io::Cursor::new(&mut data[..]);
515        let mut prev_doc_id = 0u32;
516
517        let mut actual_count = 0usize;
518        for (doc_id, tf) in iter {
519            if actual_count >= count {
520                return None;
521            }
522            let delta = doc_id.checked_sub(prev_doc_id)?;
523            if write_vint(&mut cursor, delta as u64).is_err() {
524                return None;
525            }
526            if write_vint(&mut cursor, tf as u64).is_err() {
527                return None;
528            }
529            prev_doc_id = doc_id;
530            actual_count += 1;
531        }
532
533        if actual_count != count {
534            return None;
535        }
536
537        let data_len = cursor.position() as u8;
538
539        Some(TermInfo::Inline {
540            doc_freq: count as u8,
541            data,
542            data_len,
543        })
544    }
545
546    /// Get document frequency
547    pub fn doc_freq(&self) -> u32 {
548        match self {
549            TermInfo::Inline { doc_freq, .. } => *doc_freq as u32,
550            TermInfo::External { doc_freq, .. } => *doc_freq,
551        }
552    }
553
554    /// Check if this is an inline posting list
555    pub fn is_inline(&self) -> bool {
556        matches!(self, TermInfo::Inline { .. })
557    }
558
559    /// Get external posting info (offset, len) - returns None for inline
560    pub fn external_info(&self) -> Option<(u64, u64)> {
561        match self {
562            TermInfo::External {
563                posting_offset,
564                posting_len,
565                ..
566            } => Some((*posting_offset, *posting_len)),
567            TermInfo::Inline { .. } => None,
568        }
569    }
570
571    /// Get position info (offset, len) - returns None for inline or if no positions
572    pub fn position_info(&self) -> Option<(u64, u64)> {
573        match self {
574            TermInfo::External {
575                position_offset,
576                position_len,
577                ..
578            } if *position_len > 0 => Some((*position_offset, *position_len)),
579            _ => None,
580        }
581    }
582
583    /// Decode inline postings into (doc_ids, term_freqs)
584    /// Returns None if this is an external reference
585    pub fn decode_inline(&self) -> Option<(Vec<u32>, Vec<u32>)> {
586        match self {
587            TermInfo::Inline {
588                doc_freq,
589                data,
590                data_len,
591            } => {
592                if *doc_freq == 0
593                    || *doc_freq as usize > MAX_INLINE_POSTINGS
594                    || *data_len as usize > data.len()
595                {
596                    return None;
597                }
598                let mut doc_ids = Vec::with_capacity(*doc_freq as usize);
599                let mut term_freqs = Vec::with_capacity(*doc_freq as usize);
600                let mut reader = &data[..*data_len as usize];
601                let mut prev_doc_id = 0u32;
602
603                for _ in 0..*doc_freq {
604                    let delta = read_vint(&mut reader).ok()? as u32;
605                    let tf = read_vint(&mut reader).ok()? as u32;
606                    let doc_id = prev_doc_id.checked_add(delta)?;
607                    doc_ids.push(doc_id);
608                    term_freqs.push(tf);
609                    prev_doc_id = doc_id;
610                }
611
612                Some((doc_ids, term_freqs))
613            }
614            TermInfo::External { .. } => None,
615        }
616    }
617}
618
619impl SSTableValue for TermInfo {
620    fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
621        match self {
622            TermInfo::Inline {
623                doc_freq,
624                data,
625                data_len,
626            } => {
627                if *doc_freq == 0
628                    || *doc_freq as usize > MAX_INLINE_POSTINGS
629                    || *data_len as usize > data.len()
630                {
631                    return Err(io::Error::new(
632                        io::ErrorKind::InvalidInput,
633                        "invalid inline TermInfo",
634                    ));
635                }
636                // Tag byte 0xFF = inline marker
637                writer.write_u8(0xFF)?;
638                writer.write_u8(*doc_freq)?;
639                writer.write_u8(*data_len)?;
640                writer.write_all(&data[..*data_len as usize])?;
641            }
642            TermInfo::External {
643                posting_offset,
644                posting_len,
645                doc_freq,
646                position_offset,
647                position_len,
648            } => {
649                // Tag byte 0x00 = external marker (no positions)
650                // Tag byte 0x01 = external with positions
651                if *position_len > 0 {
652                    writer.write_u8(0x01)?;
653                    write_vint(writer, *doc_freq as u64)?;
654                    write_vint(writer, *posting_offset)?;
655                    write_vint(writer, *posting_len)?;
656                    write_vint(writer, *position_offset)?;
657                    write_vint(writer, *position_len)?;
658                } else {
659                    writer.write_u8(0x00)?;
660                    write_vint(writer, *doc_freq as u64)?;
661                    write_vint(writer, *posting_offset)?;
662                    write_vint(writer, *posting_len)?;
663                }
664            }
665        }
666        Ok(())
667    }
668
669    fn deserialize<R: Read>(reader: &mut R) -> io::Result<Self> {
670        let tag = reader.read_u8()?;
671
672        if tag == 0xFF {
673            // Inline
674            let doc_freq = reader.read_u8()?;
675            let data_len = reader.read_u8()?;
676            if doc_freq == 0 || doc_freq as usize > MAX_INLINE_POSTINGS || data_len as usize > 16 {
677                return Err(io::Error::new(
678                    io::ErrorKind::InvalidData,
679                    "invalid inline TermInfo lengths",
680                ));
681            }
682            let mut data = [0u8; 16];
683            reader.read_exact(&mut data[..data_len as usize])?;
684            Ok(TermInfo::Inline {
685                doc_freq,
686                data,
687                data_len,
688            })
689        } else if tag == 0x00 {
690            // External (no positions)
691            let doc_freq = read_vint(reader)? as u32;
692            let posting_offset = read_vint(reader)?;
693            let posting_len = read_vint(reader)?;
694            Ok(TermInfo::External {
695                posting_offset,
696                posting_len,
697                doc_freq,
698                position_offset: 0,
699                position_len: 0,
700            })
701        } else if tag == 0x01 {
702            // External with positions
703            let doc_freq = read_vint(reader)? as u32;
704            let posting_offset = read_vint(reader)?;
705            let posting_len = read_vint(reader)?;
706            let position_offset = read_vint(reader)?;
707            let position_len = read_vint(reader)?;
708            Ok(TermInfo::External {
709                posting_offset,
710                posting_len,
711                doc_freq,
712                position_offset,
713                position_len,
714            })
715        } else {
716            Err(io::Error::new(
717                io::ErrorKind::InvalidData,
718                format!("Invalid TermInfo tag: {}", tag),
719            ))
720        }
721    }
722}
723
724/// Write variable-length integer
725pub fn write_vint<W: Write + ?Sized>(writer: &mut W, mut value: u64) -> io::Result<()> {
726    loop {
727        let byte = (value & 0x7F) as u8;
728        value >>= 7;
729        if value == 0 {
730            writer.write_u8(byte)?;
731            return Ok(());
732        } else {
733            writer.write_u8(byte | 0x80)?;
734        }
735    }
736}
737
738/// Read variable-length integer
739pub fn read_vint<R: Read>(reader: &mut R) -> io::Result<u64> {
740    let mut result = 0u64;
741    let mut shift = 0;
742
743    loop {
744        let byte = reader.read_u8()?;
745        result |= ((byte & 0x7F) as u64) << shift;
746        if byte & 0x80 == 0 {
747            return Ok(result);
748        }
749        shift += 7;
750        if shift >= 64 {
751            return Err(io::Error::new(
752                io::ErrorKind::InvalidData,
753                "varint too long",
754            ));
755        }
756    }
757}
758
759/// Compute common prefix length
760pub fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
761    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
762}
763
764/// SSTable statistics for debugging
765#[derive(Debug, Clone)]
766pub struct SSTableStats {
767    pub num_blocks: usize,
768    pub num_sparse_entries: usize,
769    pub num_entries: u64,
770    pub has_bloom_filter: bool,
771    pub has_dictionary: bool,
772    pub bloom_filter_size: usize,
773    pub dictionary_size: usize,
774}
775
776/// SSTable writer configuration
777#[derive(Debug, Clone)]
778pub struct SSTableWriterConfig {
779    /// Compression level (1-22, higher = better compression but slower)
780    pub compression_level: CompressionLevel,
781    /// Whether to train and use a dictionary for compression
782    pub use_dictionary: bool,
783    /// Dictionary size in bytes (default 64KB)
784    pub dict_size: usize,
785    /// Whether to build a bloom filter
786    pub use_bloom_filter: bool,
787    /// Bloom filter bits per key (default 10 = ~1% false positive rate)
788    pub bloom_bits_per_key: usize,
789}
790
791impl Default for SSTableWriterConfig {
792    fn default() -> Self {
793        Self::from_optimization(crate::structures::IndexOptimization::default())
794    }
795}
796
797impl SSTableWriterConfig {
798    /// Create config from IndexOptimization mode
799    pub fn from_optimization(optimization: crate::structures::IndexOptimization) -> Self {
800        use crate::structures::IndexOptimization;
801        match optimization {
802            IndexOptimization::Adaptive => Self {
803                compression_level: CompressionLevel::BETTER, // Level 9
804                use_dictionary: false,
805                dict_size: DEFAULT_DICT_SIZE,
806                use_bloom_filter: true, // Bloom is cheap (~1.25 B/key) and avoids needless block reads
807                bloom_bits_per_key: BLOOM_BITS_PER_KEY,
808            },
809            IndexOptimization::SizeOptimized => Self {
810                compression_level: CompressionLevel::MAX, // Level 22
811                use_dictionary: true,
812                dict_size: DEFAULT_DICT_SIZE,
813                use_bloom_filter: true,
814                bloom_bits_per_key: BLOOM_BITS_PER_KEY,
815            },
816            IndexOptimization::PerformanceOptimized => Self {
817                compression_level: CompressionLevel::FAST, // Level 1
818                use_dictionary: false,
819                dict_size: DEFAULT_DICT_SIZE,
820                use_bloom_filter: true, // Bloom helps skip blocks fast
821                bloom_bits_per_key: BLOOM_BITS_PER_KEY,
822            },
823        }
824    }
825
826    /// Fast configuration - prioritize write speed over compression
827    pub fn fast() -> Self {
828        Self::from_optimization(crate::structures::IndexOptimization::PerformanceOptimized)
829    }
830
831    /// Maximum compression configuration - prioritize size over speed
832    pub fn max_compression() -> Self {
833        Self::from_optimization(crate::structures::IndexOptimization::SizeOptimized)
834    }
835}
836
837/// SSTable writer with optimizations:
838/// - Dictionary compression for blocks (if dictionary provided)
839/// - Configurable compression level
840/// - Block index prefix compression
841/// - Bloom filter for fast negative lookups
842pub struct SSTableWriter<W: Write, V: SSTableValue> {
843    writer: W,
844    block_buffer: Vec<u8>,
845    prev_key: Vec<u8>,
846    index: Vec<BlockIndexEntry>,
847    current_offset: u64,
848    num_entries: u64,
849    block_first_key: Option<Vec<u8>>,
850    config: SSTableWriterConfig,
851    /// Pre-trained dictionary for compression (optional)
852    dictionary: Option<CompressionDict>,
853    /// Bloom filter key hashes — compact (u64, u64) pairs instead of full keys.
854    /// Filter is built at finish() time with correct sizing.
855    bloom_hashes: Vec<(u64, u64)>,
856    _phantom: std::marker::PhantomData<V>,
857}
858
859impl<W: Write, V: SSTableValue> SSTableWriter<W, V> {
860    /// Create a new SSTable writer with default configuration
861    pub fn new(writer: W) -> Self {
862        Self::with_config(writer, SSTableWriterConfig::default())
863    }
864
865    /// Create a new SSTable writer with custom configuration
866    pub fn with_config(writer: W, config: SSTableWriterConfig) -> Self {
867        Self {
868            writer,
869            block_buffer: Vec::with_capacity(BLOCK_SIZE),
870            prev_key: Vec::new(),
871            index: Vec::new(),
872            current_offset: 0,
873            num_entries: 0,
874            block_first_key: None,
875            config,
876            dictionary: None,
877            bloom_hashes: Vec::new(),
878            _phantom: std::marker::PhantomData,
879        }
880    }
881
882    /// Create a new SSTable writer with a pre-trained dictionary
883    pub fn with_dictionary(
884        writer: W,
885        config: SSTableWriterConfig,
886        dictionary: CompressionDict,
887    ) -> Self {
888        Self {
889            writer,
890            block_buffer: Vec::with_capacity(BLOCK_SIZE),
891            prev_key: Vec::new(),
892            index: Vec::new(),
893            current_offset: 0,
894            num_entries: 0,
895            block_first_key: None,
896            config,
897            dictionary: Some(dictionary),
898            bloom_hashes: Vec::new(),
899            _phantom: std::marker::PhantomData,
900        }
901    }
902
903    pub fn insert(&mut self, key: &[u8], value: &V) -> io::Result<()> {
904        if self.block_first_key.is_none() {
905            self.block_first_key = Some(key.to_vec());
906        }
907
908        // Store compact hash pair for bloom filter (16 bytes vs ~48+ per key)
909        if self.config.use_bloom_filter {
910            self.bloom_hashes.push(bloom_hash_pair(key));
911        }
912
913        let prefix_len = common_prefix_len(&self.prev_key, key);
914        let suffix = &key[prefix_len..];
915
916        write_vint(&mut self.block_buffer, prefix_len as u64)?;
917        write_vint(&mut self.block_buffer, suffix.len() as u64)?;
918        self.block_buffer.extend_from_slice(suffix);
919        value.serialize(&mut self.block_buffer)?;
920
921        self.prev_key.clear();
922        self.prev_key.extend_from_slice(key);
923        self.num_entries += 1;
924
925        if self.block_buffer.len() >= BLOCK_SIZE {
926            self.flush_block()?;
927        }
928
929        Ok(())
930    }
931
932    /// Flush and compress the current block
933    fn flush_block(&mut self) -> io::Result<()> {
934        if self.block_buffer.is_empty() {
935            return Ok(());
936        }
937
938        // Compress block with dictionary if available
939        let compressed = if let Some(ref dict) = self.dictionary {
940            crate::compression::compress_with_dict(
941                &self.block_buffer,
942                self.config.compression_level,
943                dict,
944            )?
945        } else {
946            crate::compression::compress(&self.block_buffer, self.config.compression_level)?
947        };
948
949        if let Some(first_key) = self.block_first_key.take() {
950            self.index.push(BlockIndexEntry {
951                first_key,
952                offset: self.current_offset,
953                length: compressed.len() as u32,
954            });
955        }
956
957        self.writer.write_all(&compressed)?;
958        self.current_offset += compressed.len() as u64;
959        self.block_buffer.clear();
960        self.prev_key.clear();
961
962        Ok(())
963    }
964
965    pub fn finish(mut self) -> io::Result<W> {
966        // Flush any remaining data
967        self.flush_block()?;
968
969        // Build bloom filter from collected hashes (properly sized)
970        let bloom_filter = if self.config.use_bloom_filter && !self.bloom_hashes.is_empty() {
971            let mut bloom =
972                BloomFilter::new(self.bloom_hashes.len(), self.config.bloom_bits_per_key);
973            for (h1, h2) in &self.bloom_hashes {
974                bloom.insert_hashed(*h1, *h2);
975            }
976            Some(bloom)
977        } else {
978            None
979        };
980
981        let data_end_offset = self.current_offset;
982
983        // Build memory-efficient block index
984        // Convert to (key, BlockAddr) pairs for the new index format
985        let entries: Vec<(Vec<u8>, BlockAddr)> = self
986            .index
987            .iter()
988            .map(|e| {
989                (
990                    e.first_key.clone(),
991                    BlockAddr {
992                        offset: e.offset,
993                        length: e.length,
994                    },
995                )
996            })
997            .collect();
998
999        // Build FST-based index if native feature is enabled, otherwise use mmap index
1000        #[cfg(feature = "native")]
1001        let index_bytes = FstBlockIndex::build(&entries)?;
1002        #[cfg(not(feature = "native"))]
1003        let index_bytes = MmapBlockIndex::build(&entries)?;
1004
1005        // Write index bytes with length prefix
1006        self.writer
1007            .write_u32::<LittleEndian>(index_bytes.len() as u32)?;
1008        self.writer.write_all(&index_bytes)?;
1009        self.current_offset += 4 + index_bytes.len() as u64;
1010
1011        // Write bloom filter if present
1012        let bloom_offset = if let Some(ref bloom) = bloom_filter {
1013            let bloom_data = bloom.to_bytes();
1014            let offset = self.current_offset;
1015            self.writer.write_all(&bloom_data)?;
1016            self.current_offset += bloom_data.len() as u64;
1017            offset
1018        } else {
1019            0
1020        };
1021
1022        // Write dictionary if present
1023        let dict_offset = if let Some(ref dict) = self.dictionary {
1024            let dict_bytes = dict.as_bytes();
1025            let offset = self.current_offset;
1026            self.writer
1027                .write_u32::<LittleEndian>(dict_bytes.len() as u32)?;
1028            self.writer.write_all(dict_bytes)?;
1029            self.current_offset += 4 + dict_bytes.len() as u64;
1030            offset
1031        } else {
1032            0
1033        };
1034
1035        // Write extended footer
1036        self.writer.write_u64::<LittleEndian>(data_end_offset)?;
1037        self.writer.write_u64::<LittleEndian>(self.num_entries)?;
1038        self.writer.write_u64::<LittleEndian>(bloom_offset)?; // 0 if no bloom
1039        self.writer.write_u64::<LittleEndian>(dict_offset)?; // 0 if no dict
1040        self.writer
1041            .write_u8(self.config.compression_level.0 as u8)?;
1042        self.writer.write_u32::<LittleEndian>(SSTABLE_MAGIC)?;
1043
1044        Ok(self.writer)
1045    }
1046}
1047
1048/// Block index entry
1049#[derive(Debug, Clone)]
1050struct BlockIndexEntry {
1051    first_key: Vec<u8>,
1052    offset: u64,
1053    length: u32,
1054}
1055
1056/// Async SSTable reader - loads blocks on demand via FileHandle
1057///
1058/// Memory-efficient design:
1059/// - Block index uses FST (native) or mmap'd raw bytes - no heap allocation for keys
1060/// - Block addresses stored in bitpacked format
1061/// - Bloom filter and dictionary optional
1062pub struct AsyncSSTableReader<V: SSTableValue> {
1063    /// FileHandle for the data portion (blocks only) - fetches ranges on demand
1064    data_slice: FileHandle,
1065    /// Memory-efficient block index (FST or mmap)
1066    block_index: BlockIndex,
1067    num_entries: u64,
1068    /// Hot cache for decompressed blocks
1069    cache: RwLock<BlockCache>,
1070    /// Bloom filter for fast negative lookups (optional)
1071    bloom_filter: Option<BloomFilter>,
1072    /// Compression dictionary (optional)
1073    dictionary: Option<CompressionDict>,
1074    /// Compression level used
1075    #[allow(dead_code)]
1076    compression_level: CompressionLevel,
1077    _phantom: std::marker::PhantomData<V>,
1078}
1079
1080/// Bounded block cache with a contention-free read path.
1081///
1082/// Normal reads use [`BlockCache::peek`] under a shared lock and deliberately
1083/// do not promote hits, so normal eviction order is insertion order. `get()`
1084/// still promotes entries for exclusive warm-up and race-resolution paths.
1085struct BlockCache {
1086    blocks: FxHashMap<u64, Arc<[u8]>>,
1087    lru_order: std::collections::VecDeque<u64>,
1088    max_blocks: usize,
1089}
1090
1091impl BlockCache {
1092    fn new(max_blocks: usize) -> Self {
1093        Self {
1094            blocks: FxHashMap::default(),
1095            lru_order: std::collections::VecDeque::with_capacity(max_blocks),
1096            max_blocks,
1097        }
1098    }
1099
1100    fn get(&mut self, offset: u64) -> Option<Arc<[u8]>> {
1101        if self.blocks.contains_key(&offset) {
1102            self.promote(offset);
1103            self.blocks.get(&offset).map(Arc::clone)
1104        } else {
1105            None
1106        }
1107    }
1108
1109    /// Read-only cache probe — no LRU promotion, safe behind a read lock.
1110    fn peek(&self, offset: u64) -> Option<Arc<[u8]>> {
1111        self.blocks.get(&offset).map(Arc::clone)
1112    }
1113
1114    fn insert(&mut self, offset: u64, block: Arc<[u8]>) {
1115        if self.max_blocks == 0 {
1116            return;
1117        }
1118        if self.blocks.contains_key(&offset) {
1119            self.promote(offset);
1120            return;
1121        }
1122        while self.blocks.len() >= self.max_blocks {
1123            if let Some(evict_offset) = self.lru_order.pop_front() {
1124                self.blocks.remove(&evict_offset);
1125            } else {
1126                break;
1127            }
1128        }
1129        self.blocks.insert(offset, block);
1130        self.lru_order.push_back(offset);
1131    }
1132
1133    /// Move entry to MRU position (back of deque)
1134    fn promote(&mut self, offset: u64) {
1135        if let Some(pos) = self.lru_order.iter().position(|&k| k == offset) {
1136            self.lru_order.remove(pos);
1137            self.lru_order.push_back(offset);
1138        }
1139    }
1140}
1141
1142impl<V: SSTableValue> AsyncSSTableReader<V> {
1143    /// Open an SSTable from a FileHandle
1144    /// Only loads the footer and index into memory, data blocks fetched on-demand
1145    ///
1146    /// Uses FST-based (native) or mmap'd block index (no heap allocation for keys)
1147    pub async fn open(file_handle: FileHandle, cache_blocks: usize) -> io::Result<Self> {
1148        let file_len = file_handle.len();
1149        if file_len < 37 {
1150            return Err(io::Error::new(
1151                io::ErrorKind::InvalidData,
1152                "SSTable too small",
1153            ));
1154        }
1155
1156        // Read footer (37 bytes)
1157        // Format: data_end(8) + num_entries(8) + bloom_offset(8) + dict_offset(8) + compression_level(1) + magic(4)
1158        let footer_bytes = file_handle
1159            .read_bytes_range(file_len - 37..file_len)
1160            .await?;
1161
1162        let mut reader = footer_bytes.as_slice();
1163        let data_end_offset = reader.read_u64::<LittleEndian>()?;
1164        let num_entries = reader.read_u64::<LittleEndian>()?;
1165        let bloom_offset = reader.read_u64::<LittleEndian>()?;
1166        let dict_offset = reader.read_u64::<LittleEndian>()?;
1167        let compression_level = CompressionLevel(reader.read_u8()? as i32);
1168        let magic = reader.read_u32::<LittleEndian>()?;
1169
1170        if magic != SSTABLE_MAGIC {
1171            return Err(io::Error::new(
1172                io::ErrorKind::InvalidData,
1173                format!("Invalid SSTable magic: 0x{:08X}", magic),
1174            ));
1175        }
1176
1177        let footer_start = file_len - 37;
1178        if data_end_offset > footer_start {
1179            return Err(io::Error::new(
1180                io::ErrorKind::InvalidData,
1181                "SSTable data section extends past its footer",
1182            ));
1183        }
1184        if bloom_offset != 0 && (bloom_offset < data_end_offset || bloom_offset >= footer_start) {
1185            return Err(io::Error::new(
1186                io::ErrorKind::InvalidData,
1187                "SSTable bloom filter offset is out of bounds",
1188            ));
1189        }
1190        if dict_offset != 0
1191            && (dict_offset < data_end_offset
1192                || dict_offset >= footer_start
1193                || (bloom_offset != 0 && dict_offset <= bloom_offset))
1194        {
1195            return Err(io::Error::new(
1196                io::ErrorKind::InvalidData,
1197                "SSTable dictionary offset is out of bounds",
1198            ));
1199        }
1200
1201        // Read index section
1202        let index_start = data_end_offset;
1203        let index_end = if bloom_offset != 0 {
1204            bloom_offset
1205        } else if dict_offset != 0 {
1206            dict_offset
1207        } else {
1208            footer_start
1209        };
1210        let index_bytes = file_handle.read_bytes_range(index_start..index_end).await?;
1211
1212        // Parse block index (length-prefixed FST or mmap index)
1213        let mut idx_reader = index_bytes.as_slice();
1214        let index_len = idx_reader.read_u32::<LittleEndian>()? as usize;
1215
1216        if index_len != idx_reader.len() {
1217            return Err(io::Error::new(
1218                io::ErrorKind::InvalidData,
1219                "Index data truncated",
1220            ));
1221        }
1222
1223        let index_data = index_bytes.slice(4..4 + index_len);
1224
1225        // Try FST first (when fst-index feature available), fall back to mmap
1226        #[cfg(feature = "fst-index")]
1227        let block_index = match FstBlockIndex::load(index_data.clone()) {
1228            Ok(fst_idx) => BlockIndex::Fst(fst_idx),
1229            Err(_) => BlockIndex::Mmap(MmapBlockIndex::load(index_data)?),
1230        };
1231        #[cfg(not(feature = "fst-index"))]
1232        let block_index = BlockIndex::Mmap(MmapBlockIndex::load(index_data)?);
1233
1234        let mut expected_offset = 0u64;
1235        for addr in block_index.all_addrs() {
1236            let end = addr.offset.checked_add(addr.length as u64).ok_or_else(|| {
1237                io::Error::new(io::ErrorKind::InvalidData, "SSTable block range overflow")
1238            })?;
1239            if addr.length == 0 || addr.offset != expected_offset || end > data_end_offset {
1240                return Err(io::Error::new(
1241                    io::ErrorKind::InvalidData,
1242                    "SSTable block addresses are inconsistent",
1243                ));
1244            }
1245            expected_offset = end;
1246        }
1247        if expected_offset != data_end_offset {
1248            return Err(io::Error::new(
1249                io::ErrorKind::InvalidData,
1250                "SSTable block addresses do not cover the data section",
1251            ));
1252        }
1253
1254        // Load bloom filter if present
1255        let bloom_filter = if bloom_offset > 0 {
1256            let bloom_start = bloom_offset;
1257            let bloom_end = if dict_offset != 0 {
1258                dict_offset
1259            } else {
1260                footer_start
1261            };
1262            // Read bloom filter size first (12 bytes header)
1263            let bloom_header_end = bloom_start.checked_add(12).ok_or_else(|| {
1264                io::Error::new(io::ErrorKind::InvalidData, "bloom filter range overflow")
1265            })?;
1266            if bloom_header_end > bloom_end {
1267                return Err(io::Error::new(
1268                    io::ErrorKind::UnexpectedEof,
1269                    "bloom filter header is truncated",
1270                ));
1271            }
1272            let bloom_header = file_handle
1273                .read_bytes_range(bloom_start..bloom_header_end)
1274                .await?;
1275            let num_words = u32::from_le_bytes([
1276                bloom_header[8],
1277                bloom_header[9],
1278                bloom_header[10],
1279                bloom_header[11],
1280            ]) as u64;
1281            let bloom_size = num_words
1282                .checked_mul(8)
1283                .and_then(|bytes| bytes.checked_add(12))
1284                .ok_or_else(|| {
1285                    io::Error::new(io::ErrorKind::InvalidData, "bloom filter size overflow")
1286                })?;
1287            let actual_bloom_size = bloom_end - bloom_start;
1288            if bloom_size != actual_bloom_size {
1289                return Err(io::Error::new(
1290                    io::ErrorKind::InvalidData,
1291                    "bloom filter length is inconsistent",
1292                ));
1293            }
1294            let bloom_data = file_handle.read_bytes_range(bloom_start..bloom_end).await?;
1295            Some(BloomFilter::from_owned_bytes(bloom_data)?)
1296        } else {
1297            None
1298        };
1299
1300        // Load dictionary if present
1301        let dictionary = if dict_offset > 0 {
1302            let dict_start = dict_offset;
1303            // Read dictionary size first
1304            let dict_header_end = dict_start.checked_add(4).ok_or_else(|| {
1305                io::Error::new(io::ErrorKind::InvalidData, "dictionary range overflow")
1306            })?;
1307            if dict_header_end > footer_start {
1308                return Err(io::Error::new(
1309                    io::ErrorKind::UnexpectedEof,
1310                    "dictionary header is truncated",
1311                ));
1312            }
1313            let dict_len_bytes = file_handle
1314                .read_bytes_range(dict_start..dict_header_end)
1315                .await?;
1316            let dict_len = u32::from_le_bytes([
1317                dict_len_bytes[0],
1318                dict_len_bytes[1],
1319                dict_len_bytes[2],
1320                dict_len_bytes[3],
1321            ]) as u64;
1322            if dict_len > MAX_SSTABLE_DICTIONARY_BYTES {
1323                return Err(io::Error::new(
1324                    io::ErrorKind::InvalidData,
1325                    "SSTable dictionary exceeds safety limit",
1326                ));
1327            }
1328            let dict_end = dict_header_end.checked_add(dict_len).ok_or_else(|| {
1329                io::Error::new(io::ErrorKind::InvalidData, "dictionary range overflow")
1330            })?;
1331            if dict_end != footer_start {
1332                return Err(io::Error::new(
1333                    io::ErrorKind::InvalidData,
1334                    "SSTable dictionary length is inconsistent",
1335                ));
1336            }
1337            let dict_data = file_handle
1338                .read_bytes_range(dict_header_end..dict_end)
1339                .await?;
1340            Some(CompressionDict::from_owned_bytes(dict_data))
1341        } else {
1342            None
1343        };
1344
1345        // Create a lazy slice for just the data portion
1346        let data_slice = file_handle.slice(0..data_end_offset);
1347
1348        Ok(Self {
1349            data_slice,
1350            block_index,
1351            num_entries,
1352            cache: RwLock::new(BlockCache::new(cache_blocks)),
1353            bloom_filter,
1354            dictionary,
1355            compression_level,
1356            _phantom: std::marker::PhantomData,
1357        })
1358    }
1359
1360    /// Number of entries
1361    pub fn num_entries(&self) -> u64 {
1362        self.num_entries
1363    }
1364
1365    /// Get stats about this SSTable for debugging
1366    pub fn stats(&self) -> SSTableStats {
1367        SSTableStats {
1368            num_blocks: self.block_index.len(),
1369            num_sparse_entries: 0, // No longer using sparse index separately
1370            num_entries: self.num_entries,
1371            has_bloom_filter: self.bloom_filter.is_some(),
1372            has_dictionary: self.dictionary.is_some(),
1373            bloom_filter_size: self
1374                .bloom_filter
1375                .as_ref()
1376                .map(|b| b.size_bytes())
1377                .unwrap_or(0),
1378            dictionary_size: self.dictionary.as_ref().map(|d| d.len()).unwrap_or(0),
1379        }
1380    }
1381
1382    /// Number of blocks currently in the cache
1383    pub fn cached_blocks(&self) -> usize {
1384        self.cache.read().blocks.len()
1385    }
1386
1387    /// Heap bytes retained by decompressed cached blocks.
1388    ///
1389    /// This deliberately reports the actual block lengths instead of assuming
1390    /// the configured writer block size: compression dictionaries and boundary
1391    /// blocks make the retained size variable.
1392    pub fn cached_bytes(&self) -> usize {
1393        self.cache
1394            .read()
1395            .blocks
1396            .values()
1397            .map(|block| block.len())
1398            .sum()
1399    }
1400
1401    /// Look up a key (async - may need to load block)
1402    ///
1403    /// Uses bloom filter for fast negative lookups, then memory-efficient
1404    /// block index to locate the block, reducing I/O to typically 1 block read.
1405    pub async fn get(&self, key: &[u8]) -> io::Result<Option<V>> {
1406        log::debug!(
1407            "SSTable::get called, key_len={}, total_blocks={}",
1408            key.len(),
1409            self.block_index.len()
1410        );
1411
1412        // Check bloom filter first - fast negative lookup
1413        if let Some(ref bloom) = self.bloom_filter
1414            && !bloom.may_contain(key)
1415        {
1416            log::debug!("SSTable::get bloom filter negative");
1417            return Ok(None);
1418        }
1419
1420        // Use block index to find the block that could contain the key
1421        let block_idx = match self.block_index.locate(key) {
1422            Some(idx) => idx,
1423            None => {
1424                log::debug!("SSTable::get key not found (before first block)");
1425                return Ok(None);
1426            }
1427        };
1428
1429        log::debug!("SSTable::get loading block_idx={}", block_idx);
1430
1431        // Now we know exactly which block to load - single I/O
1432        let block_data = self.load_block(block_idx).await?;
1433        self.search_block(&block_data, key)
1434    }
1435
1436    /// Batch lookup multiple keys with optimized I/O
1437    ///
1438    /// Groups keys by block and loads each block only once, reducing
1439    /// I/O from N reads to at most N reads (often fewer if keys share blocks).
1440    /// Uses bloom filter to skip keys that definitely don't exist.
1441    pub async fn get_batch(&self, keys: &[&[u8]]) -> io::Result<Vec<Option<V>>> {
1442        if keys.is_empty() {
1443            return Ok(Vec::new());
1444        }
1445
1446        // Map each key to its block index
1447        let mut key_to_block: Vec<(usize, usize)> = Vec::with_capacity(keys.len());
1448        for (key_idx, key) in keys.iter().enumerate() {
1449            // Check bloom filter first
1450            if let Some(ref bloom) = self.bloom_filter
1451                && !bloom.may_contain(key)
1452            {
1453                key_to_block.push((key_idx, usize::MAX)); // Definitely not present
1454                continue;
1455            }
1456
1457            match self.block_index.locate(key) {
1458                Some(block_idx) => key_to_block.push((key_idx, block_idx)),
1459                None => key_to_block.push((key_idx, usize::MAX)), // Mark as not found
1460            }
1461        }
1462
1463        // Group keys by block
1464        let mut blocks_to_load: Vec<usize> = key_to_block
1465            .iter()
1466            .filter(|(_, b)| *b != usize::MAX)
1467            .map(|(_, b)| *b)
1468            .collect();
1469        blocks_to_load.sort_unstable();
1470        blocks_to_load.dedup();
1471
1472        // Load all needed blocks (this is where I/O happens)
1473        for &block_idx in &blocks_to_load {
1474            let _ = self.load_block(block_idx).await?;
1475        }
1476
1477        // Now search each key in its block (all blocks are cached)
1478        let mut results = vec![None; keys.len()];
1479        for (key_idx, block_idx) in key_to_block {
1480            if block_idx == usize::MAX {
1481                continue;
1482            }
1483            let block_data = self.load_block(block_idx).await?; // Will hit cache
1484            results[key_idx] = self.search_block(&block_data, keys[key_idx])?;
1485        }
1486
1487        Ok(results)
1488    }
1489
1490    /// Preload all data blocks into memory
1491    ///
1492    /// Call this after open() to eliminate all I/O during subsequent lookups.
1493    /// Useful when the SSTable is small enough to fit in memory.
1494    pub async fn preload_all_blocks(&self) -> io::Result<()> {
1495        for block_idx in 0..self.block_index.len() {
1496            self.load_block(block_idx).await?;
1497        }
1498        Ok(())
1499    }
1500
1501    /// Prefetch all data blocks via a single bulk I/O operation.
1502    ///
1503    /// Reads the entire compressed data section in one call, then decompresses
1504    /// each block and populates the cache. This turns N individual reads into 1.
1505    /// Cache capacity is expanded to hold all blocks.
1506    pub async fn prefetch_all_data_bulk(&self) -> io::Result<()> {
1507        let num_blocks = self.block_index.len();
1508        if num_blocks == 0 {
1509            return Ok(());
1510        }
1511
1512        // Find total data extent
1513        let mut max_end: u64 = 0;
1514        for i in 0..num_blocks {
1515            if let Some(addr) = self.block_index.get_addr(i) {
1516                let end = addr.offset.checked_add(addr.length as u64).ok_or_else(|| {
1517                    io::Error::new(io::ErrorKind::InvalidData, "SSTable block range overflow")
1518                })?;
1519                max_end = max_end.max(end);
1520            }
1521        }
1522
1523        // Single bulk read of entire data section
1524        let all_data = self.data_slice.read_bytes_range(0..max_end).await?;
1525        let buf = all_data.as_slice();
1526
1527        // Expand cache and decompress all blocks
1528        let mut cache = self.cache.write();
1529        cache.max_blocks = cache.max_blocks.max(num_blocks);
1530        for i in 0..num_blocks {
1531            let addr = self.block_index.get_addr(i).unwrap();
1532            if cache.get(addr.offset).is_some() {
1533                continue;
1534            }
1535            let start = usize::try_from(addr.offset).map_err(|_| {
1536                io::Error::new(io::ErrorKind::InvalidData, "SSTable block offset too large")
1537            })?;
1538            let end = addr
1539                .offset
1540                .checked_add(addr.length as u64)
1541                .and_then(|end| usize::try_from(end).ok())
1542                .ok_or_else(|| {
1543                    io::Error::new(io::ErrorKind::InvalidData, "SSTable block range overflow")
1544                })?;
1545            let compressed = buf.get(start..end).ok_or_else(|| {
1546                io::Error::new(io::ErrorKind::UnexpectedEof, "SSTable block is truncated")
1547            })?;
1548            let decompressed = if let Some(ref dict) = self.dictionary {
1549                crate::compression::decompress_with_dict_limited(
1550                    compressed,
1551                    dict,
1552                    MAX_SSTABLE_BLOCK_BYTES,
1553                )?
1554            } else {
1555                crate::compression::decompress_limited(compressed, MAX_SSTABLE_BLOCK_BYTES)?
1556            };
1557            cache.insert(addr.offset, Arc::from(decompressed));
1558        }
1559
1560        Ok(())
1561    }
1562
1563    /// Load a block (checks cache first, then loads from FileSlice)
1564    /// Uses dictionary decompression if dictionary is present
1565    async fn load_block(&self, block_idx: usize) -> io::Result<Arc<[u8]>> {
1566        let addr = self.block_index.get_addr(block_idx).ok_or_else(|| {
1567            io::Error::new(io::ErrorKind::InvalidInput, "Block index out of range")
1568        })?;
1569
1570        // Fast path: read-lock peek (no LRU promotion, zero writer contention)
1571        {
1572            if let Some(block) = self.cache.read().peek(addr.offset) {
1573                return Ok(block);
1574            }
1575        }
1576
1577        log::debug!(
1578            "SSTable::load_block idx={} CACHE MISS, reading bytes [{}-{}]",
1579            block_idx,
1580            addr.offset,
1581            addr.offset + addr.length as u64
1582        );
1583
1584        // Load from FileSlice
1585        let range = addr.byte_range();
1586        let compressed = self.data_slice.read_bytes_range(range).await?;
1587
1588        // Decompress with dictionary if available
1589        let decompressed = if let Some(ref dict) = self.dictionary {
1590            crate::compression::decompress_with_dict_limited(
1591                compressed.as_slice(),
1592                dict,
1593                MAX_SSTABLE_BLOCK_BYTES,
1594            )?
1595        } else {
1596            crate::compression::decompress_limited(compressed.as_slice(), MAX_SSTABLE_BLOCK_BYTES)?
1597        };
1598
1599        let block: Arc<[u8]> = Arc::from(decompressed);
1600
1601        // Insert into cache under the write lock.
1602        {
1603            let mut cache = self.cache.write();
1604            cache.insert(addr.offset, Arc::clone(&block));
1605        }
1606
1607        Ok(block)
1608    }
1609
1610    /// Synchronous block load — only works for Inline (mmap/RAM) file handles.
1611    #[cfg(feature = "sync")]
1612    fn load_block_sync(&self, block_idx: usize) -> io::Result<Arc<[u8]>> {
1613        let addr = self.block_index.get_addr(block_idx).ok_or_else(|| {
1614            io::Error::new(io::ErrorKind::InvalidInput, "Block index out of range")
1615        })?;
1616
1617        // Fast path: read-lock peek (no LRU promotion, zero writer contention)
1618        {
1619            if let Some(block) = self.cache.read().peek(addr.offset) {
1620                return Ok(block);
1621            }
1622        }
1623
1624        // Load from FileSlice (sync — requires Inline handle)
1625        let range = addr.byte_range();
1626        let compressed = self.data_slice.read_bytes_range_sync(range)?;
1627
1628        // Decompress with dictionary if available
1629        let decompressed = if let Some(ref dict) = self.dictionary {
1630            crate::compression::decompress_with_dict_limited(
1631                compressed.as_slice(),
1632                dict,
1633                MAX_SSTABLE_BLOCK_BYTES,
1634            )?
1635        } else {
1636            crate::compression::decompress_limited(compressed.as_slice(), MAX_SSTABLE_BLOCK_BYTES)?
1637        };
1638
1639        let block: Arc<[u8]> = Arc::from(decompressed);
1640
1641        // Insert into cache under the write lock.
1642        {
1643            let mut cache = self.cache.write();
1644            cache.insert(addr.offset, Arc::clone(&block));
1645        }
1646
1647        Ok(block)
1648    }
1649
1650    /// Synchronous key lookup — only works for Inline (mmap/RAM) file handles.
1651    #[cfg(feature = "sync")]
1652    pub fn get_sync(&self, key: &[u8]) -> io::Result<Option<V>> {
1653        // Check bloom filter first — fast negative lookup
1654        if let Some(ref bloom) = self.bloom_filter
1655            && !bloom.may_contain(key)
1656        {
1657            return Ok(None);
1658        }
1659
1660        // Use block index to find the block that could contain the key
1661        let block_idx = match self.block_index.locate(key) {
1662            Some(idx) => idx,
1663            None => {
1664                return Ok(None);
1665            }
1666        };
1667
1668        let block_data = self.load_block_sync(block_idx)?;
1669        self.search_block(&block_data, key)
1670    }
1671
1672    fn search_block(&self, block_data: &[u8], target_key: &[u8]) -> io::Result<Option<V>> {
1673        let mut reader = block_data;
1674        let mut current_key = Vec::new();
1675
1676        while !reader.is_empty() {
1677            let common_prefix_len = read_vint(&mut reader)? as usize;
1678            let suffix_len = read_vint(&mut reader)? as usize;
1679
1680            if suffix_len > reader.len() {
1681                return Err(io::Error::new(
1682                    io::ErrorKind::UnexpectedEof,
1683                    "SSTable block suffix truncated",
1684                ));
1685            }
1686            current_key.truncate(common_prefix_len);
1687            current_key.extend_from_slice(&reader[..suffix_len]);
1688            reader = &reader[suffix_len..];
1689
1690            let value = V::deserialize(&mut reader)?;
1691
1692            match current_key.as_slice().cmp(target_key) {
1693                std::cmp::Ordering::Equal => return Ok(Some(value)),
1694                std::cmp::Ordering::Greater => return Ok(None),
1695                std::cmp::Ordering::Less => continue,
1696            }
1697        }
1698
1699        Ok(None)
1700    }
1701
1702    /// Prefetch blocks for a key range
1703    pub async fn prefetch_range(&self, start_key: &[u8], end_key: &[u8]) -> io::Result<()> {
1704        let start_block = self.block_index.locate(start_key).unwrap_or(0);
1705        let end_block = self
1706            .block_index
1707            .locate(end_key)
1708            .unwrap_or(self.block_index.len().saturating_sub(1));
1709
1710        for block_idx in start_block..=end_block.min(self.block_index.len().saturating_sub(1)) {
1711            let _ = self.load_block(block_idx).await?;
1712        }
1713
1714        Ok(())
1715    }
1716
1717    /// Iterate over all entries (loads blocks as needed)
1718    pub fn iter(&self) -> AsyncSSTableIterator<'_, V> {
1719        AsyncSSTableIterator::new(self)
1720    }
1721
1722    /// Get all entries as a vector (for merging)
1723    pub async fn all_entries(&self) -> io::Result<Vec<(Vec<u8>, V)>> {
1724        let mut results = Vec::new();
1725
1726        for block_idx in 0..self.block_index.len() {
1727            let block_data = self.load_block(block_idx).await?;
1728            let mut reader = &block_data[..];
1729            let mut current_key = Vec::new();
1730
1731            while !reader.is_empty() {
1732                let common_prefix_len = read_vint(&mut reader)? as usize;
1733                let suffix_len = read_vint(&mut reader)? as usize;
1734
1735                if suffix_len > reader.len() {
1736                    return Err(io::Error::new(
1737                        io::ErrorKind::UnexpectedEof,
1738                        "SSTable block suffix truncated",
1739                    ));
1740                }
1741                current_key.truncate(common_prefix_len);
1742                current_key.extend_from_slice(&reader[..suffix_len]);
1743                reader = &reader[suffix_len..];
1744
1745                let value = V::deserialize(&mut reader)?;
1746                results.push((current_key.clone(), value));
1747            }
1748        }
1749
1750        Ok(results)
1751    }
1752
1753    /// Scan all entries whose key starts with `prefix`.
1754    ///
1755    /// Uses the block index to locate the starting block, then iterates
1756    /// forward collecting matching entries. Early-terminates once keys
1757    /// exceed the prefix range (keys are sorted).
1758    pub async fn prefix_scan(&self, prefix: &[u8]) -> io::Result<Vec<(Vec<u8>, V)>> {
1759        let (results, _) = self.prefix_scan_limited(prefix, usize::MAX).await?;
1760        Ok(results)
1761    }
1762
1763    /// Prefix scan with an explicit result budget. The boolean indicates that
1764    /// at least one additional matching entry existed beyond the budget.
1765    pub async fn prefix_scan_limited(
1766        &self,
1767        prefix: &[u8],
1768        max_results: usize,
1769    ) -> io::Result<PrefixScanResult<V>> {
1770        if self.block_index.is_empty() || prefix.is_empty() {
1771            return Ok((Vec::new(), false));
1772        }
1773
1774        let start_block = match self.block_index.locate(prefix) {
1775            Some(idx) => idx,
1776            None => return Ok((Vec::new(), false)),
1777        };
1778
1779        let mut results = Vec::new();
1780
1781        for block_idx in start_block..self.block_index.len() {
1782            let block_data = self.load_block(block_idx).await?;
1783            let mut reader = &block_data[..];
1784            let mut current_key = Vec::new();
1785
1786            while !reader.is_empty() {
1787                let common_prefix_len = read_vint(&mut reader)? as usize;
1788                let suffix_len = read_vint(&mut reader)? as usize;
1789
1790                if suffix_len > reader.len() {
1791                    return Err(io::Error::new(
1792                        io::ErrorKind::UnexpectedEof,
1793                        "SSTable block suffix truncated",
1794                    ));
1795                }
1796                current_key.truncate(common_prefix_len);
1797                current_key.extend_from_slice(&reader[..suffix_len]);
1798                reader = &reader[suffix_len..];
1799
1800                let value = V::deserialize(&mut reader)?;
1801
1802                if current_key.starts_with(prefix) {
1803                    if results.len() >= max_results {
1804                        return Ok((results, true));
1805                    }
1806                    results.push((current_key.clone(), value));
1807                } else if current_key.as_slice() > prefix {
1808                    // Keys are sorted — past the prefix range, done
1809                    return Ok((results, false));
1810                }
1811            }
1812        }
1813
1814        Ok((results, false))
1815    }
1816
1817    /// Synchronous prefix scan — requires Inline (mmap/RAM) file handles.
1818    #[cfg(feature = "sync")]
1819    pub fn prefix_scan_sync(&self, prefix: &[u8]) -> io::Result<Vec<(Vec<u8>, V)>> {
1820        let (results, _) = self.prefix_scan_limited_sync(prefix, usize::MAX)?;
1821        Ok(results)
1822    }
1823
1824    /// Synchronous prefix scan with an explicit result budget.
1825    #[cfg(feature = "sync")]
1826    pub fn prefix_scan_limited_sync(
1827        &self,
1828        prefix: &[u8],
1829        max_results: usize,
1830    ) -> io::Result<PrefixScanResult<V>> {
1831        if self.block_index.is_empty() || prefix.is_empty() {
1832            return Ok((Vec::new(), false));
1833        }
1834
1835        let start_block = match self.block_index.locate(prefix) {
1836            Some(idx) => idx,
1837            None => return Ok((Vec::new(), false)),
1838        };
1839
1840        let mut results = Vec::new();
1841
1842        for block_idx in start_block..self.block_index.len() {
1843            let block_data = self.load_block_sync(block_idx)?;
1844            let mut reader = &block_data[..];
1845            let mut current_key = Vec::new();
1846
1847            while !reader.is_empty() {
1848                let common_prefix_len = read_vint(&mut reader)? as usize;
1849                let suffix_len = read_vint(&mut reader)? as usize;
1850
1851                if suffix_len > reader.len() {
1852                    return Err(io::Error::new(
1853                        io::ErrorKind::UnexpectedEof,
1854                        "SSTable block suffix truncated",
1855                    ));
1856                }
1857                current_key.truncate(common_prefix_len);
1858                current_key.extend_from_slice(&reader[..suffix_len]);
1859                reader = &reader[suffix_len..];
1860
1861                let value = V::deserialize(&mut reader)?;
1862
1863                if current_key.starts_with(prefix) {
1864                    if results.len() >= max_results {
1865                        return Ok((results, true));
1866                    }
1867                    results.push((current_key.clone(), value));
1868                } else if current_key.as_slice() > prefix {
1869                    return Ok((results, false));
1870                }
1871            }
1872        }
1873
1874        Ok((results, false))
1875    }
1876}
1877
1878/// Async iterator over SSTable entries
1879pub struct AsyncSSTableIterator<'a, V: SSTableValue> {
1880    reader: &'a AsyncSSTableReader<V>,
1881    current_block: usize,
1882    block_data: Option<Arc<[u8]>>,
1883    block_offset: usize,
1884    current_key: Vec<u8>,
1885    finished: bool,
1886}
1887
1888impl<'a, V: SSTableValue> AsyncSSTableIterator<'a, V> {
1889    fn new(reader: &'a AsyncSSTableReader<V>) -> Self {
1890        Self {
1891            reader,
1892            current_block: 0,
1893            block_data: None,
1894            block_offset: 0,
1895            current_key: Vec::new(),
1896            finished: reader.block_index.is_empty(),
1897        }
1898    }
1899
1900    async fn load_next_block(&mut self) -> io::Result<bool> {
1901        if self.current_block >= self.reader.block_index.len() {
1902            self.finished = true;
1903            return Ok(false);
1904        }
1905
1906        self.block_data = Some(self.reader.load_block(self.current_block).await?);
1907        self.block_offset = 0;
1908        self.current_key.clear();
1909        self.current_block += 1;
1910        Ok(true)
1911    }
1912
1913    /// Advance to next entry (async)
1914    pub async fn next(&mut self) -> io::Result<Option<(Vec<u8>, V)>> {
1915        if self.finished {
1916            return Ok(None);
1917        }
1918
1919        if self.block_data.is_none() && !self.load_next_block().await? {
1920            return Ok(None);
1921        }
1922
1923        loop {
1924            let block = self.block_data.as_ref().unwrap();
1925            if self.block_offset >= block.len() {
1926                if !self.load_next_block().await? {
1927                    return Ok(None);
1928                }
1929                continue;
1930            }
1931
1932            let mut reader = &block[self.block_offset..];
1933            let start_len = reader.len();
1934
1935            let common_prefix_len = read_vint(&mut reader)? as usize;
1936            let suffix_len = read_vint(&mut reader)? as usize;
1937
1938            if suffix_len > reader.len() {
1939                return Err(io::Error::new(
1940                    io::ErrorKind::UnexpectedEof,
1941                    "SSTable block suffix truncated",
1942                ));
1943            }
1944            self.current_key.truncate(common_prefix_len);
1945            self.current_key.extend_from_slice(&reader[..suffix_len]);
1946            reader = &reader[suffix_len..];
1947
1948            let value = V::deserialize(&mut reader)?;
1949
1950            self.block_offset += start_len - reader.len();
1951
1952            return Ok(Some((self.current_key.clone(), value)));
1953        }
1954    }
1955}
1956
1957#[cfg(test)]
1958mod tests {
1959    use super::*;
1960
1961    #[test]
1962    fn test_bloom_filter_basic() {
1963        let mut bloom = BloomFilter::new(100, 10);
1964
1965        bloom.insert(b"hello");
1966        bloom.insert(b"world");
1967        bloom.insert(b"test");
1968
1969        assert!(bloom.may_contain(b"hello"));
1970        assert!(bloom.may_contain(b"world"));
1971        assert!(bloom.may_contain(b"test"));
1972
1973        // These should likely return false (with ~1% false positive rate)
1974        assert!(!bloom.may_contain(b"notfound"));
1975        assert!(!bloom.may_contain(b"missing"));
1976    }
1977
1978    #[test]
1979    fn test_bloom_filter_serialization() {
1980        let mut bloom = BloomFilter::new(100, 10);
1981        bloom.insert(b"key1");
1982        bloom.insert(b"key2");
1983
1984        let bytes = bloom.to_bytes();
1985        let restored = BloomFilter::from_owned_bytes(OwnedBytes::new(bytes)).unwrap();
1986
1987        assert!(restored.may_contain(b"key1"));
1988        assert!(restored.may_contain(b"key2"));
1989        assert!(!restored.may_contain(b"key3"));
1990    }
1991
1992    #[test]
1993    fn test_bloom_filter_false_positive_rate() {
1994        let num_keys = 10000;
1995        let mut bloom = BloomFilter::new(num_keys, BLOOM_BITS_PER_KEY);
1996
1997        // Insert keys
1998        for i in 0..num_keys {
1999            let key = format!("key_{}", i);
2000            bloom.insert(key.as_bytes());
2001        }
2002
2003        // All inserted keys should be found
2004        for i in 0..num_keys {
2005            let key = format!("key_{}", i);
2006            assert!(bloom.may_contain(key.as_bytes()));
2007        }
2008
2009        // Check false positive rate on non-existent keys
2010        let mut false_positives = 0;
2011        let test_count = 10000;
2012        for i in 0..test_count {
2013            let key = format!("nonexistent_{}", i);
2014            if bloom.may_contain(key.as_bytes()) {
2015                false_positives += 1;
2016            }
2017        }
2018
2019        // With 10 bits per key, expect ~1% false positive rate
2020        // Allow up to 3% due to hash function variance
2021        let fp_rate = false_positives as f64 / test_count as f64;
2022        assert!(
2023            fp_rate < 0.03,
2024            "False positive rate {} is too high",
2025            fp_rate
2026        );
2027    }
2028
2029    #[test]
2030    fn test_sstable_writer_config() {
2031        use crate::structures::IndexOptimization;
2032
2033        // Default = Adaptive
2034        let config = SSTableWriterConfig::default();
2035        assert_eq!(config.compression_level.0, 9); // BETTER
2036        assert!(config.use_bloom_filter); // Bloom always on — cheap and fast
2037        assert!(!config.use_dictionary);
2038
2039        // Adaptive
2040        let adaptive = SSTableWriterConfig::from_optimization(IndexOptimization::Adaptive);
2041        assert_eq!(adaptive.compression_level.0, 9);
2042        assert!(adaptive.use_bloom_filter);
2043        assert!(!adaptive.use_dictionary);
2044
2045        // SizeOptimized
2046        let size = SSTableWriterConfig::from_optimization(IndexOptimization::SizeOptimized);
2047        assert_eq!(size.compression_level.0, 22); // MAX
2048        assert!(size.use_bloom_filter);
2049        assert!(size.use_dictionary);
2050
2051        // PerformanceOptimized
2052        let perf = SSTableWriterConfig::from_optimization(IndexOptimization::PerformanceOptimized);
2053        assert_eq!(perf.compression_level.0, 1); // FAST
2054        assert!(perf.use_bloom_filter); // Bloom helps skip blocks fast
2055        assert!(!perf.use_dictionary);
2056
2057        // Aliases
2058        let fast = SSTableWriterConfig::fast();
2059        assert_eq!(fast.compression_level.0, 1);
2060
2061        let max = SSTableWriterConfig::max_compression();
2062        assert_eq!(max.compression_level.0, 22);
2063    }
2064
2065    #[test]
2066    fn test_vint_roundtrip() {
2067        let test_values = [0u64, 1, 127, 128, 255, 256, 16383, 16384, u64::MAX];
2068
2069        for &val in &test_values {
2070            let mut buf = Vec::new();
2071            write_vint(&mut buf, val).unwrap();
2072            let mut reader = buf.as_slice();
2073            let decoded = read_vint(&mut reader).unwrap();
2074            assert_eq!(val, decoded, "Failed for value {}", val);
2075        }
2076    }
2077
2078    #[test]
2079    fn test_common_prefix_len() {
2080        assert_eq!(common_prefix_len(b"hello", b"hello"), 5);
2081        assert_eq!(common_prefix_len(b"hello", b"help"), 3);
2082        assert_eq!(common_prefix_len(b"hello", b"world"), 0);
2083        assert_eq!(common_prefix_len(b"", b"hello"), 0);
2084        assert_eq!(common_prefix_len(b"hello", b""), 0);
2085    }
2086}