Skip to main content

turbo_persistence/
static_sorted_file_builder.rs

1use std::{
2    borrow::Cow,
3    collections::VecDeque,
4    io::{BufWriter, Write},
5    path::{Path, PathBuf},
6};
7
8use anyhow::{Context, Result};
9use byteorder::{BE, ByteOrder, WriteBytesExt};
10use either::Either;
11use fs_err::File;
12
13use crate::{
14    Compression,
15    compression::{Compressor, checksum_block},
16    constants::{MAX_INLINE_VALUE_SIZE, MAX_SMALL_VALUE_SIZE, MIN_SMALL_VALUE_BLOCK_SIZE},
17    meta_file::MetaEntryFlags,
18    static_sorted_file::{
19        BLOB_VALUE_REF_SIZE, BLOCK_TYPE_INDEX, FIXED_KEY_BLOCK_MIXED_VALUE_TYPE, FixedRegions,
20        KEY_BLOCK_ENTRY_TYPE_BLOB, KEY_BLOCK_ENTRY_TYPE_INLINE_MIN,
21        KEY_BLOCK_ENTRY_TYPE_KEY_DELETED, KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN,
22        KEY_BLOCK_ENTRY_TYPE_MEDIUM, KEY_BLOCK_ENTRY_TYPE_SMALL,
23        KEY_BLOCK_TABLE_ENTRY_SIZE_NO_HASH, KEY_DELETED_REF_SIZE, KeyBlockLayout,
24        MEDIUM_VALUE_REF_SIZE, SMALL_VALUE_REF_SIZE, key_block_table_stride,
25    },
26};
27
28/// Size of the per-block header on disk: 4 bytes uncompressed_size + 4 bytes CRC32 checksum.
29pub const BLOCK_HEADER_SIZE: usize = 8;
30
31/// The maximum number of entries that should go into a single key block
32const MAX_KEY_BLOCK_ENTRIES: usize = MAX_KEY_BLOCK_SIZE / KEY_BLOCK_ENTRY_META_OVERHEAD;
33/// The maximum bytes that should go into a single key block
34// Note this must fit into 3 bytes length
35const MAX_KEY_BLOCK_SIZE: usize = 16 * 1024;
36/// Overhead of bytes that should be counted for entries in a key block in addition to the key size.
37/// This covers the worst case (small values):
38/// - 1 byte type (key block header)
39/// - 3 bytes position (key block header)
40/// - 8 bytes hash (optional, but unknown at collection time)
41/// - 2 bytes block index
42/// - 2 bytes size
43/// - 4 bytes position in block
44const KEY_BLOCK_ENTRY_META_OVERHEAD: usize = 20;
45/// The aimed false positive rate for the AMQF
46const AMQF_FALSE_POSITIVE_RATE: f64 = 0.01;
47/// Assumed average small value size for pre-allocation estimates.
48/// Intentionally conservative (small values range from MAX_INLINE_VALUE_SIZE+1 to
49/// MAX_SMALL_VALUE_SIZE = 4096): a low estimate over-counts value blocks, which is
50/// preferable to under-allocating vectors.
51const AVG_SMALL_VALUE_SIZE: usize = 64;
52
53/// Safety margin for block index capacity estimation in
54/// [`StreamingSstWriter::has_block_index_capacity`]. Accounts for rounding in the entry-count and
55/// byte-size based estimates of pending key blocks.
56const BLOCK_INDEX_CAPACITY_BUFFER: usize = 16;
57
58/// Minimum key size (in bytes) for attempting LZ4 compression on key blocks.
59///
60/// For small keys (below this threshold), compression is unlikely to find enough to work with and
61/// only wastes CPU time, so we skip the attempt entirely.
62const MIN_KEY_SIZE_FOR_COMPRESSION: usize = 16;
63
64/// Maximum key length that can use fixed-size key block layout.
65///
66/// The on-disk fixed-key header stores the key size as a single byte, so keys longer than this
67/// fall back to variable-size layout.
68const MAX_FIXED_KEY_LEN: usize = u8::MAX as usize;
69
70/// Maximum value size that can use fixed-size key block layout.
71///
72/// Mixed-type fixed blocks store the value size in a single header byte, since it can no longer be
73/// derived from a single shared entry type.
74const MAX_FIXED_VAL_SIZE: usize = u8::MAX as usize;
75
76/// Newtype for the key block entry type byte.
77///
78/// This encodes what kind of value reference an entry has (small, medium, blob, deleted, or
79/// inline with embedded length). See `KEY_BLOCK_ENTRY_TYPE_*` constants.
80#[derive(Clone, Copy, PartialEq, Eq, Debug)]
81struct EntryType(u8);
82
83/// Tracks whether a key block's entries are uniform enough for fixed-size layout.
84///
85/// Fixed layout needs a uniform *stride*, which requires a uniform key length and a uniform value
86/// size. A uniform value *type* is a stronger condition that additionally lets the type be hoisted
87/// into the block header; when types differ but sizes agree, the type is stored per entry instead
88/// (1 byte, still cheaper than the 4-byte offset table entry a variable block would need).
89///
90/// State transitions:
91/// - `Unknown` → first entry → `Fixed`
92/// - `Fixed` + matching key_len and value type → stays `Fixed`
93/// - `Fixed` + matching key_len and value *size* → `Fixed` with `value_type: None`
94/// - `Fixed` + mismatched key_len or value size → `Variable`
95/// - `Variable` → stays `Variable`
96#[derive(Clone, Copy)]
97enum KeyBlockFormat {
98    /// No entries yet — format undetermined.
99    Unknown,
100    /// All entries so far have uniform key length and value size.
101    Fixed {
102        key_len: u8,
103        val_size: u8,
104        /// The shared entry type, or `None` if entries have differing types of the same size.
105        value_type: Option<EntryType>,
106    },
107    /// Entries have mixed key lengths or value sizes; must use offset table.
108    Variable,
109}
110
111impl KeyBlockFormat {
112    /// Updates the format after seeing an entry with the given key length and value type.
113    ///
114    /// A `Fixed` state is only reachable when all entries have matching key length and value size,
115    /// and the key length fits in a u8 (required by the on-disk header).
116    fn update(&mut self, key_len: usize, value_type: EntryType) {
117        let val_size = value_type_val_size(value_type);
118        *self = match *self {
119            KeyBlockFormat::Unknown => {
120                if key_len <= MAX_FIXED_KEY_LEN && val_size <= MAX_FIXED_VAL_SIZE {
121                    KeyBlockFormat::Fixed {
122                        key_len: key_len as u8,
123                        val_size: val_size as u8,
124                        value_type: Some(value_type),
125                    }
126                } else {
127                    KeyBlockFormat::Variable
128                }
129            }
130            KeyBlockFormat::Fixed {
131                key_len: k,
132                val_size: s,
133                value_type: v,
134            } if k as usize == key_len && s as usize == val_size => KeyBlockFormat::Fixed {
135                key_len: k,
136                val_size: s,
137                // Collapse to `None` as soon as two entries disagree on type.
138                value_type: v.filter(|v| *v == value_type),
139            },
140            KeyBlockFormat::Fixed { .. } | KeyBlockFormat::Variable => KeyBlockFormat::Variable,
141        };
142    }
143}
144
145/// Copy-able snapshot of the accumulator state needed by [`flush_key_block`].
146#[derive(Clone, Copy)]
147struct KeyBlockFlushInfo {
148    max_key_len: usize,
149    min_key_len: usize,
150    format: KeyBlockFormat,
151}
152
153impl KeyBlockFlushInfo {
154    /// The shared key length when every entry in the block has the same one, else `None`.
155    fn uniform_key_len(&self) -> Option<usize> {
156        (self.min_key_len == self.max_key_len).then_some(self.max_key_len)
157    }
158}
159
160/// Tracks the accumulated state of the current incomplete key block.
161///
162/// During streaming, this sits on [`StreamingSstWriter`] and tracks the tail of the resolved
163/// prefix. Entries are added one at a time; when [`should_flush`](Self::should_flush) returns
164/// `true`, the caller should flush the block and call [`reset`](Self::reset).
165struct KeyBlockAccumulator {
166    /// Accumulated byte size (keys + per-entry overhead) of entries in this block.
167    size: usize,
168    /// Number of entries accumulated so far.
169    entry_count: usize,
170    /// Maximum key length among accumulated entries (determines whether hashes are stored).
171    max_key_len: usize,
172    min_key_len: usize,
173    /// Hash of the most recently added entry (used to avoid splitting entries with equal hashes
174    /// across blocks).
175    last_hash: u64,
176    /// Whether the block qualifies for fixed-size layout.
177    format: KeyBlockFormat,
178}
179
180impl KeyBlockAccumulator {
181    fn new() -> Self {
182        Self {
183            size: 0,
184            entry_count: 0,
185            max_key_len: 0,
186            min_key_len: usize::MAX,
187            last_hash: 0,
188            format: KeyBlockFormat::Unknown,
189        }
190    }
191
192    /// Records a new entry in the accumulator.
193    fn add(&mut self, key_len: usize, key_hash: u64, value_type: EntryType) {
194        self.size += key_len + KEY_BLOCK_ENTRY_META_OVERHEAD;
195        self.max_key_len = self.max_key_len.max(key_len);
196        self.min_key_len = self.min_key_len.min(key_len);
197        self.entry_count += 1;
198        self.last_hash = key_hash;
199        self.format.update(key_len, value_type);
200    }
201
202    /// Snapshots the state needed by `flush_key_block`.
203    fn flush_info(&self) -> KeyBlockFlushInfo {
204        KeyBlockFlushInfo {
205            max_key_len: self.max_key_len,
206            min_key_len: self.min_key_len,
207            format: self.format,
208        }
209    }
210
211    /// Returns `true` if the block should be flushed before adding an entry with the given key
212    /// length and hash. Returns `false` for empty blocks and when the next entry shares its hash
213    /// with the current last entry (to avoid splitting equal-hash runs).
214    fn should_flush(&self, next_key_len: usize, next_key_hash: u64) -> bool {
215        if self.entry_count == 0 {
216            return false;
217        }
218        let would_exceed_size =
219            self.size + next_key_len + KEY_BLOCK_ENTRY_META_OVERHEAD > MAX_KEY_BLOCK_SIZE;
220        let would_exceed_entries = self.entry_count >= MAX_KEY_BLOCK_ENTRIES;
221        // Never split entries with the same hash across blocks.
222        (would_exceed_size || would_exceed_entries) && self.last_hash != next_key_hash
223    }
224
225    /// Resets the accumulator for a new key block.
226    fn reset(&mut self) {
227        self.size = 0;
228        self.entry_count = 0;
229        self.max_key_len = 0;
230        self.min_key_len = usize::MAX;
231        self.format = KeyBlockFormat::Unknown;
232        // last_hash is intentionally not reset -- it is overwritten on the next add() call.
233    }
234}
235
236/// Chooses a key block's layout from the longest key it holds.
237fn choose_layout(max_key_len: usize) -> KeyBlockLayout {
238    // Short keys are cheap enough to compare directly that storing an 8-byte hash per entry costs
239    // more space than the comparison saves, so those blocks omit it and reorder entries by key.
240    if max_key_len > 32 {
241        KeyBlockLayout::HashThenKey
242    } else {
243        KeyBlockLayout::KeyOnly
244    }
245}
246
247#[inline]
248fn be_key_u32(key: &[u8]) -> u32 {
249    u32::from_be_bytes(key.try_into().expect("4-byte key"))
250}
251
252#[inline]
253fn be_key_u64(key: &[u8]) -> u64 {
254    u64::from_be_bytes(key.try_into().expect("8-byte key"))
255}
256
257/// Trait for entries from that SST files can be created
258pub trait Entry {
259    /// Returns the hash of the key
260    fn key_hash(&self) -> u64;
261    /// Returns the length of the key
262    fn key_len(&self) -> usize;
263    /// Returns the key's bytes.
264    fn key_bytes(&self) -> &[u8];
265    /// Writes the key to a buffer
266    fn write_key_to(&self, buf: &mut Vec<u8>) {
267        buf.extend_from_slice(self.key_bytes());
268    }
269
270    /// Returns the value
271    fn value(&self) -> EntryValue<'_>;
272}
273
274impl<E: Entry> Entry for &E {
275    fn key_hash(&self) -> u64 {
276        (*self).key_hash()
277    }
278    fn key_len(&self) -> usize {
279        (*self).key_len()
280    }
281    fn key_bytes(&self) -> &[u8] {
282        (*self).key_bytes()
283    }
284    fn value(&self) -> EntryValue<'_> {
285        (*self).value()
286    }
287}
288
289/// Reference to a value
290#[derive(Copy, Clone)]
291pub enum EntryValue<'l> {
292    /// Inline value stored directly in the key block.
293    Inline { value: &'l [u8] },
294    /// Small-sized value. They are stored in shared value blocks.
295    Small { value: &'l [u8] },
296    /// Medium-sized value. They are stored in their own value block.
297    Medium { value: &'l [u8] },
298    /// Medium-sized value. They are stored in their own value block. In the raw form as on disk.
299    MediumRaw {
300        /// The uncompressed size of the block data. `0` means the block is stored uncompressed
301        /// (and thus the size is the `len` of the block)
302        uncompressed_size: u32,
303        /// CRC32 checksum of the on-disk block data (after compression).
304        checksum: u32,
305        block: &'l [u8],
306    },
307    /// Large-sized value. They are stored in a blob file.
308    Large { blob: u32 },
309    /// Tombstone. The value was removed.
310    KeyDeleted,
311    /// Key-value tombstone. Only the one carried value was removed; other values for the same key
312    /// survive. MultiValue families only. The value must be at most [`MAX_INLINE_VALUE_SIZE`]
313    /// bytes.
314    KeyValueDeleted { value: &'l [u8] },
315}
316
317#[derive(Debug, Clone)]
318pub struct StaticSortedFileBuilderMeta<'a> {
319    /// The minimum hash of the keys in the SST file
320    pub min_hash: u64,
321    /// The maximum hash of the keys in the SST file
322    pub max_hash: u64,
323    /// The AMQF data
324    pub amqf: Cow<'a, [u8]>,
325    /// The number of blocks in the SST file
326    pub block_count: u16,
327    /// The file size of the SST file
328    pub size: u64,
329    /// The status flags for this SST file
330    pub flags: MetaEntryFlags,
331    /// The number of entries in the SST file
332    pub entries: u64,
333    /// The number of tombstone entries (`KeyDeleted` and `KeyValueDeleted`) in the SST file.
334    pub tombstones: u64,
335}
336
337/// Writes an SST file from a pre-sorted slice of entries.
338///
339/// Entries must be sorted in (key-hash, key) order, the same contract as
340/// [`StreamingSstWriter::add`].
341///
342/// This is a convenience wrapper around [`StreamingSstWriter`] for callers that already have all
343/// entries in memory.
344// TODO: Consider adding a variant that takes ownership (Vec<E> or drain iterator)
345// to free entry memory as blocks are written.
346pub fn write_static_stored_file<E: Entry>(
347    entries: &[E],
348    file: &Path,
349    flags: MetaEntryFlags,
350    compression: Compression,
351) -> Result<(StaticSortedFileBuilderMeta<'static>, File)> {
352    debug_assert!(
353        entries
354            .iter()
355            .map(|e| (e.key_hash(), e.key_bytes()))
356            .is_sorted()
357    );
358    let mut writer = StreamingSstWriter::new(file, flags, entries.len() as u64, compression)?;
359    for entry in entries {
360        if let Err(err) = writer.add(entry) {
361            writer.cancel();
362            return Err(err);
363        }
364    }
365    writer.close()
366}
367
368// ---------------------------------------------------------------------------
369// Block I/O helpers (free functions for borrow-checker friendliness)
370// ---------------------------------------------------------------------------
371
372/// Writes a raw (already-formatted) block to the file. Returns the block index assigned.
373///
374/// `uncompressed_size` is the original uncompressed size of the block data, or `0` if the block
375/// is stored uncompressed.
376fn write_raw_block_to_file(
377    file: &mut BufWriter<File>,
378    block_offsets: &mut Vec<u32>,
379    uncompressed_size: u32,
380    checksum: u32,
381    block: &[u8],
382) -> Result<u16> {
383    let block_index: u16 = block_offsets
384        .len()
385        .try_into()
386        .expect("Block index overflow");
387
388    let len: u32 = (block.len() + BLOCK_HEADER_SIZE).try_into().unwrap();
389    let offset = block_offsets
390        .last()
391        .copied()
392        .unwrap_or_default()
393        .checked_add(len)
394        .expect("Block offset overflow");
395    block_offsets.push(offset);
396
397    file.write_u32::<BE>(uncompressed_size)
398        .context("Failed to write uncompressed size")?;
399    file.write_u32::<BE>(checksum)
400        .context("Failed to write checksum")?;
401    file.write_all(block)
402        .context("Failed to write block data")?;
403    Ok(block_index)
404}
405
406/// Writes a block to the file, optionally compressing it. Returns the block index assigned.
407fn write_block_to_file(
408    file: &mut BufWriter<File>,
409    compress_buffer: &mut Vec<u8>,
410    block_offsets: &mut Vec<u32>,
411    block: &[u8],
412    try_compress: bool,
413    compressor: &mut Compressor,
414) -> Result<u16> {
415    let (uncompressed_size, data_to_write): (u32, &[u8]) = if try_compress {
416        compressor.compress_into_buffer(block, compress_buffer)?;
417        // Same threshold as LevelDB/RocksDB: require at least 12.5% savings.
418        if compress_buffer.len() < block.len() - (block.len() / 8) {
419            (block.len().try_into().unwrap(), compress_buffer)
420        } else {
421            (0, block)
422        }
423    } else {
424        (0, block)
425    };
426
427    // Checksum is computed on the on-disk data (after compression).
428    let checksum = checksum_block(data_to_write);
429
430    write_raw_block_to_file(
431        file,
432        block_offsets,
433        uncompressed_size,
434        checksum,
435        data_to_write,
436    )
437}
438
439// ---------------------------------------------------------------------------
440// StreamingSstWriter
441// ---------------------------------------------------------------------------
442
443/// Where a key entry's value lives (or will live once the small block flushes).
444enum ValueRef {
445    /// Value in a known small value block (already flushed).
446    Small {
447        block_index: u16,
448        offset: u32,
449        size: u16,
450    },
451    /// Value is in a small value block that hasn't been written yet. Will be resolved in-place
452    /// to [`ValueRef::Small`] when the small block is flushed.
453    PendingSmall {
454        #[cfg(debug_assertions)]
455        small_block_id: u16,
456        offset: u32,
457        size: u16,
458    },
459    /// Medium value already written to its own block.
460    Medium { block_index: u16 },
461    /// Inline value (stored directly in the key block).
462    Inline {
463        data: [u8; MAX_INLINE_VALUE_SIZE],
464        len: u8,
465    },
466    /// Large blob stored externally.
467    Blob { blob_id: u32 },
468    /// Tombstone.
469    KeyDeleted,
470    /// Key-value tombstone: deletes only the carried value from the key's group. The value is
471    /// stored inline, exactly like [`ValueRef::Inline`].
472    KeyValueDeleted {
473        data: [u8; MAX_INLINE_VALUE_SIZE],
474        len: u8,
475    },
476}
477
478impl ValueRef {
479    /// Returns the key block entry type for this value reference.
480    fn entry_type(&self) -> EntryType {
481        EntryType(match self {
482            ValueRef::Small { .. } | ValueRef::PendingSmall { .. } => KEY_BLOCK_ENTRY_TYPE_SMALL,
483            ValueRef::Medium { .. } => KEY_BLOCK_ENTRY_TYPE_MEDIUM,
484            ValueRef::Inline { len, .. } => KEY_BLOCK_ENTRY_TYPE_INLINE_MIN + *len,
485            ValueRef::Blob { .. } => KEY_BLOCK_ENTRY_TYPE_BLOB,
486            ValueRef::KeyDeleted => KEY_BLOCK_ENTRY_TYPE_KEY_DELETED,
487            ValueRef::KeyValueDeleted { len, .. } => {
488                KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN + *len
489            }
490        })
491    }
492
493    /// Writes the value bytes for this reference to a buffer.
494    ///
495    /// This is the shared serialization logic used by both variable-size and fixed-size key block
496    /// builders.
497    fn write_value_to(&self, buffer: &mut Vec<u8>) {
498        match self {
499            ValueRef::Small {
500                block_index,
501                offset,
502                size,
503            } => {
504                let mut scratch = [0u8; 8];
505                BE::write_u16(&mut scratch, *block_index);
506                BE::write_u16(&mut scratch[2..], *size);
507                BE::write_u32(&mut scratch[4..], *offset);
508                buffer.extend(&scratch);
509            }
510            ValueRef::Medium { block_index } => {
511                let mut scratch = [0u8; 2];
512                BE::write_u16(&mut scratch, *block_index);
513                buffer.extend(scratch);
514            }
515            ValueRef::Inline { data, len } => {
516                buffer.extend(&data[..*len as usize]);
517            }
518            ValueRef::Blob { blob_id } => {
519                let mut scratch = [0u8; 4];
520                BE::write_u32(&mut scratch, *blob_id);
521                buffer.extend(scratch);
522            }
523            ValueRef::KeyDeleted => { /* no value bytes */ }
524            ValueRef::KeyValueDeleted { data, len } => {
525                buffer.extend(&data[..*len as usize]);
526            }
527            ValueRef::PendingSmall { .. } => {
528                unreachable!("PendingSmall should have been resolved");
529            }
530        }
531    }
532}
533
534struct PendingEntry<E> {
535    entry: E,
536    value_ref: ValueRef,
537}
538
539/// A streaming SST file writer that writes blocks to disk incrementally.
540///
541/// Instead of materializing all entries in memory and then writing all value blocks followed by all
542/// key blocks, this writer interleaves block writes as entries arrive. Medium values are written
543/// immediately, small values are accumulated into blocks, and key blocks are flushed as soon as
544/// their value references are all resolved.
545///
546/// The SST reader is block-index-addressed (not file-position-addressed), so interleaving block
547/// types is fully compatible.
548pub struct StreamingSstWriter<E: Entry> {
549    // File I/O. Wrapped in Option so close() can take ownership without a partial-move
550    // compile error (partial moves are forbidden when the type has a Drop impl).
551    file: Option<BufWriter<File>>,
552    file_path: PathBuf,
553    /// Whether close() successfully finalized the SST, so Drop must preserve the file.
554    preserve_file: bool,
555    compress_buffer: Vec<u8>,
556    block_offsets: Vec<u32>,
557    compressor: Compressor,
558
559    /// Pending key entries waiting to be flushed as key blocks.
560    ///
561    /// Entries are appended at the back and drained from the front once flushed.
562    ///
563    /// ```text
564    ///  Resolved entries              Unresolved entries
565    ///  (value block index known)     (PendingSmall references)
566    /// |------------------------------|--------------------------|
567    /// 0                     first_pending_small_index         len()
568    ///
569    ///  ^-- current_key_block tracks      ^-- these wait for
570    ///      the incomplete tail block         flush_small_value_block()
571    ///      within this region                to resolve them
572    /// ```
573    ///
574    /// [`advance_boundary_to`](Self::advance_boundary_to) scans the resolved prefix, flushes
575    /// complete key blocks from the front, and drains them. When a small value block is flushed,
576    /// all `PendingSmall` entries are resolved in-place and the boundary advances to `len()`.
577    ///
578    /// **Unbounded growth note:** If a small number of small values appear early, followed by
579    /// many medium/inline values, the queue grows because the front entries block on the
580    /// unflushed small value block while the back keeps accepting resolved entries.
581    pending_keys: VecDeque<PendingEntry<E>>,
582
583    /// Index into `pending_keys` of the first entry that has a `PendingSmall` reference for the
584    /// current (unflushed) small value block. All entries before this index are fully resolved
585    /// (their value block indices are known). Equals `pending_keys.len()` when no pending small
586    /// entries exist.
587    first_pending_small_index: usize,
588
589    /// The current small_block_id being accumulated into (debug-only consistency check).
590    #[cfg(debug_assertions)]
591    current_small_block_id: u16,
592
593    // Pending small value block buffer.
594    pending_small_value_block: Vec<u8>,
595
596    // Reusable buffer for building key blocks
597    key_buffer: Vec<u8>,
598
599    // Reusable buffer for the tail region of a key block: the key (when the search region holds
600    // hashes) and the value. Appended to `key_buffer` when the block is finished.
601    key_value_buffer: Vec<u8>,
602
603    // Collected key hashes truncated to u32 for deferred AMQF construction via sorted Builder
604    // in close(). Fingerprint size is always <32 bits, so the lower 32 bits suffice.
605    collected_fingerprints: Vec<u32>,
606
607    // Index block data: (first_hash, block_index) for each key block written
608    key_block_boundaries: Vec<(u64, u16)>,
609
610    // Metadata
611    min_hash: u64,
612    max_hash: u64,
613    entry_count: u64,
614    tombstone_count: u64,
615    flags: MetaEntryFlags,
616
617    // Fullness tracking (for compaction callers)
618    total_key_size: usize,
619    total_value_size: usize,
620
621    /// Total byte size of keys in `pending_keys` (for block capacity estimation).
622    pending_key_total_size: usize,
623
624    /// State of the current incomplete key block at the tail of the resolved prefix.
625    current_key_block: KeyBlockAccumulator,
626
627    /// Set to `true` by `close()` or `cancel()` so the Drop guard can detect writers dropped
628    /// without completing their lifecycle.
629    #[cfg(debug_assertions)]
630    finished: bool,
631}
632
633impl<E: Entry> StreamingSstWriter<E> {
634    /// Creates a new streaming SST writer.
635    ///
636    /// `max_entry_count` is used to pre-allocate buffers and estimate block counts.
637    pub fn new(
638        file: &Path,
639        flags: MetaEntryFlags,
640        max_entry_count: u64,
641        compression: Compression,
642    ) -> Result<Self> {
643        let file_path = file.to_owned();
644        let file = BufWriter::new(File::create(file)?);
645        let compressor = Compressor::new(compression)?;
646
647        // Estimate number of key blocks based on max entry count.
648        // Each key block holds up to MAX_KEY_BLOCK_ENTRIES entries.
649        let estimated_key_blocks = (max_entry_count as usize)
650            .div_ceil(MAX_KEY_BLOCK_ENTRIES)
651            .max(1);
652        // Estimate value blocks assuming all entries are small values of average size.
653        // Each small value block holds ~MIN_SMALL_VALUE_BLOCK_SIZE / AVG_SMALL_VALUE_SIZE entries.
654        let entries_per_value_block = MIN_SMALL_VALUE_BLOCK_SIZE / AVG_SMALL_VALUE_SIZE;
655        let estimated_value_blocks = (max_entry_count as usize)
656            .div_ceil(entries_per_value_block)
657            .max(1);
658        let estimated_total_blocks = estimated_key_blocks + estimated_value_blocks + 1;
659
660        Ok(Self {
661            file: Some(file),
662            file_path,
663            preserve_file: false,
664            compress_buffer: Vec::with_capacity(MIN_SMALL_VALUE_BLOCK_SIZE + MAX_SMALL_VALUE_SIZE),
665            block_offsets: Vec::with_capacity(estimated_total_blocks),
666            compressor,
667            pending_keys: VecDeque::with_capacity(entries_per_value_block),
668            first_pending_small_index: 0,
669            #[cfg(debug_assertions)]
670            current_small_block_id: 0,
671            pending_small_value_block: Vec::with_capacity(
672                MIN_SMALL_VALUE_BLOCK_SIZE + MAX_SMALL_VALUE_SIZE,
673            ),
674            // `FixedKeyBlockBuilder::finish` appends the tail back into `key_buffer`, so it still
675            // holds a whole block. The tail buffer is only used by fixed-size blocks and is
676            // `reserve`d to the exact region size per block, so it starts empty.
677            key_buffer: Vec::with_capacity(MAX_KEY_BLOCK_SIZE),
678            key_value_buffer: Vec::new(),
679            collected_fingerprints: Vec::with_capacity(max_entry_count as usize),
680            key_block_boundaries: Vec::with_capacity(estimated_key_blocks),
681            min_hash: u64::MAX,
682            max_hash: 0,
683            entry_count: 0,
684            tombstone_count: 0,
685            flags,
686            total_key_size: 0,
687            total_value_size: 0,
688            pending_key_total_size: 0,
689            current_key_block: KeyBlockAccumulator::new(),
690            #[cfg(debug_assertions)]
691            finished: false,
692        })
693    }
694
695    /// Returns true if the SST file has reached capacity limits.
696    ///
697    /// This is intended for compaction callers that need to split output across multiple SST files.
698    pub fn is_full(&self, max_entries: usize, max_data_size: usize) -> bool {
699        self.entry_count as usize >= max_entries
700            || self.total_key_size + self.total_value_size >= max_data_size
701            || !self.has_block_index_capacity()
702    }
703
704    /// Returns true if the SST file has room for more blocks without overflowing the `u16` block
705    /// index. Uses the exact count of blocks already written plus a conservative estimate of
706    /// blocks still needed for pending entries and the index.
707    fn has_block_index_capacity(&self) -> bool {
708        let blocks_written = self.block_offsets.len();
709        // Blocks still needed:
710        // - 1 pending small value block (if buffer is non-empty)
711        // - key blocks for pending entries (upper bound from both entry count and byte size)
712        // - 1 index block
713        let pending_small_block = usize::from(!self.pending_small_value_block.is_empty());
714        let pending_key_blocks = self
715            .pending_keys
716            .len()
717            .div_ceil(MAX_KEY_BLOCK_ENTRIES)
718            .max(self.pending_key_total_size.div_ceil(MAX_KEY_BLOCK_SIZE))
719            .max(1);
720        let index_block = 1;
721        let buffer = BLOCK_INDEX_CAPACITY_BUFFER;
722        blocks_written + pending_small_block + pending_key_blocks + index_block + buffer
723            < u16::MAX as usize
724    }
725
726    /// Adds an entry to the SST file. Entries must be added in (key-hash, key) order.
727    pub fn add(&mut self, entry: E) -> Result<()> {
728        let key_hash = entry.key_hash();
729        let key_len = entry.key_len();
730
731        // Update metadata
732        if self.entry_count == 0 {
733            self.min_hash = key_hash;
734        }
735        self.max_hash = key_hash;
736        self.entry_count += 1;
737
738        // Collect hash for deferred AMQF construction in close()
739        self.collected_fingerprints.push(key_hash as u32);
740
741        // Track key size for fullness and block capacity
742        self.total_key_size += key_len;
743        self.pending_key_total_size += key_len;
744
745        // Route value
746        let value_ref = match entry.value() {
747            EntryValue::Medium { value } => {
748                self.total_value_size += value.len();
749                let block_index = write_block_to_file(
750                    self.file.as_mut().unwrap(),
751                    &mut self.compress_buffer,
752                    &mut self.block_offsets,
753                    value,
754                    true,
755                    &mut self.compressor,
756                )
757                .context("Failed to write value block")?;
758                ValueRef::Medium { block_index }
759            }
760            EntryValue::MediumRaw {
761                uncompressed_size,
762                checksum,
763                block,
764            } => {
765                // Note: tracks compressed block size (not uncompressed) unlike EntryValue::Medium.
766                // Both are acceptable approximations of disk usage for is_full() thresholds.
767                self.total_value_size += block.len();
768                let block_index = write_raw_block_to_file(
769                    self.file.as_mut().unwrap(),
770                    &mut self.block_offsets,
771                    uncompressed_size,
772                    checksum,
773                    block,
774                )
775                .context("Failed to write compressed value block")?;
776                ValueRef::Medium { block_index }
777            }
778            EntryValue::Small { value } => {
779                self.total_value_size += value.len();
780
781                let offset = self.pending_small_value_block.len() as u32;
782                let size: u16 = value.len().try_into().unwrap();
783                self.pending_small_value_block.extend_from_slice(value);
784
785                // Track where the first PendingSmall entry is in the queue
786                if self.first_pending_small_index >= self.pending_keys.len() {
787                    self.first_pending_small_index = self.pending_keys.len();
788                }
789
790                let value_ref = ValueRef::PendingSmall {
791                    #[cfg(debug_assertions)]
792                    small_block_id: self.current_small_block_id,
793                    offset,
794                    size,
795                };
796
797                self.push_pending_key_entry(entry, value_ref);
798
799                // Eagerly flush the small block AFTER pushing the new entry. This resolves
800                // the just-pushed entry immediately via advance_boundary_to(), so key blocks
801                // can be flushed incrementally.
802                if self.pending_small_value_block.len() >= MIN_SMALL_VALUE_BLOCK_SIZE {
803                    self.flush_small_value_block()?;
804                }
805
806                return Ok(());
807            }
808            EntryValue::Inline { value } => {
809                debug_assert!(value.len() <= MAX_INLINE_VALUE_SIZE);
810                let mut data = [0u8; MAX_INLINE_VALUE_SIZE];
811                data[..value.len()].copy_from_slice(value);
812                ValueRef::Inline {
813                    data,
814                    len: value.len() as u8,
815                }
816            }
817            EntryValue::Large { blob } => ValueRef::Blob { blob_id: blob },
818            EntryValue::KeyDeleted => {
819                self.tombstone_count += 1;
820                ValueRef::KeyDeleted
821            }
822            EntryValue::KeyValueDeleted { value } => {
823                self.tombstone_count += 1;
824                // Enforced by `WriteBatch::delete_value`, which rejects oversized values.
825                debug_assert!(value.len() <= MAX_INLINE_VALUE_SIZE);
826                let mut data = [0u8; MAX_INLINE_VALUE_SIZE];
827                data[..value.len()].copy_from_slice(value);
828                ValueRef::KeyValueDeleted {
829                    data,
830                    len: value.len() as u8,
831                }
832            }
833        };
834
835        self.push_pending_key_entry(entry, value_ref);
836        self.try_flush_key_blocks()
837    }
838
839    /// Abandons this writer without flushing buffered data or finalizing the SST file.
840    pub fn cancel(mut self) {
841        self.discard_partial_file();
842        #[cfg(debug_assertions)]
843        {
844            self.finished = true;
845        }
846    }
847
848    /// Closes the raw handle without flushing its buffer and best-effort removes the partial SST.
849    fn discard_partial_file(&mut self) {
850        if let Some(file) = self.file.take() {
851            // Unlike dropping BufWriter, into_parts() does not attempt to flush its buffer.
852            let (file, _) = file.into_parts();
853            drop(file);
854        }
855        // Startup recovery is the fallback if deletion itself fails (for example on Windows if
856        // another handle is still open). Never replace the error that caused cancellation.
857        let _ = fs_err::remove_file(&self.file_path);
858    }
859
860    /// Appends a new entry to the pending-keys queue.
861    fn push_pending_key_entry(&mut self, entry: E, value_ref: ValueRef) {
862        self.pending_keys
863            .push_back(PendingEntry { entry, value_ref });
864    }
865
866    /// Advances `first_pending_small_index` past the just-pushed entry if it is resolved and
867    /// sits right at the current boundary. Flushes complete key blocks incrementally.
868    ///
869    /// Must be called immediately after [`push_pending_key_entry`] with a resolved
870    /// (non-`PendingSmall`) entry.
871    fn try_flush_key_blocks(&mut self) -> Result<()> {
872        debug_assert!(!matches!(
873            self.pending_keys.back().unwrap().value_ref,
874            ValueRef::PendingSmall { .. }
875        ));
876        if self.first_pending_small_index != self.pending_keys.len() - 1 {
877            // Boundary is blocked by earlier unresolved PendingSmall entries.
878            return Ok(());
879        }
880        self.advance_boundary_to(self.pending_keys.len())
881    }
882
883    /// Advances the resolved boundary from its current position to `new_boundary`,
884    /// incrementally tracking key block sizes and flushing complete key blocks.
885    ///
886    /// All entries in `pending_keys[self.first_pending_small_index..new_boundary]`
887    /// must have resolved (non-`PendingSmall`) value references.
888    fn advance_boundary_to(&mut self, new_boundary: usize) -> Result<()> {
889        let mut last_flushed_end = 0usize;
890        // Cumulative key sizes of all entries visited so far, and the snapshot at the last
891        // flush point. The difference at the end gives the total key size of drained entries.
892        let mut cumulative_key_size = 0usize;
893        let mut flushed_key_size = 0usize;
894
895        for i in self.first_pending_small_index..new_boundary {
896            let entry = &self.pending_keys[i];
897            let key_len = entry.entry.key_len();
898            let key_hash = entry.entry.key_hash();
899            let value_type = entry.value_ref.entry_type();
900
901            if self.current_key_block.should_flush(key_len, key_hash) {
902                let block_end = last_flushed_end + self.current_key_block.entry_count;
903                let info = self.current_key_block.flush_info();
904                self.flush_key_block(last_flushed_end, block_end, info)?;
905                flushed_key_size = cumulative_key_size;
906                last_flushed_end = block_end;
907                self.current_key_block.reset();
908            }
909
910            cumulative_key_size += key_len;
911            self.current_key_block.add(key_len, key_hash, value_type);
912        }
913
914        if last_flushed_end > 0 {
915            self.pending_key_total_size -= flushed_key_size;
916            self.pending_keys.drain(..last_flushed_end);
917        }
918
919        self.first_pending_small_index = new_boundary - last_flushed_end;
920        Ok(())
921    }
922
923    /// Flushes the current pending small value block to disk and resolves all `PendingSmall`
924    /// entries in-place.
925    fn flush_small_value_block(&mut self) -> Result<()> {
926        // Early return if empty -- this simplifies trailing small value block handling in
927        // `close()` where we call this unconditionally.
928        if self.pending_small_value_block.is_empty() {
929            return Ok(());
930        }
931
932        let block_index = write_block_to_file(
933            self.file.as_mut().unwrap(),
934            &mut self.compress_buffer,
935            &mut self.block_offsets,
936            &self.pending_small_value_block,
937            true,
938            &mut self.compressor,
939        )
940        .context("Failed to write small value block")?;
941
942        // Resolve all PendingSmall entries for this block in-place.
943        // Only scan from first_pending_small_index -- entries before it are guaranteed
944        // already resolved (from previous flush calls).
945        #[cfg(debug_assertions)]
946        let flushed_id = self.current_small_block_id;
947        for i in self.first_pending_small_index..self.pending_keys.len() {
948            let entry = &mut self.pending_keys[i];
949            if let ValueRef::PendingSmall {
950                #[cfg(debug_assertions)]
951                small_block_id,
952                offset,
953                size,
954            } = entry.value_ref
955            {
956                #[cfg(debug_assertions)]
957                debug_assert_eq!(
958                    small_block_id, flushed_id,
959                    "all pending small entries must reference the small value block that was just \
960                     written"
961                );
962                entry.value_ref = ValueRef::Small {
963                    block_index,
964                    offset,
965                    size,
966                };
967            }
968        }
969
970        // All PendingSmall entries are now resolved. Advance the boundary through all of
971        // them, flushing key blocks incrementally as we go.
972        self.advance_boundary_to(self.pending_keys.len())?;
973
974        // Advance to next small block id (debug-only consistency check)
975        #[cfg(debug_assertions)]
976        {
977            self.current_small_block_id += 1;
978        }
979        self.pending_small_value_block.clear();
980
981        Ok(())
982    }
983
984    /// Flushes a single key block from `pending_keys[start..end]`.
985    ///
986    /// Potentially reorders the keys into key order if we are not storing hashes.
987    fn flush_key_block(&mut self, start: usize, end: usize, info: KeyBlockFlushInfo) -> Result<()> {
988        let entry_count = end - start;
989        let layout = choose_layout(info.max_key_len);
990        let try_compress = info.max_key_len >= MIN_KEY_SIZE_FOR_COMPRESSION;
991
992        // Read the boundary hash before reordering, which would move a different entry to `start`.
993        // The index block must keep routing by the block's lowest hash.
994        let first_hash = self.pending_keys[start].entry.key_hash();
995        // Split the borrow of `self` so the block builders can hold `&mut key_buffer` while the
996        // loops read `pending_keys`.
997        let Self {
998            key_buffer,
999            key_value_buffer,
1000            pending_keys,
1001            ..
1002        } = self;
1003        key_buffer.clear();
1004        // The layout fixes the order entries must be written in, the same way it fixes their
1005        // encoding, so deriving the order from the same `layout` the builders encode by keeps the
1006        // two from disagreeing. `KeyOnly` blocks are searched by key and need a re-sorted copy;
1007        // `HashThenKey` blocks are already in the caller's `(hash, key)` order and are yielded
1008        // straight from `pending_keys` with no allocation.
1009        let block_entries = |start: usize, end: usize| {
1010            if layout == KeyBlockLayout::HashThenKey {
1011                return Either::Left(pending_keys.range(start..end));
1012            }
1013            let mut entries: Vec<&PendingEntry<E>> = pending_keys.range(start..end).collect();
1014            // Stable sort is important to preserve relative order of tombstones
1015            match info.uniform_key_len() {
1016                Some(4) => entries.sort_by_key(|&e| be_key_u32(e.entry.key_bytes())),
1017                Some(8) => entries.sort_by_key(|&e| be_key_u64(e.entry.key_bytes())),
1018                _ => entries.sort_by_key(|&e| e.entry.key_bytes()),
1019            }
1020            Either::Right(entries.into_iter())
1021        };
1022
1023        if let KeyBlockFormat::Fixed {
1024            key_len: key_size,
1025            val_size,
1026            value_type,
1027        } = info.format
1028        {
1029            let mut builder = FixedKeyBlockBuilder::new(
1030                key_buffer,
1031                key_value_buffer,
1032                entry_count as u32,
1033                layout,
1034                key_size,
1035                val_size,
1036                value_type,
1037            );
1038            for pending in block_entries(start, end) {
1039                builder.put(&pending.entry, &pending.value_ref);
1040            }
1041            builder.finish();
1042        } else {
1043            let mut builder = KeyBlockBuilder::new(key_buffer, entry_count as u32, layout);
1044            for pending in block_entries(start, end) {
1045                builder.put(&pending.entry, &pending.value_ref);
1046            }
1047            builder.finish();
1048        }
1049
1050        let block_index = write_block_to_file(
1051            self.file.as_mut().unwrap(),
1052            &mut self.compress_buffer,
1053            &mut self.block_offsets,
1054            &self.key_buffer,
1055            try_compress,
1056            &mut self.compressor,
1057        )
1058        .context("Failed to write key block")?;
1059        self.key_block_boundaries.push((first_hash, block_index));
1060
1061        Ok(())
1062    }
1063
1064    /// Finishes writing the SST file. Flushes remaining blocks, writes the index, and returns
1065    /// metadata.
1066    pub fn close(mut self) -> Result<(StaticSortedFileBuilderMeta<'static>, File)> {
1067        #[cfg(debug_assertions)]
1068        {
1069            self.finished = true;
1070        }
1071
1072        // Flush remaining small value block (even if under MIN_SMALL_VALUE_BLOCK_SIZE).
1073        self.flush_small_value_block()?;
1074
1075        // Now all PendingSmall entries are resolved. Flush all remaining key blocks.
1076        self.flush_remaining_key_blocks()?;
1077
1078        assert!(
1079            !self.key_block_boundaries.is_empty(),
1080            "StreamingSstWriter::close() called with no entries"
1081        );
1082
1083        let mut file = self.file.take().unwrap();
1084
1085        // Write index block (never compressed). Buffer into a Vec first so we can
1086        // compute the checksum, then write via the standard block helper.
1087        let index_entry_count: u16 = (self.key_block_boundaries.len() - 1)
1088            .try_into()
1089            .expect("Index entries count overflow");
1090        let index_block_size: usize =
1091            INDEX_BLOCK_HEADER_SIZE + index_entry_count as usize * INDEX_BLOCK_ENTRY_SIZE;
1092        let mut index_buf = Vec::with_capacity(index_block_size);
1093        {
1094            let first_block = self.key_block_boundaries[0].1;
1095            let mut index_block = IndexBlockBuilder::new(&mut index_buf, first_block);
1096            for &(hash, block) in &self.key_block_boundaries[1..] {
1097                index_block.put(hash, block);
1098            }
1099        }
1100        let index_checksum = checksum_block(&index_buf);
1101        write_raw_block_to_file(
1102            &mut file,
1103            &mut self.block_offsets,
1104            0,
1105            index_checksum,
1106            &index_buf,
1107        )
1108        .context("Failed to write index block")?;
1109
1110        // Write block offset table
1111        for offset in &self.block_offsets {
1112            file.write_u32::<BE>(*offset)
1113                .context("Failed to write block offset")?;
1114        }
1115
1116        let block_count: u16 = self
1117            .block_offsets
1118            .len()
1119            .try_into()
1120            .expect("Block count overflow");
1121
1122        // Build AMQF from collected hashes using sorted Builder insertion.
1123        // Hashes are already sorted by key_hash (SST invariant), but fingerprints
1124        // (truncated hashes) may not be sorted, so we sort by `fingerprint & mask`.
1125        let actual_count = self.collected_fingerprints.len() as u64;
1126        let mut builder = qfilter::Builder::new(
1127            qfilter::Filter::new(actual_count.max(1), AMQF_FALSE_POSITIVE_RATE)
1128                .expect("Filter can't be constructed"),
1129        );
1130
1131        let fp_size = builder.fingerprint_size();
1132        assert!(fp_size < 32, "fp_size {fp_size} exceeds u32");
1133        let fp_mask = (1u32 << fp_size) - 1;
1134        // Mask in-place to fingerprint size and sort.
1135        self.collected_fingerprints
1136            .sort_unstable_by_key(|&h| h & fp_mask);
1137        for &h in &self.collected_fingerprints {
1138            builder
1139                .insert_fingerprint(false, h as u64)
1140                .expect("AMQF insert failed");
1141        }
1142        let filter = builder.into_filter();
1143
1144        // Serialize AMQF using postcard for zero-copy deserialization via FilterRef
1145        let amqf = postcard::to_allocvec(&filter).expect("AMQF serialization failed");
1146
1147        // Compute file size from block offsets rather than calling stream_position()
1148        // (which requires a flush + seek).
1149        let last_block_end = self.block_offsets.last().copied().unwrap_or_default() as u64;
1150        let offset_table_size = block_count as u64 * size_of::<u32>() as u64;
1151        let file_size = last_block_end + offset_table_size;
1152
1153        let meta = StaticSortedFileBuilderMeta {
1154            min_hash: self.min_hash,
1155            max_hash: self.max_hash,
1156            amqf: Cow::Owned(amqf),
1157            block_count,
1158            size: file_size,
1159            flags: self.flags,
1160            entries: self.entry_count,
1161            tombstones: self.tombstone_count,
1162        };
1163
1164        let file = file.into_inner()?;
1165        self.preserve_file = true;
1166        Ok((meta, file))
1167    }
1168
1169    /// Flushes all remaining entries as key blocks. Called from `close()` after all small value
1170    /// blocks have been flushed, so all PendingSmall entries are resolved.
1171    ///
1172    /// This loop mirrors [`advance_boundary_to`], but uses a local accumulator (since the
1173    /// `self.current_key_block` state is stale) and flushes the final incomplete block
1174    /// (unlike `advance_boundary_to`, which keeps it for more entries during streaming).
1175    fn flush_remaining_key_blocks(&mut self) -> Result<()> {
1176        if self.pending_keys.is_empty() {
1177            return Ok(());
1178        }
1179
1180        // After flush_small_value_block() in close(), no PendingSmall entries should remain.
1181        // first_pending_small_index may be non-zero (when all entries are medium/inline/etc
1182        // and advance_boundary_to was never called), but it must equal pending_keys.len(),
1183        // meaning no entries after the boundary exist.
1184        debug_assert_eq!(
1185            self.first_pending_small_index,
1186            self.pending_keys.len(),
1187            "expected no unresolved PendingSmall entries after flush_small_value_block"
1188        );
1189
1190        let total = self.pending_keys.len();
1191        let mut block_start = 0;
1192        let mut acc = KeyBlockAccumulator::new();
1193
1194        for i in 0..total {
1195            let entry = &self.pending_keys[i];
1196            let key_len = entry.entry.key_len();
1197            let key_hash = entry.entry.key_hash();
1198            let value_type = entry.value_ref.entry_type();
1199
1200            if acc.should_flush(key_len, key_hash) {
1201                self.flush_key_block(block_start, i, acc.flush_info())?;
1202                block_start = i;
1203                acc.reset();
1204            }
1205
1206            acc.add(key_len, key_hash, value_type);
1207        }
1208
1209        // Flush the final block
1210        if block_start < total {
1211            self.flush_key_block(block_start, total, acc.flush_info())?;
1212        }
1213
1214        // Free VecDeque memory. Numeric fields are not reset because close() consumes self.
1215        self.pending_keys.clear();
1216        Ok(())
1217    }
1218}
1219
1220impl<E: Entry> Drop for StreamingSstWriter<E> {
1221    fn drop(&mut self) {
1222        if !self.preserve_file {
1223            self.discard_partial_file();
1224        }
1225
1226        // Skip assertion during panic unwinding to avoid a double-panic (which would abort).
1227        #[cfg(debug_assertions)]
1228        if !std::thread::panicking() {
1229            assert!(
1230                self.finished || self.entry_count == 0,
1231                "StreamingSstWriter dropped without calling close() or cancel()"
1232            );
1233        }
1234    }
1235}
1236
1237// ---------------------------------------------------------------------------
1238// KeyBlockBuilder
1239// ---------------------------------------------------------------------------
1240
1241/// Builder for a single key block.
1242///
1243/// Entries are added via [`Self::put`], which writes key data and value references into the buffer.
1244/// The block format uses a fixed-size header table followed by variable-length entry data.
1245struct KeyBlockBuilder<'l> {
1246    current_entry: usize,
1247    header_size: usize,
1248    /// Whether entries hoist their hash into the table slot. Chosen at construction and consulted
1249    /// by [`Self::put`], so a caller cannot pair a block with the wrong entry encoding.
1250    layout: KeyBlockLayout,
1251    /// Bytes per offset table entry, which is wider when the block stores hashes.
1252    table_stride: usize,
1253    buffer: &'l mut Vec<u8>,
1254}
1255
1256/// The size of the key block header (block type + entry count).
1257const KEY_BLOCK_HEADER_SIZE: usize = 4;
1258
1259impl<'l> KeyBlockBuilder<'l> {
1260    /// Creates a new key block builder for the number of entries.
1261    fn new(buffer: &'l mut Vec<u8>, entry_count: u32, layout: KeyBlockLayout) -> Self {
1262        debug_assert!(entry_count < (1 << 24));
1263
1264        const ESTIMATED_KEY_SIZE: usize = 16;
1265        let table_stride = key_block_table_stride(layout.hash_len());
1266        buffer.reserve(entry_count as usize * (ESTIMATED_KEY_SIZE + table_stride));
1267        let block_type = layout.block_type(false);
1268        buffer.write_u8(block_type).unwrap();
1269        buffer.write_u24::<BE>(entry_count).unwrap();
1270        // Reserve the offset table; each entry's slot is filled in as it is written.
1271        buffer.resize(buffer.len() + entry_count as usize * table_stride, 0);
1272        Self {
1273            current_entry: 0,
1274            header_size: buffer.len(),
1275            layout,
1276            table_stride,
1277            buffer,
1278        }
1279    }
1280
1281    /// Writes the type and payload position into the current entry's table slot.
1282    ///
1283    /// The word sits at the end of the slot, after the hash for a `HashThenKey` block.
1284    fn write_entry_header(&mut self, entry_type: EntryType) {
1285        let pos = self.buffer.len() - self.header_size;
1286        let slot = KEY_BLOCK_HEADER_SIZE + self.current_entry * self.table_stride;
1287        let word_offset = slot + self.table_stride - KEY_BLOCK_TABLE_ENTRY_SIZE_NO_HASH;
1288        let header = (pos as u32) | ((entry_type.0 as u32) << 24);
1289        BE::write_u32(&mut self.buffer[word_offset..word_offset + 4], header);
1290    }
1291
1292    /// Writes a single entry (table slot + maybe hash? + key + value data) to the block.
1293    fn put<E: Entry>(&mut self, entry: &E, value_ref: &ValueRef) {
1294        self.write_entry_header(value_ref.entry_type());
1295        if self.layout == KeyBlockLayout::HashThenKey {
1296            let slot = KEY_BLOCK_HEADER_SIZE + self.current_entry * self.table_stride;
1297            self.buffer[slot..slot + size_of::<u64>()]
1298                .copy_from_slice(&entry.key_hash().to_be_bytes());
1299        }
1300        entry.write_key_to(self.buffer);
1301        value_ref.write_value_to(self.buffer);
1302        self.current_entry += 1;
1303    }
1304    /// Returns the key block buffer.
1305    fn finish(self) -> &'l mut Vec<u8> {
1306        self.buffer
1307    }
1308}
1309
1310// ---------------------------------------------------------------------------
1311// FixedKeyBlockBuilder
1312// ---------------------------------------------------------------------------
1313
1314/// The size of the fixed-size key block header (block type + entry count + key size + value type).
1315/// Mixed-type blocks append one more byte for the value size.
1316const FIXED_KEY_BLOCK_HEADER_SIZE: usize = 6;
1317
1318/// Builder for a fixed-size key block where all entries share the same key size and value size.
1319///
1320/// No offset table is written — entry positions are computed arithmetically from the stride. When
1321/// entries share a value size but not a value type, the header records
1322/// [`FIXED_KEY_BLOCK_MIXED_VALUE_TYPE`] and each entry carries its own type byte before its value.
1323///
1324/// Entries are written as two regions rather than interleaved, so that the bytes a lookup's binary
1325/// search probes are contiguous: the search region holds only what the lookup compares first (the
1326/// hash for `HashThenKey`, the key for `KeyOnly`), and everything else follows in the tail region,
1327/// addressed by the same entry index. [`FixedRegions`] derives that geometry for both this builder
1328/// and the reader; see [`KEY_BLOCK_TABLE_ENTRY_SIZE_WITH_HASH`] for why the compared bytes are
1329/// hoisted out of the payload.
1330struct FixedKeyBlockBuilder<'l> {
1331    /// Receives the header and then the search region.
1332    buffer: &'l mut Vec<u8>,
1333    /// Accumulates the tail region, appended to `buffer` by [`Self::finish`].
1334    tail: &'l mut Vec<u8>,
1335    /// Whether each entry writes its own type byte (set for mixed-type blocks).
1336    per_entry_type: bool,
1337    /// Which of the two regions the key goes in: the search region for `KeyOnly`, the tail for
1338    /// `HashThenKey`. Also checks that callers pair the layout with the matching `put` method.
1339    layout: KeyBlockLayout,
1340}
1341
1342impl<'l> FixedKeyBlockBuilder<'l> {
1343    fn new(
1344        buffer: &'l mut Vec<u8>,
1345        tail: &'l mut Vec<u8>,
1346        entry_count: u32,
1347        layout: KeyBlockLayout,
1348        key_size: u8,
1349        val_size: u8,
1350        value_type: Option<EntryType>,
1351    ) -> Self {
1352        let per_entry_type = value_type.is_none();
1353        // The two regions partition the entry bytes: the search region takes the bytes compared
1354        // first, the tail takes the rest. `FixedRegions` owns that split for reader and writer
1355        // alike, so the geometry is derived in one place. Its `val_size` includes the per-entry
1356        // type byte, which the block header keeps separate from the value size.
1357        let FixedRegions {
1358            search_stride,
1359            tail_stride,
1360            ..
1361        } = FixedRegions::new(
1362            entry_count as usize,
1363            layout,
1364            key_size as usize,
1365            val_size as usize + usize::from(per_entry_type),
1366        );
1367        // `finish` appends the tail back into `buffer`, so reserve room for the whole block here
1368        // and the append never reallocates.
1369        buffer.reserve(
1370            FIXED_KEY_BLOCK_HEADER_SIZE + entry_count as usize * (search_stride + tail_stride),
1371        );
1372        tail.clear();
1373        tail.reserve(entry_count as usize * tail_stride);
1374
1375        let block_type = layout.block_type(true);
1376        buffer.extend_from_slice(&[
1377            block_type,
1378            (entry_count >> 16) as u8,
1379            (entry_count >> 8) as u8,
1380            entry_count as u8,
1381            key_size,
1382            value_type.map_or(FIXED_KEY_BLOCK_MIXED_VALUE_TYPE, |ty| ty.0),
1383        ]);
1384        // Mixed-type blocks cannot derive the value size from the header's type byte, so it is
1385        // written explicitly.
1386        if per_entry_type {
1387            buffer.push(val_size);
1388        }
1389
1390        Self {
1391            buffer,
1392            tail,
1393            per_entry_type,
1394            layout,
1395        }
1396    }
1397
1398    /// Writes a single entry, splitting it between the two regions according to the block's
1399    /// layout: `HashThenKey` puts the hash in the search region and the key in the tail, `KeyOnly`
1400    /// puts the key itself in the search region. The layout decides that, not the caller.
1401    fn put<E: Entry>(&mut self, entry: &E, value_ref: &ValueRef) {
1402        match self.layout {
1403            KeyBlockLayout::HashThenKey => {
1404                self.buffer
1405                    .extend_from_slice(&entry.key_hash().to_be_bytes());
1406                entry.write_key_to(self.tail);
1407            }
1408            KeyBlockLayout::KeyOnly => entry.write_key_to(self.buffer),
1409        }
1410        self.put_tail(value_ref);
1411    }
1412
1413    /// Appends the parts of an entry that the search never reads.
1414    fn put_tail(&mut self, value_ref: &ValueRef) {
1415        if self.per_entry_type {
1416            self.tail.push(value_ref.entry_type().0);
1417        }
1418        value_ref.write_value_to(self.tail);
1419    }
1420
1421    fn finish(self) -> &'l mut Vec<u8> {
1422        self.buffer.extend_from_slice(self.tail);
1423        self.buffer
1424    }
1425}
1426
1427/// Returns the value size for a given entry type (builder-side, infallible).
1428///
1429/// This mirrors `entry_val_size` in the reader but panics on invalid types since the builder
1430/// only produces valid types.
1431fn value_type_val_size(ty: EntryType) -> usize {
1432    match ty.0 {
1433        KEY_BLOCK_ENTRY_TYPE_SMALL => SMALL_VALUE_REF_SIZE,
1434        KEY_BLOCK_ENTRY_TYPE_MEDIUM => MEDIUM_VALUE_REF_SIZE,
1435        KEY_BLOCK_ENTRY_TYPE_BLOB => BLOB_VALUE_REF_SIZE,
1436        KEY_BLOCK_ENTRY_TYPE_KEY_DELETED => KEY_DELETED_REF_SIZE,
1437        // Must precede the inline arm: both are open-ended and the tombstone range sits above it.
1438        ty if ty >= KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN => {
1439            (ty - KEY_BLOCK_ENTRY_TYPE_KEY_VALUE_DELETED_MIN) as usize
1440        }
1441        ty if ty >= KEY_BLOCK_ENTRY_TYPE_INLINE_MIN => {
1442            (ty - KEY_BLOCK_ENTRY_TYPE_INLINE_MIN) as usize
1443        }
1444        _ => panic!("Invalid key block entry type: {:?}", ty),
1445    }
1446}
1447
1448// ---------------------------------------------------------------------------
1449// IndexBlockBuilder
1450// ---------------------------------------------------------------------------
1451
1452/// Builder for a single index block.
1453struct IndexBlockBuilder<W: Write> {
1454    writer: W,
1455}
1456
1457/// Size of a single index block entry (u64 hash + u16 block index).
1458pub(crate) const INDEX_BLOCK_ENTRY_SIZE: usize = size_of::<u64>() + size_of::<u16>();
1459
1460/// Size of the index block header (u8 type + u16 first_block).
1461pub(crate) const INDEX_BLOCK_HEADER_SIZE: usize = size_of::<u8>() + size_of::<u16>();
1462
1463impl<W: Write> IndexBlockBuilder<W> {
1464    /// Creates a new builder for an index block with the specified number of entries and a pointer
1465    /// to the first block.
1466    fn new(mut writer: W, first_block: u16) -> Self {
1467        writer.write_u8(BLOCK_TYPE_INDEX).unwrap();
1468        writer.write_u16::<BE>(first_block).unwrap();
1469        Self { writer }
1470    }
1471
1472    /// Adds a hash boundary to the index block.
1473    fn put(&mut self, hash: u64, block: u16) {
1474        self.writer.write_u64::<BE>(hash).unwrap();
1475        self.writer.write_u16::<BE>(block).unwrap();
1476    }
1477}
1478
1479#[cfg(test)]
1480mod tests {
1481    use super::*;
1482    use crate::{
1483        key::hash_key,
1484        lookup_entry::LookupValue,
1485        static_sorted_file::{
1486            BlockCache, SstLookupResult, StaticSortedFile, StaticSortedFileMetaData,
1487        },
1488    };
1489
1490    fn make_cache() -> BlockCache {
1491        BlockCache::with(
1492            100,
1493            4 * 1024 * 1024,
1494            Default::default(),
1495            Default::default(),
1496            Default::default(),
1497        )
1498    }
1499
1500    /// A simple entry type for testing with configurable value type.
1501    struct TestEntry {
1502        key: Vec<u8>,
1503        hash: u64,
1504        value_kind: TestValueKind,
1505    }
1506
1507    enum TestValueKind {
1508        Inline(Vec<u8>),
1509        Small(Vec<u8>),
1510        Medium(Vec<u8>),
1511        /// Already-formatted block with `uncompressed_size = 0` (stored as-is).
1512        MediumRaw(Vec<u8>),
1513        Blob(u32),
1514        KeyDeleted,
1515        KeyValueDeleted(Vec<u8>),
1516    }
1517
1518    impl TestEntry {
1519        fn new(key: &[u8], value_kind: TestValueKind) -> Self {
1520            let key = key.to_vec();
1521            let hash = hash_key(&key);
1522            Self {
1523                key,
1524                hash,
1525                value_kind,
1526            }
1527        }
1528
1529        fn small(key: &[u8], value: &[u8]) -> Self {
1530            Self::new(key, TestValueKind::Small(value.to_vec()))
1531        }
1532
1533        fn inline(key: &[u8], value: &[u8]) -> Self {
1534            debug_assert!(value.len() <= MAX_INLINE_VALUE_SIZE);
1535            Self::new(key, TestValueKind::Inline(value.to_vec()))
1536        }
1537
1538        fn medium(key: &[u8], value: &[u8]) -> Self {
1539            Self::new(key, TestValueKind::Medium(value.to_vec()))
1540        }
1541
1542        fn blob(key: &[u8], blob_id: u32) -> Self {
1543            Self::new(key, TestValueKind::Blob(blob_id))
1544        }
1545
1546        fn deleted(key: &[u8]) -> Self {
1547            Self::new(key, TestValueKind::KeyDeleted)
1548        }
1549
1550        fn medium_raw(key: &[u8], value: &[u8]) -> Self {
1551            Self::new(key, TestValueKind::MediumRaw(value.to_vec()))
1552        }
1553
1554        fn expected_value(&self) -> Option<&[u8]> {
1555            match &self.value_kind {
1556                TestValueKind::Inline(v)
1557                | TestValueKind::Small(v)
1558                | TestValueKind::Medium(v)
1559                | TestValueKind::MediumRaw(v) => Some(v),
1560                _ => None,
1561            }
1562        }
1563    }
1564
1565    impl Entry for TestEntry {
1566        fn key_hash(&self) -> u64 {
1567            self.hash
1568        }
1569
1570        fn key_len(&self) -> usize {
1571            self.key.len()
1572        }
1573
1574        fn key_bytes(&self) -> &[u8] {
1575            &self.key
1576        }
1577
1578        fn value(&self) -> EntryValue<'_> {
1579            match &self.value_kind {
1580                TestValueKind::Inline(v) => EntryValue::Inline { value: v },
1581                TestValueKind::Small(v) => EntryValue::Small { value: v },
1582                TestValueKind::Medium(v) => EntryValue::Medium { value: v },
1583                TestValueKind::MediumRaw(v) => EntryValue::MediumRaw {
1584                    // uncompressed_size = 0 means the block is stored as-is (no compression).
1585                    uncompressed_size: 0,
1586                    checksum: checksum_block(v),
1587                    block: v,
1588                },
1589                TestValueKind::Blob(id) => EntryValue::Large { blob: *id },
1590                TestValueKind::KeyDeleted => EntryValue::KeyDeleted,
1591                TestValueKind::KeyValueDeleted(v) => EntryValue::KeyValueDeleted { value: v },
1592            }
1593        }
1594    }
1595
1596    /// Sort entries by (hash, key) (required by SST writer).
1597    fn sort_entries(entries: &mut [TestEntry]) {
1598        entries.sort_by(|a, b| a.hash.cmp(&b.hash).then_with(|| a.key.cmp(&b.key)));
1599    }
1600
1601    /// Open an SST file for lookup given a path and metadata.
1602    fn open_sst(
1603        dir: &Path,
1604        seq: u32,
1605        meta: &StaticSortedFileBuilderMeta<'_>,
1606    ) -> Result<StaticSortedFile> {
1607        StaticSortedFile::open(
1608            dir,
1609            StaticSortedFileMetaData {
1610                sequence_number: seq,
1611                block_count: meta.block_count,
1612            },
1613            Compression::Lz4,
1614            crate::mmap_access_mode(),
1615        )
1616    }
1617
1618    /// Helper: write entries via StreamingSstWriter, return meta.
1619    fn write_sst(
1620        dir: &Path,
1621        seq: u32,
1622        entries: &[TestEntry],
1623        flags: MetaEntryFlags,
1624    ) -> Result<StaticSortedFileBuilderMeta<'static>> {
1625        let sst_path = dir.join(format!("{seq:08}.sst"));
1626        let mut writer =
1627            StreamingSstWriter::new(&sst_path, flags, entries.len() as u64, Compression::Lz4)?;
1628        for entry in entries {
1629            if let Err(err) = writer.add(entry) {
1630                writer.cancel();
1631                return Err(err);
1632            }
1633        }
1634        let (meta, _file) = writer.close()?;
1635        Ok(meta)
1636    }
1637
1638    /// Lookup a key in an SST file and assert it matches the expected value kind.
1639    fn assert_lookup(
1640        sst: &StaticSortedFile,
1641        entry: &TestEntry,
1642        kc: &BlockCache,
1643        vc: &BlockCache,
1644    ) -> Result<()> {
1645        let result = sst.lookup::<_, false>(entry.hash, &entry.key, kc, vc)?;
1646        match (&entry.value_kind, result) {
1647            (_, SstLookupResult::Found(values))
1648                if values.len() == 1 && matches!(values[0], LookupValue::Slice { .. }) =>
1649            {
1650                let LookupValue::Slice { value } = &values[0] else {
1651                    unreachable!()
1652                };
1653                let expected = entry
1654                    .expected_value()
1655                    .expect("Got Slice but entry has no value");
1656                assert_eq!(
1657                    value.as_ref(),
1658                    expected,
1659                    "value mismatch for key {:?}",
1660                    std::str::from_utf8(&entry.key)
1661                );
1662            }
1663            (TestValueKind::Blob(expected_id), SstLookupResult::Found(values))
1664                if values.len() == 1 && matches!(values[0], LookupValue::Blob { .. }) =>
1665            {
1666                let LookupValue::Blob { sequence_number } = &values[0] else {
1667                    unreachable!()
1668                };
1669                assert_eq!(*sequence_number, *expected_id);
1670            }
1671            (TestValueKind::KeyDeleted, SstLookupResult::Found(values))
1672                if values.len() == 1 && matches!(values[0], LookupValue::KeyDeleted) => {}
1673            (TestValueKind::KeyValueDeleted(expected), SstLookupResult::Found(values))
1674                if values.len() == 1
1675                    && matches!(values[0], LookupValue::KeyValueDeleted { .. }) =>
1676            {
1677                let LookupValue::KeyValueDeleted { value } = &values[0] else {
1678                    unreachable!()
1679                };
1680                assert_eq!(
1681                    value.as_ref(),
1682                    expected.as_slice(),
1683                    "tombstone value mismatch for key {:?}",
1684                    std::str::from_utf8(&entry.key)
1685                );
1686            }
1687            _ => {
1688                panic!(
1689                    "Unexpected lookup result for key {:?}",
1690                    std::str::from_utf8(&entry.key)
1691                );
1692            }
1693        }
1694        Ok(())
1695    }
1696
1697    #[test]
1698    fn single_inline_entry() -> Result<()> {
1699        let dir = tempfile::tempdir()?;
1700        let mut entries = vec![TestEntry::inline(b"key1", b"val1")];
1701        sort_entries(&mut entries);
1702
1703        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
1704        assert_eq!(meta.entries, 1);
1705
1706        let sst = open_sst(dir.path(), 1, &meta)?;
1707        let kc = make_cache();
1708        let vc = make_cache();
1709        assert_lookup(&sst, &entries[0], &kc, &vc)?;
1710        Ok(())
1711    }
1712
1713    #[test]
1714    fn single_small_entry() -> Result<()> {
1715        let dir = tempfile::tempdir()?;
1716        let value = vec![0xAB; 100]; // > MAX_INLINE_VALUE_SIZE, <= MAX_SMALL_VALUE_SIZE
1717        let mut entries = vec![TestEntry::small(b"skey", &value)];
1718        sort_entries(&mut entries);
1719
1720        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
1721        assert_eq!(meta.entries, 1);
1722
1723        let sst = open_sst(dir.path(), 1, &meta)?;
1724        let kc = make_cache();
1725        let vc = make_cache();
1726        assert_lookup(&sst, &entries[0], &kc, &vc)?;
1727        Ok(())
1728    }
1729
1730    #[test]
1731    fn single_medium_entry() -> Result<()> {
1732        let dir = tempfile::tempdir()?;
1733        let value = vec![0xCD; 8192]; // > MAX_SMALL_VALUE_SIZE
1734        let mut entries = vec![TestEntry::medium(b"mkey", &value)];
1735        sort_entries(&mut entries);
1736
1737        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
1738        assert_eq!(meta.entries, 1);
1739
1740        let sst = open_sst(dir.path(), 1, &meta)?;
1741        let kc = make_cache();
1742        let vc = make_cache();
1743        assert_lookup(&sst, &entries[0], &kc, &vc)?;
1744        Ok(())
1745    }
1746
1747    #[test]
1748    fn single_blob_entry() -> Result<()> {
1749        let dir = tempfile::tempdir()?;
1750        let mut entries = vec![TestEntry::blob(b"bkey", 42)];
1751        sort_entries(&mut entries);
1752
1753        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
1754        assert_eq!(meta.entries, 1);
1755
1756        let sst = open_sst(dir.path(), 1, &meta)?;
1757        let kc = make_cache();
1758        let vc = make_cache();
1759        assert_lookup(&sst, &entries[0], &kc, &vc)?;
1760        Ok(())
1761    }
1762
1763    #[test]
1764    fn single_deleted_entry() -> Result<()> {
1765        let dir = tempfile::tempdir()?;
1766        let mut entries = vec![TestEntry::deleted(b"dkey")];
1767        sort_entries(&mut entries);
1768
1769        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
1770        assert_eq!(meta.entries, 1);
1771
1772        let sst = open_sst(dir.path(), 1, &meta)?;
1773        let kc = make_cache();
1774        let vc = make_cache();
1775        assert_lookup(&sst, &entries[0], &kc, &vc)?;
1776        Ok(())
1777    }
1778
1779    #[test]
1780    fn many_small_values() -> Result<()> {
1781        let dir = tempfile::tempdir()?;
1782        // Create enough small entries to trigger multiple small value block flushes.
1783        // MIN_SMALL_VALUE_BLOCK_SIZE = 8KB, each value is 200 bytes -> ~40 entries per block.
1784        let count = 200;
1785        let mut entries: Vec<TestEntry> = (0..count)
1786            .map(|i| {
1787                let key = format!("key-{i:04}");
1788                let value = vec![(i & 0xFF) as u8; 200];
1789                TestEntry::small(key.as_bytes(), &value)
1790            })
1791            .collect();
1792        sort_entries(&mut entries);
1793
1794        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
1795        assert_eq!(meta.entries, count as u64);
1796
1797        let sst = open_sst(dir.path(), 1, &meta)?;
1798        let kc = make_cache();
1799        let vc = make_cache();
1800
1801        for entry in &entries {
1802            assert_lookup(&sst, entry, &kc, &vc)?;
1803        }
1804        Ok(())
1805    }
1806
1807    /// Deterministic, incompressible bytes, so that blocks keep their size on disk.
1808    #[cfg(all(feature = "mmap", unix))]
1809    fn noise(seed: u64, len: usize) -> Vec<u8> {
1810        let mut state = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
1811        (0..len)
1812            .map(|_| {
1813                state ^= state << 13;
1814                state ^= state >> 7;
1815                state ^= state << 17;
1816                state as u8
1817            })
1818            .collect()
1819    }
1820
1821    /// Small values in multi-page blocks plus a few medium values.
1822    #[cfg(all(feature = "mmap", unix))]
1823    fn will_need_entries() -> Vec<TestEntry> {
1824        let mut entries: Vec<TestEntry> = (0..300)
1825            .map(|i| TestEntry::small(format!("key-{i:04}").as_bytes(), &noise(i, 300)))
1826            .chain((0..3).map(|i| {
1827                TestEntry::medium(format!("med-{i}").as_bytes(), &noise(1000 + i, 20_000))
1828            }))
1829            .collect();
1830        sort_entries(&mut entries);
1831        entries
1832    }
1833
1834    /// Lookup mappings carry `MADV_RANDOM` (`rr` in `VmFlags`) and no `MADV_SEQUENTIAL` (`sr`).
1835    #[cfg(all(feature = "mmap", target_os = "linux", not(miri)))]
1836    #[test]
1837    fn lookup_mmap_has_random_advice() -> Result<()> {
1838        let dir = tempfile::tempdir()?;
1839        let entries = will_need_entries();
1840        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
1841        let sst = open_sst(dir.path(), 1, &meta)?;
1842        let path = dir.path().join("00000001.sst").canonicalize()?;
1843        let path = path.to_str().unwrap();
1844
1845        let smaps = std::fs::read_to_string("/proc/self/smaps")?;
1846        let mut lines = smaps.lines();
1847        lines
1848            .find(|line| line.ends_with(path))
1849            .expect("SST mapping not found in /proc/self/smaps");
1850        let flags = lines
1851            .find_map(|line| line.strip_prefix("VmFlags:"))
1852            .expect("VmFlags line not found");
1853        let flags: Vec<&str> = flags.split_whitespace().collect();
1854        assert!(flags.contains(&"rr"), "expected MADV_RANDOM, got {flags:?}");
1855        assert!(
1856            !flags.contains(&"sr"),
1857            "unexpected MADV_SEQUENTIAL, got {flags:?}"
1858        );
1859        drop(sst);
1860        Ok(())
1861    }
1862
1863    /// `MADV_WILLNEED` covers only multi-page blocks: key and small value blocks on their first
1864    /// read, medium value blocks on every read. The block-offset table and the index block are read
1865    /// with `pread` at open, so they are never hinted.
1866    #[cfg(all(feature = "mmap", unix))]
1867    #[test]
1868    fn will_need_hints_multi_page_blocks_on_first_read() -> Result<()> {
1869        use std::ops::Range;
1870
1871        use crate::static_sorted_file::will_need_log;
1872
1873        // SAFETY: `sysconf` has no preconditions.
1874        let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
1875        let multi_page =
1876            |range: &Range<usize>| range.start / page_size != (range.end - 1) / page_size;
1877
1878        let dir = tempfile::tempdir()?;
1879        let entries = will_need_entries();
1880        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
1881        will_need_log::take();
1882
1883        // Open: nothing, since the table and the index block are read with `pread`.
1884        let sst = open_sst(dir.path(), 1, &meta)?;
1885        let opened = will_need_log::take();
1886        assert!(opened.is_empty(), "{opened:?}");
1887
1888        // First pass: each multi-page key and small value block is hinted once.
1889        let kc = make_cache();
1890        let vc = make_cache();
1891        let small = || {
1892            entries
1893                .iter()
1894                .filter(|e| matches!(e.value_kind, TestValueKind::Small(_)))
1895        };
1896        for entry in small() {
1897            assert_lookup(&sst, entry, &kc, &vc)?;
1898        }
1899        let first = will_need_log::take();
1900        assert!(!first.is_empty(), "no multi-page block was hinted");
1901        assert!(first.iter().all(multi_page), "{first:?}");
1902        let mut unique = first.clone();
1903        unique.sort_by_key(|range| range.start);
1904        unique.dedup();
1905        assert_eq!(
1906            unique.len(),
1907            first.len(),
1908            "a block was hinted twice: {first:?}"
1909        );
1910
1911        // Second pass, with empty caches: every block is verified already, so no hints.
1912        let kc = make_cache();
1913        let vc = make_cache();
1914        for entry in small() {
1915            assert_lookup(&sst, entry, &kc, &vc)?;
1916        }
1917        assert_eq!(will_need_log::take(), Vec::<Range<usize>>::new());
1918
1919        // Medium value blocks are hinted on every read.
1920        let medium = entries
1921            .iter()
1922            .find(|e| matches!(e.value_kind, TestValueKind::Medium(_)))
1923            .unwrap();
1924        assert_lookup(&sst, medium, &kc, &vc)?;
1925        let first_read = will_need_log::take();
1926        assert_lookup(&sst, medium, &kc, &vc)?;
1927        let second_read = will_need_log::take();
1928        let value_block = second_read
1929            .last()
1930            .expect("medium value block was not hinted")
1931            .clone();
1932        assert!(multi_page(&value_block));
1933        assert!(value_block.len() > 20_000, "{value_block:?}");
1934        assert_eq!(first_read.last(), Some(&value_block));
1935        Ok(())
1936    }
1937
1938    #[test]
1939    fn many_medium_values() -> Result<()> {
1940        let dir = tempfile::tempdir()?;
1941        let count = 50;
1942        let mut entries: Vec<TestEntry> = (0..count)
1943            .map(|i| {
1944                let key = format!("mkey-{i:04}");
1945                let value = vec![(i & 0xFF) as u8; 8192];
1946                TestEntry::medium(key.as_bytes(), &value)
1947            })
1948            .collect();
1949        sort_entries(&mut entries);
1950
1951        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
1952        assert_eq!(meta.entries, count as u64);
1953
1954        let sst = open_sst(dir.path(), 1, &meta)?;
1955        let kc = make_cache();
1956        let vc = make_cache();
1957
1958        for entry in &entries {
1959            assert_lookup(&sst, entry, &kc, &vc)?;
1960        }
1961        Ok(())
1962    }
1963
1964    #[test]
1965    fn mixed_value_types() -> Result<()> {
1966        let dir = tempfile::tempdir()?;
1967        let mut entries = vec![
1968            TestEntry::inline(b"a-inline", b"tiny"),
1969            TestEntry::small(b"b-small", &[0x11; 200]),
1970            TestEntry::medium(b"c-medium", &[0x22; 8192]),
1971            TestEntry::blob(b"d-blob", 99),
1972            TestEntry::deleted(b"e-deleted"),
1973            TestEntry::small(b"f-small2", &[0x33; 300]),
1974            TestEntry::inline(b"g-inline2", b"mini"),
1975            TestEntry::medium(b"h-medium2", &[0x44; 16384]),
1976        ];
1977        sort_entries(&mut entries);
1978
1979        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
1980        assert_eq!(meta.entries, 8);
1981
1982        let sst = open_sst(dir.path(), 1, &meta)?;
1983        let kc = make_cache();
1984        let vc = make_cache();
1985
1986        for entry in &entries {
1987            assert_lookup(&sst, entry, &kc, &vc)?;
1988        }
1989        Ok(())
1990    }
1991
1992    #[test]
1993    #[cfg(debug_assertions)]
1994    fn cancel_after_failed_add_does_not_trigger_drop_assertion() {
1995        let dir = tempfile::tempdir().unwrap();
1996        let sst_path = dir.path().join("test.sst");
1997        let mut writer =
1998            StreamingSstWriter::new(&sst_path, MetaEntryFlags::default(), 1, Compression::Lz4)
1999                .unwrap();
2000
2001        // Replace the output with an unbuffered, read-only handle to force a deterministic write
2002        // error without relying on the filesystem being full.
2003        drop(writer.file.take());
2004        writer.file = Some(BufWriter::with_capacity(0, File::open(&sst_path).unwrap()));
2005        let error = writer
2006            .add(TestEntry::medium(b"key", &[0; 8192]))
2007            .unwrap_err();
2008        assert!(format!("{error:#}").contains("Failed to write value block"));
2009
2010        let cancel_result =
2011            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| writer.cancel()));
2012        assert!(
2013            cancel_result.is_ok(),
2014            "cancelling a writer after an add error must not trigger the lifecycle assertion"
2015        );
2016        assert!(!sst_path.exists(), "cancel should remove the partial SST");
2017    }
2018
2019    #[test]
2020    fn failed_close_removes_partial_file() {
2021        let dir = tempfile::tempdir().unwrap();
2022        let sst_path = dir.path().join("test.sst");
2023        let mut writer =
2024            StreamingSstWriter::new(&sst_path, MetaEntryFlags::default(), 1, Compression::Lz4)
2025                .unwrap();
2026        writer.add(TestEntry::inline(b"key", b"value")).unwrap();
2027
2028        // Force close() to fail while flushing the pending key block.
2029        drop(writer.file.take());
2030        writer.file = Some(BufWriter::with_capacity(0, File::open(&sst_path).unwrap()));
2031        let error = writer.close().unwrap_err();
2032
2033        assert!(format!("{error:#}").contains("Failed to write key block"));
2034        assert!(
2035            !sst_path.exists(),
2036            "a failed close should remove the partial SST"
2037        );
2038    }
2039
2040    #[test]
2041    fn is_full_entry_count_limit() {
2042        let dir = tempfile::tempdir().unwrap();
2043        let sst_path = dir.path().join("test.sst");
2044        let mut writer =
2045            StreamingSstWriter::new(&sst_path, MetaEntryFlags::default(), 100, Compression::Lz4)
2046                .unwrap();
2047
2048        let max_entries = 50;
2049        for i in 0..max_entries {
2050            let key = format!("k{i:06}");
2051            let entry = TestEntry::inline(key.as_bytes(), &[0; 4]);
2052            writer.add(entry).unwrap();
2053        }
2054
2055        assert_eq!(writer.entry_count, max_entries as u64);
2056        assert!(
2057            writer.is_full(max_entries, usize::MAX),
2058            "Should be full when entry count reaches max_entries"
2059        );
2060        assert!(
2061            !writer.is_full(max_entries + 1, usize::MAX),
2062            "Should not be full when limit is higher"
2063        );
2064        writer.close().unwrap();
2065    }
2066
2067    #[test]
2068    fn is_full_data_size_limit() {
2069        let dir = tempfile::tempdir().unwrap();
2070        let sst_path = dir.path().join("test.sst");
2071        let mut writer =
2072            StreamingSstWriter::new(&sst_path, MetaEntryFlags::default(), 100, Compression::Lz4)
2073                .unwrap();
2074
2075        let value = vec![0u8; 1000];
2076        for i in 0..10 {
2077            let key = format!("k{i:06}");
2078            let entry = TestEntry::small(key.as_bytes(), &value);
2079            writer.add(entry).unwrap();
2080        }
2081
2082        let total = writer.total_key_size + writer.total_value_size;
2083        assert!(total > 10_000, "total data should exceed 10KB");
2084        assert!(writer.is_full(usize::MAX, total - 1));
2085        assert!(!writer.is_full(usize::MAX, total + 1));
2086        writer.close().unwrap();
2087    }
2088
2089    #[test]
2090    fn write_static_stored_file_matches_streaming() -> Result<()> {
2091        let dir = tempfile::tempdir()?;
2092
2093        let mut entries: Vec<TestEntry> = (0..100)
2094            .map(|i| {
2095                let key = format!("rkey-{i:04}");
2096                if i % 3 == 0 {
2097                    TestEntry::inline(key.as_bytes(), &[(i & 0xFF) as u8; 4])
2098                } else if i % 3 == 1 {
2099                    TestEntry::small(key.as_bytes(), &[(i & 0xFF) as u8; 200])
2100                } else {
2101                    TestEntry::medium(key.as_bytes(), &[(i & 0xFF) as u8; 8192])
2102                }
2103            })
2104            .collect();
2105        sort_entries(&mut entries);
2106
2107        // Write via convenience function
2108        let batch_path = dir.path().join("00000001.sst");
2109        let (meta1, _) = write_static_stored_file(
2110            &entries,
2111            &batch_path,
2112            MetaEntryFlags::default(),
2113            Compression::Lz4,
2114        )?;
2115
2116        // Write via streaming API
2117        let streaming_path = dir.path().join("00000002.sst");
2118        let mut writer = StreamingSstWriter::new(
2119            &streaming_path,
2120            MetaEntryFlags::default(),
2121            entries.len() as u64,
2122            Compression::Lz4,
2123        )?;
2124        for entry in &entries {
2125            if let Err(err) = writer.add(entry) {
2126                writer.cancel();
2127                return Err(err);
2128            }
2129        }
2130        let (meta2, _) = writer.close()?;
2131
2132        // Metadata should match
2133        assert_eq!(meta1.entries, meta2.entries);
2134        assert_eq!(meta1.min_hash, meta2.min_hash);
2135        assert_eq!(meta1.max_hash, meta2.max_hash);
2136        assert_eq!(meta1.block_count, meta2.block_count);
2137
2138        // Both files should produce the same lookup results
2139        let sst1 = StaticSortedFile::open(
2140            dir.path(),
2141            StaticSortedFileMetaData {
2142                sequence_number: 1,
2143                block_count: meta1.block_count,
2144            },
2145            Compression::Lz4,
2146            crate::mmap_access_mode(),
2147        )?;
2148        let sst2 = StaticSortedFile::open(
2149            dir.path(),
2150            StaticSortedFileMetaData {
2151                sequence_number: 2,
2152                block_count: meta2.block_count,
2153            },
2154            Compression::Lz4,
2155            crate::mmap_access_mode(),
2156        )?;
2157        let kc = make_cache();
2158        let vc = make_cache();
2159
2160        for entry in &entries {
2161            let r1 = sst1.lookup::<_, false>(entry.hash, &entry.key, &kc, &vc)?;
2162            let r2 = sst2.lookup::<_, false>(entry.hash, &entry.key, &kc, &vc)?;
2163            match (&r1, &r2) {
2164                (SstLookupResult::Found(v1), SstLookupResult::Found(v2))
2165                    if v1.len() == 1 && v2.len() == 1 =>
2166                {
2167                    match (&v1[0], &v2[0]) {
2168                        (
2169                            LookupValue::Slice { value: val1 },
2170                            LookupValue::Slice { value: val2 },
2171                        ) => {
2172                            assert_eq!(
2173                                val1.as_ref(),
2174                                val2.as_ref(),
2175                                "Value mismatch for key {:?}",
2176                                std::str::from_utf8(&entry.key)
2177                            );
2178                        }
2179                        (LookupValue::KeyDeleted, LookupValue::KeyDeleted) => {}
2180                        (
2181                            LookupValue::Blob {
2182                                sequence_number: s1,
2183                            },
2184                            LookupValue::Blob {
2185                                sequence_number: s2,
2186                            },
2187                        ) => {
2188                            assert_eq!(s1, s2);
2189                        }
2190                        _ => panic!(
2191                            "Mismatched results for key {:?}",
2192                            std::str::from_utf8(&entry.key)
2193                        ),
2194                    }
2195                }
2196                _ => panic!(
2197                    "Mismatched results for key {:?}",
2198                    std::str::from_utf8(&entry.key)
2199                ),
2200            }
2201        }
2202        Ok(())
2203    }
2204
2205    #[test]
2206    #[should_panic(expected = "StreamingSstWriter::close() called with no entries")]
2207    fn close_empty_writer_panics() {
2208        let dir = tempfile::tempdir().unwrap();
2209        let sst_path = dir.path().join("empty.sst");
2210        let writer = StreamingSstWriter::<TestEntry>::new(
2211            &sst_path,
2212            MetaEntryFlags::default(),
2213            0,
2214            Compression::Lz4,
2215        )
2216        .unwrap();
2217        writer.close().unwrap();
2218    }
2219
2220    #[test]
2221    fn key_block_boundary_at_max_entries() -> Result<()> {
2222        let dir = tempfile::tempdir()?;
2223        let count = MAX_KEY_BLOCK_ENTRIES + 1;
2224        let mut entries: Vec<TestEntry> = (0..count)
2225            .map(|i| {
2226                let key = format!("boundary-{i:06}");
2227                TestEntry::inline(key.as_bytes(), &[0u8; 4])
2228            })
2229            .collect();
2230        sort_entries(&mut entries);
2231
2232        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
2233        assert_eq!(meta.entries, count as u64);
2234        // count > MAX_KEY_BLOCK_ENTRIES so we need at least 2 key blocks plus 1 index block
2235        assert!(
2236            meta.block_count >= 3,
2237            "expected at least 2 key blocks + 1 index block"
2238        );
2239
2240        let sst = open_sst(dir.path(), 1, &meta)?;
2241        let kc = make_cache();
2242        let vc = make_cache();
2243        for entry in &entries {
2244            assert_lookup(&sst, entry, &kc, &vc)?;
2245        }
2246        Ok(())
2247    }
2248
2249    /// Reads the first key block of an SST, returning its raw (uncompressed) bytes.
2250    ///
2251    /// Block 0 is always a key block; the block offset table sits at the end of the file.
2252    fn read_first_block(dir: &Path, seq: u32, block_count: u16) -> Result<Vec<u8>> {
2253        let data = fs_err::read(dir.join(format!("{seq:08}.sst")))?;
2254        let offsets_start = data.len() - block_count as usize * size_of::<u32>();
2255        let end = BE::read_u32(&data[offsets_start..]) as usize;
2256        let raw = &data[..end];
2257        // Each block is prefixed by BLOCK_HEADER_SIZE bytes: 4B uncompressed size + 4B checksum.
2258        // An uncompressed size of 0 means the block is stored as-is.
2259        let uncompressed_size = BE::read_u32(raw);
2260        let body = &raw[BLOCK_HEADER_SIZE..];
2261        Ok(if uncompressed_size == 0 {
2262            body.to_vec()
2263        } else {
2264            crate::compression::decompress_into_arc(Compression::Lz4, uncompressed_size, body)?
2265                .to_vec()
2266        })
2267    }
2268
2269    /// A tombstone and a value of the same size keep the block in fixed layout.
2270    ///
2271    /// This is what makes tombstones cheap for uniform-key families like the task cache: without
2272    /// the mixed-type layout, one tombstone would demote its whole block to the variable format
2273    /// and add a 4-byte offset table entry for every entry in it.
2274    #[test]
2275    fn fixed_layout_survives_mixed_value_types_of_equal_size() -> Result<()> {
2276        let dir = tempfile::tempdir()?;
2277
2278        // Uniform 8-byte keys, uniform 4-byte values, but two different entry types.
2279        let mut entries: Vec<TestEntry> = (0..64u64)
2280            .map(|i| {
2281                let key = format!("k-{i:06}");
2282                if i % 4 == 0 {
2283                    TestEntry::new(
2284                        key.as_bytes(),
2285                        TestValueKind::KeyValueDeleted(vec![0xAAu8; 4]),
2286                    )
2287                } else {
2288                    TestEntry::inline(key.as_bytes(), &[0xBBu8; 4])
2289                }
2290            })
2291            .collect();
2292        sort_entries(&mut entries);
2293
2294        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
2295        let block = read_first_block(dir.path(), 1, meta.block_count)?;
2296
2297        assert_eq!(
2298            KeyBlockLayout::from_block_type(block[0]).map(|(_, fixed)| fixed),
2299            Some(true),
2300            "mixed value types of equal size should stay in fixed layout, got block type {}",
2301            block[0]
2302        );
2303        assert_eq!(
2304            block[5], FIXED_KEY_BLOCK_MIXED_VALUE_TYPE,
2305            "block should be marked mixed-type"
2306        );
2307        assert_eq!(block[6], 4, "value size should be recorded in the header");
2308
2309        // The layout is only useful if it still reads back correctly.
2310        let sst = open_sst(dir.path(), 1, &meta)?;
2311        let kc = make_cache();
2312        let vc = make_cache();
2313        for entry in &entries {
2314            assert_lookup(&sst, entry, &kc, &vc)?;
2315        }
2316        Ok(())
2317    }
2318
2319    /// Differing value *sizes* cannot share a stride, so the block must fall back to variable
2320    /// layout rather than silently misreading entries.
2321    #[test]
2322    fn mixed_value_sizes_fall_back_to_variable_layout() -> Result<()> {
2323        let dir = tempfile::tempdir()?;
2324
2325        let mut entries: Vec<TestEntry> = (0..64u64)
2326            .map(|i| {
2327                let key = format!("k-{i:06}");
2328                let len = if i % 4 == 0 { 2 } else { 4 };
2329                TestEntry::inline(key.as_bytes(), &vec![0xCCu8; len])
2330            })
2331            .collect();
2332        sort_entries(&mut entries);
2333
2334        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
2335        let block = read_first_block(dir.path(), 1, meta.block_count)?;
2336        assert_eq!(
2337            KeyBlockLayout::from_block_type(block[0]).map(|(_, fixed)| fixed),
2338            Some(false),
2339            "differing value sizes should use variable layout, got block type {}",
2340            block[0]
2341        );
2342
2343        let sst = open_sst(dir.path(), 1, &meta)?;
2344        let kc = make_cache();
2345        let vc = make_cache();
2346        for entry in &entries {
2347            assert_lookup(&sst, entry, &kc, &vc)?;
2348        }
2349        Ok(())
2350    }
2351
2352    #[test]
2353    fn single_medium_raw_entry() -> Result<()> {
2354        let dir = tempfile::tempdir()?;
2355        let value = vec![0xBE; 8192];
2356        let mut entries = vec![TestEntry::medium_raw(b"rkey", &value)];
2357        sort_entries(&mut entries);
2358
2359        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default())?;
2360        assert_eq!(meta.entries, 1);
2361
2362        let sst = open_sst(dir.path(), 1, &meta)?;
2363        let kc = make_cache();
2364        let vc = make_cache();
2365        assert_lookup(&sst, &entries[0], &kc, &vc)?;
2366        Ok(())
2367    }
2368
2369    /// Flip a single byte in an SST file at the given position.
2370    fn corrupt_sst_byte(dir: &Path, seq: u32, pos: u64) {
2371        use std::io::{Seek, SeekFrom, Write as _};
2372
2373        let sst_path = dir.join(format!("{seq:08}.sst"));
2374        let file_bytes = std::fs::read(&sst_path).unwrap();
2375        let original = file_bytes[pos as usize];
2376        let mut file = std::fs::OpenOptions::new()
2377            .write(true)
2378            .open(&sst_path)
2379            .unwrap();
2380        file.seek(SeekFrom::Start(pos)).unwrap();
2381        file.write_all(&[original ^ 0xFF]).unwrap();
2382        file.sync_all().unwrap();
2383    }
2384
2385    /// Assert that looking up the first entry in a corrupted SST returns a corruption error.
2386    fn assert_corruption_detected(
2387        dir: &Path,
2388        seq: u32,
2389        meta: &StaticSortedFileBuilderMeta<'_>,
2390        entries: &[TestEntry],
2391    ) {
2392        let sst = open_sst(dir, seq, meta).unwrap();
2393        let kc = make_cache();
2394        let vc = make_cache();
2395        match sst.lookup::<_, false>(entries[0].hash, &entries[0].key, &kc, &vc) {
2396            Err(err) => {
2397                let msg = format!("{err}");
2398                assert!(
2399                    msg.contains("corruption"),
2400                    "Expected corruption error, got: {msg}"
2401                );
2402            }
2403            Ok(_) => panic!("Expected checksum error, but lookup succeeded"),
2404        }
2405    }
2406
2407    #[test]
2408    fn checksum_detects_corrupted_compressed_block() {
2409        let dir = tempfile::tempdir().unwrap();
2410        // Medium value is large enough to get its own value block, which will be compressed
2411        let value = vec![0xCD; 8192];
2412        let entries = vec![TestEntry::medium(b"mkey", &value)];
2413
2414        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default()).unwrap();
2415
2416        // Corrupt the stored checksum of the first block (bytes 4..8).
2417        // This guarantees a mismatch regardless of whether LZ4 decompression succeeds.
2418        corrupt_sst_byte(dir.path(), 1, 4);
2419        assert_corruption_detected(dir.path(), 1, &meta, &entries);
2420    }
2421
2422    #[test]
2423    fn checksum_detects_corrupted_uncompressed_block() {
2424        let dir = tempfile::tempdir().unwrap();
2425        // Single inline entry - the key block will be small and likely stored uncompressed
2426        let entries = vec![TestEntry::inline(b"key1", b"val1")];
2427
2428        let meta = write_sst(dir.path(), 1, &entries, MetaEntryFlags::default()).unwrap();
2429
2430        // Corrupt a byte in the first block's data (after the 8-byte header)
2431        corrupt_sst_byte(dir.path(), 1, BLOCK_HEADER_SIZE as u64 + 1);
2432        assert_corruption_detected(dir.path(), 1, &meta, &entries);
2433    }
2434
2435    #[test]
2436    fn be_key_order_matches_byte_order() {
2437        let keys4: Vec<[u8; 4]> = vec![
2438            [0, 0, 0, 0],
2439            [0, 0, 0, 1],
2440            [0, 0, 1, 0],
2441            [0x7f, 0xff, 0xff, 0xff],
2442            [0x80, 0, 0, 0],
2443            [0xff, 0xfe, 0, 0],
2444            [0xff, 0xff, 0xff, 0xff],
2445        ];
2446        for a in &keys4 {
2447            for b in &keys4 {
2448                assert_eq!(
2449                    be_key_u32(a).cmp(&be_key_u32(b)),
2450                    a[..].cmp(&b[..]),
2451                    "u32 order disagrees with byte order for {a:?} vs {b:?}"
2452                );
2453            }
2454        }
2455        let keys8: Vec<[u8; 8]> = vec![
2456            [0; 8],
2457            [0, 0, 0, 0, 0, 0, 0, 1],
2458            [0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
2459            [0x80, 0, 0, 0, 0, 0, 0, 0],
2460            [0xff; 8],
2461        ];
2462        for a in &keys8 {
2463            for b in &keys8 {
2464                assert_eq!(
2465                    be_key_u64(a).cmp(&be_key_u64(b)),
2466                    a[..].cmp(&b[..]),
2467                    "u64 order disagrees with byte order for {a:?} vs {b:?}"
2468                );
2469            }
2470        }
2471    }
2472
2473    /// `uniform_key_len` must only report a length when the block's keys really are all that long,
2474    /// since the specialized sorts are unsound otherwise.
2475    #[test]
2476    fn uniform_key_len_requires_equal_lengths() {
2477        let mut acc = KeyBlockAccumulator::new();
2478        assert_eq!(acc.flush_info().uniform_key_len(), None, "empty block");
2479
2480        let ty = EntryType(KEY_BLOCK_ENTRY_TYPE_INLINE_MIN);
2481        acc.add(8, 1, ty);
2482        acc.add(8, 2, ty);
2483        assert_eq!(acc.flush_info().uniform_key_len(), Some(8));
2484
2485        acc.add(4, 3, ty);
2486        assert_eq!(
2487            acc.flush_info().uniform_key_len(),
2488            None,
2489            "mixed lengths must not report a uniform length"
2490        );
2491
2492        acc.reset();
2493        acc.add(4, 4, ty);
2494        assert_eq!(
2495            acc.flush_info().uniform_key_len(),
2496            Some(4),
2497            "reset clears min"
2498        );
2499    }
2500}