use aeternusdb::{Db, DbConfig, DbError};
use std::sync::Arc;
use std::thread;
use tempfile::TempDir;
fn small_buffer_config() -> DbConfig {
DbConfig {
write_buffer_size: 1024,
min_compaction_threshold: 4,
max_compaction_threshold: 32,
tombstone_compaction_ratio: 0.3,
thread_pool_size: 2,
..DbConfig::default()
}
}
fn reopen(path: &std::path::Path) -> Db {
Db::open(path, DbConfig::default()).expect("reopen")
}
#[test]
fn open_close_empty() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
db.close().unwrap();
}
#[test]
fn close_is_idempotent() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
db.close().unwrap();
db.close().unwrap(); }
#[test]
fn drop_without_close() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
db.put(b"key", b"value").unwrap();
drop(db);
let db = reopen(dir.path());
assert_eq!(db.get(b"key").unwrap(), Some(b"value".to_vec()));
db.close().unwrap();
}
#[test]
fn put_get_single() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
db.put(b"hello", b"world").unwrap();
assert_eq!(db.get(b"hello").unwrap(), Some(b"world".to_vec()));
db.close().unwrap();
}
#[test]
fn put_overwrite() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
db.put(b"key", b"v1").unwrap();
db.put(b"key", b"v2").unwrap();
assert_eq!(db.get(b"key").unwrap(), Some(b"v2".to_vec()));
db.close().unwrap();
}
#[test]
fn delete_key() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
db.put(b"key", b"value").unwrap();
assert_eq!(db.get(b"key").unwrap(), Some(b"value".to_vec()));
db.delete(b"key").unwrap();
assert_eq!(db.get(b"key").unwrap(), None);
db.close().unwrap();
}
#[test]
fn delete_range_basic() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
for c in b'a'..=b'e' {
db.put(&[c], &[c]).unwrap();
}
db.delete_range(b"b", b"d").unwrap();
assert_eq!(db.get(b"a").unwrap(), Some(vec![b'a']));
assert_eq!(db.get(b"b").unwrap(), None);
assert_eq!(db.get(b"c").unwrap(), None);
assert_eq!(db.get(b"d").unwrap(), Some(vec![b'd']));
assert_eq!(db.get(b"e").unwrap(), Some(vec![b'e']));
db.close().unwrap();
}
#[test]
fn get_nonexistent_key() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
assert_eq!(db.get(b"missing").unwrap(), None);
db.close().unwrap();
}
#[test]
fn scan_basic() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
db.put(b"a", b"1").unwrap();
db.put(b"b", b"2").unwrap();
db.put(b"c", b"3").unwrap();
db.put(b"d", b"4").unwrap();
let results = db.scan(b"b", b"d").unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0], (b"b".to_vec(), b"2".to_vec()));
assert_eq!(results[1], (b"c".to_vec(), b"3".to_vec()));
db.close().unwrap();
}
#[test]
fn scan_empty_range() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
db.put(b"a", b"1").unwrap();
let results = db.scan(b"z", b"a").unwrap();
assert!(results.is_empty());
let results = db.scan(b"x", b"z").unwrap();
assert!(results.is_empty());
db.close().unwrap();
}
#[test]
fn scan_excludes_deleted_keys() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
db.put(b"a", b"1").unwrap();
db.put(b"b", b"2").unwrap();
db.put(b"c", b"3").unwrap();
db.delete(b"b").unwrap();
let results = db.scan(b"a", b"d").unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].0, b"a".to_vec());
assert_eq!(results[1].0, b"c".to_vec());
db.close().unwrap();
}
#[test]
fn persistence_across_reopen() {
let dir = TempDir::new().unwrap();
{
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
db.put(b"persist_key", b"persist_value").unwrap();
db.close().unwrap();
}
{
let db = reopen(dir.path());
assert_eq!(
db.get(b"persist_key").unwrap(),
Some(b"persist_value".to_vec())
);
db.close().unwrap();
}
}
#[test]
fn persistence_many_writes() {
let dir = TempDir::new().unwrap();
{
let db = Db::open(dir.path(), small_buffer_config()).unwrap();
for i in 0..500u32 {
let key = format!("key_{:04}", i);
let val = format!("val_{:04}", i);
db.put(key.as_bytes(), val.as_bytes()).unwrap();
}
db.close().unwrap();
}
{
let db = Db::open(dir.path(), small_buffer_config()).unwrap();
for i in 0..500u32 {
let key = format!("key_{:04}", i);
let val = format!("val_{:04}", i);
assert_eq!(
db.get(key.as_bytes()).unwrap(),
Some(val.into_bytes()),
"key_{:04} should be present after reopen",
i
);
}
db.close().unwrap();
}
}
#[test]
fn persistence_deletes_survive_reopen() {
let dir = TempDir::new().unwrap();
{
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
db.put(b"alive", b"yes").unwrap();
db.put(b"dead", b"soon").unwrap();
db.delete(b"dead").unwrap();
db.close().unwrap();
}
{
let db = reopen(dir.path());
assert_eq!(db.get(b"alive").unwrap(), Some(b"yes".to_vec()));
assert_eq!(db.get(b"dead").unwrap(), None);
db.close().unwrap();
}
}
#[test]
fn major_compaction() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), small_buffer_config()).unwrap();
for i in 0..200u32 {
let key = format!("mc_{:04}", i);
let val = format!("val_{:04}", i);
db.put(key.as_bytes(), val.as_bytes()).unwrap();
}
db.close().unwrap();
let db = Db::open(dir.path(), small_buffer_config()).unwrap();
let compacted = db.major_compact().unwrap();
assert!(compacted, "should have compacted multiple SSTables");
for i in 0..200u32 {
let key = format!("mc_{:04}", i);
let val = format!("val_{:04}", i);
assert_eq!(
db.get(key.as_bytes()).unwrap(),
Some(val.into_bytes()),
"mc_{:04} should survive major compaction",
i
);
}
db.close().unwrap();
}
#[test]
fn major_compaction_removes_deleted_keys() {
let dir = TempDir::new().unwrap();
{
let db = Db::open(dir.path(), small_buffer_config()).unwrap();
for i in 0..100u32 {
let key = format!("del_{:04}", i);
let val = format!("val_{:04}", i);
db.put(key.as_bytes(), val.as_bytes()).unwrap();
}
for i in (0..100u32).step_by(2) {
let key = format!("del_{:04}", i);
db.delete(key.as_bytes()).unwrap();
}
db.close().unwrap();
}
{
let db = Db::open(dir.path(), small_buffer_config()).unwrap();
db.major_compact().unwrap();
for i in 0..100u32 {
let key = format!("del_{:04}", i);
if i % 2 == 0 {
assert_eq!(db.get(key.as_bytes()).unwrap(), None);
} else {
let val = format!("val_{:04}", i);
assert_eq!(db.get(key.as_bytes()).unwrap(), Some(val.into_bytes()));
}
}
db.close().unwrap();
}
}
#[test]
fn config_write_buffer_too_small() {
let dir = TempDir::new().unwrap();
let config = DbConfig {
write_buffer_size: 100,
..DbConfig::default()
};
let err = Db::open(dir.path(), config).unwrap_err();
assert!(matches!(err, DbError::InvalidConfig(_)));
}
#[test]
fn config_min_threshold_too_small() {
let dir = TempDir::new().unwrap();
let config = DbConfig {
min_compaction_threshold: 1,
..DbConfig::default()
};
let err = Db::open(dir.path(), config).unwrap_err();
assert!(matches!(err, DbError::InvalidConfig(_)));
}
#[test]
fn config_max_below_min() {
let dir = TempDir::new().unwrap();
let config = DbConfig {
min_compaction_threshold: 8,
max_compaction_threshold: 4,
..DbConfig::default()
};
let err = Db::open(dir.path(), config).unwrap_err();
assert!(matches!(err, DbError::InvalidConfig(_)));
}
#[test]
fn config_tombstone_ratio_out_of_range() {
let dir = TempDir::new().unwrap();
let config = DbConfig {
tombstone_compaction_ratio: 0.0,
..DbConfig::default()
};
assert!(matches!(
Db::open(dir.path(), config).unwrap_err(),
DbError::InvalidConfig(_)
));
let config = DbConfig {
tombstone_compaction_ratio: 1.5,
..DbConfig::default()
};
assert!(matches!(
Db::open(dir.path(), config).unwrap_err(),
DbError::InvalidConfig(_)
));
}
#[test]
fn config_zero_threads() {
let dir = TempDir::new().unwrap();
let config = DbConfig {
thread_pool_size: 0,
..DbConfig::default()
};
let err = Db::open(dir.path(), config).unwrap_err();
assert!(matches!(err, DbError::InvalidConfig(_)));
}
#[test]
fn operations_after_close() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
db.close().unwrap();
assert!(matches!(db.put(b"k", b"v"), Err(DbError::Closed)));
assert!(matches!(db.get(b"k"), Err(DbError::Closed)));
assert!(matches!(db.delete(b"k"), Err(DbError::Closed)));
assert!(matches!(db.delete_range(b"a", b"z"), Err(DbError::Closed)));
assert!(matches!(db.scan(b"a", b"z"), Err(DbError::Closed)));
assert!(matches!(db.major_compact(), Err(DbError::Closed)));
}
#[test]
fn empty_key_rejected() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
assert!(matches!(
db.put(b"", b"v"),
Err(DbError::InvalidArgument(_))
));
assert!(matches!(
db.put(b"k", b""),
Err(DbError::InvalidArgument(_))
));
assert!(matches!(db.get(b""), Err(DbError::InvalidArgument(_))));
assert!(matches!(db.delete(b""), Err(DbError::InvalidArgument(_))));
assert!(matches!(
db.scan(b"", b"z"),
Err(DbError::InvalidArgument(_))
));
assert!(matches!(
db.scan(b"a", b""),
Err(DbError::InvalidArgument(_))
));
db.close().unwrap();
}
#[test]
fn delete_range_invalid_args() {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), DbConfig::default()).unwrap();
assert!(matches!(
db.delete_range(b"z", b"a"),
Err(DbError::InvalidArgument(_))
));
assert!(matches!(
db.delete_range(b"x", b"x"),
Err(DbError::InvalidArgument(_))
));
db.close().unwrap();
}
#[test]
fn concurrent_writes_and_reads() {
let dir = TempDir::new().unwrap();
let db = Arc::new(Db::open(dir.path(), DbConfig::default()).unwrap());
let mut handles = vec![];
for t in 0..4u32 {
let db = Arc::clone(&db);
handles.push(thread::spawn(move || {
for i in 0..100u32 {
let key = format!("t{}_k{:04}", t, i);
let val = format!("t{}_v{:04}", t, i);
db.put(key.as_bytes(), val.as_bytes()).unwrap();
}
}));
}
for h in handles {
h.join().unwrap();
}
for t in 0..4u32 {
for i in 0..100u32 {
let key = format!("t{}_k{:04}", t, i);
let val = format!("t{}_v{:04}", t, i);
assert_eq!(
db.get(key.as_bytes()).unwrap(),
Some(val.into_bytes()),
"missing: {key}"
);
}
}
db.close().unwrap();
}
#[test]
fn concurrent_reads_during_writes() {
let dir = TempDir::new().unwrap();
let db = Arc::new(Db::open(dir.path(), DbConfig::default()).unwrap());
for i in 0..50u32 {
let key = format!("pre_{:04}", i);
let val = format!("val_{:04}", i);
db.put(key.as_bytes(), val.as_bytes()).unwrap();
}
let mut handles = vec![];
{
let db = Arc::clone(&db);
handles.push(thread::spawn(move || {
for i in 50..150u32 {
let key = format!("pre_{:04}", i);
let val = format!("val_{:04}", i);
db.put(key.as_bytes(), val.as_bytes()).unwrap();
}
}));
}
for _ in 0..3 {
let db = Arc::clone(&db);
handles.push(thread::spawn(move || {
for i in 0..50u32 {
let key = format!("pre_{:04}", i);
let val = format!("val_{:04}", i);
assert_eq!(
db.get(key.as_bytes()).unwrap(),
Some(val.into_bytes()),
"reader couldn't find {key}"
);
}
}));
}
for h in handles {
h.join().unwrap();
}
db.close().unwrap();
}
#[test]
fn full_lifecycle_with_compaction() {
let dir = TempDir::new().unwrap();
{
let db = Db::open(dir.path(), small_buffer_config()).unwrap();
for i in 0..300u32 {
let key = format!("life_{:04}", i);
let val = format!("val_{:04}", i);
db.put(key.as_bytes(), val.as_bytes()).unwrap();
}
for i in (0..300u32).step_by(2) {
let key = format!("life_{:04}", i);
db.delete(key.as_bytes()).unwrap();
}
db.delete_range(b"life_0200", b"life_0250").unwrap();
db.close().unwrap();
}
{
let db = Db::open(dir.path(), small_buffer_config()).unwrap();
db.major_compact().unwrap();
for i in 0..300u32 {
let key = format!("life_{:04}", i);
let result = db.get(key.as_bytes()).unwrap();
if i % 2 == 0 {
assert_eq!(result, None, "{key} should be deleted (even)");
} else if (200..250).contains(&i) {
assert_eq!(result, None, "{key} should be range-deleted");
} else {
let val = format!("val_{:04}", i);
assert_eq!(result, Some(val.into_bytes()), "{key} should exist");
}
}
let scan = db.scan(b"life_0000", b"life_9999").unwrap();
let expected_count = 150 - 25;
assert_eq!(
scan.len(),
expected_count,
"scan should return {expected_count} surviving keys"
);
db.close().unwrap();
}
}