aedb 0.2.3

Embedded Rust storage engine with transactional commits, WAL durability, and snapshot-consistent reads
Documentation
use aedb::backup::{BACKUP_MANIFEST_FILE, load_backup_manifest, verify_backup_files};
use aedb::config::{AedbConfig, RecoveryMode};
use aedb::offline;
use std::fs;
use std::path::{Path, PathBuf};

fn main() {
    if let Err(e) = run() {
        eprintln!("error: {e}");
        std::process::exit(1);
    }
}

fn run() -> Result<(), String> {
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 2 {
        print_usage();
        return Err("missing command".into());
    }
    match args[1].as_str() {
        "backup" => match args.get(2).map(String::as_str) {
            Some("list") => cmd_backup_list(&args[3..]),
            Some("verify") => cmd_backup_verify(&args[3..]),
            Some(other) => Err(format!("unknown backup command: {other}")),
            None => Err("missing backup subcommand".into()),
        },
        "dump" => match args.get(2).map(String::as_str) {
            Some("export") => cmd_dump_export(&args[3..]),
            Some("restore") => cmd_dump_restore(&args[3..]),
            Some("parity") => cmd_dump_parity(&args[3..]),
            Some(other) => Err(format!("unknown dump command: {other}")),
            None => Err("missing dump subcommand".into()),
        },
        "check" => match args.get(2).map(String::as_str) {
            Some("invariants") => cmd_check_invariants(&args[3..]),
            Some(other) => Err(format!("unknown check command: {other}")),
            None => Err("missing check subcommand".into()),
        },
        other => {
            print_usage();
            Err(format!("unknown top-level command: {other}"))
        }
    }
}

fn cmd_backup_list(args: &[String]) -> Result<(), String> {
    let root = parse_flag_value(args, "--root").ok_or("--root is required")?;
    let hmac_key = parse_hmac_key_hex(args)?;
    let root = PathBuf::from(root);

    let mut entries = fs::read_dir(&root)
        .map_err(|e| format!("read_dir {}: {e}", root.display()))?
        .filter_map(|e| e.ok())
        .filter(|e| e.path().is_dir())
        .collect::<Vec<_>>();
    entries.sort_by_key(|e| e.file_name());

    for entry in entries {
        let dir = entry.path();
        if !dir.join(BACKUP_MANIFEST_FILE).exists() {
            continue;
        }
        match load_backup_manifest(&dir, hmac_key.as_deref()) {
            Ok(m) => {
                println!(
                    "{}\t{}\t{}\t{}\t{}",
                    dir.display(),
                    m.backup_id,
                    m.backup_type,
                    m.checkpoint_seq,
                    m.wal_head_seq
                );
            }
            Err(err) => {
                println!("{}\tINVALID\t{}", dir.display(), err);
            }
        }
    }
    Ok(())
}

fn cmd_backup_verify(args: &[String]) -> Result<(), String> {
    let backup_dir = parse_flag_value(args, "--backup-dir").ok_or("--backup-dir is required")?;
    let hmac_key = parse_hmac_key_hex(args)?;
    let dir = Path::new(&backup_dir);
    let manifest = load_backup_manifest(dir, hmac_key.as_deref())
        .map_err(|e| format!("load manifest: {e}"))?;
    verify_backup_files(dir, &manifest).map_err(|e| format!("verify files: {e}"))?;
    println!(
        "ok\t{}\t{}\t{}\t{}",
        backup_dir, manifest.backup_id, manifest.checkpoint_seq, manifest.wal_head_seq
    );
    Ok(())
}

fn cmd_dump_export(args: &[String]) -> Result<(), String> {
    let data_dir = parse_flag_value(args, "--data-dir").ok_or("--data-dir is required")?;
    let out = parse_flag_value(args, "--out").ok_or("--out is required")?;
    let config = parse_recovery_config(args)?;
    let report = offline::export_snapshot_dump(Path::new(&data_dir), &config, Path::new(&out))
        .map_err(|e| format!("export dump: {e}"))?;
    println!(
        "ok\t{}\t{}\t{}\t{}",
        report.current_seq, report.parity_checksum_hex, report.table_rows, report.kv_entries
    );
    Ok(())
}

fn cmd_dump_restore(args: &[String]) -> Result<(), String> {
    let dump = parse_flag_value(args, "--dump").ok_or("--dump is required")?;
    let data_dir = parse_flag_value(args, "--data-dir").ok_or("--data-dir is required")?;
    let config = parse_recovery_config(args)?;
    let report = offline::restore_snapshot_dump(Path::new(&dump), Path::new(&data_dir), &config)
        .map_err(|e| format!("restore dump: {e}"))?;
    println!(
        "ok\t{}\t{}\t{}\t{}",
        report.current_seq, report.parity_checksum_hex, report.table_rows, report.kv_entries
    );
    Ok(())
}

fn cmd_dump_parity(args: &[String]) -> Result<(), String> {
    let dump = parse_flag_value(args, "--dump").ok_or("--dump is required")?;
    let data_dir = parse_flag_value(args, "--data-dir").ok_or("--data-dir is required")?;
    let config = parse_recovery_config(args)?;
    let report =
        offline::parity_report_against_data_dir(Path::new(&dump), Path::new(&data_dir), &config)
            .map_err(|e| format!("parity report: {e}"))?;
    println!(
        "{}\t{}\t{}",
        if report.matches { "ok" } else { "mismatch" },
        report.expected_checksum_hex,
        report.actual_checksum_hex
    );
    if report.matches {
        Ok(())
    } else {
        Err("parity mismatch".into())
    }
}

fn cmd_check_invariants(args: &[String]) -> Result<(), String> {
    let data_dir = parse_flag_value(args, "--data-dir").ok_or("--data-dir is required")?;
    let config = parse_recovery_config(args)?;
    let report = offline::invariant_report(Path::new(&data_dir), &config)
        .map_err(|e| format!("invariants: {e}"))?;
    println!(
        "{}\t{}\t{}\t{}",
        if report.ok { "ok" } else { "violations" },
        report.table_count,
        report.table_rows,
        report.kv_entries
    );
    for v in &report.violations {
        println!("violation\t{v}");
    }
    if report.ok {
        Ok(())
    } else {
        Err("invariant violations found".into())
    }
}

fn parse_flag_value(args: &[String], flag: &str) -> Option<String> {
    for arg_index in 0..args.len() {
        if args[arg_index] == flag {
            return args.get(arg_index + 1).cloned();
        }
    }
    None
}

fn parse_hmac_key_hex(args: &[String]) -> Result<Option<Vec<u8>>, String> {
    let Some(hex_key) = parse_flag_value(args, "--hmac-key-hex") else {
        return Ok(None);
    };
    hex::decode(hex_key)
        .map(Some)
        .map_err(|e| format!("invalid --hmac-key-hex: {e}"))
}

fn parse_recovery_config(args: &[String]) -> Result<AedbConfig, String> {
    let mut cfg = AedbConfig::default();
    if args.iter().any(|a| a == "--permissive") {
        cfg.recovery_mode = RecoveryMode::Permissive;
        cfg.hash_chain_required = false;
    }
    if let Some(key) = parse_hmac_key_hex(args)? {
        cfg = cfg.with_hmac_key(key);
    }
    Ok(cfg)
}

fn print_usage() {
    eprintln!("usage:");
    eprintln!("  aedb backup list --root <backup-root> [--hmac-key-hex <hex>]");
    eprintln!("  aedb backup verify --backup-dir <backup-dir> [--hmac-key-hex <hex>]");
    eprintln!(
        "  aedb dump export --data-dir <aedb-dir> --out <dump.aedbdump> [--hmac-key-hex <hex>] [--permissive]"
    );
    eprintln!(
        "  aedb dump restore --dump <dump.aedbdump> --data-dir <target-dir> [--hmac-key-hex <hex>] [--permissive]"
    );
    eprintln!(
        "  aedb dump parity --dump <dump.aedbdump> --data-dir <aedb-dir> [--hmac-key-hex <hex>] [--permissive]"
    );
    eprintln!(
        "  aedb check invariants --data-dir <aedb-dir> [--hmac-key-hex <hex>] [--permissive]"
    );
}