#![cfg(not(target_arch = "wasm32"))]
use regolith::{Db, Options, Range, WriteBatch};
use tempfile::TempDir;
mod common;
use common::{fill_sequential, force_compaction, open, verify_sequential_keys};
#[test]
fn db_is_empty_after_open() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
assert_eq!(db.get(b"missing").unwrap(), None);
assert!(db.scan(None, None).unwrap().is_empty());
}
#[test]
fn empty_key_round_trips() {
let dir = TempDir::new().unwrap();
{
let db = open(&dir);
db.put(b"", b"root").unwrap();
assert_eq!(db.get(b"").unwrap(), Some(b"root".to_vec()));
}
{
let db = open(&dir);
assert_eq!(db.get(b"").unwrap(), Some(b"root".to_vec()));
db.delete(b"").unwrap();
assert_eq!(db.get(b"").unwrap(), None);
}
}
#[test]
fn empty_value_is_distinct_from_missing_after_reopen() {
let dir = TempDir::new().unwrap();
{
let db = open(&dir);
db.put(b"exists", b"").unwrap();
db.put(b"also", b"v").unwrap();
}
let db = open(&dir);
assert_eq!(db.get(b"exists").unwrap(), Some(vec![]));
assert_eq!(db.get(b"missing").unwrap(), None);
assert_eq!(db.get(b"also").unwrap(), Some(b"v".to_vec()));
}
#[test]
fn get_from_immutable_memtable_still_visible() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
db.put(b"pinned", b"v").unwrap();
for i in 0..200 {
let k = format!("filler_{:04}", i);
db.put(k.as_bytes(), &[0u8; 64]).unwrap();
}
assert_eq!(db.get(b"pinned").unwrap(), Some(b"v".to_vec()));
}
#[test]
fn get_level0_newer_file_shadows_older() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
db.put(b"k", b"old").unwrap();
force_compaction(&db);
db.put(b"k", b"new").unwrap();
assert_eq!(db.get(b"k").unwrap(), Some(b"new".to_vec()));
force_compaction(&db);
assert_eq!(db.get(b"k").unwrap(), Some(b"new".to_vec()));
}
#[test]
fn get_picks_correct_file_across_levels() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
fill_sequential(&db, 500);
force_compaction(&db);
for i in [0usize, 100, 250, 499] {
let k = format!("key_{:06}", i);
let v = format!("val_{:06}", i);
assert_eq!(db.get(k.as_bytes()).unwrap(), Some(v.into_bytes()));
}
assert_eq!(db.get(b"missing").unwrap(), None);
}
#[test]
fn get_encounters_empty_level_between_populated_ones() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
for i in 0..1000 {
let k = format!("k_{:06}", i);
db.put(k.as_bytes(), b"v").unwrap();
}
force_compaction(&db);
assert_eq!(db.get(b"k_000500").unwrap(), Some(b"v".to_vec()));
}
#[test]
fn snapshot_hides_later_writes() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
db.put(b"k", b"v1").unwrap();
let snap = db.snapshot();
db.put(b"k", b"v2").unwrap();
force_compaction(&db);
assert_eq!(snap.get(b"k").unwrap(), Some(b"v1".to_vec()));
assert_eq!(db.get(b"k").unwrap(), Some(b"v2".to_vec()));
}
#[test]
fn identical_snapshots_see_same_state() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
db.put(b"k", b"v1").unwrap();
let s1 = db.snapshot();
let s2 = db.snapshot();
db.put(b"k", b"v2").unwrap();
assert_eq!(s1.get(b"k").unwrap(), s2.get(b"k").unwrap());
}
#[test]
fn iter_empty_database_is_never_valid() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
let mut it = db.iter();
it.seek_to_first();
assert!(!it.valid());
it.seek_to_last();
assert!(!it.valid());
}
#[test]
fn iter_single_entry_is_valid_exactly_once() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
db.put(b"only", b"one").unwrap();
let mut it = db.iter();
it.seek_to_first();
assert!(it.valid());
assert_eq!(it.key(), Some(&b"only"[..]));
assert_eq!(it.value(), Some(&b"one"[..]));
it.next();
assert!(!it.valid());
}
#[test]
fn iter_small_and_large_values_mixed() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
db.put(b"k_small", b"s").unwrap();
db.put(b"k_large", &vec![0xAB; 100_000]).unwrap();
db.put(b"k_medium", &vec![0x42; 1024]).unwrap();
let mut it = db.iter();
it.seek_to_first();
let mut seen = 0;
while it.valid() {
seen += 1;
let v = it.value().unwrap();
match it.key().unwrap() {
b"k_small" => assert_eq!(v.len(), 1),
b"k_medium" => assert_eq!(v.len(), 1024),
b"k_large" => assert_eq!(v.len(), 100_000),
other => panic!("unexpected key {other:?}"),
}
it.next();
}
assert_eq!(seen, 3);
}
#[test]
fn iter_skips_deleted_keys_after_compaction() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
db.put(b"alive", b"1").unwrap();
db.put(b"dead", b"2").unwrap();
force_compaction(&db);
db.delete(b"dead").unwrap();
force_compaction(&db);
let mut it = db.iter();
it.seek_to_first();
let mut seen_keys = Vec::new();
while it.valid() {
seen_keys.push(it.key().unwrap().to_vec());
it.next();
}
assert_eq!(seen_keys, vec![b"alive".to_vec()]);
}
#[test]
fn iter_reverse_walks_backward() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
for c in b'a'..=b'e' {
db.put(&[c], &[c]).unwrap();
}
let mut it = db.iter();
it.seek_to_last();
let mut rev = Vec::new();
while it.valid() {
rev.push(it.key().unwrap().to_vec());
it.prev();
}
assert_eq!(
rev,
vec![
b"e".to_vec(),
b"d".to_vec(),
b"c".to_vec(),
b"b".to_vec(),
b"a".to_vec(),
]
);
}
#[test]
fn recover_with_empty_wal_does_not_crash() {
let dir = TempDir::new().unwrap();
drop(open(&dir));
let db = open(&dir);
assert!(db.scan(None, None).unwrap().is_empty());
}
#[test]
fn recover_with_large_wal_replays_every_entry() {
let dir = TempDir::new().unwrap();
{
let opts = Options {
write_buffer_size: 64 * 1024 * 1024,
..Options::default()
};
let db = Db::open(dir.path(), opts).unwrap();
for i in 0..5_000 {
let k = format!("k_{:06}", i);
let v = format!("v_{}", i);
db.put(k.as_bytes(), v.as_bytes()).unwrap();
}
}
let db = open(&dir);
for i in [0usize, 2_500, 4_999] {
let k = format!("k_{:06}", i);
let v = format!("v_{}", i);
assert_eq!(db.get(k.as_bytes()).unwrap(), Some(v.into_bytes()));
}
}
#[test]
fn recover_with_multiple_memtables_preserves_all_writes() {
let dir = TempDir::new().unwrap();
{
let db = open(&dir);
fill_sequential(&db, 500);
}
let db = open(&dir);
verify_sequential_keys(&db, 500);
}
#[test]
fn seq_number_preserved_across_reopen() {
let dir = TempDir::new().unwrap();
{
let db = open(&dir);
db.put(b"k", b"before").unwrap();
}
let db = open(&dir);
let snap = db.snapshot();
db.put(b"k", b"after").unwrap();
assert_eq!(snap.get(b"k").unwrap(), Some(b"before".to_vec()));
assert_eq!(db.get(b"k").unwrap(), Some(b"after".to_vec()));
}
#[test]
fn approximate_sizes_grows_with_range_width() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
for i in 0..1_000 {
let k = format!("k_{:06}", i);
db.put(k.as_bytes(), &[0u8; 128]).unwrap();
}
force_compaction(&db);
let narrow = db.get_approximate_sizes(&[Range::new(b"k_000000", b"k_000001")]);
let wide = db.get_approximate_sizes(&[Range::new(b"k_000000", b"k_000999")]);
assert_eq!(narrow.len(), 1);
assert_eq!(wide.len(), 1);
assert!(wide[0] > narrow[0]);
}
#[test]
fn write_batch_empty_len_counts() {
let b = WriteBatch::new();
assert_eq!(b.len(), 0);
assert_eq!(b.merge_count(), 0);
assert_eq!(b.range_delete_count(), 0);
assert!(b.is_empty());
}
#[test]
fn write_batch_put_delete_delete_range_counted_separately() {
let mut b = WriteBatch::new();
b.put(b"a", b"1");
b.put(b"b", b"2");
b.delete(b"c");
b.delete_range(b"d", b"f");
b.merge(b"g", b"m1");
b.merge(b"g", b"m2");
assert_eq!(b.len(), 3); assert_eq!(b.range_delete_count(), 1);
assert_eq!(b.merge_count(), 2);
assert!(!b.is_empty());
}
#[test]
fn write_batch_degenerate_range_delete_is_ignored() {
let mut b = WriteBatch::new();
b.delete_range(b"x", b"x");
b.delete_range(b"z", b"a");
assert_eq!(b.range_delete_count(), 0);
}
#[test]
fn write_batch_put_delete_on_same_key_keeps_last_op() {
let mut b = WriteBatch::new();
b.put(b"k", b"v");
b.delete(b"k");
assert_eq!(b.len(), 2);
let dir = TempDir::new().unwrap();
let db = open(&dir);
db.put(b"k", b"prior").unwrap();
db.write(b).unwrap();
assert_eq!(db.get(b"k").unwrap(), None);
}
#[test]
fn write_batch_apply_is_atomic_under_reopen() {
let dir = TempDir::new().unwrap();
{
let db = open(&dir);
let mut b = WriteBatch::new();
b.put(b"a", b"1");
b.put(b"b", b"2");
b.put(b"c", b"3");
db.write(b).unwrap();
}
let db = open(&dir);
let present = [b"a".as_ref(), b"b", b"c"]
.iter()
.filter(|k| db.get(k).unwrap().is_some())
.count();
assert!(present == 0 || present == 3, "partial batch: {present}/3");
}
#[test]
fn write_batch_range_delete_hides_every_key_in_range() {
let dir = TempDir::new().unwrap();
let db = open(&dir);
for k in [b"b".as_ref(), b"c", b"d", b"e"] {
db.put(k, b"v").unwrap();
}
db.put(b"a", b"keep_before").unwrap();
db.put(b"z", b"keep_after").unwrap();
let mut batch = WriteBatch::new();
batch.delete_range(b"b", b"f");
db.write(batch).unwrap();
assert_eq!(db.get(b"a").unwrap(), Some(b"keep_before".to_vec()));
assert_eq!(db.get(b"b").unwrap(), None);
assert_eq!(db.get(b"c").unwrap(), None);
assert_eq!(db.get(b"d").unwrap(), None);
assert_eq!(db.get(b"e").unwrap(), None);
assert_eq!(db.get(b"z").unwrap(), Some(b"keep_after".to_vec()));
}
#[test]
fn sequences_order_snapshots_against_commits() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), Options::default()).unwrap();
let before = db.latest_sequence();
let snap_before = db.snapshot();
assert_eq!(snap_before.sequence(), before);
let mut batch = WriteBatch::new();
batch.put(b"a", b"1");
batch.put(b"b", b"2");
let commit = db.write_sequenced(batch).unwrap();
assert!(commit > before, "a commit must advance the horizon");
assert_eq!(snap_before.sequence(), before);
assert_eq!(snap_before.get(b"a").unwrap(), None);
let snap_after = db.snapshot();
assert!(snap_after.sequence() >= commit);
assert_eq!(snap_after.get(b"a").unwrap(), Some(b"1".to_vec()));
let idle = db.write_sequenced(WriteBatch::new()).unwrap();
assert_eq!(idle, db.latest_sequence());
}
#[test]
fn concurrent_commits_report_distinct_sequences() {
use std::collections::HashSet;
use std::sync::Arc;
let dir = TempDir::new().unwrap();
let db = Arc::new(Db::open(dir.path(), Options::default()).unwrap());
let mut handles = Vec::new();
for t in 0..8u32 {
let db = Arc::clone(&db);
handles.push(std::thread::spawn(move || {
let mut seqs = Vec::new();
for i in 0..50u32 {
let mut batch = WriteBatch::new();
batch.put(format!("k{t}_{i}").as_bytes(), b"v");
seqs.push(db.write_sequenced(batch).unwrap());
}
seqs
}));
}
let all: Vec<u64> = handles
.into_iter()
.flat_map(|h| h.join().unwrap())
.collect();
let unique: HashSet<u64> = all.iter().copied().collect();
assert_eq!(
unique.len(),
all.len(),
"every commit must get its own sequence"
);
assert_eq!(db.latest_sequence(), *all.iter().max().unwrap());
}