use super::test_symbol;
use crate::*;
#[test]
fn test_resolve_edges_same_dir_priority() {
let db = Database::open_memory().unwrap();
let caller = test_symbol("process", SymbolKind::Function, "src/main.py", 1);
let same_dir = test_symbol("helper", SymbolKind::Function, "src/utils.py", 1);
let other_dir = test_symbol("helper", SymbolKind::Function, "lib/utils.py", 1);
db.insert_symbols(&[caller.clone(), same_dir.clone(), other_dir.clone()])
.unwrap();
let edge = Edge {
source_id: caller.id.clone(),
target_name: "helper".to_string(),
target_id: None,
kind: EdgeKind::Calls,
file_path: "src/main.py".to_string(),
line: 5,
provenance: None,
};
db.insert_edge(&edge).unwrap();
let resolved = db.resolve_edges().unwrap();
assert_eq!(resolved, 1);
let refs = db.refs("helper", None).unwrap();
let call_edge = refs
.iter()
.find(|(e, _)| e.kind == EdgeKind::Calls)
.unwrap();
assert_eq!(call_edge.0.target_id.as_ref().unwrap(), &same_dir.id);
}
#[test]
fn test_resolve_edges_ambiguous_no_resolve() {
let db = Database::open_memory().unwrap();
let caller = test_symbol("process", SymbolKind::Function, "app/main.py", 1);
let sym1 = test_symbol("helper", SymbolKind::Function, "pkg_a/utils.py", 1);
let sym2 = test_symbol("helper", SymbolKind::Function, "pkg_b/utils.py", 1);
db.insert_symbols(&[caller.clone(), sym1, sym2]).unwrap();
let edge = Edge {
source_id: caller.id.clone(),
target_name: "helper".to_string(),
target_id: None,
kind: EdgeKind::Calls,
file_path: "app/main.py".to_string(),
line: 5,
provenance: None,
};
db.insert_edge(&edge).unwrap();
let resolved = db.resolve_edges().unwrap();
assert_eq!(resolved, 0);
}
#[test]
fn test_resolve_edges_same_file_priority() {
let db = Database::open_memory().unwrap();
let caller = test_symbol("process", SymbolKind::Function, "a.py", 1);
let same_file = test_symbol("helper", SymbolKind::Function, "a.py", 20);
let other_file = test_symbol("helper", SymbolKind::Function, "b.py", 1);
db.insert_symbols(&[caller.clone(), same_file.clone(), other_file])
.unwrap();
let edge = Edge {
source_id: caller.id.clone(),
target_name: "helper".to_string(),
target_id: None,
kind: EdgeKind::Calls,
file_path: "a.py".to_string(),
line: 5,
provenance: None,
};
db.insert_edge(&edge).unwrap();
let resolved = db.resolve_edges().unwrap();
assert_eq!(resolved, 1);
let refs = db.refs("helper", None).unwrap();
let call_edge = refs
.iter()
.find(|(e, _)| e.kind == EdgeKind::Calls)
.unwrap();
assert_eq!(call_edge.0.target_id.as_ref().unwrap(), &same_file.id);
}
#[test]
fn test_resolve_edges_php_fqcn_target_same_file() {
let db = Database::open_memory().unwrap();
let base = test_symbol("BaseService", SymbolKind::Class, "auth/service.php", 1);
let child = test_symbol("AuthService", SymbolKind::Class, "auth/service.php", 30);
db.insert_symbols(&[base.clone(), child.clone()]).unwrap();
db.insert_edge(&Edge::new(
&child.id,
"App\\Auth\\BaseService",
EdgeKind::Inherits,
"auth/service.php",
30,
))
.unwrap();
let resolved = db.resolve_edges().unwrap();
assert_eq!(resolved, 1);
let refs = db.refs("App\\Auth\\BaseService", None).unwrap();
assert_eq!(refs[0].0.target_id.as_ref().unwrap(), &base.id);
}
#[test]
fn test_resolve_edges_php_fqcn_target_prefers_class_over_import_symbol() {
let db = Database::open_memory().unwrap();
let class_sym = test_symbol("AppError", SymbolKind::Class, "exceptions.php", 1);
let child = test_symbol("TokenError", SymbolKind::Class, "auth/tokens.php", 10);
let import_sym = test_symbol("App\\AppError", SymbolKind::Import, "auth/tokens.php", 1);
db.insert_symbols(&[class_sym.clone(), child.clone(), import_sym])
.unwrap();
db.insert_edge(&Edge::new(
&child.id,
"App\\AppError",
EdgeKind::Inherits,
"auth/tokens.php",
10,
))
.unwrap();
db.resolve_edges().unwrap();
let refs = db.refs("App\\AppError", None).unwrap();
let inherits = refs
.iter()
.find(|(e, _)| e.kind == EdgeKind::Inherits)
.unwrap();
assert_eq!(inherits.0.target_id.as_ref().unwrap(), &class_sym.id);
}
#[test]
fn test_hierarchy_finds_children_of_fqcn_resolved_target() {
let db = Database::open_memory().unwrap();
let base = test_symbol("BaseService", SymbolKind::Class, "auth/service.php", 1);
let child = test_symbol(
"PaymentProcessor",
SymbolKind::Class,
"services/payment.php",
5,
);
db.insert_symbols(&[base.clone(), child.clone()]).unwrap();
db.insert_edge(&Edge::new(
&child.id,
"App\\Auth\\BaseService",
EdgeKind::Inherits,
"services/payment.php",
5,
))
.unwrap();
db.resolve_edges().unwrap();
let pairs = db.hierarchy("BaseService").unwrap();
assert_eq!(
pairs,
vec![("PaymentProcessor".to_string(), "BaseService".to_string())]
);
}
#[test]
fn test_resolve_edges_class_over_constructor() {
let db = Database::open_memory().unwrap();
let caller = test_symbol("handleLogin", SymbolKind::Method, "auth/Service.java", 10);
let logger_class = test_symbol("Logger", SymbolKind::Class, "util/Logger.java", 1);
let logger_ctor = test_symbol("Logger", SymbolKind::Method, "util/Logger.java", 5);
db.insert_symbols(&[caller.clone(), logger_class.clone(), logger_ctor])
.unwrap();
let edge = Edge {
source_id: caller.id.clone(),
target_name: "Logger".to_string(),
target_id: None,
kind: EdgeKind::References,
file_path: "auth/Service.java".to_string(),
line: 12,
provenance: None,
};
db.insert_edge(&edge).unwrap();
let resolved = db.resolve_edges().unwrap();
assert_eq!(resolved, 1);
let refs = db.refs("Logger", None).unwrap();
let ref_edge = refs
.iter()
.find(|(e, _)| e.kind == EdgeKind::References)
.unwrap();
assert_eq!(ref_edge.0.target_id.as_ref().unwrap(), &logger_class.id);
}
#[test]
fn test_resolve_edges_class_over_constructor_still_ambiguous_with_three() {
let db = Database::open_memory().unwrap();
let caller = test_symbol("main", SymbolKind::Function, "app.java", 1);
let sym_class = test_symbol("Foo", SymbolKind::Class, "a/Foo.java", 1);
let sym_ctor = test_symbol("Foo", SymbolKind::Method, "a/Foo.java", 5);
let sym_func = test_symbol("Foo", SymbolKind::Function, "b/Foo.java", 1);
db.insert_symbols(&[caller.clone(), sym_class, sym_ctor, sym_func])
.unwrap();
let edge = Edge {
source_id: caller.id.clone(),
target_name: "Foo".to_string(),
target_id: None,
kind: EdgeKind::Calls,
file_path: "app.java".to_string(),
line: 5,
provenance: None,
};
db.insert_edge(&edge).unwrap();
let resolved = db.resolve_edges().unwrap();
assert_eq!(resolved, 0);
}
#[test]
fn test_resolve_edges_multipass_import_then_call() {
let db = Database::open_memory().unwrap();
let import_sym = test_symbol("util.Logger", SymbolKind::Import, "auth/service.java", 1);
let caller = test_symbol("authenticate", SymbolKind::Method, "auth/service.java", 10);
let logger_class = test_symbol("Logger", SymbolKind::Class, "util/Logger.java", 1);
let logger_ctor = test_symbol("Logger", SymbolKind::Method, "util/Logger.java", 5);
db.insert_symbols(&[
import_sym.clone(),
caller.clone(),
logger_class.clone(),
logger_ctor,
])
.unwrap();
let import_edge = Edge {
source_id: import_sym.id.clone(),
target_name: "Logger".to_string(),
target_id: None,
kind: EdgeKind::Imports,
file_path: "auth/service.java".to_string(),
line: 1,
provenance: None,
};
db.insert_edge(&import_edge).unwrap();
let ref_edge = Edge {
source_id: caller.id.clone(),
target_name: "Logger".to_string(),
target_id: None,
kind: EdgeKind::References,
file_path: "auth/service.java".to_string(),
line: 15,
provenance: None,
};
db.insert_edge(&ref_edge).unwrap();
let resolved = db.resolve_edges().unwrap();
assert_eq!(resolved, 2);
let refs = db.refs("Logger", None).unwrap();
let reference = refs
.iter()
.find(|(e, _)| e.kind == EdgeKind::References)
.unwrap();
assert_eq!(reference.0.target_id.as_ref().unwrap(), &logger_class.id);
}
#[test]
fn test_resolve_edges_function_over_method() {
let db = Database::open_memory().unwrap();
let caller = test_symbol("process", SymbolKind::Function, "app/main.rb", 1);
let top_fn = test_symbol("get_logger", SymbolKind::Function, "utils/helpers.rb", 6);
let mod_method = test_symbol("get_logger", SymbolKind::Method, "utils/logging.rb", 6);
db.insert_symbols(&[caller.clone(), top_fn.clone(), mod_method])
.unwrap();
let edge = Edge {
source_id: caller.id.clone(),
target_name: "get_logger".to_string(),
target_id: None,
kind: EdgeKind::Calls,
file_path: "app/main.rb".to_string(),
line: 5,
provenance: None,
};
db.insert_edge(&edge).unwrap();
let resolved = db.resolve_edges().unwrap();
assert_eq!(resolved, 1);
let refs = db.refs("get_logger", None).unwrap();
let call_edge = refs
.iter()
.find(|(e, _)| e.kind == EdgeKind::Calls)
.unwrap();
assert_eq!(call_edge.0.target_id.as_ref().unwrap(), &top_fn.id);
}
#[test]
fn test_resolve_edges_two_functions_still_ambiguous() {
let db = Database::open_memory().unwrap();
let caller = test_symbol("main", SymbolKind::Function, "app.rb", 1);
let fn1 = test_symbol("helper", SymbolKind::Function, "a/utils.rb", 1);
let fn2 = test_symbol("helper", SymbolKind::Function, "b/utils.rb", 1);
db.insert_symbols(&[caller.clone(), fn1, fn2]).unwrap();
let edge = Edge {
source_id: caller.id.clone(),
target_name: "helper".to_string(),
target_id: None,
kind: EdgeKind::Calls,
file_path: "app.rb".to_string(),
line: 5,
provenance: None,
};
db.insert_edge(&edge).unwrap();
let resolved = db.resolve_edges().unwrap();
assert_eq!(resolved, 0);
}
#[test]
fn test_callees_query() {
let db = Database::open_memory().unwrap();
let caller = test_symbol("process", SymbolKind::Function, "a.py", 1);
let callee1 = test_symbol("fetch", SymbolKind::Function, "b.py", 1);
let callee2 = test_symbol("save", SymbolKind::Function, "c.py", 1);
db.insert_symbols(&[caller.clone(), callee1, callee2])
.unwrap();
db.insert_edges(&[
Edge {
source_id: caller.id.clone(),
target_name: "fetch".to_string(),
target_id: None,
kind: EdgeKind::Calls,
file_path: "a.py".to_string(),
line: 5,
provenance: None,
},
Edge {
source_id: caller.id.clone(),
target_name: "save".to_string(),
target_id: None,
kind: EdgeKind::Calls,
file_path: "a.py".to_string(),
line: 6,
provenance: None,
},
])
.unwrap();
let callees = db.callees("process").unwrap();
assert_eq!(callees.len(), 2);
let targets: Vec<&str> = callees.iter().map(|e| e.target_name.as_str()).collect();
assert!(targets.contains(&"fetch"));
assert!(targets.contains(&"save"));
}
#[test]
fn test_invalidate_dangling_edges_after_symbol_removal() {
let db = Database::open_memory().unwrap();
let sym_a = test_symbol("foo", SymbolKind::Function, "a.py", 1);
db.insert_symbol(&sym_a).unwrap();
let sym_b = test_symbol("bar", SymbolKind::Function, "b.py", 1);
db.insert_symbol(&sym_b).unwrap();
let edge = Edge::new(&sym_b.id, "foo", EdgeKind::Calls, "b.py", 5);
db.insert_edge(&edge).unwrap();
let resolved = db.resolve_edges().unwrap();
assert_eq!(resolved, 1);
db.conn
.execute("DELETE FROM symbols WHERE id = ?1", params![sym_a.id])
.unwrap();
let dirty = std::collections::HashSet::from(["a.py".to_string()]);
let invalidated = db.invalidate_edges_targeting(&dirty).unwrap();
assert_eq!(invalidated, 1);
let edges = db.callees("bar").unwrap();
assert!(
edges.iter().all(|e| e.target_id.is_none()),
"edge should be unresolved after invalidation"
);
}
#[test]
fn test_scoped_resolution_after_symbol_changes() {
let db = Database::open_memory().unwrap();
let sym_a = test_symbol("foo", SymbolKind::Function, "a.py", 1);
db.insert_symbol(&sym_a).unwrap();
let sym_b = test_symbol("bar", SymbolKind::Function, "b.py", 1);
db.insert_symbol(&sym_b).unwrap();
db.insert_edge(&Edge::new(&sym_b.id, "foo", EdgeKind::Calls, "b.py", 5))
.unwrap();
db.resolve_edges().unwrap();
db.delete_symbol(&sym_a.id).unwrap();
db.insert_symbol(&sym_a).unwrap();
let dirty = std::collections::HashSet::from(["a.py".to_string()]);
let re_resolved = db.resolve_edges_scoped(&dirty).unwrap();
assert_eq!(re_resolved, 1);
}
#[test]
fn test_compute_in_degrees_scoped() {
let db = Database::open_memory().unwrap();
let foo = test_symbol("foo", SymbolKind::Function, "a.py", 1);
let bar = test_symbol("bar", SymbolKind::Function, "b.py", 1);
let baz = test_symbol("baz", SymbolKind::Function, "c.py", 1);
db.insert_symbol(&foo).unwrap();
db.insert_symbol(&bar).unwrap();
db.insert_symbol(&baz).unwrap();
db.insert_edge(&Edge::new(&bar.id, "foo", EdgeKind::Calls, "b.py", 5))
.unwrap();
db.insert_edge(&Edge::new(&baz.id, "foo", EdgeKind::Calls, "c.py", 3))
.unwrap();
db.resolve_edges().unwrap();
db.compute_in_degrees().unwrap();
let results = db.search("foo", None, None, 10).unwrap();
assert_eq!(results[0].in_degree, 2);
let dirty = std::collections::HashSet::from(["b.py".to_string()]);
db.compute_in_degrees_scoped(&dirty).unwrap();
let results = db.search("foo", None, None, 10).unwrap();
assert_eq!(results[0].in_degree, 2);
}
#[test]
fn test_tier2_import_resolution_plan_uses_kind_target_index() {
let db = Database::open_memory().unwrap();
let mut stmt = db
.conn
.prepare(
"EXPLAIN QUERY PLAN SELECT s.id FROM symbols s
INNER JOIN edges ie ON ie.kind = 'imports' AND ie.target_name = ?1
AND ie.target_id IS NOT NULL
INNER JOIN symbols is2 ON is2.id = ie.source_id AND is2.file_path = ?2
INNER JOIN symbols resolved ON resolved.id = ie.target_id
WHERE s.name = ?1 AND s.kind != 'import'
AND s.file_path = resolved.file_path
LIMIT 1",
)
.unwrap();
let plan = stmt
.query_map(params!["x", "y"], |row| row.get::<_, String>(3))
.unwrap()
.collect::<std::result::Result<Vec<_>, _>>()
.unwrap()
.join("\n");
assert!(
plan.contains("idx_edges_kind_target"),
"tier-2 must drive off edges(kind, target_name); got plan:\n{plan}"
);
}
#[test]
fn test_refs_plan_uses_multi_index_or_not_full_scan() {
let db = Database::open_memory().unwrap();
let syms: Vec<Symbol> = (0..400)
.map(|i| test_symbol(&format!("s{i}"), SymbolKind::Function, "a.py", i))
.collect();
db.insert_symbols(&syms).unwrap();
let edges: Vec<Edge> = (0..400)
.map(|i| {
let mut e = Edge::new(
&syms[i as usize].id,
format!("t{i}"),
EdgeKind::Calls,
"a.py",
i,
);
if i % 2 == 0 {
e.target_id = Some(syms[i as usize].id.clone());
}
e
})
.collect();
db.insert_edges(&edges).unwrap();
db.conn.execute_batch("ANALYZE;").unwrap();
let explain = |sql: &str| -> String {
let mut stmt = db.conn.prepare(sql).unwrap();
stmt.query_map(params!["x"], |row| row.get::<_, String>(3))
.unwrap()
.collect::<std::result::Result<Vec<_>, _>>()
.unwrap()
.join("\n")
};
let assert_no_edge_scan = |plan: &str, ctx: &str| {
assert!(
!plan.contains("SCAN e\n") && !plan.ends_with("SCAN e") && !plan.contains("SCAN edges"),
"refs() {ctx} must not full-scan edges; got plan:\n{plan}"
);
};
let unfiltered = explain(
"EXPLAIN QUERY PLAN
SELECT e.id FROM edges e
LEFT JOIN symbols s ON e.source_id = s.id
WHERE e.target_name = ?1
OR e.target_id IN (SELECT id FROM symbols WHERE name = ?1)",
);
assert!(
unfiltered.contains("MULTI-INDEX OR"),
"refs() unfiltered must use a multi-index OR; got plan:\n{unfiltered}"
);
assert!(
unfiltered.contains("idx_edges_target (target_name="),
"refs() literal arm must seek idx_edges_target on target_name; got plan:\n{unfiltered}"
);
assert!(
unfiltered.contains("idx_edges_target_id (target_id="),
"refs() resolved arm must seek idx_edges_target_id on target_id; got plan:\n{unfiltered}"
);
assert_no_edge_scan(&unfiltered, "unfiltered");
let kind_filtered = explain(
"EXPLAIN QUERY PLAN
SELECT e.id FROM edges e
LEFT JOIN symbols s ON e.source_id = s.id
WHERE (e.target_name = ?1 AND e.kind = 'calls')
OR (e.target_id IN (SELECT id FROM symbols WHERE name = ?1)
AND e.kind = 'calls')",
);
assert!(
kind_filtered.contains("MULTI-INDEX OR"),
"refs() kind-filtered must use a multi-index OR; got plan:\n{kind_filtered}"
);
assert!(
kind_filtered.contains("idx_edges_kind_target (kind=? AND target_name="),
"refs() kind-filtered literal arm must seek (kind, target_name); got plan:\n{kind_filtered}"
);
assert!(
kind_filtered.contains("idx_edges_target_id (target_id="),
"refs() kind-filtered resolved arm must seek target_id; got plan:\n{kind_filtered}"
);
assert_no_edge_scan(&kind_filtered, "kind-filtered");
}
#[test]
fn test_impact_recursive_step_avoids_full_edge_scan() {
let db = Database::open_memory().unwrap();
let mut stmt = db
.conn
.prepare(
"EXPLAIN QUERY PLAN
WITH RECURSIVE impacted(edge_id, source_id, target_name, target_id,
kind, file_path, line, resolution_source, source_name, depth) AS (
SELECT e.id, e.source_id, e.target_name, e.target_id, e.kind,
e.file_path, e.line, e.resolution_source, s.name, 1
FROM edges e LEFT JOIN symbols s ON e.source_id = s.id
WHERE e.target_name = ?1
OR e.target_id IN (SELECT id FROM symbols WHERE name = ?1)
UNION
SELECT e.id, e.source_id, e.target_name, e.target_id, e.kind,
e.file_path, e.line, e.resolution_source, s.name, i.depth + 1
FROM impacted i
JOIN edges e ON e.target_name = i.source_name
LEFT JOIN symbols s ON e.source_id = s.id
WHERE i.source_name IS NOT NULL AND i.depth < ?2
UNION
SELECT e.id, e.source_id, e.target_name, e.target_id, e.kind,
e.file_path, e.line, e.resolution_source, s.name, i.depth + 1
FROM impacted i
JOIN symbols t ON t.name = i.source_name
JOIN edges e ON e.target_id = t.id
LEFT JOIN symbols s ON e.source_id = s.id
WHERE i.source_name IS NOT NULL AND i.depth < ?2)
SELECT source_id, MIN(depth) FROM impacted GROUP BY edge_id
ORDER BY depth, edge_id",
)
.unwrap();
let plan = stmt
.query_map(params!["x", 3], |row| row.get::<_, String>(3))
.unwrap()
.collect::<std::result::Result<Vec<_>, _>>()
.unwrap()
.join("\n");
assert!(
plan.contains("idx_edges_target (target_name="),
"impact() literal arm must seek idx_edges_target on target_name; got plan:\n{plan}"
);
assert!(
plan.contains("idx_edges_target_id (target_id="),
"impact() resolved arm must seek idx_edges_target_id on target_id; got plan:\n{plan}"
);
assert!(
!plan.contains("CORRELATED"),
"impact() must not run a correlated subquery per edge; got plan:\n{plan}"
);
assert!(
!plan.contains("SCAN e\n") && !plan.ends_with("SCAN e") && !plan.contains("SCAN edges"),
"impact() must not full-scan edges; got plan:\n{plan}"
);
}
#[test]
fn test_per_file_edge_delete_uses_file_index() {
let db = Database::open_memory().unwrap();
let mut stmt = db
.conn
.prepare("EXPLAIN QUERY PLAN DELETE FROM edges WHERE file_path = ?1")
.unwrap();
let plan = stmt
.query_map(params!["a.py"], |row| row.get::<_, String>(3))
.unwrap()
.collect::<std::result::Result<Vec<_>, _>>()
.unwrap()
.join("\n");
assert!(
plan.contains("idx_edges_file"),
"per-file edge delete must drive off edges(file_path); got plan:\n{plan}"
);
}
#[test]
fn test_compute_in_degrees_plan_has_no_correlated_subquery() {
let db = Database::open_memory().unwrap();
let mut stmt = db
.conn
.prepare(
"EXPLAIN QUERY PLAN
UPDATE symbols SET in_degree = counts.cnt
FROM (
SELECT target_id, COUNT(*) AS cnt
FROM edges WHERE target_id IS NOT NULL
GROUP BY target_id
) AS counts
WHERE symbols.id = counts.target_id",
)
.unwrap();
let plan = stmt
.query_map([], |row| row.get::<_, String>(3))
.unwrap()
.collect::<std::result::Result<Vec<_>, _>>()
.unwrap()
.join("\n");
assert!(
!plan.to_uppercase().contains("CORRELATED"),
"in-degree UPDATE must not use a correlated subquery; got plan:\n{plan}"
);
}
#[test]
fn test_compute_in_degrees_scoped_resets_target_that_lost_edge() {
let db = Database::open_memory().unwrap();
let foo = test_symbol("foo", SymbolKind::Function, "a.py", 1);
let bar = test_symbol("bar", SymbolKind::Function, "b.py", 1);
let baz = test_symbol("baz", SymbolKind::Function, "c.py", 1);
db.insert_symbol(&foo).unwrap();
db.insert_symbol(&bar).unwrap();
db.insert_symbol(&baz).unwrap();
db.insert_edge(&Edge::new(&bar.id, "foo", EdgeKind::Calls, "b.py", 5))
.unwrap();
db.insert_edge(&Edge::new(&baz.id, "foo", EdgeKind::Calls, "c.py", 3))
.unwrap();
db.resolve_edges().unwrap();
db.compute_in_degrees().unwrap();
let results = db.search("foo", None, None, 10).unwrap();
assert_eq!(results[0].in_degree, 2);
db.clear_edges_for_file("b.py").unwrap();
let dirty = std::collections::HashSet::from(["b.py".to_string()]);
db.invalidate_edges_targeting(&dirty).unwrap();
db.resolve_edges_scoped(&dirty).unwrap();
db.compute_in_degrees_scoped(&dirty).unwrap();
let results = db.search("foo", None, None, 10).unwrap();
assert_eq!(results[0].in_degree, 1);
}