overgraph 0.11.0

An absurdly fast embedded graph database. Pure Rust, sub-microsecond reads.
Documentation
use overgraph::{
    DatabaseEngine, DbOptions, PrunePolicy, UpsertEdgeOptions, UpsertNodeOptions, WalSyncMode,
};
use std::process::Command;
use tempfile::TempDir;

fn inspect_binary() -> &'static str {
    env!("CARGO_BIN_EXE_overgraph-inspect")
}

#[test]
fn test_inspect_empty_db() {
    let dir = TempDir::new().unwrap();

    let opts = DbOptions {
        create_if_missing: true,
        wal_sync_mode: WalSyncMode::Immediate,
        ..DbOptions::default()
    };
    let db = DatabaseEngine::open(dir.path(), &opts).unwrap();
    db.close().unwrap();

    let output = Command::new(inspect_binary())
        .arg(dir.path().to_str().unwrap())
        .output()
        .expect("failed to run overgraph-inspect");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        output.status.success(),
        "inspect failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(stdout.contains("OverGraph Database:"));
    assert!(stdout.contains("Version:       1"));
    assert!(stdout.contains("Segments: 0"));
    assert!(stdout.contains("WAL: 1 generation(s)"));
}

#[test]
fn test_inspect_with_data() {
    let dir = TempDir::new().unwrap();

    let opts = DbOptions {
        create_if_missing: true,
        wal_sync_mode: WalSyncMode::Immediate,
        compact_after_n_flushes: 0,
        ..DbOptions::default()
    };
    let db = DatabaseEngine::open(dir.path(), &opts).unwrap();

    for i in 0..10 {
        db.upsert_node(
            "Person",
            &format!("node_{}", i),
            UpsertNodeOptions::default(),
        )
        .unwrap();
    }
    let n1 = db
        .upsert_node("Company", "a", UpsertNodeOptions::default())
        .unwrap();
    let n2 = db
        .upsert_node("Company", "b", UpsertNodeOptions::default())
        .unwrap();
    db.upsert_edge(n1, n2, "RELATES_TO", UpsertEdgeOptions::default())
        .unwrap();

    db.flush().unwrap();
    db.close().unwrap();

    let output = Command::new(inspect_binary())
        .arg(dir.path().to_str().unwrap())
        .output()
        .expect("failed to run overgraph-inspect");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        output.status.success(),
        "inspect failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(stdout.contains("Segments: 1"));
    // Verify the segment table shows 12 nodes and 1 edge in the right-aligned columns
    assert!(stdout.contains("        12"));
    assert!(stdout.contains("Segment Sizes"));
    assert!(stdout.contains("seg_0001"));
    assert!(stdout.contains("Next node ID:  13"));
    assert!(stdout.contains("Next edge ID:  2"));
}

#[test]
fn test_inspect_multiple_segments() {
    let dir = TempDir::new().unwrap();

    let opts = DbOptions {
        create_if_missing: true,
        wal_sync_mode: WalSyncMode::Immediate,
        compact_after_n_flushes: 0,
        ..DbOptions::default()
    };
    let db = DatabaseEngine::open(dir.path(), &opts).unwrap();

    for i in 0..5 {
        db.upsert_node(
            "Person",
            &format!("batch1_{}", i),
            UpsertNodeOptions::default(),
        )
        .unwrap();
    }
    db.flush().unwrap();

    for i in 0..3 {
        db.upsert_node(
            "Person",
            &format!("batch2_{}", i),
            UpsertNodeOptions::default(),
        )
        .unwrap();
    }
    db.flush().unwrap();

    db.close().unwrap();

    let output = Command::new(inspect_binary())
        .arg(dir.path().to_str().unwrap())
        .output()
        .expect("failed to run overgraph-inspect");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(output.status.success());
    assert!(stdout.contains("Segments: 2"));
    assert!(stdout.contains("seg_0001"));
    assert!(stdout.contains("seg_0002"));
    // Total row should appear for multi-segment DBs
    assert!(stdout.contains("Total"));
}

#[test]
fn test_inspect_with_prune_policies() {
    let dir = TempDir::new().unwrap();

    let opts = DbOptions {
        create_if_missing: true,
        wal_sync_mode: WalSyncMode::Immediate,
        ..DbOptions::default()
    };
    let db = DatabaseEngine::open(dir.path(), &opts).unwrap();

    db.set_prune_policy(
        "old_memories",
        PrunePolicy {
            max_age_ms: Some(86_400_000),
            max_weight: Some(0.1),
            label: Some("Article".to_string()),
        },
    )
    .unwrap();

    db.close().unwrap();

    let output = Command::new(inspect_binary())
        .arg(dir.path().to_str().unwrap())
        .output()
        .expect("failed to run overgraph-inspect");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(output.status.success());
    assert!(stdout.contains("Prune Policies: 1"));
    assert!(stdout.contains("old_memories"));
    assert!(stdout.contains("max_age=86400000ms"));
    assert!(stdout.contains("max_weight=0.1"));
    assert!(stdout.contains("label=Article"));
}

#[test]
fn test_inspect_json_with_data() {
    let dir = TempDir::new().unwrap();

    let opts = DbOptions {
        create_if_missing: true,
        wal_sync_mode: WalSyncMode::Immediate,
        compact_after_n_flushes: 0,
        ..DbOptions::default()
    };
    let db = DatabaseEngine::open(dir.path(), &opts).unwrap();

    for i in 0..5 {
        db.upsert_node(
            "Person",
            &format!("node_{}", i),
            UpsertNodeOptions::default(),
        )
        .unwrap();
    }
    db.flush().unwrap();
    db.close().unwrap();

    let output = Command::new(inspect_binary())
        .args(["--json", dir.path().to_str().unwrap()])
        .output()
        .expect("failed to run overgraph-inspect");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        output.status.success(),
        "inspect --json failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let parsed: serde_json::Value =
        serde_json::from_str(&stdout).expect("output should be valid JSON");
    assert_eq!(parsed["manifest_version"], 1);
    assert_eq!(parsed["segment_count"], 1);
    assert_eq!(parsed["total_nodes"], 5);
    assert_eq!(parsed["total_edges"], 0);
    assert_eq!(parsed["next_node_id"], 6);
    assert_eq!(parsed["next_edge_id"], 1);
    assert!(parsed["wal_bytes"].as_u64().unwrap() > 0);
    assert_eq!(parsed["segments"].as_array().unwrap().len(), 1);
    assert_eq!(parsed["segments"][0]["node_count"], 5);
    assert!(parsed["segments"][0]["size_bytes"].as_u64().unwrap() > 0);
}

#[test]
fn test_inspect_json_with_multi_label_data() {
    let dir = TempDir::new().unwrap();

    let opts = DbOptions {
        create_if_missing: true,
        wal_sync_mode: WalSyncMode::Immediate,
        compact_after_n_flushes: 0,
        ..DbOptions::default()
    };
    let db = DatabaseEngine::open(dir.path(), &opts).unwrap();

    db.upsert_node("Person", "person-only", UpsertNodeOptions::default())
        .unwrap();
    db.upsert_node(
        &["Person", "Employee"],
        "person-employee",
        UpsertNodeOptions::default(),
    )
    .unwrap();
    db.flush().unwrap();
    db.close().unwrap();

    let output = Command::new(inspect_binary())
        .args(["--json", dir.path().to_str().unwrap()])
        .output()
        .expect("failed to run overgraph-inspect");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        output.status.success(),
        "inspect --json failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let parsed: serde_json::Value =
        serde_json::from_str(&stdout).expect("output should be valid JSON");
    assert_eq!(parsed["segment_count"], 1);
    assert_eq!(parsed["total_nodes"], 2);
    assert_eq!(parsed["segments"][0]["node_count"], 2);
}

#[test]
fn test_inspect_json_uninitialized() {
    let dir = TempDir::new().unwrap();

    let output = Command::new(inspect_binary())
        .args(["--json", dir.path().to_str().unwrap()])
        .output()
        .expect("failed to run overgraph-inspect");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(output.status.success());

    let parsed: serde_json::Value =
        serde_json::from_str(&stdout).expect("output should be valid JSON");
    assert_eq!(parsed["initialized"], false);
    assert!(parsed["wal_bytes"].is_null());
}

#[test]
fn test_inspect_nonexistent_path() {
    let output = Command::new(inspect_binary())
        .arg("/tmp/does_not_exist_overgraph_test")
        .output()
        .expect("failed to run overgraph-inspect");

    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("not a directory"));
}

#[test]
fn test_inspect_no_args() {
    let output = Command::new(inspect_binary())
        .output()
        .expect("failed to run overgraph-inspect");

    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("Usage:"));
}

#[test]
fn test_inspect_uninitialized_dir() {
    let dir = TempDir::new().unwrap();

    let output = Command::new(inspect_binary())
        .arg(dir.path().to_str().unwrap())
        .output()
        .expect("failed to run overgraph-inspect");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(output.status.success());
    assert!(stdout.contains("No manifest found"));
    assert!(stdout.contains("WAL: not present"));
}