1use std::collections::HashSet;
14use std::sync::Arc;
15
16use crate::parser::ast::pattern::{EdgeRange, RepetitionType};
17use linkrs_core::types::graph_schema::EdgeDirection;
18use linkrs_core::Value;
19
20pub type PlannerError = String;
21
22#[derive(Debug, Clone)]
23pub struct VLPConfig {
24 pub max_unbounded_depth: usize,
25 pub enable_path_pruning: bool,
26 pub use_bfs_for_shortest: bool,
27 pub batch_size: usize,
28 pub max_path_memory_mb: usize,
29 pub unbounded_timeout_secs: Option<u64>,
30}
31
32impl Default for VLPConfig {
33 fn default() -> Self {
34 Self {
35 max_unbounded_depth: 100,
36 enable_path_pruning: true,
37 use_bfs_for_shortest: true,
38 batch_size: 1000,
39 max_path_memory_mb: 512,
40 unbounded_timeout_secs: Some(30),
41 }
42 }
43}
44
45impl VLPConfig {
46 pub fn for_production() -> Self {
47 Self {
48 max_unbounded_depth: 100,
49 enable_path_pruning: true,
50 use_bfs_for_shortest: true,
51 batch_size: 1000,
52 max_path_memory_mb: 512,
53 unbounded_timeout_secs: Some(30),
54 }
55 }
56
57 pub fn for_analytics() -> Self {
58 Self {
59 max_unbounded_depth: 500,
60 enable_path_pruning: true,
61 use_bfs_for_shortest: true,
62 batch_size: 5000,
63 max_path_memory_mb: 2048,
64 unbounded_timeout_secs: Some(300),
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
70pub struct VariableLengthPathSpec {
71 pub edge_types: Vec<String>,
72 pub direction: EdgeDirection,
73 pub range: EdgeRange,
74 pub properties: Vec<(Arc<str>, Value)>,
75}
76
77impl VariableLengthPathSpec {
78 pub fn new(
79 edge_types: Vec<String>,
80 direction: EdgeDirection,
81 range: EdgeRange,
82 properties: Vec<(Arc<str>, Value)>,
83 ) -> Self {
84 Self {
85 edge_types,
86 direction,
87 range,
88 properties,
89 }
90 }
91
92 pub fn min_hops(&self) -> usize {
93 self.range.min.unwrap_or(1)
94 }
95
96 pub fn max_hops(&self, default_max: usize) -> usize {
97 self.range.max.unwrap_or(default_max)
98 }
99
100 pub fn is_bounded(&self) -> bool {
101 self.range.max.is_some()
102 }
103
104 pub fn is_single_hop(&self) -> bool {
105 self.range.min == Some(1) && self.range.max == Some(1)
106 }
107
108 pub fn is_zero_inclusive(&self) -> bool {
109 self.range.min == Some(0)
110 }
111}
112
113#[derive(Debug, Clone)]
114pub struct VariableLengthPathPlan {
115 pub strategy: VLPStrategy,
116 pub edge_types: Vec<String>,
117 pub direction: EdgeDirection,
118 pub min_hops: usize,
119 pub max_hops: usize,
120 pub properties: Vec<(Arc<str>, Value)>,
121 pub pruning: Option<PruningConfig>,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq)]
125pub enum VLPStrategy {
126 Exact,
127 Range,
128 Unbounded,
129 BFS,
130 Iterative,
131}
132
133#[derive(Debug, Clone)]
134pub struct PruningConfig {
135 pub strategy: PruningStrategy,
136 pub enable_cycle_detection: bool,
137 pub max_visited_nodes: usize,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq)]
141pub enum PruningStrategy {
142 None,
143 NoCycles,
144 ShortestPath,
145 PredicateBased,
146}
147
148pub struct VariableLengthPathPlanner {
149 config: VLPConfig,
150}
151
152impl VariableLengthPathPlanner {
153 pub fn new() -> Self {
154 Self {
155 config: VLPConfig::default(),
156 }
157 }
158
159 pub fn with_config(config: VLPConfig) -> Self {
160 Self { config }
161 }
162
163 pub fn plan(
164 &self,
165 spec: &VariableLengthPathSpec,
166 ) -> Result<VariableLengthPathPlan, PlannerError> {
167 let min_hops = spec.min_hops();
168 let max_hops = spec.max_hops(self.config.max_unbounded_depth);
169
170 if min_hops > max_hops {
171 return Err(format!(
172 "Invalid range: min ({}) > max ({})",
173 min_hops, max_hops
174 ));
175 }
176
177 let strategy = self.select_strategy(min_hops, max_hops, spec.is_bounded());
178
179 let pruning = if self.config.enable_path_pruning {
180 Some(self.create_pruning_config(&strategy))
181 } else {
182 None
183 };
184
185 Ok(VariableLengthPathPlan {
186 strategy,
187 edge_types: spec.edge_types.clone(),
188 direction: spec.direction,
189 min_hops,
190 max_hops,
191 properties: spec.properties.clone(),
192 pruning,
193 })
194 }
195
196 fn select_strategy(&self, min_hops: usize, max_hops: usize, is_bounded: bool) -> VLPStrategy {
197 if min_hops == max_hops {
198 return VLPStrategy::Exact;
199 }
200
201 if !is_bounded {
202 return VLPStrategy::Unbounded;
203 }
204
205 if self.config.use_bfs_for_shortest && min_hops == 1 {
206 return VLPStrategy::BFS;
207 }
208
209 if max_hops - min_hops <= 3 {
210 return VLPStrategy::Range;
211 }
212
213 VLPStrategy::Iterative
214 }
215
216 fn create_pruning_config(&self, strategy: &VLPStrategy) -> PruningConfig {
217 let pruning_strategy = match strategy {
218 VLPStrategy::BFS => PruningStrategy::ShortestPath,
219 VLPStrategy::Unbounded => PruningStrategy::NoCycles,
220 _ => PruningStrategy::NoCycles,
221 };
222
223 PruningConfig {
224 strategy: pruning_strategy,
225 enable_cycle_detection: true,
226 max_visited_nodes: self.config.batch_size * 10,
227 }
228 }
229
230 pub fn plan_from_repetition(
231 &self,
232 rep_type: &RepetitionType,
233 edge_types: Vec<String>,
234 direction: EdgeDirection,
235 properties: Vec<(Arc<str>, Value)>,
236 ) -> Result<VariableLengthPathPlan, PlannerError> {
237 let range = match rep_type {
238 RepetitionType::ZeroOrMore => EdgeRange::at_least(0),
239 RepetitionType::OneOrMore => EdgeRange::at_least(1),
240 RepetitionType::ZeroOrOne => EdgeRange::range(0, 1),
241 RepetitionType::Exactly(n) => EdgeRange::fixed(*n),
242 RepetitionType::Range(min, max) => EdgeRange::range(*min, *max),
243 };
244
245 let spec = VariableLengthPathSpec::new(edge_types, direction, range, properties);
246 self.plan(&spec)
247 }
248}
249
250impl Default for VariableLengthPathPlanner {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256pub struct PathPruner {
257 strategy: PruningStrategy,
258 visited: HashSet<Value>,
259 best_length: Option<usize>,
260 max_visited: usize,
261}
262
263impl PathPruner {
264 pub fn new(strategy: PruningStrategy) -> Self {
265 Self {
266 strategy,
267 visited: HashSet::new(),
268 best_length: None,
269 max_visited: usize::MAX,
270 }
271 }
272
273 pub fn with_max_visited(mut self, max: usize) -> Self {
274 self.max_visited = max;
275 self
276 }
277
278 pub fn should_prune(&mut self, path: &[Value]) -> bool {
279 match self.strategy {
280 PruningStrategy::None => false,
281
282 PruningStrategy::NoCycles => {
283 let mut seen = HashSet::new();
284 for vertex in path {
285 if !seen.insert(vertex.clone()) {
286 return true;
287 }
288 }
289 false
290 }
291
292 PruningStrategy::ShortestPath => {
293 if let Some(best) = self.best_length {
294 if path.len() > best {
295 return true;
296 }
297 }
298 false
299 }
300
301 PruningStrategy::PredicateBased => false,
302 }
303 }
304
305 pub fn should_prune_vertex(&mut self, vertex: &Value) -> bool {
306 if self.visited.len() >= self.max_visited {
307 return true;
308 }
309
310 match self.strategy {
311 PruningStrategy::NoCycles | PruningStrategy::ShortestPath => {
312 !self.visited.insert(vertex.clone())
313 }
314 _ => false,
315 }
316 }
317
318 pub fn update_best(&mut self, length: usize) {
319 if let Some(ref mut best) = self.best_length {
320 if length < *best {
321 *best = length;
322 }
323 } else {
324 self.best_length = Some(length);
325 }
326 }
327
328 pub fn reset(&mut self) {
329 self.visited.clear();
330 self.best_length = None;
331 }
332
333 pub fn visited_count(&self) -> usize {
334 self.visited.len()
335 }
336}
337
338pub struct PathExpansionStats {
339 pub paths_found: usize,
340 pub paths_pruned: usize,
341 pub max_depth_reached: usize,
342 pub vertices_visited: usize,
343}
344
345impl PathExpansionStats {
346 pub fn new() -> Self {
347 Self {
348 paths_found: 0,
349 paths_pruned: 0,
350 max_depth_reached: 0,
351 vertices_visited: 0,
352 }
353 }
354
355 pub fn record_path(&mut self, depth: usize) {
356 self.paths_found += 1;
357 if depth > self.max_depth_reached {
358 self.max_depth_reached = depth;
359 }
360 }
361
362 pub fn record_prune(&mut self) {
363 self.paths_pruned += 1;
364 }
365
366 pub fn record_vertex(&mut self) {
367 self.vertices_visited += 1;
368 }
369}
370
371impl Default for PathExpansionStats {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn test_vlp_config_default() {
383 let config = VLPConfig::default();
384 assert_eq!(config.max_unbounded_depth, 100);
385 assert!(config.enable_path_pruning);
386 assert!(config.use_bfs_for_shortest);
387 }
388
389 #[test]
390 fn test_vlp_spec_min_max_hops() {
391 let spec = VariableLengthPathSpec::new(
392 vec!["KNOWS".to_string()],
393 EdgeDirection::Out,
394 EdgeRange::range(1, 5),
395 vec![],
396 );
397
398 assert_eq!(spec.min_hops(), 1);
399 assert_eq!(spec.max_hops(100), 5);
400 assert!(spec.is_bounded());
401 assert!(!spec.is_single_hop());
402 }
403
404 #[test]
405 fn test_vlp_spec_single_hop() {
406 let spec = VariableLengthPathSpec::new(
407 vec!["KNOWS".to_string()],
408 EdgeDirection::Out,
409 EdgeRange::fixed(1),
410 vec![],
411 );
412
413 assert!(spec.is_single_hop());
414 }
415
416 #[test]
417 fn test_vlp_spec_zero_inclusive() {
418 let spec = VariableLengthPathSpec::new(
419 vec!["KNOWS".to_string()],
420 EdgeDirection::Out,
421 EdgeRange::at_least(0),
422 vec![],
423 );
424
425 assert!(spec.is_zero_inclusive());
426 }
427
428 #[test]
429 fn test_vlp_planner_exact() {
430 let planner = VariableLengthPathPlanner::new();
431 let spec = VariableLengthPathSpec::new(
432 vec!["KNOWS".to_string()],
433 EdgeDirection::Out,
434 EdgeRange::fixed(3),
435 vec![],
436 );
437
438 let plan = planner.plan(&spec).unwrap();
439 assert_eq!(plan.strategy, VLPStrategy::Exact);
440 assert_eq!(plan.min_hops, 3);
441 assert_eq!(plan.max_hops, 3);
442 }
443
444 #[test]
445 fn test_vlp_planner_range() {
446 let planner = VariableLengthPathPlanner::new();
447 let spec = VariableLengthPathSpec::new(
448 vec!["KNOWS".to_string()],
449 EdgeDirection::Out,
450 EdgeRange::range(1, 3),
451 vec![],
452 );
453
454 let plan = planner.plan(&spec).unwrap();
455 assert_eq!(plan.strategy, VLPStrategy::BFS);
456 assert_eq!(plan.min_hops, 1);
457 assert_eq!(plan.max_hops, 3);
458 }
459
460 #[test]
461 fn test_vlp_planner_unbounded() {
462 let planner = VariableLengthPathPlanner::new();
463 let spec = VariableLengthPathSpec::new(
464 vec!["KNOWS".to_string()],
465 EdgeDirection::Out,
466 EdgeRange::at_least(1),
467 vec![],
468 );
469
470 let plan = planner.plan(&spec).unwrap();
471 assert_eq!(plan.strategy, VLPStrategy::Unbounded);
472 assert_eq!(plan.min_hops, 1);
473 assert_eq!(plan.max_hops, 100);
474 }
475
476 #[test]
477 fn test_path_pruner_no_cycles() {
478 let mut pruner = PathPruner::new(PruningStrategy::NoCycles);
479
480 let path = vec![Value::string("a"), Value::string("b"), Value::string("a")];
481
482 assert!(pruner.should_prune(&path));
483 }
484
485 #[test]
486 fn test_path_pruner_shortest_path() {
487 let mut pruner = PathPruner::new(PruningStrategy::ShortestPath);
488
489 pruner.update_best(3);
490
491 let short_path = vec![Value::string("a"), Value::string("b")];
492 assert!(!pruner.should_prune(&short_path));
493
494 let long_path = vec![
495 Value::string("a"),
496 Value::string("b"),
497 Value::string("c"),
498 Value::string("d"),
499 ];
500 assert!(pruner.should_prune(&long_path));
501 }
502
503 #[test]
504 fn test_path_pruner_vertex_tracking() {
505 let mut pruner = PathPruner::new(PruningStrategy::NoCycles);
506
507 assert!(!pruner.should_prune_vertex(&Value::string("a")));
508 assert!(pruner.should_prune_vertex(&Value::string("a")));
509 assert!(!pruner.should_prune_vertex(&Value::string("b")));
510
511 assert_eq!(pruner.visited_count(), 2);
512 }
513
514 #[test]
515 fn test_path_expansion_stats() {
516 let mut stats = PathExpansionStats::new();
517
518 stats.record_path(3);
519 stats.record_path(5);
520 stats.record_prune();
521 stats.record_vertex();
522
523 assert_eq!(stats.paths_found, 2);
524 assert_eq!(stats.paths_pruned, 1);
525 assert_eq!(stats.max_depth_reached, 5);
526 assert_eq!(stats.vertices_visited, 1);
527 }
528}