use anyhow::{Context, Result, bail};
use sqry_core::graph::unified::concurrent::GraphSnapshot;
use sqry_core::graph::unified::materialize::find_nodes_by_name;
use sqry_core::graph::unified::node::id::NodeId;
pub fn run(snapshot: &GraphSnapshot, symbol: &str, json: bool) -> Result<()> {
let node_ids = find_nodes_by_name(snapshot, symbol);
if node_ids.is_empty() {
bail!("No nodes found matching '{symbol}'");
}
let fact_epoch = snapshot.fact_epoch();
if json {
print_json(snapshot, &node_ids, fact_epoch)?;
} else {
print_text(snapshot, &node_ids, fact_epoch)?;
}
Ok(())
}
fn print_text(snapshot: &GraphSnapshot, node_ids: &[NodeId], fact_epoch: u64) -> Result<()> {
println!("Snapshot fact_epoch: {fact_epoch}");
println!();
for &node_id in node_ids {
let Some(entry) = snapshot.nodes().get(node_id) else {
continue;
};
let name = snapshot
.strings()
.resolve(entry.name)
.context("unresolved node name")?;
println!("Node: {name} ({node_id:?})");
println!(" kind: {:?}", entry.kind);
if let Some(qn) = entry.qualified_name
&& let Some(qname) = snapshot.strings().resolve(qn)
{
println!(" qualified_name: {qname}");
}
match snapshot.node_provenance(node_id) {
Some(prov) => {
println!(" first_seen_epoch: {}", prov.first_seen_epoch);
println!(" last_seen_epoch: {}", prov.last_seen_epoch);
println!(" content_hash: {}", hex::encode(prov.content_hash));
}
None => {
println!(" provenance: <none>");
}
}
if let Some(file_path) = snapshot.files().resolve(entry.file) {
println!(" file: {}", file_path.display());
if let Some(fprov) = snapshot.file_provenance(entry.file) {
println!(" file_indexed_at: {}", fprov.indexed_at);
println!(" file_content_hash: {}", hex::encode(fprov.content_hash));
println!(" file_is_external: {}", fprov.is_external);
if let Some(uri) = fprov.source_uri {
println!(" file_source_uri: StringId({uri:?})");
}
}
}
print_edge_provenance_summary(snapshot);
println!();
}
Ok(())
}
fn print_edge_provenance_summary(snapshot: &GraphSnapshot) {
use sqry_core::graph::unified::edge::id::EdgeId;
let edge_stats = snapshot.edges().stats().forward;
let total_edges = edge_stats.csr_edge_count + edge_stats.delta_edge_count;
let mut count = 0u64;
let mut min_epoch = u64::MAX;
let mut max_epoch = 0u64;
for edge_idx in 0..total_edges {
if let Ok(idx) = u32::try_from(edge_idx) {
let eid = EdgeId::new(idx);
if let Some(eprov) = snapshot.edge_provenance(eid) {
count += 1;
min_epoch = min_epoch.min(eprov.first_seen_epoch);
max_epoch = max_epoch.max(eprov.first_seen_epoch);
}
}
}
if count > 0 {
println!(" edges_with_provenance: {count}");
println!(" edge_first_seen_epoch_range: [{min_epoch}, {max_epoch}]");
} else {
println!(" edges_with_provenance: 0");
}
}
fn print_json(snapshot: &GraphSnapshot, node_ids: &[NodeId], fact_epoch: u64) -> Result<()> {
let mut results = Vec::new();
for &node_id in node_ids {
let Some(entry) = snapshot.nodes().get(node_id) else {
continue;
};
let name = snapshot
.strings()
.resolve(entry.name)
.context("unresolved node name")?;
let node_prov = snapshot.node_provenance(node_id).map(|p| {
serde_json::json!({
"first_seen_epoch": p.first_seen_epoch,
"last_seen_epoch": p.last_seen_epoch,
"content_hash": hex::encode(p.content_hash),
})
});
let file_path = snapshot
.files()
.resolve(entry.file)
.map(|p| p.display().to_string());
let file_prov = snapshot.file_provenance(entry.file).map(|fp| {
serde_json::json!({
"indexed_at": fp.indexed_at,
"content_hash": hex::encode(fp.content_hash),
"is_external": fp.is_external,
"source_uri": fp.source_uri.map(|id| format!("StringId({id:?})")),
})
});
results.push(serde_json::json!({
"node_id": format!("{node_id:?}"),
"name": name.as_ref(),
"kind": format!("{:?}", entry.kind),
"qualified_name": entry.qualified_name.and_then(|qn| {
snapshot.strings().resolve(qn).map(|s| s.as_ref().to_string())
}),
"provenance": node_prov,
"file": file_path,
"file_provenance": file_prov,
}));
}
let edge_summary = {
use sqry_core::graph::unified::edge::id::EdgeId;
let edge_stats = snapshot.edges().stats().forward;
let total = edge_stats.csr_edge_count + edge_stats.delta_edge_count;
let mut count = 0u64;
let mut min_ep = u64::MAX;
let mut max_ep = 0u64;
for idx in 0..total {
if let Ok(i) = u32::try_from(idx) {
let eid = EdgeId::new(i);
if let Some(ep) = snapshot.edge_provenance(eid) {
count += 1;
min_ep = min_ep.min(ep.first_seen_epoch);
max_ep = max_ep.max(ep.first_seen_epoch);
}
}
}
if count > 0 {
serde_json::json!({
"edges_with_provenance": count,
"first_seen_epoch_min": min_ep,
"first_seen_epoch_max": max_ep,
})
} else {
serde_json::json!({ "edges_with_provenance": 0 })
}
};
let output = serde_json::json!({
"fact_epoch": fact_epoch,
"nodes": results,
"edge_provenance_summary": edge_summary,
});
println!(
"{}",
serde_json::to_string_pretty(&output).context("JSON serialization")?
);
Ok(())
}