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
28pub const BLOCK_HEADER_SIZE: usize = 8;
30
31const MAX_KEY_BLOCK_ENTRIES: usize = MAX_KEY_BLOCK_SIZE / KEY_BLOCK_ENTRY_META_OVERHEAD;
33const MAX_KEY_BLOCK_SIZE: usize = 16 * 1024;
36const KEY_BLOCK_ENTRY_META_OVERHEAD: usize = 20;
45const AMQF_FALSE_POSITIVE_RATE: f64 = 0.01;
47const AVG_SMALL_VALUE_SIZE: usize = 64;
52
53const BLOCK_INDEX_CAPACITY_BUFFER: usize = 16;
57
58const MIN_KEY_SIZE_FOR_COMPRESSION: usize = 16;
63
64const MAX_FIXED_KEY_LEN: usize = u8::MAX as usize;
69
70const MAX_FIXED_VAL_SIZE: usize = u8::MAX as usize;
75
76#[derive(Clone, Copy, PartialEq, Eq, Debug)]
81struct EntryType(u8);
82
83#[derive(Clone, Copy)]
97enum KeyBlockFormat {
98 Unknown,
100 Fixed {
102 key_len: u8,
103 val_size: u8,
104 value_type: Option<EntryType>,
106 },
107 Variable,
109}
110
111impl KeyBlockFormat {
112 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 value_type: v.filter(|v| *v == value_type),
139 },
140 KeyBlockFormat::Fixed { .. } | KeyBlockFormat::Variable => KeyBlockFormat::Variable,
141 };
142 }
143}
144
145#[derive(Clone, Copy)]
147struct KeyBlockFlushInfo {
148 max_key_len: usize,
149 min_key_len: usize,
150 format: KeyBlockFormat,
151}
152
153impl KeyBlockFlushInfo {
154 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
160struct KeyBlockAccumulator {
166 size: usize,
168 entry_count: usize,
170 max_key_len: usize,
172 min_key_len: usize,
173 last_hash: u64,
176 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 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 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 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 (would_exceed_size || would_exceed_entries) && self.last_hash != next_key_hash
223 }
224
225 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 }
234}
235
236fn choose_layout(max_key_len: usize) -> KeyBlockLayout {
238 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
257pub trait Entry {
259 fn key_hash(&self) -> u64;
261 fn key_len(&self) -> usize;
263 fn key_bytes(&self) -> &[u8];
265 fn write_key_to(&self, buf: &mut Vec<u8>) {
267 buf.extend_from_slice(self.key_bytes());
268 }
269
270 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#[derive(Copy, Clone)]
291pub enum EntryValue<'l> {
292 Inline { value: &'l [u8] },
294 Small { value: &'l [u8] },
296 Medium { value: &'l [u8] },
298 MediumRaw {
300 uncompressed_size: u32,
303 checksum: u32,
305 block: &'l [u8],
306 },
307 Large { blob: u32 },
309 KeyDeleted,
311 KeyValueDeleted { value: &'l [u8] },
315}
316
317#[derive(Debug, Clone)]
318pub struct StaticSortedFileBuilderMeta<'a> {
319 pub min_hash: u64,
321 pub max_hash: u64,
323 pub amqf: Cow<'a, [u8]>,
325 pub block_count: u16,
327 pub size: u64,
329 pub flags: MetaEntryFlags,
331 pub entries: u64,
333 pub tombstones: u64,
335}
336
337pub 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
368fn 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
406fn 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 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 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
439enum ValueRef {
445 Small {
447 block_index: u16,
448 offset: u32,
449 size: u16,
450 },
451 PendingSmall {
454 #[cfg(debug_assertions)]
455 small_block_id: u16,
456 offset: u32,
457 size: u16,
458 },
459 Medium { block_index: u16 },
461 Inline {
463 data: [u8; MAX_INLINE_VALUE_SIZE],
464 len: u8,
465 },
466 Blob { blob_id: u32 },
468 KeyDeleted,
470 KeyValueDeleted {
473 data: [u8; MAX_INLINE_VALUE_SIZE],
474 len: u8,
475 },
476}
477
478impl ValueRef {
479 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 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 => { }
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
539pub struct StreamingSstWriter<E: Entry> {
549 file: Option<BufWriter<File>>,
552 file_path: PathBuf,
553 preserve_file: bool,
555 compress_buffer: Vec<u8>,
556 block_offsets: Vec<u32>,
557 compressor: Compressor,
558
559 pending_keys: VecDeque<PendingEntry<E>>,
582
583 first_pending_small_index: usize,
588
589 #[cfg(debug_assertions)]
591 current_small_block_id: u16,
592
593 pending_small_value_block: Vec<u8>,
595
596 key_buffer: Vec<u8>,
598
599 key_value_buffer: Vec<u8>,
602
603 collected_fingerprints: Vec<u32>,
606
607 key_block_boundaries: Vec<(u64, u16)>,
609
610 min_hash: u64,
612 max_hash: u64,
613 entry_count: u64,
614 tombstone_count: u64,
615 flags: MetaEntryFlags,
616
617 total_key_size: usize,
619 total_value_size: usize,
620
621 pending_key_total_size: usize,
623
624 current_key_block: KeyBlockAccumulator,
626
627 #[cfg(debug_assertions)]
630 finished: bool,
631}
632
633impl<E: Entry> StreamingSstWriter<E> {
634 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 let estimated_key_blocks = (max_entry_count as usize)
650 .div_ceil(MAX_KEY_BLOCK_ENTRIES)
651 .max(1);
652 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 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 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 fn has_block_index_capacity(&self) -> bool {
708 let blocks_written = self.block_offsets.len();
709 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 pub fn add(&mut self, entry: E) -> Result<()> {
728 let key_hash = entry.key_hash();
729 let key_len = entry.key_len();
730
731 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 self.collected_fingerprints.push(key_hash as u32);
740
741 self.total_key_size += key_len;
743 self.pending_key_total_size += key_len;
744
745 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 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 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 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 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 pub fn cancel(mut self) {
841 self.discard_partial_file();
842 #[cfg(debug_assertions)]
843 {
844 self.finished = true;
845 }
846 }
847
848 fn discard_partial_file(&mut self) {
850 if let Some(file) = self.file.take() {
851 let (file, _) = file.into_parts();
853 drop(file);
854 }
855 let _ = fs_err::remove_file(&self.file_path);
858 }
859
860 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 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 return Ok(());
879 }
880 self.advance_boundary_to(self.pending_keys.len())
881 }
882
883 fn advance_boundary_to(&mut self, new_boundary: usize) -> Result<()> {
889 let mut last_flushed_end = 0usize;
890 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 fn flush_small_value_block(&mut self) -> Result<()> {
926 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 #[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 self.advance_boundary_to(self.pending_keys.len())?;
973
974 #[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 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 let first_hash = self.pending_keys[start].entry.key_hash();
995 let Self {
998 key_buffer,
999 key_value_buffer,
1000 pending_keys,
1001 ..
1002 } = self;
1003 key_buffer.clear();
1004 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 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 pub fn close(mut self) -> Result<(StaticSortedFileBuilderMeta<'static>, File)> {
1067 #[cfg(debug_assertions)]
1068 {
1069 self.finished = true;
1070 }
1071
1072 self.flush_small_value_block()?;
1074
1075 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 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 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 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 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 let amqf = postcard::to_allocvec(&filter).expect("AMQF serialization failed");
1146
1147 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 fn flush_remaining_key_blocks(&mut self) -> Result<()> {
1176 if self.pending_keys.is_empty() {
1177 return Ok(());
1178 }
1179
1180 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 if block_start < total {
1211 self.flush_key_block(block_start, total, acc.flush_info())?;
1212 }
1213
1214 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 #[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
1237struct KeyBlockBuilder<'l> {
1246 current_entry: usize,
1247 header_size: usize,
1248 layout: KeyBlockLayout,
1251 table_stride: usize,
1253 buffer: &'l mut Vec<u8>,
1254}
1255
1256const KEY_BLOCK_HEADER_SIZE: usize = 4;
1258
1259impl<'l> KeyBlockBuilder<'l> {
1260 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 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 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 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 fn finish(self) -> &'l mut Vec<u8> {
1306 self.buffer
1307 }
1308}
1309
1310const FIXED_KEY_BLOCK_HEADER_SIZE: usize = 6;
1317
1318struct FixedKeyBlockBuilder<'l> {
1331 buffer: &'l mut Vec<u8>,
1333 tail: &'l mut Vec<u8>,
1335 per_entry_type: bool,
1337 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 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 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 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 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 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
1427fn 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 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
1448struct IndexBlockBuilder<W: Write> {
1454 writer: W,
1455}
1456
1457pub(crate) const INDEX_BLOCK_ENTRY_SIZE: usize = size_of::<u64>() + size_of::<u16>();
1459
1460pub(crate) const INDEX_BLOCK_HEADER_SIZE: usize = size_of::<u8>() + size_of::<u16>();
1462
1463impl<W: Write> IndexBlockBuilder<W> {
1464 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 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 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 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,
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 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 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 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 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]; 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]; 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 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 #[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 #[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 #[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 #[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 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 let sst = open_sst(dir.path(), 1, &meta)?;
1885 let opened = will_need_log::take();
1886 assert!(opened.is_empty(), "{opened:?}");
1887
1888 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 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 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 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 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 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 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 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 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 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 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 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 #[test]
2275 fn fixed_layout_survives_mixed_value_types_of_equal_size() -> Result<()> {
2276 let dir = tempfile::tempdir()?;
2277
2278 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 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 #[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 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 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 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_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 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_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 #[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}