1use std::collections::{BTreeMap, HashSet};
9
10use crate::config::Context;
11use crate::graph::code_graph;
12use crate::search::fts;
13use crate::visibility;
14
15pub fn graph_boost(ctx: &Context, conn: Option<&mut postgres::Client>, query: &str) -> Vec<String> {
20 if ctx.falkordb.is_none() {
21 return vec![];
22 }
23
24 let Some(conn) = conn else {
25 return vec![];
26 };
27 let mut resolved =
28 fts::search_symbols_exact_first_visible(conn, query, ctx, None, None, &[], 1).results;
29 let Some(symbol) = resolved.pop() else {
30 return vec![];
31 };
32 let graph_ctx = visibility::context_for_source_project(ctx, &symbol.project_id);
33
34 let callers = code_graph::find_caller_ids(&graph_ctx, &symbol.id, 10).unwrap_or_default();
35 let usages = code_graph::find_usage_ids(&graph_ctx, &symbol.id, 10).unwrap_or_default();
36
37 let mut ids = Vec::new();
38 let mut seen = HashSet::new();
39 for id in callers.into_iter().chain(usages) {
40 if !id.is_empty() && seen.insert(id.clone()) {
41 ids.push(id);
42 }
43 }
44 ids
45}
46
47pub fn graph_expand(
54 ctx: &Context,
55 conn: Option<&mut postgres::Client>,
56 seed_ids: &[String],
57) -> Vec<String> {
58 if seed_ids.is_empty() || ctx.falkordb.is_none() {
59 return vec![];
60 }
61
62 let mut by_project: BTreeMap<String, Vec<String>> = BTreeMap::new();
63 let Some(conn) = conn else {
64 return vec![];
65 };
66 if let Ok(symbols) = visibility::visible_symbols_by_ids(conn, ctx, seed_ids) {
67 for symbol in symbols {
68 by_project
69 .entry(symbol.project_id)
70 .or_default()
71 .push(symbol.id);
72 }
73 }
74
75 graph_expand_grouped(ctx, by_project, |graph_ctx, ids_for_project| {
76 let callees =
78 code_graph::find_callee_ids_batch(graph_ctx, ids_for_project, 30).unwrap_or_default();
79 let callers =
81 code_graph::find_caller_ids_batch(graph_ctx, ids_for_project, 30).unwrap_or_default();
82 (callees, callers)
83 })
84}
85
86fn graph_expand_grouped(
87 ctx: &Context,
88 by_project: BTreeMap<String, Vec<String>>,
89 mut graph_neighbors: impl FnMut(&Context, &[String]) -> (Vec<String>, Vec<String>),
90) -> Vec<String> {
91 let mut ids = Vec::new();
92 let mut seen = HashSet::new();
93 for (project_id, ids_for_project) in by_project {
94 let graph_ctx = visibility::context_for_source_project(ctx, &project_id);
95 let (callees, callers) = graph_neighbors(&graph_ctx, &ids_for_project);
96 for id in callees.into_iter().chain(callers) {
97 if id.is_empty() || !seen.insert(id.clone()) {
98 continue;
99 }
100 ids.push(id);
101 }
102 }
103 ids
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use std::path::PathBuf;
110
111 fn make_ctx_no_falkordb() -> Context {
112 Context {
113 database_url: "postgresql://localhost/nonexistent".to_string(),
114 project_root: PathBuf::from("/nonexistent"),
115 project_id: "test".to_string(),
116 quiet: true,
117 falkordb: None,
118 qdrant: None,
119 embedding: None,
120 code_vectors: crate::config::CodeVectorSettings::default(),
121 daemon_url: None,
122 index_scope: crate::config::ProjectIndexScope::Single,
123 }
124 }
125
126 fn make_ctx_with_overlay() -> Context {
127 Context {
128 database_url: "postgresql://localhost/nonexistent".to_string(),
129 project_root: PathBuf::from("/overlay"),
130 project_id: "overlay".to_string(),
131 quiet: true,
132 falkordb: Some(crate::config::FalkorConfig {
133 host: "127.0.0.1".to_string(),
134 port: 16379,
135 password: None,
136 graph_name: "g".to_string(),
137 }),
138 qdrant: None,
139 embedding: None,
140 code_vectors: crate::config::CodeVectorSettings::default(),
141 daemon_url: None,
142 index_scope: crate::config::ProjectIndexScope::Overlay {
143 overlay_project_id: "overlay".to_string(),
144 overlay_root: PathBuf::from("/overlay"),
145 parent_project_id: "parent".to_string(),
146 parent_root: PathBuf::from("/parent"),
147 },
148 }
149 }
150
151 #[test]
152 fn test_graph_boost_no_falkordb() {
153 let ctx = make_ctx_no_falkordb();
154 let result = graph_boost(&ctx, None, "some_function");
155 assert!(result.is_empty());
156 }
157
158 #[test]
159 fn test_graph_expand_no_falkordb() {
160 let ctx = make_ctx_no_falkordb();
161 let result = graph_expand(&ctx, None, &["some_function".to_string()]);
162 assert!(result.is_empty());
163 }
164
165 #[test]
166 fn test_graph_expand_empty_seeds() {
167 let ctx = make_ctx_no_falkordb();
168 let result = graph_expand(&ctx, None, &[]);
169 assert!(result.is_empty());
170 }
171
172 #[test]
173 fn graph_expand_grouped_expands_each_project_scope_and_dedupes() {
174 let ctx = make_ctx_with_overlay();
175 let by_project = BTreeMap::from([
176 (
177 "overlay".to_string(),
178 vec!["overlay-seed-1".to_string(), "overlay-seed-2".to_string()],
179 ),
180 ("parent".to_string(), vec!["parent-seed".to_string()]),
181 ]);
182 let mut calls = Vec::new();
183
184 let expanded = graph_expand_grouped(&ctx, by_project, |graph_ctx, ids| {
185 calls.push((graph_ctx.project_id.clone(), ids.to_vec()));
186 match graph_ctx.project_id.as_str() {
187 "overlay" => (
188 vec!["impl-a".to_string(), "shared".to_string()],
189 vec!["caller-a".to_string(), "shared".to_string()],
190 ),
191 "parent" => (
192 vec!["parent-impl".to_string(), "impl-a".to_string()],
193 vec!["".to_string(), "parent-caller".to_string()],
194 ),
195 other => panic!("unexpected project {other}"),
196 }
197 });
198
199 assert_eq!(
200 calls,
201 vec![
202 (
203 "overlay".to_string(),
204 vec!["overlay-seed-1".to_string(), "overlay-seed-2".to_string()]
205 ),
206 ("parent".to_string(), vec!["parent-seed".to_string()])
207 ]
208 );
209 assert_eq!(
210 expanded,
211 vec![
212 "impl-a".to_string(),
213 "shared".to_string(),
214 "caller-a".to_string(),
215 "parent-impl".to_string(),
216 "parent-caller".to_string()
217 ]
218 );
219 }
220}