1use std::cmp::Ordering;
2use std::collections::{BinaryHeap, HashSet, VecDeque};
3
4use crate::executor::streaming::query_registry::CancelToken;
5use crate::executor::traversal::config::{TraversalConfig, TraversalOrder, VisitedPolicy};
6use crate::executor::traversal::graph_reader::{edge_key, EdgeKey, TraversalGraphReader};
7use crate::executor::traversal::stats::TraversalStats;
8use crate::parser::ast::pattern::PathSemantic;
9use linkrs_core::error::QueryError;
10use linkrs_core::types::storage_ids::VertexId;
11use linkrs_core::{Edge, Vertex};
12
13#[derive(Debug, Clone)]
14pub struct TraversalItem {
15 pub vertex_id: VertexId,
16 pub vertex: Vertex,
17 pub depth: u32,
18 pub edge: Option<Edge>,
19 path_vertices: HashSet<VertexId>,
20 path_edges: HashSet<EdgeKey>,
21}
22
23#[derive(Debug, Clone)]
24pub struct TraversalEvent {
25 pub vertex: Vertex,
26 pub depth: u32,
27 pub edge: Option<Edge>,
28}
29
30pub struct TraversalRuntime<'a> {
31 pub reader: TraversalGraphReader<'a>,
32 pub config: TraversalConfig,
33 pub stats: TraversalStats,
34
35 frontier: VecDeque<TraversalItem>,
36 visited: HashSet<VertexId>,
37 results: VecDeque<TraversalEvent>,
38 exhausted: bool,
39 total_emitted: usize,
40
41 cancel_token: Option<CancelToken>,
45}
46
47impl<'a> TraversalRuntime<'a> {
48 pub fn new(reader: TraversalGraphReader<'a>, config: TraversalConfig) -> Self {
49 Self {
50 reader,
51 config,
52 stats: TraversalStats::default(),
53 frontier: VecDeque::new(),
54 visited: HashSet::new(),
55 results: VecDeque::new(),
56 exhausted: false,
57 total_emitted: 0,
58 cancel_token: None,
59 }
60 }
61
62 pub fn with_cancel_token(mut self, token: CancelToken) -> Self {
64 self.cancel_token = Some(token);
65 self
66 }
67
68 pub fn set_cancel_token(&mut self, token: CancelToken) {
70 self.cancel_token = Some(token);
71 }
72
73 pub fn check_cancel(&self) -> Result<(), QueryError> {
76 if let Some(ref token) = self.cancel_token {
77 if token.is_cancelled() {
78 return Err(QueryError::execution(
79 "Query cancelled during traversal".to_string(),
80 ));
81 }
82 }
83 Ok(())
84 }
85
86 pub fn reset(&mut self) {
87 self.frontier.clear();
88 self.visited.clear();
89 self.results.clear();
90 self.exhausted = false;
91 self.total_emitted = 0;
92 self.stats = TraversalStats::default();
93 }
94
95 fn check_limit(&self) -> bool {
96 self.total_emitted >= self.config.limit
97 }
98
99 fn should_emit(&self, depth: u32) -> bool {
100 depth >= self.config.min_depth && depth <= self.config.max_depth
101 }
102
103 fn should_visit(&self, vertex_id: &VertexId) -> bool {
104 match self.config.visited_policy {
105 VisitedPolicy::None => true,
106 VisitedPolicy::PerSeed | VisitedPolicy::Global => !self.visited.contains(vertex_id),
107 }
108 }
109
110 pub fn seed_from_vertex(&mut self, vertex: Vertex) {
111 let vid = *vertex.vid();
112
113 if self.config.visited_policy != VisitedPolicy::None {
114 self.visited.insert(vid);
115 }
116
117 self.frontier.push_back(TraversalItem {
118 vertex_id: vid,
119 vertex,
120 depth: 0,
121 edge: None,
122 path_vertices: HashSet::from([vid]),
123 path_edges: HashSet::new(),
124 });
125 }
126
127 fn expand_frontier(&mut self) -> Result<(), QueryError> {
128 if matches!(
129 self.config.path_semantic,
130 Some(PathSemantic::WeightedShortest(_))
131 ) {
132 return self.expand_weighted();
133 }
134 while let Some(item) = match self.config.order {
135 TraversalOrder::Bfs => self.frontier.pop_front(),
136 TraversalOrder::Dfs => self.frontier.pop_back(),
137 } {
138 self.check_cancel()?;
140
141 if self.exhausted || self.check_limit() {
142 self.exhausted = true;
143 return Ok(());
144 }
145
146 if item.depth >= self.config.max_depth {
147 continue;
148 }
149
150 let edges = self.reader.get_edges(
151 &self.config.space_name,
152 &item.vertex_id,
153 self.config.direction,
154 &self.config.edge_types,
155 );
156 self.stats.record_edge_scan(edges.len());
157
158 for edge in &edges {
159 self.check_cancel()?;
160
161 if self.check_limit() {
162 self.exhausted = true;
163 return Ok(());
164 }
165
166 let neighbor_id =
167 self.reader
168 .get_neighbor_id(edge, &item.vertex_id, self.config.direction);
169
170 let parent_vertices = &item.path_vertices;
171 let parent_edges = &item.path_edges;
172 let path_rejected = is_path_rejected(
177 self.config.path_semantic.clone(),
178 parent_vertices,
179 parent_edges,
180 &neighbor_id,
181 &edge_key(edge),
182 );
183 if path_rejected || !self.should_visit(&neighbor_id) {
184 continue;
185 }
186
187 if self.config.visited_policy != VisitedPolicy::None {
188 self.visited.insert(neighbor_id);
189 }
190
191 if let Some(vertex) = self.reader.get_neighbor_vertex(
192 &self.config.space_name,
193 edge,
194 &neighbor_id,
195 &self.config.vertex_tag,
196 ) {
197 self.stats.record_vertex_visit();
198 let new_depth = item.depth + 1;
199 self.stats.update_depth(new_depth);
200
201 if self.should_emit(new_depth) {
202 self.results.push_back(TraversalEvent {
203 vertex: vertex.clone(),
204 depth: new_depth,
205 edge: Some(edge.clone()),
206 });
207 self.total_emitted += 1;
208 self.stats.record_path_emitted();
209 }
210
211 let mut path_vertices = item.path_vertices.clone();
212 path_vertices.insert(neighbor_id);
213 let mut path_edges = item.path_edges.clone();
214 path_edges.insert(edge_key(edge));
215 self.frontier.push_back(TraversalItem {
216 vertex_id: neighbor_id,
217 vertex,
218 depth: new_depth,
219 edge: Some(edge.clone()),
220 path_vertices,
221 path_edges,
222 });
223 }
224 }
225
226 self.stats.update_frontier(self.frontier.len());
227 }
228
229 self.exhausted = true;
230 Ok(())
231 }
232
233 fn expand_weighted(&mut self) -> Result<(), QueryError> {
241 let weight_prop = match &self.config.path_semantic {
242 Some(PathSemantic::WeightedShortest(p)) => p.clone(),
243 _ => String::new(),
244 };
245
246 let seeds: Vec<TraversalItem> = std::mem::take(&mut self.frontier).into_iter().collect();
250 if seeds.is_empty() {
251 self.exhausted = true;
252 return Ok(());
253 }
254
255 for seed in seeds {
256 self.check_cancel()?;
257 if self.exhausted || self.check_limit() {
258 self.exhausted = true;
259 return Ok(());
260 }
261 let seed_id = seed.vertex_id;
265 self.visited.remove(&seed_id);
266
267 let mut dist: std::collections::HashMap<VertexId, f64> =
268 std::collections::HashMap::new();
269 dist.insert(seed_id, 0.0);
270 let mut heap: BinaryHeap<WeightedItem> = BinaryHeap::new();
271 heap.push(WeightedItem {
272 cost: 0.0,
273 depth: 0,
274 vertex: seed.vertex,
275 vertex_id: seed_id,
276 edge: None,
277 });
278
279 while let Some(top) = heap.pop() {
280 self.check_cancel()?;
281 if self.exhausted || self.check_limit() {
282 self.exhausted = true;
283 return Ok(());
284 }
285 if self.visited.contains(&top.vertex_id) {
287 continue;
288 }
289 self.visited.insert(top.vertex_id);
290
291 if self.should_emit(top.depth) {
292 self.results.push_back(TraversalEvent {
293 vertex: top.vertex.clone(),
294 depth: top.depth,
295 edge: top.edge.clone(),
296 });
297 self.total_emitted += 1;
298 self.stats.record_path_emitted();
299 }
300
301 if top.depth >= self.config.max_depth {
302 continue;
303 }
304
305 let edges = self.reader.get_edges(
306 &self.config.space_name,
307 &top.vertex_id,
308 self.config.direction,
309 &self.config.edge_types,
310 );
311 self.stats.record_edge_scan(edges.len());
312
313 for edge in &edges {
314 self.check_cancel()?;
315 let neighbor_id =
316 self.reader
317 .get_neighbor_id(edge, &top.vertex_id, self.config.direction);
318 let weight = edge_weight(edge, &weight_prop);
319 let next_cost = top.cost + weight;
320 if self.visited.contains(&neighbor_id) {
321 continue;
322 }
323 let better = match dist.get(&neighbor_id) {
324 Some(old) => next_cost < *old,
325 None => true,
326 };
327 if !better {
328 continue;
329 }
330 dist.insert(neighbor_id, next_cost);
331 if let Some(vertex) = self.reader.get_neighbor_vertex(
332 &self.config.space_name,
333 edge,
334 &neighbor_id,
335 &self.config.vertex_tag,
336 ) {
337 self.stats.record_vertex_visit();
338 let new_depth = top.depth + 1;
339 self.stats.update_depth(new_depth);
340 heap.push(WeightedItem {
341 cost: next_cost,
342 depth: new_depth,
343 vertex,
344 vertex_id: neighbor_id,
345 edge: Some(edge.clone()),
346 });
347 }
348 }
349 self.stats.update_frontier(heap.len());
350 }
351 }
352
353 self.exhausted = true;
354 Ok(())
355 }
356
357 pub fn next_event(&mut self) -> Option<TraversalEvent> {
358 if let Some(event) = self.results.pop_front() {
359 return Some(event);
360 }
361
362 if self.exhausted {
363 return None;
364 }
365
366 if self.expand_frontier().is_err() {
367 self.exhausted = true;
368 return None;
369 }
370
371 self.results.pop_front()
372 }
373
374 pub fn stats(&self) -> &TraversalStats {
375 &self.stats
376 }
377}
378
379#[derive(Debug, Clone)]
383struct WeightedItem {
384 cost: f64,
385 depth: u32,
386 vertex: Vertex,
387 vertex_id: VertexId,
388 edge: Option<Edge>,
389}
390
391impl PartialEq for WeightedItem {
392 fn eq(&self, other: &Self) -> bool {
393 self.cost == other.cost
394 }
395}
396
397impl Eq for WeightedItem {}
398
399impl PartialOrd for WeightedItem {
400 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
401 Some(self.cmp(other))
402 }
403}
404
405impl Ord for WeightedItem {
406 fn cmp(&self, other: &Self) -> Ordering {
407 other
408 .cost
409 .partial_cmp(&self.cost)
410 .unwrap_or(Ordering::Equal)
411 }
412}
413
414fn edge_weight(edge: &Edge, prop: &str) -> f64 {
417 super::weighted::read_edge_weight(edge, prop)
418}
419
420fn is_path_rejected(
423 semantic: Option<PathSemantic>,
424 parent_vertices: &HashSet<VertexId>,
425 parent_edges: &HashSet<EdgeKey>,
426 neighbor: &VertexId,
427 edge_key: &EdgeKey,
428) -> bool {
429 match semantic {
430 Some(PathSemantic::Trail) => parent_edges.contains(edge_key),
431 Some(PathSemantic::Acyclic) => parent_vertices.contains(neighbor),
432 _ => false,
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 fn vid(n: i64) -> VertexId {
441 VertexId::try_from_int64(n).expect("valid vertex id")
442 }
443
444 fn edge_key(src: i64, dst: i64) -> EdgeKey {
445 (vid(src), vid(dst), "KNOWS".to_string(), 0)
446 }
447
448 #[test]
449 fn trail_rejects_repeated_edge() {
450 let parents: HashSet<VertexId> = HashSet::from([vid(1), vid(2)]);
451 let edges: HashSet<EdgeKey> = HashSet::from([edge_key(2, 3)]);
452 assert!(is_path_rejected(
454 Some(PathSemantic::Trail),
455 &parents,
456 &edges,
457 &vid(3),
458 &edge_key(2, 3)
459 ));
460 assert!(!is_path_rejected(
462 Some(PathSemantic::Trail),
463 &parents,
464 &edges,
465 &vid(1),
466 &edge_key(3, 1)
467 ));
468 }
469
470 #[test]
471 fn acyclic_rejects_repeated_vertex() {
472 let parents: HashSet<VertexId> = HashSet::from([vid(1), vid(2)]);
473 let edges: HashSet<EdgeKey> = HashSet::new();
474 assert!(is_path_rejected(
476 Some(PathSemantic::Acyclic),
477 &parents,
478 &edges,
479 &vid(2),
480 &edge_key(3, 2)
481 ));
482 assert!(!is_path_rejected(
483 Some(PathSemantic::Acyclic),
484 &parents,
485 &edges,
486 &vid(3),
487 &edge_key(2, 3)
488 ));
489 }
490
491 #[test]
492 fn walk_and_shortest_impose_no_per_path_check() {
493 let parents: HashSet<VertexId> = HashSet::from([vid(1)]);
494 let edges: HashSet<EdgeKey> = HashSet::from([edge_key(1, 2)]);
495 for semantic in [
496 None,
497 Some(PathSemantic::Walk),
498 Some(PathSemantic::Shortest),
499 Some(PathSemantic::AllShortest),
500 ] {
501 assert!(
502 !is_path_rejected(semantic.clone(), &parents, &edges, &vid(1), &edge_key(1, 2)),
503 "unexpected per-path rejection for {semantic:?}"
504 );
505 }
506 }
507}