sombra 0.3.6

High-performance graph database with ACID transactions, single-file storage, and bindings for Rust, TypeScript, and Python
Documentation
#![allow(clippy::uninlined_format_args)]

use sombra::{Config, GraphDB, Result};
use std::env;
use std::fs;
use std::process;

fn print_usage() {
    eprintln!("┌─────────────────────────────────────────────┐");
    eprintln!("│          Sombra Database Repair             │");
    eprintln!("└─────────────────────────────────────────────┘");
    eprintln!();
    eprintln!("USAGE:");
    eprintln!("    sombra-repair <database> <command>");
    eprintln!();
    eprintln!("COMMANDS:");
    eprintln!("    checkpoint       Force WAL checkpoint");
    eprintln!("    vacuum           Compact database");
    eprintln!();
    eprintln!("EXAMPLES:");
    eprintln!("    sombra-repair graph.db checkpoint");
    eprintln!("    sombra-repair graph.db vacuum");
    eprintln!();
    eprintln!("WARNING:");
    eprintln!("    Always backup your database before repair!");
    eprintln!();
}

fn print_header(title: &str) {
    let width = 60;
    let padding = (width - title.len() - 2) / 2;
    println!();
    println!("{}", "".repeat(width));
    println!(
        "{}{title}{}",
        " ".repeat(padding),
        " ".repeat(width - padding - title.len())
    );
    println!("{}", "".repeat(width));
    println!();
}

fn print_section(title: &str) {
    println!();
    println!("─── {} {}", title, "".repeat(55 - title.len()));
}

fn format_bytes(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;

    if bytes >= GB {
        format!("{:.2} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2} KB", bytes as f64 / KB as f64)
    } else {
        format!("{bytes} B")
    }
}

fn cmd_checkpoint(db_path: &str) -> Result<()> {
    print_header("CHECKPOINT WAL");

    let wal_path = format!("{db_path}-wal");

    let wal_size_before = match fs::metadata(&wal_path) {
        Ok(meta) => Some(meta.len()),
        Err(_) => None,
    };

    if let Some(size) = wal_size_before {
        println!("  WAL size before: {}", format_bytes(size));
    } else {
        println!("  No WAL file found");
    }

    println!();
    println!("  Performing checkpoint...");

    let config = Config::balanced();
    let mut db = GraphDB::open_with_config(db_path, config)?;
    db.checkpoint()?;

    drop(db);

    let wal_size_after = match fs::metadata(&wal_path) {
        Ok(meta) => Some(meta.len()),
        Err(_) => None,
    };

    print_section("Results");

    if let Some(size) = wal_size_after {
        println!("  WAL size after: {}", format_bytes(size));

        if let Some(before) = wal_size_before {
            let saved = before.saturating_sub(size);
            if saved > 0 {
                println!("  Space reclaimed: {}", format_bytes(saved));
            }
        }
    } else {
        println!("  WAL file removed (clean state)");
    }

    println!();
    println!("  ✓ Checkpoint completed successfully");
    println!();

    Ok(())
}

fn cmd_vacuum(db_path: &str) -> Result<()> {
    print_header("VACUUM DATABASE");

    let size_before = fs::metadata(db_path)?.len();

    println!("  Database size before: {}", format_bytes(size_before));
    println!();
    println!("  Compacting database...");
    println!("  (This may take a while for large databases)");
    println!();

    let config = Config::balanced();
    let mut db = GraphDB::open_with_config(db_path, config)?;

    db.checkpoint()?;

    print_section("Results");

    let size_after = fs::metadata(db_path)?.len();
    println!("  Database size after: {}", format_bytes(size_after));

    if size_before > size_after {
        let saved = size_before - size_after;
        let percent = (saved as f64 / size_before as f64) * 100.0;
        println!(
            "  Space reclaimed: {} ({:.1}%)",
            format_bytes(saved),
            percent
        );
    } else {
        println!("  No space reclaimed (database already compact)");
    }

    println!();
    println!("  ✓ Vacuum completed successfully");
    println!();

    Ok(())
}

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() != 3 {
        print_usage();
        process::exit(1);
    }

    let db_path = &args[1];
    let command = &args[2];

    if !std::path::Path::new(db_path).exists() {
        eprintln!();
        eprintln!("╔══════════════════════════════════════════════════════════╗");
        eprintln!("║                         ERROR                            ║");
        eprintln!("╚══════════════════════════════════════════════════════════╝");
        eprintln!();
        eprintln!("  Database file not found: {db_path}");
        eprintln!();
        process::exit(1);
    }

    println!();
    println!("  ⚠  WARNING: Always backup your database before repair!");
    println!();
    print!("  Continue? [y/N] ");

    use std::io::{self, Write};
    io::stdout().flush().unwrap();

    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();

    if !input.trim().eq_ignore_ascii_case("y") {
        println!();
        println!("  Aborted.");
        println!();
        process::exit(0);
    }

    let result = match command.as_str() {
        "checkpoint" => cmd_checkpoint(db_path),
        "vacuum" => cmd_vacuum(db_path),
        _ => {
            eprintln!();
            eprintln!("Error: Unknown command '{command}'");
            eprintln!();
            print_usage();
            process::exit(1);
        }
    };

    if let Err(e) = result {
        eprintln!();
        eprintln!("╔══════════════════════════════════════════════════════════╗");
        eprintln!("║                         ERROR                            ║");
        eprintln!("╚══════════════════════════════════════════════════════════╝");
        eprintln!();
        eprintln!("  {e}");
        eprintln!();
        process::exit(1);
    }
}