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 #[must_use]
89 pub fn workspace_root(&self) -> Option<&Path> {
90 self.workspace_root.as_deref()
91 }
92
93 pub fn iter(&self) -> impl Iterator<Item = QueryMatch<'_>> + '_ {
95 self.matches.iter().filter_map(|&id| {
96 self.graph.nodes().get(id).map(|entry| QueryMatch {
97 id,
98 entry,
99 graph: &self.graph,
100 workspace_root: self.workspace_root.as_deref(),
101 })
102 })
103 }
104
105 pub fn sort_by_location(&mut self) {
109 let graph = &self.graph;
110 self.matches.sort_by(|&a, &b| {
111 let ea = graph.nodes().get(a);
112 let eb = graph.nodes().get(b);
113 match (ea, eb) {
114 (Some(ea), Some(eb)) => {
115 let path_a = graph.files().resolve(ea.file);
117 let path_b = graph.files().resolve(eb.file);
118 let name_a = graph.strings().resolve(ea.name);
120 let name_b = graph.strings().resolve(eb.name);
121 path_a
122 .cmp(&path_b)
123 .then(ea.start_line.cmp(&eb.start_line))
124 .then(name_a.cmp(&name_b))
125 }
126 _ => std::cmp::Ordering::Equal,
127 }
128 });
129 }
130
131 #[must_use]
133 pub fn into_node_ids(self) -> Vec<NodeId> {
134 self.matches
135 }
136
137 #[must_use]
139 pub fn into_parts(self) -> (Arc<CodeGraph>, Vec<NodeId>) {
140 (self.graph, self.matches)
141 }
142}
143
144impl std::fmt::Debug for QueryResults {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 f.debug_struct("QueryResults")
147 .field("match_count", &self.matches.len())
148 .field("graph", &self.graph)
149 .field("workspace_root", &self.workspace_root)
150 .finish()
151 }
152}
153
154pub struct QueryMatch<'a> {
159 pub id: NodeId,
161 pub entry: &'a NodeEntry,
163 graph: &'a CodeGraph,
165 workspace_root: Option<&'a Path>,
167}
168
169impl QueryMatch<'_> {
170 #[must_use]
172 pub fn name(&self) -> Option<Arc<str>> {
173 self.graph.strings().resolve(self.entry.name)
174 }
175
176 #[must_use]
178 pub fn kind(&self) -> NodeKind {
179 self.entry.kind
180 }
181
182 #[must_use]
184 pub fn file_path(&self) -> Option<Arc<Path>> {
185 self.graph.files().resolve(self.entry.file)
186 }
187
188 #[must_use]
190 pub fn relative_path(&self) -> Option<PathBuf> {
191 let path = self.file_path()?;
192 if let Some(root) = self.workspace_root {
193 path.strip_prefix(root)
194 .ok()
195 .map(std::path::Path::to_path_buf)
196 } else {
197 Some(path.to_path_buf())
198 }
199 }
200
201 #[inline]
203 #[must_use]
204 pub fn start_line(&self) -> u32 {
205 self.entry.start_line
206 }
207
208 #[inline]
210 #[must_use]
211 pub fn end_line(&self) -> u32 {
212 self.entry.end_line
213 }
214
215 #[inline]
217 #[must_use]
218 pub fn start_column(&self) -> u32 {
219 self.entry.start_column
220 }
221
222 #[inline]
224 #[must_use]
225 pub fn end_column(&self) -> u32 {
226 self.entry.end_column
227 }
228
229 #[inline]
231 #[must_use]
232 pub fn start_byte(&self) -> u32 {
233 self.entry.start_byte
234 }
235
236 #[inline]
238 #[must_use]
239 pub fn end_byte(&self) -> u32 {
240 self.entry.end_byte
241 }
242
243 #[must_use]
247 pub fn visibility(&self) -> Option<Arc<str>> {
248 self.entry
249 .visibility
250 .and_then(|id| self.graph.strings().resolve(id))
251 }
252
253 #[must_use]
255 pub fn signature(&self) -> Option<Arc<str>> {
256 self.entry
257 .signature
258 .and_then(|id| self.graph.strings().resolve(id))
259 }
260
261 #[must_use]
263 pub fn qualified_name(&self) -> Option<Arc<str>> {
264 self.entry
265 .qualified_name
266 .and_then(|id| self.graph.strings().resolve(id))
267 }
268
269 #[must_use]
271 pub fn doc(&self) -> Option<Arc<str>> {
272 self.entry
273 .doc
274 .and_then(|id| self.graph.strings().resolve(id))
275 }
276
277 #[inline]
279 #[must_use]
280 pub fn is_async(&self) -> bool {
281 self.entry.is_async
282 }
283
284 #[inline]
286 #[must_use]
287 pub fn is_static(&self) -> bool {
288 self.entry.is_static
289 }
290
291 #[must_use]
296 pub fn language(&self) -> Option<Language> {
297 self.graph.files().language_for_file(self.entry.file)
298 }
299
300 #[must_use]
302 pub fn graph(&self) -> &CodeGraph {
303 self.graph
304 }
305}
306
307impl std::fmt::Debug for QueryMatch<'_> {
308 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309 f.debug_struct("QueryMatch")
310 .field("id", &self.id)
311 .field("kind", &self.entry.kind)
312 .field("name", &self.name())
313 .field("start_line", &self.entry.start_line)
314 .finish()
315 }
316}
317
318pub struct JoinResults {
323 graph: Arc<CodeGraph>,
325 pairs: Vec<(NodeId, NodeId)>,
327 edge_kind: JoinEdgeKind,
329 workspace_root: Option<PathBuf>,
331 truncated: bool,
333}
334
335impl JoinResults {
336 #[must_use]
338 pub fn new(
339 graph: Arc<CodeGraph>,
340 pairs: Vec<(NodeId, NodeId)>,
341 edge_kind: JoinEdgeKind,
342 truncated: bool,
343 ) -> Self {
344 Self {
345 graph,
346 pairs,
347 edge_kind,
348 workspace_root: None,
349 truncated,
350 }
351 }
352
353 #[must_use]
355 pub fn with_workspace_root(mut self, root: PathBuf) -> Self {
356 self.workspace_root = Some(root);
357 self
358 }
359
360 #[inline]
362 #[must_use]
363 pub fn len(&self) -> usize {
364 self.pairs.len()
365 }
366
367 #[inline]
369 #[must_use]
370 pub fn is_empty(&self) -> bool {
371 self.pairs.is_empty()
372 }
373
374 #[must_use]
376 pub fn edge_kind(&self) -> &JoinEdgeKind {
377 &self.edge_kind
378 }
379
380 #[must_use]
382 pub fn truncated(&self) -> bool {
383 self.truncated
384 }
385
386 #[must_use]
388 pub fn graph(&self) -> &CodeGraph {
389 &self.graph
390 }
391
392 pub fn iter(&self) -> impl Iterator<Item = JoinMatch<'_>> + '_ {
394 self.pairs.iter().filter_map(|&(left_id, right_id)| {
395 let left = self.graph.nodes().get(left_id)?;
396 let right = self.graph.nodes().get(right_id)?;
397 Some(JoinMatch {
398 left: QueryMatch {
399 id: left_id,
400 entry: left,
401 graph: &self.graph,
402 workspace_root: self.workspace_root.as_deref(),
403 },
404 right: QueryMatch {
405 id: right_id,
406 entry: right,
407 graph: &self.graph,
408 workspace_root: self.workspace_root.as_deref(),
409 },
410 edge_kind: &self.edge_kind,
411 })
412 })
413 }
414}
415
416impl std::fmt::Debug for JoinResults {
417 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418 f.debug_struct("JoinResults")
419 .field("graph", &self.graph)
420 .field("pairs", &self.pairs)
421 .field("edge_kind", &self.edge_kind)
422 .field("workspace_root", &self.workspace_root)
423 .field("truncated", &self.truncated)
424 .finish()
425 }
426}
427
428pub struct JoinMatch<'a> {
430 pub left: QueryMatch<'a>,
432 pub right: QueryMatch<'a>,
434 pub edge_kind: &'a JoinEdgeKind,
436}
437
438pub enum QueryOutput {
440 Results(QueryResults),
442 Join(JoinResults),
444 Aggregation(AggregationResult),
446}
447
448impl QueryOutput {
449 #[must_use]
451 pub fn len(&self) -> usize {
452 match self {
453 Self::Results(r) => r.len(),
454 Self::Join(j) => j.len(),
455 Self::Aggregation(_) => 0,
456 }
457 }
458
459 #[must_use]
461 pub fn is_empty(&self) -> bool {
462 match self {
463 Self::Results(r) => r.is_empty(),
464 Self::Join(j) => j.is_empty(),
465 Self::Aggregation(_) => false,
466 }
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn test_query_results_empty() {
476 let graph = Arc::new(CodeGraph::new());
477 let results = QueryResults::new(graph, vec![]);
478 assert!(results.is_empty());
479 assert_eq!(results.len(), 0);
480 assert_eq!(results.iter().count(), 0);
481 }
482
483 #[test]
484 fn test_query_results_debug() {
485 let graph = Arc::new(CodeGraph::new());
486 let results = QueryResults::new(graph, vec![]);
487 let debug_str = format!("{:?}", results);
488 assert!(debug_str.contains("QueryResults"));
489 assert!(debug_str.contains("match_count"));
490 }
491
492 #[test]
493 fn test_query_results_with_workspace_root() {
494 let graph = Arc::new(CodeGraph::new());
495 let results =
496 QueryResults::new(graph, vec![]).with_workspace_root(PathBuf::from("/test/path"));
497 assert_eq!(results.workspace_root(), Some(Path::new("/test/path")));
498 }
499
500 #[test]
501 fn test_query_results_into_parts() {
502 let graph = Arc::new(CodeGraph::new());
503 let results = QueryResults::new(graph.clone(), vec![]);
504 let (returned_graph, matches) = results.into_parts();
505 assert!(Arc::ptr_eq(&returned_graph, &graph));
506 assert!(matches.is_empty());
507 }
508}