candystore 1.0.0

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

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

#[test]
fn test_many_inserts_trigger_splits() -> Result<(), Error> {
    const KEYS: usize = 5000;

    let dir = tempdir().unwrap();
    let db = CandyStore::open(dir.path(), Config::default())?;

    for key_idx in 0..KEYS {
        let key = format!("split_key_{key_idx:05}");
        let value = format!("split_val_{key_idx:05}");
        assert!(matches!(db.set(&key, &value)?, SetStatus::CreatedNew));
    }

    for key_idx in 0..KEYS {
        let key = format!("split_key_{key_idx:05}");
        let value = format!("split_val_{key_idx:05}");
        assert_eq!(db.get(&key)?, Some(value.into()));
    }

    Ok(())
}

#[test]
fn test_rotation_preserves_reads() -> Result<(), Error> {
    let dir = tempdir().unwrap();
    let db = CandyStore::open(dir.path(), common::small_file_config())?;

    for key_idx in 0..512 {
        let key = format!("rotate_key_{key_idx:04}");
        let value = format!("rotate_val_{key_idx:04}_{}", "x".repeat(64));
        assert!(matches!(db.set(&key, &value)?, SetStatus::CreatedNew));
    }

    for key_idx in 0..512 {
        let key = format!("rotate_key_{key_idx:04}");
        assert!(db.get(&key)?.is_some(), "missing key after rotation: {key}");
    }

    Ok(())
}

#[test]
fn test_new_data_files_are_preallocated() -> Result<(), Error> {
    let dir = tempdir().unwrap();
    let config = common::small_file_config();
    let db = CandyStore::open(dir.path(), config)?;

    let file_len = std::fs::metadata(dir.path().join("data_0000"))
        .map_err(Error::IOError)?
        .len();
    assert_eq!(file_len, 4096 + config.max_data_file_size as u64);

    db.set("prealloc", [7u8; 128])?;
    drop(db);

    let reopened_len = std::fs::metadata(dir.path().join("data_0000"))
        .map_err(Error::IOError)?
        .len();
    assert_eq!(reopened_len, 4096 + config.max_data_file_size as u64);

    Ok(())
}

#[test]
fn test_splits_and_rotation_with_small_files() -> Result<(), Error> {
    const KEYS: usize = 5000;

    let dir = tempdir().unwrap();
    let db = CandyStore::open(dir.path(), common::small_file_config())?;

    for key_idx in 0..KEYS {
        let key = format!("split_rotate_key_{key_idx:05}");
        let value = format!("split_rotate_val_{key_idx:05}_{}", "x".repeat(48));
        assert!(matches!(db.set(&key, &value)?, SetStatus::CreatedNew));
    }

    for key_idx in 0..KEYS {
        let key = format!("split_rotate_key_{key_idx:05}");
        let value = format!("split_rotate_val_{key_idx:05}_{}", "x".repeat(48));
        assert_eq!(db.get(&key)?, Some(value.into()));
    }

    let data_file_count = 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_file_count > 1,
        "expected rotation to create multiple data files"
    );

    let index_rows_size = std::fs::metadata(dir.path().join("rows"))
        .map_err(Error::IOError)?
        .len();
    assert!(
        index_rows_size > 4 * 4096,
        "expected index rows growth after row splitting, got rows size {index_rows_size}"
    );

    Ok(())
}

#[test]
fn test_splits_rotation_and_reopen_with_small_files() -> Result<(), Error> {
    const KEYS: usize = 5000;

    let dir = tempdir().unwrap();

    {
        let db = CandyStore::open(dir.path(), common::small_file_config())?;
        for key_idx in 0..KEYS {
            let key = format!("reopen_split_rotate_key_{key_idx:05}");
            let value = format!("reopen_split_rotate_val_{key_idx:05}_{}", "x".repeat(48));
            assert!(matches!(db.set(&key, &value)?, SetStatus::CreatedNew));
        }
    }

    let db = CandyStore::open(dir.path(), common::small_file_config())?;
    for key_idx in 0..KEYS {
        let key = format!("reopen_split_rotate_key_{key_idx:05}");
        let value = format!("reopen_split_rotate_val_{key_idx:05}_{}", "x".repeat(48));
        assert_eq!(db.get(&key)?, Some(value.into()));
    }

    let data_file_count = 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_file_count > 1,
        "expected rotation to persist multiple data files after reopen"
    );

    Ok(())
}