emdb 0.7.0

A lightweight, high-performance embedded database for Rust.
Documentation
use std::fs::OpenOptions;
use std::io::Write;

use emdb::{Emdb, Result};

const HEADER_LEN: usize = 64;
const LEGACY_MAGIC: [u8; 8] = *b"EMDB\0\0\0\0";
const OP_INSERT: u8 = 0;
const OP_BATCH_BEGIN: u8 = 4;
const OP_BATCH_END: u8 = 5;

fn tmp_path(name: &str) -> std::path::PathBuf {
    let mut path = std::env::temp_dir();
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0_u128, |d| d.as_nanos());
    path.push(format!("emdb-migration-{name}-{nanos}.emdb"));
    path
}

fn cleanup(path: &std::path::Path) {
    let wal = std::path::PathBuf::from(format!("{}.wal", path.display()));
    let bak = std::path::PathBuf::from(format!("{}.bak", path.display()));
    let lock = std::path::PathBuf::from(format!("{}.lock", path.display()));

    let _db_removed = std::fs::remove_file(path);
    let _wal_removed = std::fs::remove_file(wal);
    let _bak_removed = std::fs::remove_file(bak);
    let _lock_removed = std::fs::remove_file(lock);
}

fn write_legacy_header(path: &std::path::Path, format_ver: u32, last_tx_id: u64) -> Result<()> {
    let mut file = OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open(path)?;

    let mut header = [0_u8; HEADER_LEN];
    header[0..8].copy_from_slice(&LEGACY_MAGIC);
    header[8..12].copy_from_slice(&format_ver.to_le_bytes());
    header[32..40].copy_from_slice(&last_tx_id.to_le_bytes());
    file.write_all(&header)?;
    file.flush()?;
    Ok(())
}

fn append_record(path: &std::path::Path, op_type: u8, payload_tail: &[u8]) -> Result<()> {
    let mut payload = Vec::new();
    payload.push(op_type);
    payload.extend_from_slice(&0_u64.to_le_bytes());
    payload.extend_from_slice(payload_tail);

    let crc = crc32fast::hash(&payload);

    let mut record = Vec::new();
    record.extend_from_slice(&(payload.len() as u32).to_le_bytes());
    record.extend_from_slice(&payload);
    record.extend_from_slice(&crc.to_le_bytes());

    let mut file = OpenOptions::new().append(true).open(path)?;
    file.write_all(&record)?;
    file.flush()?;
    Ok(())
}

fn append_insert(path: &std::path::Path, key: &[u8], value: &[u8]) -> Result<()> {
    let mut payload = Vec::new();
    payload.extend_from_slice(&(key.len() as u32).to_le_bytes());
    payload.extend_from_slice(key);
    payload.extend_from_slice(&(value.len() as u32).to_le_bytes());
    payload.extend_from_slice(value);

    #[cfg(feature = "ttl")]
    payload.extend_from_slice(&0_u64.to_le_bytes());

    append_record(path, OP_INSERT, &payload)
}

fn append_batch_begin(path: &std::path::Path, tx_id: u64, op_count: u32) -> Result<()> {
    let mut payload = Vec::new();
    payload.extend_from_slice(&tx_id.to_le_bytes());
    payload.extend_from_slice(&op_count.to_le_bytes());
    append_record(path, OP_BATCH_BEGIN, &payload)
}

fn append_batch_end(path: &std::path::Path, tx_id: u64) -> Result<()> {
    append_record(path, OP_BATCH_END, &tx_id.to_le_bytes())
}

fn assert_migrated_content(db: &Emdb) -> Result<()> {
    assert_eq!(db.get("alpha")?, Some(b"one".to_vec()));
    assert_eq!(db.get("beta")?, Some(b"two".to_vec()));
    Ok(())
}

#[test]
fn opening_v1_file_auto_migrates_to_v3_without_leaving_backup() -> Result<()> {
    let path = tmp_path("v1");
    write_legacy_header(path.as_path(), 1, 0)?;
    append_insert(path.as_path(), b"alpha", b"one")?;
    append_insert(path.as_path(), b"beta", b"two")?;

    let db = Emdb::open(&path)?;
    assert_migrated_content(&db)?;
    assert!(db.path().is_some());
    assert!(path.exists());
    assert!(!std::path::PathBuf::from(format!("{}.bak", path.display())).exists());

    cleanup(path.as_path());
    Ok(())
}

#[test]
fn opening_v2_file_auto_migrates_to_v3_without_leaving_backup() -> Result<()> {
    let path = tmp_path("v2");
    write_legacy_header(path.as_path(), 2, 41)?;
    append_batch_begin(path.as_path(), 42, 2)?;
    append_insert(path.as_path(), b"alpha", b"one")?;
    append_insert(path.as_path(), b"beta", b"two")?;
    append_batch_end(path.as_path(), 42)?;

    let db = Emdb::open(&path)?;
    assert_migrated_content(&db)?;
    assert!(db.path().is_some());
    assert!(path.exists());
    assert!(!std::path::PathBuf::from(format!("{}.bak", path.display())).exists());

    cleanup(path.as_path());
    Ok(())
}

#[test]
fn explicit_migrate_on_current_file_is_a_noop_for_backup_files() -> Result<()> {
    let path = tmp_path("explicit-noop");
    let db = Emdb::open(&path)?;
    db.insert("alpha", "one")?;
    db.flush()?;
    db.migrate()?;

    assert!(path.exists());
    assert!(!std::path::PathBuf::from(format!("{}.bak", path.display())).exists());

    cleanup(path.as_path());
    Ok(())
}