use arrow::array::{Array, StringArray};
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::path::PathBuf;
fn main() {
let graph_dir = PathBuf::from(
std::env::args()
.nth(1)
.unwrap_or_else(|| "research/shared/self-graph".to_string()),
);
let nodes = read_parquet(&graph_dir.join("nodes.parquet"));
let edges = read_parquet(&graph_dir.join("edges.parquet"));
let node_rows = nodes.num_rows();
let edge_rows = edges.num_rows();
println!("Self-graph loaded: {node_rows} nodes, {edge_rows} edges\n");
let node_ids_col = nodes.column(0); let node_kinds_col = nodes.column(1); let node_names_col = nodes.column(3);
let node_ids = cast_to_string(node_ids_col);
let node_kinds = cast_to_string(node_kinds_col);
let node_names = cast_to_string(node_names_col);
let edge_sources = cast_to_string(edges.column(0));
let edge_targets = cast_to_string(edges.column(1));
let edge_preds = cast_to_string(edges.column(2));
println!("=== Query 1: What does nusy-being call? ===");
let being_nodes: HashSet<String> = (0..node_ids.len())
.filter(|&i| node_ids.value(i).contains("nusy-being"))
.map(|i| node_ids.value(i).to_string())
.collect();
let mut callees: Vec<String> = (0..edge_sources.len())
.filter(|&i| being_nodes.contains(edge_sources.value(i)) && edge_preds.value(i) == "calls")
.map(|i| edge_targets.value(i).to_string())
.collect();
callees.sort();
callees.dedup();
println!("nusy-being nodes: {}", being_nodes.len());
println!("Functions called from nusy-being: {}", callees.len());
for c in callees.iter().take(15) {
println!(" {c}");
}
if callees.len() > 15 {
println!(" ... ({} more)", callees.len() - 15);
}
println!();
println!("=== Query 2: Crates with edges into nusy-arrow-core nodes ===");
let arrow_core_nodes: HashSet<String> = (0..node_ids.len())
.filter(|&i| node_ids.value(i).contains("nusy-arrow-core"))
.map(|i| node_ids.value(i).to_string())
.collect();
let mut dep_crates: HashSet<String> = HashSet::new();
for i in 0..edge_targets.len() {
let tgt = edge_targets.value(i);
let src = edge_sources.value(i);
if arrow_core_nodes.contains(tgt)
&& let Some(c) = extract_crate_from_id(src).filter(|c| c != "nusy-arrow-core")
{
dep_crates.insert(c);
}
}
let mut dep_list: Vec<String> = dep_crates.into_iter().collect();
dep_list.sort();
println!(
"Crates with code edges into nusy-arrow-core nodes: {}",
dep_list.len()
);
for c in &dep_list {
println!(" {c}");
}
println!();
println!("=== Query 3: Test functions in the workspace ===");
let test_count = (0..node_ids.len())
.filter(|&i| {
let name = node_names.value(i);
name.starts_with("test_") || node_ids.value(i).contains("/tests/")
})
.count();
let rust_fn_count = (0..node_kinds.len())
.filter(|&i| node_kinds.value(i) == "rust_function")
.count();
let rust_method_count = (0..node_kinds.len())
.filter(|&i| node_kinds.value(i) == "rust_method")
.count();
println!("rust_function nodes: {rust_fn_count}");
println!("rust_method nodes: {rust_method_count}");
println!("Test functions (name starts with test_ or in tests/): {test_count}");
println!();
println!("=== Node Kind Breakdown ===");
let mut kind_counts: HashMap<String, usize> = HashMap::new();
for i in 0..node_kinds.len() {
*kind_counts
.entry(node_kinds.value(i).to_string())
.or_insert(0) += 1;
}
let mut kinds: Vec<(String, usize)> = kind_counts.into_iter().collect();
kinds.sort_by(|a, b| b.1.cmp(&a.1));
for (k, c) in kinds {
println!(" {k}: {c}");
}
println!("\n=== Edge Predicate Breakdown ===");
let mut pred_counts: HashMap<String, usize> = HashMap::new();
for i in 0..edge_preds.len() {
*pred_counts
.entry(edge_preds.value(i).to_string())
.or_insert(0) += 1;
}
let mut preds: Vec<(String, usize)> = pred_counts.into_iter().collect();
preds.sort_by(|a, b| b.1.cmp(&a.1));
for (p, c) in preds {
println!(" {p}: {c}");
}
}
fn extract_crate_from_id(node_id: &str) -> Option<String> {
let path_part = node_id.split_once(':')?.1;
let parts: Vec<&str> = path_part.splitn(4, '/').collect();
if parts.len() >= 3 && parts[0] == "crates" {
Some(parts[1].to_string())
} else {
None
}
}
fn cast_to_string(col: &dyn Array) -> StringArray {
use arrow::compute::cast;
use arrow::datatypes::DataType;
let utf8 = cast(col, &DataType::Utf8).expect("cast to Utf8");
utf8.as_any()
.downcast_ref::<StringArray>()
.expect("downcast to StringArray")
.clone()
}
fn read_parquet(path: &std::path::Path) -> arrow::array::RecordBatch {
let file = File::open(path).unwrap_or_else(|e| panic!("open {}: {e}", path.display()));
let builder = ParquetRecordBatchReaderBuilder::try_new(file)
.unwrap_or_else(|e| panic!("parquet builder {}: {e}", path.display()));
let mut reader = builder
.build()
.unwrap_or_else(|e| panic!("build reader {}: {e}", path.display()));
let mut batches = Vec::new();
for batch in &mut reader {
batches.push(batch.expect("read batch"));
}
assert!(!batches.is_empty(), "no data in {}", path.display());
arrow::compute::concat_batches(&batches[0].schema(), &batches).expect("concat")
}