candystore 1.2.2

A lean, efficient and fast persistent in-process key-value store
Documentation
mod common;

use std::fs;

use candystore::{CandyStore, Config, Error};
use tempfile::tempdir;

#[test]
fn test_clear_resets_byte_checkpoint_trigger() -> Result<(), Error> {
    use std::time::{Duration, Instant};
    let dir = tempdir().unwrap();
    let config = Config {
        max_data_file_size: 1024 * 1024,
        checkpoint_interval: None,
        checkpoint_delta_bytes: Some(512),
        compaction_throughput_bytes_per_sec: 0,
        ..Config::default()
    };
    let db = CandyStore::open(dir.path(), config)?;
    db.set("old", vec![1; 32 * 1024])?;
    db.checkpoint()?;
    db.clear()?;
    assert_eq!(db.stats().checkpoint_generation, 0);
    db.set("fresh", vec![2; 1024])?;
    let deadline = Instant::now() + Duration::from_secs(5);
    while db.stats().checkpoint_generation == 0 && Instant::now() < deadline {
        std::thread::yield_now();
    }
    assert!(db.stats().checkpoint_generation > 0);
    assert_eq!(db.stats().uncheckpointed_bytes, 0);
    db._abort_for_testing();
    let db = CandyStore::open(dir.path(), config)?;
    assert_eq!(db.get("fresh")?, Some(vec![2; 1024]));
    assert_eq!(db.get("old")?, None);
    Ok(())
}

#[test]
fn test_clear_resets_store_files_and_contents() -> Result<(), Error> {
    let dir = tempdir().unwrap();
    let config = Config {
        max_data_file_size: 1024,
        ..Config::default()
    };

    let db = CandyStore::open(dir.path(), config)?;

    for i in 0..100 {
        db.set(format!("key{i:04}"), vec![b'x'; 64])?;
    }
    for i in 0..50 {
        db.set(format!("key{i:04}"), vec![b'y'; 64])?;
    }
    for i in 50..75 {
        db.remove(format!("key{i:04}"))?;
    }

    let data_files_before = std::fs::read_dir(dir.path())
        .map_err(Error::IOError)?
        .filter_map(|entry| entry.ok())
        .filter(|entry| {
            entry
                .file_name()
                .to_str()
                .is_some_and(|name| name.starts_with("data_"))
        })
        .count();
    assert!(data_files_before > 1);

    fs::write(dir.path().join("extra.txt"), b"junk").map_err(Error::IOError)?;
    fs::create_dir(dir.path().join("extra_dir")).map_err(Error::IOError)?;
    fs::write(dir.path().join("extra_dir").join("nested.txt"), b"junk").map_err(Error::IOError)?;

    db.clear()?;

    assert!(db.get("key0000")?.is_none());
    assert_eq!(db.iter_items().count(), 0);
    assert!(!dir.path().join("extra.txt").exists());
    assert!(!dir.path().join("extra_dir").exists());

    let data_files_after = std::fs::read_dir(dir.path())
        .map_err(Error::IOError)?
        .filter_map(|entry| entry.ok())
        .filter(|entry| {
            entry
                .file_name()
                .to_str()
                .is_some_and(|name| name.starts_with("data_"))
        })
        .count();
    assert_eq!(data_files_after, 1);

    db.set("fresh", "value")?;
    assert_eq!(db.get("fresh")?, Some(b"value".to_vec()));
    drop(db);

    let reopened = CandyStore::open(dir.path(), config)?;
    assert!(reopened.get("key0000")?.is_none());
    assert_eq!(reopened.get("fresh")?, Some(b"value".to_vec()));

    Ok(())
}

#[test]
fn test_explicit_close_releases_lock_and_persists_clean_shutdown() -> Result<(), Error> {
    let dir = tempdir().unwrap();
    let config = Config::default();

    let db = CandyStore::open(dir.path(), config)?;
    db.set("key", "value")?;
    drop(db);

    let reopened = CandyStore::open(dir.path(), config)?;
    assert_eq!(reopened.get("key")?, Some(b"value".to_vec()));

    Ok(())
}