keyvaluedb-sqlite 0.2.1

A key-value SQLite database that implements the `KeyValueDB` trait
Documentation
//! Open-time corruption repair.

use keyvaluedb::KeyValueDB;
use keyvaluedb_sqlite::{Database, DatabaseConfig};

fn cfg(repair: bool) -> DatabaseConfig {
    DatabaseConfig::new()
        .with_columns(2)
        .with_repair_on_corrupt(repair)
}

async fn fill(db: &Database, col: u32, n: u32) {
    let mut tx = db.transaction();
    for i in 0..n {
        tx.put(
            col,
            format!("key_{i:04}").as_bytes(),
            format!("value_{i}").as_bytes(),
        );
    }
    db.write(tx).await.unwrap();
}

#[tokio::test]
async fn clean_open_has_no_repair_report() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("db");
    let db = Database::open(&path, cfg(false)).unwrap();
    fill(&db, 0, 10).await;
    drop(db);

    let db = Database::open(&path, cfg(true)).unwrap();
    assert!(db.repair_report().is_none());
    assert_eq!(
        db.get(0, b"key_0003").await.unwrap().unwrap(),
        b"value_3".to_vec()
    );
}

#[tokio::test]
async fn repair_salvages_readable_rows() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("db");
    let db = Database::open(&path, cfg(false)).unwrap();
    fill(&db, 0, 500).await;
    fill(&db, 1, 100).await;
    drop(db);

    // Trash one b-tree page in the middle of the file
    let mut bytes = std::fs::read(&path).unwrap();
    let page_size = u16::from_be_bytes([bytes[16], bytes[17]]) as usize;
    let target = (bytes.len() / page_size) / 2;
    let start = target * page_size;
    bytes[start..start + page_size].fill(0xFF);
    std::fs::write(&path, bytes).unwrap();

    let db = Database::open(&path, cfg(true)).unwrap();
    let report = db.repair_report().expect("repair should have run").clone();
    assert!(report.rows_recovered > 0, "report: {report:?}");
    assert!(report.corrupt_path.exists());

    // Most rows survive; the trashed page's rows are the acceptable loss
    let mut survivors = 0;
    for i in 0..500u32 {
        if db
            .get(0, format!("key_{i:04}").as_bytes())
            .await
            .unwrap()
            .is_some()
        {
            survivors += 1;
        }
    }
    assert!(survivors > 300, "survivors: {survivors}");

    // The repaired database accepts writes and reopens clean
    fill(&db, 0, 10).await;
    drop(db);
    let db = Database::open(&path, cfg(true)).unwrap();
    assert!(db.repair_report().is_none());
}

/// Run by hand against a copy of a real damaged store:
/// `KVDB_REPAIR_CORPSE=/path/to/copy cargo test -p keyvaluedb-sqlite --test repair -- --ignored`
#[tokio::test]
#[ignore = "needs KVDB_REPAIR_CORPSE pointing at a damaged database copy"]
async fn repair_real_corpse() {
    let src = std::env::var("KVDB_REPAIR_CORPSE").expect("set KVDB_REPAIR_CORPSE");
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("db");
    std::fs::copy(&src, &path).unwrap();

    let db = Database::open(
        &path,
        DatabaseConfig::new()
            .with_columns(1)
            .with_repair_on_corrupt(true),
    )
    .unwrap();
    let report = db.repair_report().expect("corpse should trigger repair");
    eprintln!("repair report: {report:?}");
    assert!(report.rows_recovered > 0);
}