#[cfg(test)]
mod tests {
use crate::sstable::{self, GetResult, PointEntry, RangeTombstone, Record, SSTable};
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: Option<&[u8]>, lsn: u64, timestamp: u64) -> PointEntry {
PointEntry {
key: key.to_vec(),
value: value.map(|v| v.to_vec()),
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 single_entry_sstable_get_and_scan() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("single_entry.sst");
let points = vec![point(b"only_key", Some(b"only_val"), 1, 100)];
let ranges: Vec<RangeTombstone> = vec![];
sstable::SstWriter::new(&path)
.build(points.into_iter(), 1, ranges.into_iter(), 0)
.unwrap();
let sst = SSTable::open(&path).unwrap();
assert_eq!(sst.properties.min_key, b"only_key");
assert_eq!(sst.properties.max_key, b"only_key");
assert_eq!(sst.index.len(), 1, "Single data block expected");
match sst.get(b"only_key").unwrap() {
GetResult::Put { value, lsn, .. } => {
assert_eq!(value, b"only_val");
assert_eq!(lsn, 1);
}
other => panic!("Expected Put, got {:?}", other),
}
assert_eq!(sst.get(b"other").unwrap(), GetResult::NotFound);
let records: Vec<Record> = sst.scan(b"\x00", b"\xff").unwrap().collect();
assert_eq!(records.len(), 1);
match &records[0] {
Record::Put {
key, value, lsn, ..
} => {
assert_eq!(key, b"only_key");
assert_eq!(value, b"only_val");
assert_eq!(*lsn, 1);
}
other => panic!("Expected Put, got {:?}", other),
}
}
#[test]
fn all_point_tombstones_sstable_get_and_scan() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("tombstones_only.sst");
let points = vec![
point(b"del_a", None, 1, 100),
point(b"del_b", None, 2, 101),
point(b"del_c", None, 3, 102),
];
let ranges: Vec<RangeTombstone> = vec![];
sstable::SstWriter::new(&path)
.build(points.into_iter(), 3, ranges.into_iter(), 0)
.unwrap();
let sst = SSTable::open(&path).unwrap();
for key in [b"del_a", b"del_b", b"del_c"] {
match sst.get(key).unwrap() {
GetResult::Delete { lsn, .. } => {
assert!(lsn > 0);
}
other => panic!("Expected Delete for {:?}, got {:?}", key, other),
}
}
assert_eq!(sst.get(b"del_z").unwrap(), GetResult::NotFound);
let records: Vec<Record> = sst.scan(b"del_", b"del_\xff").unwrap().collect();
assert_eq!(records.len(), 3);
for rec in &records {
match rec {
Record::Delete { .. } => {}
other => panic!("Expected Delete, got {:?}", other),
}
}
}
#[test]
fn range_tombstones_only_sstable() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("range_only.sst");
let points: Vec<PointEntry> = vec![];
let ranges = vec![
rdel(b"aaa", b"bbb", 10, 1000),
rdel(b"ccc", b"ddd", 11, 1001),
];
sstable::SstWriter::new(&path)
.build(points.into_iter(), 0, ranges.into_iter(), 2)
.unwrap();
let sst = SSTable::open(&path).unwrap();
match sst.get(b"abc").unwrap() {
GetResult::RangeDelete { lsn, .. } => assert_eq!(lsn, 10),
other => panic!("Expected RangeDelete for 'abc', got {:?}", other),
}
match sst.get(b"ccd").unwrap() {
GetResult::RangeDelete { lsn, .. } => assert_eq!(lsn, 11),
other => panic!("Expected RangeDelete for 'ccd', got {:?}", other),
}
assert_eq!(sst.get(b"zzz").unwrap(), GetResult::NotFound);
let records: Vec<Record> = sst.scan(b"\x00", b"\xff").unwrap().collect();
let range_deletes: Vec<_> = records
.iter()
.filter(|r| matches!(r, Record::RangeDelete { .. }))
.collect();
assert_eq!(
range_deletes.len(),
2,
"Should have 2 range tombstone records"
);
}
#[test]
fn minimal_mixed_sstable() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("minimal_mixed.sst");
let points = vec![
point(b"alive", Some(b"value"), 1, 100),
point(b"dead", None, 2, 101),
];
let ranges = vec![rdel(b"range_a", b"range_z", 3, 102)];
sstable::SstWriter::new(&path)
.build(points.into_iter(), 2, ranges.into_iter(), 1)
.unwrap();
let sst = SSTable::open(&path).unwrap();
match sst.get(b"alive").unwrap() {
GetResult::Put { value, .. } => assert_eq!(value, b"value"),
other => panic!("Expected Put, got {:?}", other),
}
match sst.get(b"dead").unwrap() {
GetResult::Delete { .. } => {}
other => panic!("Expected Delete, got {:?}", other),
}
match sst.get(b"range_m").unwrap() {
GetResult::RangeDelete { .. } => {}
other => panic!("Expected RangeDelete, got {:?}", other),
}
assert_eq!(sst.get(b"other").unwrap(), GetResult::NotFound);
}
#[test]
fn duplicate_keys_highest_lsn_wins() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("dup_keys.sst");
let points = vec![
point(b"key", Some(b"old"), 1, 100),
point(b"key", Some(b"mid"), 5, 500),
point(b"key", Some(b"new"), 10, 1000),
];
let ranges: Vec<RangeTombstone> = vec![];
sstable::SstWriter::new(&path)
.build(points.into_iter(), 3, ranges.into_iter(), 0)
.unwrap();
let sst = SSTable::open(&path).unwrap();
match sst.get(b"key").unwrap() {
GetResult::Put { value, lsn, .. } => {
assert_eq!(value, b"new");
assert_eq!(lsn, 10);
}
other => panic!("Expected Put with lsn=10, got {:?}", other),
}
}
}