1use crate::graph::node::Language;
21use crate::graph::unified::concurrent::CodeGraph;
22use crate::graph::unified::node::{NodeId, NodeKind};
23use crate::graph::unified::storage::arena::NodeEntry;
24use crate::query::pipeline::AggregationResult;
25use crate::query::types::JoinEdgeKind;
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28
29pub struct QueryResults {
35 graph: Arc<CodeGraph>,
37 matches: Vec<NodeId>,
39 workspace_root: Option<PathBuf>,
41}
42
43impl QueryResults {
44 #[must_use]
46 pub fn new(graph: Arc<CodeGraph>, matches: Vec<NodeId>) -> Self {
47 Self {
48 graph,
49 matches,
50 workspace_root: None,
51 }
52 }
53
54 #[must_use]
56 pub fn with_workspace_root(mut self, root: PathBuf) -> Self {
57 self.workspace_root = Some(root);
58 self
59 }
60
61 #[inline]
63 #[must_use]
64 pub fn len(&self) -> usize {
65 self.matches.len()
66 }
67
68 #[inline]
70 #[must_use]
71 pub fn is_empty(&self) -> bool {
72 self.matches.is_empty()
73 }
74
75 #[must_use]
77 pub fn node_ids(&self) -> &[NodeId] {
78 &self.matches
79 }
80
81 #[must_use]
83 pub fn graph(&self) -> &CodeGraph {
84 &self.graph
85 }
86
87 #[cfg(test)]
94 #[must_use]
95 pub(crate) fn graph_arc_for_test(&self) -> &Arc<CodeGraph> {
96 &self.graph
97 }
98
99 #[must_use]
101 pub fn workspace_root(&self) -> Option<&Path> {
102 self.workspace_root.as_deref()
103 }
104
105 pub fn iter(&self) -> impl Iterator<Item = QueryMatch<'_>> + '_ {
107 self.matches.iter().filter_map(|&id| {
108 self.graph.nodes().get(id).map(|entry| QueryMatch {
109 id,
110 entry,
111 graph: &self.graph,
112 workspace_root: self.workspace_root.as_deref(),
113 })
114 })
115 }
116
117 pub fn sort_by_location(&mut self) {
121 let graph = &self.graph;
122 self.matches.sort_by(|&a, &b| {
123 let ea = graph.nodes().get(a);
124 let eb = graph.nodes().get(b);
125 match (ea, eb) {
126 (Some(ea), Some(eb)) => {
127 let path_a = graph.files().resolve(ea.file);
129 let path_b = graph.files().resolve(eb.file);
130 let name_a = graph.strings().resolve(ea.name);
132 let name_b = graph.strings().resolve(eb.name);
133 path_a
134 .cmp(&path_b)
135 .then(ea.start_line.cmp(&eb.start_line))
136 .then(name_a.cmp(&name_b))
137 }
138 _ => std::cmp::Ordering::Equal,
139 }
140 });
141 }
142
143 #[must_use]
145 pub fn into_node_ids(self) -> Vec<NodeId> {
146 self.matches
147 }
148
149 #[must_use]
151 pub fn into_parts(self) -> (Arc<CodeGraph>, Vec<NodeId>) {
152 (self.graph, self.matches)
153 }
154}
155
156impl std::fmt::Debug for QueryResults {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 f.debug_struct("QueryResults")
159 .field("match_count", &self.matches.len())
160 .field("graph", &self.graph)
161 .field("workspace_root", &self.workspace_root)
162 .finish()
163 }
164}
165
166pub struct QueryMatch<'a> {
171 pub id: NodeId,
173 pub entry: &'a NodeEntry,
175 graph: &'a CodeGraph,
177 workspace_root: Option<&'a Path>,
179}
180
181impl QueryMatch<'_> {
182 #[must_use]
184 pub fn name(&self) -> Option<Arc<str>> {
185 self.graph.strings().resolve(self.entry.name)
186 }
187
188 #[must_use]
190 pub fn kind(&self) -> NodeKind {
191 self.entry.kind
192 }
193
194 #[must_use]
196 pub fn file_path(&self) -> Option<Arc<Path>> {
197 self.graph.files().resolve(self.entry.file)
198 }
199
200 #[must_use]
202 pub fn relative_path(&self) -> Option<PathBuf> {
203 let path = self.file_path()?;
204 if let Some(root) = self.workspace_root {
205 path.strip_prefix(root)
206 .ok()
207 .map(std::path::Path::to_path_buf)
208 } else {
209 Some(path.to_path_buf())
210 }
211 }
212
213 #[inline]
215 #[must_use]
216 pub fn start_line(&self) -> u32 {
217 self.entry.start_line
218 }
219
220 #[inline]
222 #[must_use]
223 pub fn end_line(&self) -> u32 {
224 self.entry.end_line
225 }
226
227 #[inline]
229 #[must_use]
230 pub fn start_column(&self) -> u32 {
231 self.entry.start_column
232 }
233
234 #[inline]
236 #[must_use]
237 pub fn end_column(&self) -> u32 {
238 self.entry.end_column
239 }
240
241 #[inline]
243 #[must_use]
244 pub fn start_byte(&self) -> u32 {
245 self.entry.start_byte
246 }
247
248 #[inline]
250 #[must_use]
251 pub fn end_byte(&self) -> u32 {
252 self.entry.end_byte
253 }
254
255 #[must_use]
259 pub fn visibility(&self) -> Option<Arc<str>> {
260 self.entry
261 .visibility
262 .and_then(|id| self.graph.strings().resolve(id))
263 }
264
265 #[must_use]
267 pub fn signature(&self) -> Option<Arc<str>> {
268 self.entry
269 .signature
270 .and_then(|id| self.graph.strings().resolve(id))
271 }
272
273 #[must_use]
275 pub fn qualified_name(&self) -> Option<Arc<str>> {
276 self.entry
277 .qualified_name
278 .and_then(|id| self.graph.strings().resolve(id))
279 }
280
281 #[must_use]
283 pub fn doc(&self) -> Option<Arc<str>> {
284 self.entry
285 .doc
286 .and_then(|id| self.graph.strings().resolve(id))
287 }
288
289 #[inline]
291 #[must_use]
292 pub fn is_async(&self) -> bool {
293 self.entry.is_async
294 }
295
296 #[inline]
298 #[must_use]
299 pub fn is_static(&self) -> bool {
300 self.entry.is_static
301 }
302
303 #[must_use]
308 pub fn language(&self) -> Option<Language> {
309 self.graph.files().language_for_file(self.entry.file)
310 }
311
312 #[must_use]
314 pub fn graph(&self) -> &CodeGraph {
315 self.graph
316 }
317}
318
319impl std::fmt::Debug for QueryMatch<'_> {
320 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321 f.debug_struct("QueryMatch")
322 .field("id", &self.id)
323 .field("kind", &self.entry.kind)
324 .field("name", &self.name())
325 .field("start_line", &self.entry.start_line)
326 .finish()
327 }
328}
329
330pub struct JoinResults {
335 graph: Arc<CodeGraph>,
337 pairs: Vec<(NodeId, NodeId)>,
339 edge_kind: JoinEdgeKind,
341 workspace_root: Option<PathBuf>,
343 truncated: bool,
345}
346
347impl JoinResults {
348 #[must_use]
350 pub fn new(
351 graph: Arc<CodeGraph>,
352 pairs: Vec<(NodeId, NodeId)>,
353 edge_kind: JoinEdgeKind,
354 truncated: bool,
355 ) -> Self {
356 Self {
357 graph,
358 pairs,
359 edge_kind,
360 workspace_root: None,
361 truncated,
362 }
363 }
364
365 #[must_use]
367 pub fn with_workspace_root(mut self, root: PathBuf) -> Self {
368 self.workspace_root = Some(root);
369 self
370 }
371
372 #[inline]
374 #[must_use]
375 pub fn len(&self) -> usize {
376 self.pairs.len()
377 }
378
379 #[inline]
381 #[must_use]
382 pub fn is_empty(&self) -> bool {
383 self.pairs.is_empty()
384 }
385
386 #[must_use]
388 pub fn edge_kind(&self) -> &JoinEdgeKind {
389 &self.edge_kind
390 }
391
392 #[must_use]
394 pub fn truncated(&self) -> bool {
395 self.truncated
396 }
397
398 #[must_use]
400 pub fn graph(&self) -> &CodeGraph {
401 &self.graph
402 }
403
404 pub fn iter(&self) -> impl Iterator<Item = JoinMatch<'_>> + '_ {
406 self.pairs.iter().filter_map(|&(left_id, right_id)| {
407 let left = self.graph.nodes().get(left_id)?;
408 let right = self.graph.nodes().get(right_id)?;
409 Some(JoinMatch {
410 left: QueryMatch {
411 id: left_id,
412 entry: left,
413 graph: &self.graph,
414 workspace_root: self.workspace_root.as_deref(),
415 },
416 right: QueryMatch {
417 id: right_id,
418 entry: right,
419 graph: &self.graph,
420 workspace_root: self.workspace_root.as_deref(),
421 },
422 edge_kind: &self.edge_kind,
423 })
424 })
425 }
426}
427
428impl std::fmt::Debug for JoinResults {
429 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430 f.debug_struct("JoinResults")
431 .field("graph", &self.graph)
432 .field("pairs", &self.pairs)
433 .field("edge_kind", &self.edge_kind)
434 .field("workspace_root", &self.workspace_root)
435 .field("truncated", &self.truncated)
436 .finish()
437 }
438}
439
440pub struct JoinMatch<'a> {
442 pub left: QueryMatch<'a>,
444 pub right: QueryMatch<'a>,
446 pub edge_kind: &'a JoinEdgeKind,
448}
449
450pub enum QueryOutput {
452 Results(QueryResults),
454 Join(JoinResults),
456 Aggregation(AggregationResult),
458}
459
460impl QueryOutput {
461 #[must_use]
463 pub fn len(&self) -> usize {
464 match self {
465 Self::Results(r) => r.len(),
466 Self::Join(j) => j.len(),
467 Self::Aggregation(_) => 0,
468 }
469 }
470
471 #[must_use]
473 pub fn is_empty(&self) -> bool {
474 match self {
475 Self::Results(r) => r.is_empty(),
476 Self::Join(j) => j.is_empty(),
477 Self::Aggregation(_) => false,
478 }
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn test_query_results_empty() {
488 let graph = Arc::new(CodeGraph::new());
489 let results = QueryResults::new(graph, vec![]);
490 assert!(results.is_empty());
491 assert_eq!(results.len(), 0);
492 assert_eq!(results.iter().count(), 0);
493 }
494
495 #[test]
496 fn test_query_results_debug() {
497 let graph = Arc::new(CodeGraph::new());
498 let results = QueryResults::new(graph, vec![]);
499 let debug_str = format!("{results:?}");
500 assert!(debug_str.contains("QueryResults"));
501 assert!(debug_str.contains("match_count"));
502 }
503
504 #[test]
505 fn test_query_results_with_workspace_root() {
506 let graph = Arc::new(CodeGraph::new());
507 let results =
508 QueryResults::new(graph, vec![]).with_workspace_root(PathBuf::from("/test/path"));
509 assert_eq!(results.workspace_root(), Some(Path::new("/test/path")));
510 }
511
512 #[test]
513 fn test_query_results_into_parts() {
514 let graph = Arc::new(CodeGraph::new());
515 let results = QueryResults::new(graph.clone(), vec![]);
516 let (returned_graph, matches) = results.into_parts();
517 assert!(Arc::ptr_eq(&returned_graph, &graph));
518 assert!(matches.is_empty());
519 }
520}