use overgraph::manifest::load_manifest_readonly;
use serde_json::json;
use std::path::{Path, PathBuf};
use std::{env, fs, process};
fn main() {
let args: Vec<String> = env::args().collect();
let mut json_mode = false;
let mut db_path_arg: Option<&str> = None;
for arg in &args[1..] {
match arg.as_str() {
"--json" => json_mode = true,
_ if db_path_arg.is_none() => db_path_arg = Some(arg),
_ => {
eprintln!("Usage: overgraph-inspect [--json] <db-path>");
process::exit(1);
}
}
}
let db_path = match db_path_arg {
Some(p) => PathBuf::from(p),
None => {
eprintln!("Usage: overgraph-inspect [--json] <db-path>");
process::exit(1);
}
};
if !db_path.is_dir() {
eprintln!("Error: '{}' is not a directory", db_path.display());
process::exit(1);
}
let result = if json_mode {
inspect_json(&db_path)
} else {
inspect_text(&db_path)
};
if let Err(e) = result {
eprintln!("Error: {}", e);
process::exit(1);
}
}
fn inspect_json(db_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let manifest = load_manifest_readonly(db_path)?;
let wal_files = collect_wal_files(db_path);
let wal_bytes: Option<u64> = if wal_files.is_empty() {
None
} else {
Some(wal_files.iter().map(|(_, sz)| sz).sum())
};
let output = match manifest {
Some(m) => {
let segments: Vec<serde_json::Value> = m
.segments
.iter()
.map(|seg| {
let seg_name = format!("seg_{:04}", seg.id);
let size = dir_size(&db_path.join("segments").join(&seg_name));
json!({
"id": seg.id,
"node_count": seg.node_count,
"edge_count": seg.edge_count,
"size_bytes": size,
})
})
.collect();
let policies: serde_json::Map<String, serde_json::Value> = m
.prune_policies
.iter()
.map(|(name, policy)| {
(
name.clone(),
json!({
"max_age_ms": policy.max_age_ms,
"max_weight": policy.max_weight,
"label": policy.label,
}),
)
})
.collect();
json!({
"path": db_path.display().to_string(),
"manifest_version": m.version,
"next_node_id": m.next_node_id,
"next_edge_id": m.next_edge_id,
"segment_count": m.segments.len(),
"total_nodes": m.segments.iter().map(|s| s.node_count).sum::<u64>(),
"total_edges": m.segments.iter().map(|s| s.edge_count).sum::<u64>(),
"segments": segments,
"wal_bytes": wal_bytes,
"prune_policies": policies,
})
}
None => {
json!({
"path": db_path.display().to_string(),
"initialized": false,
"wal_bytes": wal_bytes,
})
}
};
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
fn inspect_text(db_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
println!("OverGraph Database: {}", db_path.display());
println!();
let manifest = load_manifest_readonly(db_path)?;
let manifest = match manifest {
Some(m) => m,
None => {
println!(" No manifest found. Database may be empty or uninitialized.");
print_wal_info(db_path);
return Ok(());
}
};
println!("Manifest");
println!(" Version: {}", manifest.version);
println!(" Next node ID: {}", manifest.next_node_id);
println!(" Next edge ID: {}", manifest.next_edge_id);
println!();
let total_nodes: u64 = manifest.segments.iter().map(|s| s.node_count).sum();
let total_edges: u64 = manifest.segments.iter().map(|s| s.edge_count).sum();
println!("Segments: {}", manifest.segments.len());
if !manifest.segments.is_empty() {
println!(" {:>6} {:>8} {:>8}", "ID", "Nodes", "Edges");
for seg in &manifest.segments {
println!(
" {:>6} {:>8} {:>8}",
seg.id, seg.node_count, seg.edge_count
);
}
println!(" {:>6} {:>8} {:>8}", "Total", total_nodes, total_edges);
}
println!();
if !manifest.segments.is_empty() {
println!("Segment Sizes");
let mut total_size = 0u64;
for seg in &manifest.segments {
let seg_name = format!("seg_{:04}", seg.id);
let seg_dir = db_path.join("segments").join(&seg_name);
if seg_dir.is_dir() {
let size = dir_size(&seg_dir);
total_size += size;
println!(" {}: {}", seg_name, format_bytes(size));
} else {
println!(" {}: <missing>", seg_name);
}
}
if manifest.segments.len() > 1 {
println!(" Total: {}", format_bytes(total_size));
}
println!();
}
print_wal_info(db_path);
if !manifest.prune_policies.is_empty() {
println!("Prune Policies: {}", manifest.prune_policies.len());
for (name, policy) in &manifest.prune_policies {
let mut criteria = Vec::new();
if let Some(age) = policy.max_age_ms {
criteria.push(format!("max_age={}ms", age));
}
if let Some(w) = policy.max_weight {
criteria.push(format!("max_weight={}", w));
}
if let Some(label) = policy.label.as_deref() {
criteria.push(format!("label={}", label));
}
println!(" {}: {}", name, criteria.join(", "));
}
println!();
}
Ok(())
}
fn print_wal_info(db_path: &Path) {
let wal_files = collect_wal_files(db_path);
if wal_files.is_empty() {
println!("WAL: not present");
} else {
let total: u64 = wal_files.iter().map(|(_, sz)| sz).sum();
println!(
"WAL: {} generation(s) ({})",
wal_files.len(),
format_bytes(total)
);
}
println!();
}
fn collect_wal_files(db_path: &Path) -> Vec<(String, u64)> {
let mut files = Vec::new();
let legacy = db_path.join("data.wal");
if let Ok(meta) = fs::metadata(&legacy) {
files.push(("data.wal".to_string(), meta.len()));
}
if let Ok(entries) = fs::read_dir(db_path) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("wal_") && name.ends_with(".wal") {
if let Ok(meta) = entry.metadata() {
files.push((name, meta.len()));
}
}
}
}
files.sort_by(|a, b| a.0.cmp(&b.0));
files
}
fn dir_size(path: &Path) -> u64 {
let mut total = 0u64;
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
if let Ok(meta) = entry.metadata() {
if meta.is_file() {
total += meta.len();
}
}
}
}
total
}
fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = 1024 * KB;
const GB: u64 = 1024 * MB;
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!("{} B", bytes)
}
}