armdb 0.1.13

sharded bitcask key-value storage optimized for NVMe
Documentation
use armdb::{FixedConfig, FixedTree};
use tempfile::tempdir;

fn test_config() -> FixedConfig {
    FixedConfig {
        shard_count: 2,
        grow_step: 64,
        ..FixedConfig::test()
    }
}

#[test]
fn test_fixed_tree_put_get() {
    let dir = tempdir().unwrap();
    let tree = FixedTree::<[u8; 8], 8>::open(dir.path(), test_config()).unwrap();

    for i in 0..100u64 {
        let key = i.to_be_bytes();
        let value = (i * 100).to_be_bytes();
        tree.put(&key, &value).unwrap();
    }

    assert_eq!(tree.len(), 100);

    for i in 0..100u64 {
        let key = i.to_be_bytes();
        let expected = (i * 100).to_be_bytes();
        let got = tree.get(&key).expect("key should exist");
        assert_eq!(got, expected, "mismatch at key {i}");
    }
}

#[test]
fn test_fixed_tree_overwrite() {
    let dir = tempdir().unwrap();
    let tree = FixedTree::<[u8; 8], 8>::open(dir.path(), test_config()).unwrap();

    let key = 1u64.to_be_bytes();
    let value_a = 100u64.to_be_bytes();
    let value_b = 200u64.to_be_bytes();

    let old = tree.put(&key, &value_a).unwrap();
    assert!(old.is_none(), "first put should return None");
    assert_eq!(tree.get(&key).unwrap(), value_a);

    let old = tree.put(&key, &value_b).unwrap();
    assert!(old.is_some(), "second put should return Some(old)");
    assert_eq!(old.unwrap(), value_a, "should return old value");
    assert_eq!(tree.get(&key).unwrap(), value_b, "should store new value");

    assert_eq!(tree.len(), 1, "overwrite should not increase count");
}

#[test]
fn test_fixed_tree_delete() {
    let dir = tempdir().unwrap();
    let tree = FixedTree::<[u8; 8], 8>::open(dir.path(), test_config()).unwrap();

    let key = 42u64.to_be_bytes();
    let value = 99u64.to_be_bytes();

    tree.put(&key, &value).unwrap();
    assert!(tree.contains(&key));

    let deleted = tree.delete(&key).unwrap();
    assert!(
        deleted.is_some(),
        "delete should return Some for existing key"
    );
    assert_eq!(deleted.unwrap(), value, "should return deleted value");

    assert!(tree.get(&key).is_none(), "key should be gone after delete");
    assert_eq!(tree.len(), 0);

    // Delete a non-existent key should return None
    let deleted_again = tree.delete(&key).unwrap();
    assert!(
        deleted_again.is_none(),
        "deleting absent key should return None"
    );
}

#[test]
fn test_fixed_tree_recovery() {
    let dir = tempdir().unwrap();
    let db_path = dir.path().join("fixed_recovery");
    let config = test_config();

    // Phase 1: write 50 keys, flush (no clean shutdown — simulates crash)
    {
        let tree = FixedTree::<[u8; 8], 32>::open(&db_path, config.clone()).unwrap();
        for i in 0..50u64 {
            let key = i.to_be_bytes();
            let mut value = [0u8; 32];
            value[..8].copy_from_slice(&(i * 1000).to_be_bytes());
            tree.put(&key, &value).unwrap();
        }
        tree.flush().unwrap();
        // Drop without close — dirty shutdown
    }

    // Phase 2: reopen — full scan recovery
    {
        let tree = FixedTree::<[u8; 8], 32>::open(&db_path, config).unwrap();
        assert_eq!(tree.len(), 50, "all 50 entries should be recovered");

        for i in 0..50u64 {
            let key = i.to_be_bytes();
            let mut expected = [0u8; 32];
            expected[..8].copy_from_slice(&(i * 1000).to_be_bytes());
            let got = tree.get(&key).expect("key should exist after recovery");
            assert_eq!(got, expected, "value mismatch at key {i} after recovery");
        }
    }
}

#[test]
fn test_fixed_tree_clean_shutdown_recovery() {
    let dir = tempdir().unwrap();
    let db_path = dir.path().join("fixed_clean");
    let config = test_config();

    // Phase 1: write 20 keys, close (clean shutdown)
    {
        let tree = FixedTree::<[u8; 8], 8>::open(&db_path, config.clone()).unwrap();
        for i in 0..20u64 {
            let key = i.to_be_bytes();
            let value = (i * 10).to_be_bytes();
            tree.put(&key, &value).unwrap();
        }
        tree.close().unwrap();
    }

    // Phase 2: reopen — clean path recovery via bitmap sidecar
    {
        let tree = FixedTree::<[u8; 8], 8>::open(&db_path, config).unwrap();
        assert_eq!(tree.len(), 20, "all 20 entries should be recovered");

        for i in 0..20u64 {
            let key = i.to_be_bytes();
            let expected = (i * 10).to_be_bytes();
            let got = tree
                .get(&key)
                .expect("key should exist after clean recovery");
            assert_eq!(got, expected, "value mismatch at key {i}");
        }
    }
}

#[test]
fn test_fixed_tree_delete_and_reuse_slot() {
    let dir = tempdir().unwrap();
    let config = FixedConfig {
        shard_count: 1,
        grow_step: 4,
        ..FixedConfig::test()
    };
    let tree = FixedTree::<[u8; 8], 8>::open(dir.path(), config).unwrap();

    // Fill initial slots (grow_step = 4, so 4 slots)
    for i in 0..4u64 {
        let key = i.to_be_bytes();
        let value = i.to_be_bytes();
        tree.put(&key, &value).unwrap();
    }

    assert_eq!(tree.len(), 4);

    // Delete one entry — frees a slot
    let key_to_delete = 2u64.to_be_bytes();
    tree.delete(&key_to_delete).unwrap();
    assert_eq!(tree.len(), 3);

    // Insert a new key — should reuse the freed slot without growing
    let new_key = 100u64.to_be_bytes();
    let new_value = 999u64.to_be_bytes();
    tree.put(&new_key, &new_value).unwrap();
    assert_eq!(tree.len(), 4);

    // Verify the new entry exists
    let got = tree.get(&new_key).expect("new key should exist");
    assert_eq!(got, new_value);

    // Verify old entries still intact
    for i in [0u64, 1, 3] {
        let key = i.to_be_bytes();
        let expected = i.to_be_bytes();
        let got = tree.get(&key).expect("original key should exist");
        assert_eq!(got, expected, "mismatch at key {i}");
    }

    // Deleted key should not exist
    assert!(tree.get(&key_to_delete).is_none());
}