1use crate::config::Context;
2use crate::graph::typed_query;
3
4use super::super::connection::with_optional_core_graph;
5use super::super::payload::{
6 GraphBlastRadiusTarget, GraphLink, GraphNode, GraphPayload, add_link_from_row,
7 add_node_from_row, add_prefixed_node_from_row, row_string_owned, row_to_projection_metadata,
8 row_usize,
9};
10use super::payload_queries::{
11 blast_radius_center_query, blast_radius_file_call_query, blast_radius_file_import_query,
12 file_calls_query, file_symbols_query, project_overview_calls_query,
13 project_overview_defines_query, project_overview_files_query, project_overview_imports_query,
14 symbol_neighbors_query,
15};
16use super::relationship_queries::blast_radius_query;
17use super::support::{clamp_limit, dedupe_limited_blast_rows};
18
19pub fn project_overview_graph(ctx: &Context, limit: usize) -> anyhow::Result<GraphPayload> {
20 with_optional_core_graph(ctx, GraphPayload::default, |client| {
21 let limit = clamp_limit(limit);
22 let link_limit = clamp_limit(limit.saturating_mul(4));
23 let max_nodes = limit.saturating_mul(8);
24
25 let (query, params) = project_overview_files_query(&ctx.project_id, limit);
26 let file_rows = client.query(&query, Some(params))?;
27 let mut payload = GraphPayload::default();
28 for row in &file_rows {
29 add_node_from_row(&mut payload, row, "file");
30 }
31
32 let file_paths = payload
33 .nodes()
34 .iter()
35 .filter(|node| node.node_type == "file")
36 .map(|node| node.id.clone())
37 .collect::<Vec<_>>();
38 if file_paths.is_empty() {
39 return Ok(payload);
40 }
41
42 let (query, params) =
43 project_overview_imports_query(&ctx.project_id, &file_paths, link_limit);
44 for row in client.query(&query, Some(params))? {
45 add_link_from_row(&mut payload, &row);
46 if let Some(module_id) = row_string_owned(&row, &["target"]) {
47 payload.push_node(GraphNode::new(module_id.clone(), module_id, "module"));
48 }
49 if payload.node_count() >= max_nodes {
50 break;
51 }
52 }
53
54 let (query, params) =
55 project_overview_defines_query(&ctx.project_id, &file_paths, link_limit);
56 for row in client.query(&query, Some(params))? {
57 add_link_from_row(&mut payload, &row);
58 if let Some(symbol_id) = row_string_owned(&row, &["target"]) {
59 let mut node = GraphNode::new(
60 symbol_id.clone(),
61 row_string_owned(&row, &["symbol_name"]).unwrap_or(symbol_id),
62 row_string_owned(&row, &["symbol_kind"])
63 .unwrap_or_else(|| "function".to_string()),
64 );
65 node.kind = row_string_owned(&row, &["symbol_kind"]);
66 node.file_path = row_string_owned(&row, &["symbol_file_path", "source"]);
67 node.line_start = row_usize(&row, &["line_start"]);
68 payload.push_node(node);
69 }
70 if payload.node_count() >= max_nodes {
71 break;
72 }
73 }
74
75 let (query, params) =
76 project_overview_calls_query(&ctx.project_id, &file_paths, link_limit);
77 for row in client.query(&query, Some(params))? {
78 add_link_from_row(&mut payload, &row);
79 if let Some(target_id) = row_string_owned(&row, &["target"]) {
80 let mut node = GraphNode::new(
81 target_id.clone(),
82 row_string_owned(&row, &["target_name"]).unwrap_or(target_id),
83 row_string_owned(&row, &["target_type"])
84 .unwrap_or_else(|| "unresolved".to_string()),
85 );
86 node.kind = row_string_owned(&row, &["target_kind"]);
87 node.file_path = row_string_owned(&row, &["target_file_path"]);
88 node.line_start = row_usize(&row, &["target_line_start"]);
89 payload.push_node(node);
90 }
91 if payload.node_count() >= max_nodes {
92 break;
93 }
94 }
95
96 Ok(payload)
97 })
98}
99
100pub fn file_graph(ctx: &Context, file_path: &str) -> anyhow::Result<GraphPayload> {
101 with_optional_core_graph(ctx, GraphPayload::default, |client| {
102 let mut payload = GraphPayload::default();
103 let mut file_node = GraphNode::new(file_path, file_path, "file");
104 file_node.file_path = Some(file_path.to_string());
105 payload.push_node(file_node);
106
107 let (query, params) = file_symbols_query(&ctx.project_id, file_path);
108 for row in client.query(&query, Some(params))? {
109 add_node_from_row(&mut payload, &row, "function");
110 if let Some(symbol_id) = row_string_owned(&row, &["id"]) {
111 let mut link = GraphLink::new(file_path, symbol_id, "DEFINES");
112 link.metadata = row_to_projection_metadata(&row);
113 payload.links.push(link);
114 }
115 }
116
117 let (query, params) = file_calls_query(&ctx.project_id, file_path);
118 for row in client.query(&query, Some(params))? {
119 add_prefixed_node_from_row(&mut payload, &row, "source", "function");
120 add_prefixed_node_from_row(&mut payload, &row, "target", "unresolved");
121 add_link_from_row(&mut payload, &row);
122 }
123
124 Ok(payload)
125 })
126}
127
128pub fn symbol_neighbors(
129 ctx: &Context,
130 symbol_id: &str,
131 limit: usize,
132) -> anyhow::Result<GraphPayload> {
133 with_optional_core_graph(ctx, GraphPayload::default, |client| {
134 let mut payload = GraphPayload::with_center(symbol_id.to_string());
135 let (query, params) = blast_radius_center_query(&ctx.project_id, symbol_id);
136 let center_rows = client.query(&query, Some(params))?;
137 let center_node = center_rows
138 .first()
139 .and_then(|row| GraphNode::from_row(row, "function"))
140 .unwrap_or_else(|| GraphNode::new(symbol_id, symbol_id, "function"));
141 payload.push_node(center_node);
142
143 let (query, params) = symbol_neighbors_query(&ctx.project_id, symbol_id, limit);
144 let rows = client.query(&query, Some(params))?;
145
146 for row in rows {
147 add_node_from_row(&mut payload, &row, "unresolved");
148 let Some(neighbor_id) = row_string_owned(&row, &["id"]) else {
149 continue;
150 };
151 let direction = row_string_owned(&row, &["direction"]).unwrap_or_default();
152 let mut link = if direction == "outgoing" {
153 GraphLink::new(symbol_id, neighbor_id, "CALLS")
154 } else {
155 GraphLink::new(neighbor_id, symbol_id, "CALLS")
156 };
157 link.line = row_usize(&row, &["line"]);
158 link.metadata = row_to_projection_metadata(&row);
159 payload.links.push(link);
160 }
161
162 Ok(payload)
163 })
164}
165
166pub fn blast_radius_graph(
167 ctx: &Context,
168 target: GraphBlastRadiusTarget,
169 depth: usize,
170 limit: usize,
171) -> anyhow::Result<GraphPayload> {
172 with_optional_core_graph(ctx, GraphPayload::default, |client| {
173 let (center_id, mut center_node, rows) = match target {
174 GraphBlastRadiusTarget::SymbolId(symbol_id) => {
175 let (query, params) = blast_radius_center_query(&ctx.project_id, &symbol_id);
176 let center_rows = client.query(&query, Some(params))?;
177 let center_node = center_rows
178 .first()
179 .and_then(|row| GraphNode::from_row(row, "function"))
180 .unwrap_or_else(|| GraphNode::new(&symbol_id, &symbol_id, "function"));
181
182 let query = blast_radius_query(depth, limit);
183 let params =
184 typed_query::string_params(&[("project", &ctx.project_id), ("id", &symbol_id)]);
185 (symbol_id, center_node, client.query(&query, Some(params))?)
186 }
187 GraphBlastRadiusTarget::FilePath(file_path) => {
188 let mut rows = vec![];
189 let (query, params) =
190 blast_radius_file_call_query(&ctx.project_id, &file_path, depth, limit);
191 rows.extend(client.query(&query, Some(params))?);
192 let (query, params) =
193 blast_radius_file_import_query(&ctx.project_id, &file_path, depth, limit);
194 rows.extend(client.query(&query, Some(params))?);
195 let rows = dedupe_limited_blast_rows(rows, limit);
196 let mut center_node = GraphNode::new(&file_path, &file_path, "file");
197 center_node.file_path = Some(file_path.clone());
198 (file_path.clone(), center_node, rows)
199 }
200 };
201
202 center_node.blast_distance = Some(0);
203 let mut payload = GraphPayload::with_center(center_id.clone());
204 payload.push_node(center_node);
205
206 for row in rows {
207 let Some(node_id) = row_string_owned(&row, &["node_id"]) else {
208 continue;
209 };
210 let mut node = GraphNode::new(
211 node_id.clone(),
212 row_string_owned(&row, &["node_name"]).unwrap_or_else(|| node_id.clone()),
213 row_string_owned(&row, &["node_type"]).unwrap_or_else(|| "function".to_string()),
214 );
215 node.kind = row_string_owned(&row, &["kind"]);
216 node.file_path = row_string_owned(&row, &["file_path"]);
217 node.line_start = row_usize(&row, &["line"]);
218 node.blast_distance = row_usize(&row, &["distance"]);
219 payload.push_node(node);
220
221 let relation =
222 row_string_owned(&row, &["rel_type"]).unwrap_or_else(|| "call".to_string());
223 let mut link = GraphLink::new(
224 node_id,
225 ¢er_id,
226 if relation == "call" {
227 "CALLS"
228 } else {
229 "IMPORTS"
230 },
231 );
232 link.distance = row_usize(&row, &["distance"]);
233 link.metadata = row_to_projection_metadata(&row);
234 payload.links.push(link);
235 }
236
237 Ok(payload)
238 })
239}