armdb 0.1.10

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

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

#[test]
fn test_fixed_map_put_get() {
    let dir = tempdir().unwrap();
    let map = FixedMap::<[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();
        map.put(&key, &value).unwrap();
    }

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

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

#[test]
fn test_fixed_map_overwrite() {
    let dir = tempdir().unwrap();
    let map = FixedMap::<[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 = map.put(&key, &value_a).unwrap();
    assert!(old.is_none(), "first put should return None");
    assert_eq!(map.get(&key).unwrap(), value_a);

    let old = map.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!(map.get(&key).unwrap(), value_b, "should store new value");

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

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

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

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

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

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

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

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

    // Phase 1: write 50 keys, flush (no clean shutdown -- simulates crash)
    {
        let map = FixedMap::<[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());
            map.put(&key, &value).unwrap();
        }
        map.flush().unwrap();
        // Drop without close -- dirty shutdown
    }

    // Phase 2: reopen -- full scan recovery
    {
        let map = FixedMap::<[u8; 8], 32>::open(&db_path, config).unwrap();
        assert_eq!(map.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 = map.get(&key).expect("key should exist after recovery");
            assert_eq!(got, expected, "value mismatch at key {i} after recovery");
        }
    }
}