#[cfg(test)]
mod tests {
use crate::sstable::{self, PointEntry, RangeTombstone, SSTable, SSTableError};
use bloomfilter::Bloom;
use std::fs;
use tempfile::TempDir;
use tracing::Level;
use tracing_subscriber::fmt::Subscriber;
fn init_tracing() {
let _ = Subscriber::builder()
.with_max_level(Level::TRACE)
.try_init();
}
fn point(key: &[u8], value: &[u8], lsn: u64, timestamp: u64) -> PointEntry {
PointEntry {
key: key.to_vec(),
value: Some(value.to_vec()),
lsn,
timestamp,
}
}
fn del(key: &[u8], lsn: u64, timestamp: u64) -> PointEntry {
PointEntry {
key: key.to_vec(),
value: None,
lsn,
timestamp,
}
}
fn rdel(start: &[u8], end: &[u8], lsn: u64, timestamp: u64) -> RangeTombstone {
RangeTombstone {
start: start.to_vec(),
end: end.to_vec(),
lsn,
timestamp,
}
}
#[test]
fn build_and_open() {
init_tracing();
let tmp = TempDir::new().unwrap();
let sstable_path = tmp.path().join("sstable_1.bin");
let point_entries = vec![
point(b"apple", b"red", 1, 100),
point(b"banana", b"yellow", 2, 101),
point(b"cherry", b"dark-red", 3, 102),
del(b"strawberry", 4, 103),
];
let range_tombstones = vec![
rdel(b"grape", b"kiwi", 5, 110),
rdel(b"orange", b"plum", 6, 120),
];
let pt_count = point_entries.len();
let rt_count = range_tombstones.len();
sstable::SstWriter::new(&sstable_path)
.build(
point_entries.into_iter(),
pt_count,
range_tombstones.into_iter(),
rt_count,
)
.expect("Failed to build SSTable");
let meta = fs::metadata(&sstable_path).unwrap();
assert!(meta.len() > 128, "SSTable should be non-trivial in size");
assert!(sstable_path.exists());
let size = fs::metadata(&sstable_path).unwrap().len();
assert!(size > 0, "SSTable file should not be empty");
let sstable = SSTable::open(&sstable_path).expect("Failed to open SSTable");
assert_eq!(sstable.header.magic, *b"SST0");
assert_eq!(sstable.header.version, 1);
let props = &sstable.properties;
assert_eq!(props.record_count, 4);
assert_eq!(props.tombstone_count, 1);
assert_eq!(props.range_tombstones_count, 2);
assert_eq!(props.min_key, b"apple");
assert_eq!(props.max_key, b"strawberry");
assert_eq!(props.min_lsn, 1);
assert_eq!(props.max_lsn, 6);
assert_eq!(props.min_timestamp, 100);
assert_eq!(props.max_timestamp, 120);
assert_eq!(sstable.range_deletes.data.len(), 2);
assert_eq!(sstable.range_deletes.data[0].start_key, b"grape");
assert_eq!(sstable.range_deletes.data[0].end_key, b"kiwi");
assert_eq!(sstable.range_deletes.data[1].start_key, b"orange");
assert_eq!(sstable.range_deletes.data[1].end_key, b"plum");
assert!(!sstable.index.is_empty());
for ent in &sstable.index {
assert!(!ent.separator_key.is_empty());
assert!(ent.handle.offset > 0);
assert!(ent.handle.size > 0);
}
let bloom_block = &sstable.bloom;
assert!(!bloom_block.data.is_empty());
let bloom = Bloom::from_slice(&bloom_block.data).expect("Bloom decode");
assert!(bloom.check(&b"apple".to_vec()));
assert!(bloom.check(&b"banana".to_vec()));
assert!(bloom.check(&b"cherry".to_vec()));
assert!(bloom.check(&b"strawberry".to_vec()));
assert_eq!(
meta.len(),
sstable.footer.total_file_size,
"File footer `total_file_size` mismatch"
);
}
#[test]
fn build_empty_fails() {
init_tracing();
let tmp = TempDir::new().unwrap();
let sstable_path = tmp.path().join("sstable_empty.bin");
let points = vec![];
let ranges = vec![];
let pt_count = points.len();
let rt_count = ranges.len();
let result = sstable::SstWriter::new(&sstable_path)
.build(points.into_iter(), pt_count, ranges.into_iter(), rt_count)
.unwrap_err();
assert!(matches!(result, SSTableError::Internal(_)));
assert!(
result
.to_string()
.contains("Empty iterators cannot build SSTable")
);
}
#[test]
fn build_range_deletes_only() {
init_tracing();
let tmp = TempDir::new().unwrap();
let sstable_path = tmp.path().join("sstable_empty.bin");
let points = vec![];
let ranges = vec![rdel(b"a", b"f", 30, 200), rdel(b"f", b"z", 31, 201)];
let pt_count = points.len();
let rt_count = ranges.len();
sstable::SstWriter::new(&sstable_path)
.build(
points.into_iter(),
pt_count,
ranges.clone().into_iter(),
rt_count,
)
.unwrap();
let sst = SSTable::open(&sstable_path).unwrap();
assert_eq!(sst.properties.record_count, 0);
assert_eq!(sst.properties.record_count, 0);
assert_eq!(sst.properties.range_tombstones_count, ranges.len() as u64);
assert!(sst.properties.min_key.is_empty());
assert!(sst.properties.max_key.is_empty());
}
#[test]
fn build_points_only() {
init_tracing();
let tmp = TempDir::new().unwrap();
let sstable_path = tmp.path().join("sstable_points_only.bin");
let points = vec![
point(b"a", b"1", 10, 100),
point(b"b", b"2", 20, 110),
point(b"c", b"3", 30, 120),
];
let ranges = vec![];
let pt_count = points.len();
let rt_count = ranges.len();
sstable::SstWriter::new(&sstable_path)
.build(
points.clone().into_iter(),
pt_count,
ranges.into_iter(),
rt_count,
)
.unwrap();
let sst = SSTable::open(&sstable_path).unwrap();
assert_eq!(sst.properties.record_count, points.len() as u64);
assert_eq!(sst.properties.range_tombstones_count, 0);
assert_eq!(sst.properties.min_key, b"a");
assert_eq!(sst.properties.max_key, b"c");
}
}