Skip to main content

gobby_code/graph/code_graph/read/
graph_payloads.rs

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                &center_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}