armdb 0.7.0

sharded bitcask key-value storage optimized for NVMe
Documentation
//! D11/hooks F5: нечитаемая entry во время Var migrate = Err (fail-loud),
//! а не warn+skip с durable-инкрементом версии.
#![cfg(feature = "var-collections")]

use armdb::{Config, MigrateAction, NoHook, VarMap, VarTree};
use tempfile::tempdir;

/// 16 KiB — заведомо на диске (не inline), читается блочным путём.
fn big(fill: u8) -> Vec<u8> {
    vec![fill; 16 * 1024]
}

/// Обрезать все data-файлы шардов до 8 байт, чтобы DiskLoc указывал за EOF.
/// Layout: dir/shard_XXX/NNNNNN.data
fn truncate_shard_data_files(dir: &std::path::Path) {
    fn walk(dir: &std::path::Path, found: &mut bool) {
        for entry in std::fs::read_dir(dir).unwrap() {
            let path = entry.unwrap().path();
            if path.is_dir() {
                walk(&path, found);
            } else if path.extension().is_some_and(|e| e == "data") {
                let f = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
                f.set_len(8).unwrap();
                *found = true;
            }
        }
    }
    let mut found = false;
    walk(dir, &mut found);
    assert!(found, "no data files found to truncate");
}

#[test]
fn var_tree_migrate_errors_on_unreadable_value() {
    let dir = tempdir().unwrap();
    let tree: VarTree<[u8; 8], NoHook> = VarTree::open(dir.path(), Config::test()).unwrap();
    tree.put(&1u64.to_be_bytes(), &big(0xAA)).unwrap();
    tree.flush_buffers().unwrap();
    // Truncate while the handle is live: reopen after severe truncation drops
    // entries during recovery, but the in-memory index still points past EOF.
    truncate_shard_data_files(dir.path());
    assert_eq!(tree.len(), 1, "entry must remain in index after truncate");
    let res = tree.migrate(|_, _| MigrateAction::Keep);
    assert!(
        res.is_err(),
        "migrate must fail loudly on unreadable value, got {res:?}"
    );
}

#[test]
fn var_map_migrate_errors_on_unreadable_value() {
    let dir = tempdir().unwrap();
    let map: VarMap<[u8; 8], NoHook> = VarMap::open(dir.path(), Config::test()).unwrap();
    map.put(&1u64.to_be_bytes(), &big(0xBB)).unwrap();
    map.flush_buffers().unwrap();
    truncate_shard_data_files(dir.path());
    assert_eq!(map.len(), 1, "entry must remain in index after truncate");
    let res = map.migrate(|_, _| MigrateAction::Keep);
    assert!(
        res.is_err(),
        "migrate must fail loudly on unreadable value, got {res:?}"
    );
}