btree-store 0.1.7

A persistent, embedded key-value storage engine in Rust featuring a Copy-On-Write (COW) B-Tree, ACID compliance, and crash safety with multi-bucket support
Documentation
use btree_store::{BTree, Error};
use std::fs;
use tempfile::TempDir;

#[test]
fn test_compaction_preserves_data_and_shrinks_or_holds_size() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("compact.db");

    let tree = BTree::open(&db_path).unwrap();

    let value = vec![0x42u8; 8192];
    tree.exec("default", |txn| {
        for i in 0..400u32 {
            let key = format!("k{:04}", i);
            txn.put(key.as_bytes(), &value).unwrap();
        }
        Ok(())
    })
    .unwrap();

    tree.exec("default", |txn| {
        for i in 0..200u32 {
            let key = format!("k{:04}", i);
            txn.del(key.as_bytes()).unwrap();
        }
        Ok(())
    })
    .unwrap();

    tree.exec("default", |txn| {
        txn.put(b"__promote__", b"x").unwrap();
        Ok(())
    })
    .unwrap();

    let size_before = fs::metadata(&db_path).unwrap().len();

    let stats = tree.compact(u64::MAX).unwrap();
    assert!(stats.moved_pages > 0);
    assert_eq!(stats.remaining_candidates, 0);

    tree.view("default", |txn| {
        for i in 0..200u32 {
            let key = format!("k{:04}", i);
            assert_eq!(txn.get(key.as_bytes()), Err(Error::NotFound));
        }
        for i in 200..400u32 {
            let key = format!("k{:04}", i);
            assert_eq!(txn.get(key.as_bytes()).unwrap(), value);
        }
        assert_eq!(txn.get(b"__promote__").unwrap(), b"x");
        Ok(())
    })
    .unwrap();

    let size_after = fs::metadata(&db_path).unwrap().len();
    assert!(size_after <= size_before);
}

#[test]
fn test_compaction_moves_tail_pages() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_compaction_shrinks_file.db");

    let tree = BTree::open(&db_path).expect("open btree failed");

    let base_value = vec![0x11; 256];
    tree.exec("base", |txn| {
        for i in 0..1000u32 {
            let key = format!("b{:05}", i);
            txn.put(key.as_bytes(), &base_value)?;
        }
        Ok(())
    })
    .expect("commit base failed");

    tree.exec("base", |txn| {
        for i in 0..200u32 {
            let key = format!("b{:05}", i);
            txn.del(key.as_bytes())?;
        }
        Ok(())
    })
    .expect("delete base failed");

    let tail_value = vec![0x22; 16 * 1024];
    tree.exec("tail", |txn| {
        for _ in 0..40u32 {
            txn.put(b"hot", &tail_value)?;
        }
        Ok(())
    })
    .expect("commit tail failed");

    tree.exec("base", |txn| {
        txn.put(b"b00999", &base_value)?;
        Ok(())
    })
    .expect("promote freelist failed");

    let stats = tree.compact(u64::MAX).expect("compact failed");
    assert!(stats.moved_pages > 0);
    assert_eq!(stats.remaining_candidates, 0);

    tree.view("base", |txn| {
        let key = b"b00200";
        let got = txn.get(key).expect("missing key after compact");
        assert_eq!(got.len(), base_value.len());

        let key = b"b00999";
        let got = txn.get(key).expect("missing key after compact");
        assert_eq!(got.len(), base_value.len());
        Ok(())
    })
    .expect("verify after compact failed");
}

#[test]
fn test_compaction_relocates_tail_and_shrinks_file() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_compaction_relocates_tail.db");

    let tree = BTree::open(&db_path).expect("open btree failed");

    let base_value = vec![0x33u8; 8192];
    tree.exec("base", |txn| {
        for i in 0..3000u32 {
            let key = format!("k{:05}", i);
            txn.put(key.as_bytes(), &base_value)?;
        }
        Ok(())
    })
    .expect("commit base failed");

    tree.exec("base", |txn| {
        for i in 0..2000u32 {
            let key = format!("k{:05}", i);
            txn.del(key.as_bytes())?;
        }
        Ok(())
    })
    .expect("delete base failed");

    tree.exec("base", |txn| {
        txn.put(b"__promote__", b"x")?;
        Ok(())
    })
    .expect("promote freelist failed");

    let tail_value = vec![0x55u8; 16 * 1024];
    tree.exec("tail", |txn| {
        for i in 0..200u32 {
            let key = format!("t{:05}", i);
            txn.put(key.as_bytes(), &tail_value)?;
        }
        Ok(())
    })
    .expect("commit tail failed");

    let size_before = fs::metadata(&db_path).unwrap().len();

    let stats = tree.compact(u64::MAX).expect("compact failed");
    assert!(stats.moved_pages > 0);

    let size_after = fs::metadata(&db_path).unwrap().len();
    assert!(size_after < size_before);

    tree.view("base", |txn| {
        assert_eq!(txn.get(b"k00000"), Err(Error::NotFound));
        assert_eq!(txn.get(b"k01999"), Err(Error::NotFound));
        assert_eq!(txn.get(b"k02000").unwrap(), base_value);
        assert_eq!(txn.get(b"k02999").unwrap(), base_value);
        Ok(())
    })
    .expect("verify base after compact failed");

    tree.view("tail", |txn| {
        assert_eq!(txn.get(b"t00000").unwrap(), tail_value);
        assert_eq!(txn.get(b"t00199").unwrap(), tail_value);
        Ok(())
    })
    .expect("verify tail after compact failed");
}

#[test]
fn test_default_compaction_skips_when_low_address_budget_is_insufficient() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_compaction_default_guard.db");

    let tree = BTree::open(&db_path).expect("open btree failed");
    let value = vec![0xABu8; 8 * 1024];

    tree.exec("default", |txn| {
        for i in 0..1500u32 {
            let key = format!("k{:05}", i);
            txn.put(key.as_bytes(), &value)?;
        }
        Ok(())
    })
    .expect("commit failed");

    let size_before = fs::metadata(&db_path).unwrap().len();
    let stats = tree.compact(0).expect("compact failed");
    let size_after = fs::metadata(&db_path).unwrap().len();

    assert_eq!(stats.moved_pages, 0);
    assert!(stats.remaining_candidates > 0);
    assert_eq!(size_after, size_before);
}