1#![allow(clippy::expect_used)]
9
10use std::collections::HashSet;
11
12use manifoldb_core::{Edge, EdgeId, EdgeType, EntityId};
13use manifoldb_storage::Transaction;
14
15use super::Direction;
16use crate::index::AdjacencyIndex;
17use crate::store::{EdgeStore, GraphResult};
18
19#[derive(Debug, Clone)]
21pub enum StepFilter {
22 Any,
24 EdgeType(EdgeType),
26 EdgeTypes(Vec<EdgeType>),
28 Custom(String),
31}
32
33impl Default for StepFilter {
34 fn default() -> Self {
35 Self::Any
36 }
37}
38
39impl StepFilter {
40 pub fn edge_type(edge_type: impl Into<EdgeType>) -> Self {
42 Self::EdgeType(edge_type.into())
43 }
44
45 pub fn edge_types(types: impl IntoIterator<Item = EdgeType>) -> Self {
47 Self::EdgeTypes(types.into_iter().collect())
48 }
49
50 pub fn matches(&self, edge: &Edge) -> bool {
52 match self {
53 Self::Any => true,
54 Self::EdgeType(et) => &edge.edge_type == et,
55 Self::EdgeTypes(types) => types.contains(&edge.edge_type),
56 Self::Custom(_) => true, }
58 }
59}
60
61impl From<&str> for StepFilter {
62 fn from(s: &str) -> Self {
63 Self::EdgeType(EdgeType::new(s))
64 }
65}
66
67impl From<EdgeType> for StepFilter {
68 fn from(et: EdgeType) -> Self {
69 Self::EdgeType(et)
70 }
71}
72
73#[derive(Debug, Clone)]
77pub struct PathStep {
78 pub direction: Direction,
80 pub filter: StepFilter,
82 pub min_hops: usize,
84 pub max_hops: Option<usize>,
87}
88
89impl PathStep {
90 pub fn new(direction: Direction, filter: impl Into<StepFilter>) -> Self {
92 Self { direction, filter: filter.into(), min_hops: 1, max_hops: Some(1) }
93 }
94
95 pub fn any(direction: Direction) -> Self {
97 Self::new(direction, StepFilter::Any)
98 }
99
100 pub fn outgoing(edge_type: impl Into<EdgeType>) -> Self {
102 Self::new(Direction::Outgoing, StepFilter::edge_type(edge_type))
103 }
104
105 pub fn incoming(edge_type: impl Into<EdgeType>) -> Self {
107 Self::new(Direction::Incoming, StepFilter::edge_type(edge_type))
108 }
109
110 pub fn both(edge_type: impl Into<EdgeType>) -> Self {
112 Self::new(Direction::Both, StepFilter::edge_type(edge_type))
113 }
114
115 pub const fn with_hops(mut self, min: usize, max: Option<usize>) -> Self {
122 self.min_hops = min;
123 self.max_hops = max;
124 self
125 }
126
127 pub const fn variable_length(mut self, min: usize, max: usize) -> Self {
131 self.min_hops = min;
132 self.max_hops = Some(max);
133 self
134 }
135
136 pub const fn optional(mut self) -> Self {
138 self.min_hops = 0;
139 self.max_hops = Some(1);
140 self
141 }
142
143 pub const fn zero_or_more(mut self) -> Self {
145 self.min_hops = 0;
146 self.max_hops = None;
147 self
148 }
149
150 pub const fn one_or_more(mut self) -> Self {
152 self.min_hops = 1;
153 self.max_hops = None;
154 self
155 }
156
157 pub fn is_variable_length(&self) -> bool {
159 self.min_hops != 1 || self.max_hops != Some(1)
160 }
161}
162
163#[derive(Debug, Clone)]
165pub struct PatternMatch {
166 pub nodes: Vec<EntityId>,
168 pub step_edges: Vec<Vec<EdgeId>>,
171}
172
173impl PatternMatch {
174 pub fn source(&self) -> EntityId {
176 self.nodes[0]
177 }
178
179 pub fn target(&self) -> EntityId {
181 *self.nodes.last().expect("pattern match has at least one node")
182 }
183
184 pub fn all_edges(&self) -> Vec<EdgeId> {
186 self.step_edges.iter().flatten().copied().collect()
187 }
188
189 pub fn length(&self) -> usize {
191 self.step_edges.iter().map(std::vec::Vec::len).sum()
192 }
193}
194
195struct MatchContext<'a, T: Transaction> {
200 tx: &'a T,
202 current: EntityId,
204 step_idx: usize,
206 path_nodes: Vec<EntityId>,
208 path_edges: Vec<Vec<EdgeId>>,
210 visited: HashSet<EntityId>,
212 results: &'a mut Vec<PatternMatch>,
214}
215
216impl<'a, T: Transaction> MatchContext<'a, T> {
217 fn new(
219 tx: &'a T,
220 start: EntityId,
221 allow_cycles: bool,
222 results: &'a mut Vec<PatternMatch>,
223 ) -> Self {
224 let visited = if allow_cycles {
225 HashSet::new()
226 } else {
227 let mut set = HashSet::new();
228 set.insert(start);
229 set
230 };
231
232 Self {
233 tx,
234 current: start,
235 step_idx: 0,
236 path_nodes: vec![start],
237 path_edges: Vec::new(),
238 visited,
239 results,
240 }
241 }
242}
243
244#[derive(Debug, Clone, Default)]
264pub struct PathPattern {
265 steps: Vec<PathStep>,
267 limit: Option<usize>,
269 allow_cycles: bool,
271}
272
273impl PathPattern {
274 pub fn new() -> Self {
276 Self::default()
277 }
278
279 pub fn add_step(mut self, step: PathStep) -> Self {
281 self.steps.push(step);
282 self
283 }
284
285 pub fn outgoing(self, edge_type: impl Into<EdgeType>) -> Self {
287 self.add_step(PathStep::outgoing(edge_type))
288 }
289
290 pub fn incoming(self, edge_type: impl Into<EdgeType>) -> Self {
292 self.add_step(PathStep::incoming(edge_type))
293 }
294
295 pub fn both(self, edge_type: impl Into<EdgeType>) -> Self {
297 self.add_step(PathStep::both(edge_type))
298 }
299
300 pub const fn with_limit(mut self, limit: usize) -> Self {
302 self.limit = Some(limit);
303 self
304 }
305
306 pub const fn allow_cycles(mut self) -> Self {
310 self.allow_cycles = true;
311 self
312 }
313
314 pub fn steps(&self) -> &[PathStep] {
316 &self.steps
317 }
318
319 pub fn is_empty(&self) -> bool {
321 self.steps.is_empty()
322 }
323
324 pub fn find_from<T: Transaction>(
326 &self,
327 tx: &T,
328 start: EntityId,
329 ) -> GraphResult<Vec<PatternMatch>> {
330 if self.steps.is_empty() {
331 return Ok(vec![PatternMatch { nodes: vec![start], step_edges: Vec::new() }]);
333 }
334
335 const INITIAL_RESULTS_CAPACITY: usize = 64;
338
339 let mut results = Vec::with_capacity(INITIAL_RESULTS_CAPACITY);
340 let mut ctx = MatchContext::new(tx, start, self.allow_cycles, &mut results);
341
342 self.match_from_step(&mut ctx)?;
343
344 Ok(results)
345 }
346
347 pub fn find_between<T: Transaction>(
349 &self,
350 tx: &T,
351 start: EntityId,
352 end: EntityId,
353 ) -> GraphResult<Vec<PatternMatch>> {
354 let all_matches = self.find_from(tx, start)?;
355 Ok(all_matches.into_iter().filter(|m| m.target() == end).collect())
356 }
357
358 pub fn matches<T: Transaction>(&self, tx: &T, start: EntityId) -> GraphResult<bool> {
360 let pattern_with_limit =
361 Self { steps: self.steps.clone(), limit: Some(1), allow_cycles: self.allow_cycles };
362 let matches = pattern_with_limit.find_from(tx, start)?;
363 Ok(!matches.is_empty())
364 }
365
366 fn match_from_step<T: Transaction>(&self, ctx: &mut MatchContext<'_, T>) -> GraphResult<()> {
368 if let Some(limit) = self.limit {
370 if ctx.results.len() >= limit {
371 return Ok(());
372 }
373 }
374
375 if ctx.step_idx >= self.steps.len() {
377 ctx.results.push(PatternMatch {
378 nodes: ctx.path_nodes.clone(),
379 step_edges: ctx.path_edges.clone(),
380 });
381 return Ok(());
382 }
383
384 let step = &self.steps[ctx.step_idx];
385
386 if step.is_variable_length() {
388 self.match_variable_step(ctx, step)?;
389 } else {
390 self.match_single_step(ctx, step)?;
392 }
393
394 Ok(())
395 }
396
397 fn match_single_step<T: Transaction>(
398 &self,
399 ctx: &mut MatchContext<'_, T>,
400 step: &PathStep,
401 ) -> GraphResult<()> {
402 let neighbors = self.get_filtered_neighbors(ctx.tx, ctx.current, step)?;
403
404 for (neighbor, edge_id) in neighbors {
405 if !self.allow_cycles && ctx.visited.contains(&neighbor) {
406 continue;
407 }
408
409 let prev_current = ctx.current;
411 let prev_step_idx = ctx.step_idx;
412 let prev_nodes_len = ctx.path_nodes.len();
413 let prev_edges_len = ctx.path_edges.len();
414
415 ctx.path_nodes.push(neighbor);
417 ctx.path_edges.push(vec![edge_id]);
418 ctx.current = neighbor;
419 ctx.step_idx += 1;
420
421 let was_new = if self.allow_cycles { false } else { ctx.visited.insert(neighbor) };
423
424 self.match_from_step(ctx)?;
426
427 ctx.path_nodes.truncate(prev_nodes_len);
429 ctx.path_edges.truncate(prev_edges_len);
430 ctx.current = prev_current;
431 ctx.step_idx = prev_step_idx;
432
433 if was_new {
434 ctx.visited.remove(&neighbor);
435 }
436 }
437
438 Ok(())
439 }
440
441 fn match_variable_step<T: Transaction>(
442 &self,
443 ctx: &mut MatchContext<'_, T>,
444 step: &PathStep,
445 ) -> GraphResult<()> {
446 self.dfs_variable_step(ctx, step, ctx.current, 0)
450 }
451
452 fn dfs_variable_step<T: Transaction>(
455 &self,
456 ctx: &mut MatchContext<'_, T>,
457 step: &PathStep,
458 current_node: EntityId,
459 hop_count: usize,
460 ) -> GraphResult<()> {
461 if let Some(limit) = self.limit {
463 if ctx.results.len() >= limit {
464 return Ok(());
465 }
466 }
467
468 if hop_count >= step.min_hops {
470 let prev_current = ctx.current;
472 let prev_step_idx = ctx.step_idx;
473 let prev_nodes_len = ctx.path_nodes.len();
474 let prev_edges_len = ctx.path_edges.len();
475
476 ctx.current = current_node;
478 ctx.step_idx += 1;
479
480 self.match_from_step(ctx)?;
495
496 ctx.path_nodes.truncate(prev_nodes_len);
498 ctx.path_edges.truncate(prev_edges_len);
499 ctx.current = prev_current;
500 ctx.step_idx = prev_step_idx;
501 }
502
503 let can_expand = step.max_hops.map_or(true, |max| hop_count < max);
505 if !can_expand {
506 return Ok(());
507 }
508
509 if let Some(limit) = self.limit {
511 if ctx.results.len() >= limit {
512 return Ok(());
513 }
514 }
515
516 let neighbors = self.get_filtered_neighbors(ctx.tx, current_node, step)?;
518
519 for (neighbor, edge_id) in neighbors {
520 if !self.allow_cycles && ctx.visited.contains(&neighbor) {
521 continue;
522 }
523
524 ctx.path_nodes.push(neighbor);
526
527 if hop_count == 0 {
530 ctx.path_edges.push(vec![edge_id]);
531 } else {
532 if let Some(last_edges) = ctx.path_edges.last_mut() {
534 last_edges.push(edge_id);
535 }
536 }
537
538 let was_new = if self.allow_cycles { false } else { ctx.visited.insert(neighbor) };
539
540 self.dfs_variable_step(ctx, step, neighbor, hop_count + 1)?;
542
543 ctx.path_nodes.pop();
545
546 if hop_count == 0 {
547 ctx.path_edges.pop();
548 } else if let Some(last_edges) = ctx.path_edges.last_mut() {
549 last_edges.pop();
550 }
551
552 if was_new {
553 ctx.visited.remove(&neighbor);
554 }
555 }
556
557 Ok(())
558 }
559
560 fn get_filtered_neighbors<T: Transaction>(
561 &self,
562 tx: &T,
563 node: EntityId,
564 step: &PathStep,
565 ) -> GraphResult<Vec<(EntityId, EdgeId)>> {
566 const INITIAL_NEIGHBORS_CAPACITY: usize = 16;
568
569 let mut neighbors = Vec::with_capacity(INITIAL_NEIGHBORS_CAPACITY);
570
571 if step.direction.includes_outgoing() {
572 self.add_filtered_outgoing(tx, node, step, &mut neighbors)?;
573 }
574
575 if step.direction.includes_incoming() {
576 self.add_filtered_incoming(tx, node, step, &mut neighbors)?;
577 }
578
579 Ok(neighbors)
580 }
581
582 fn add_filtered_outgoing<T: Transaction>(
583 &self,
584 tx: &T,
585 node: EntityId,
586 step: &PathStep,
587 neighbors: &mut Vec<(EntityId, EdgeId)>,
588 ) -> GraphResult<()> {
589 match &step.filter {
590 StepFilter::EdgeType(et) => {
591 AdjacencyIndex::for_each_outgoing_by_type(tx, node, et, |edge_id| {
592 if let Some(edge) = EdgeStore::get(tx, edge_id)? {
593 neighbors.push((edge.target, edge_id));
594 }
595 Ok(true)
596 })?;
597 }
598 StepFilter::EdgeTypes(types) => {
599 for et in types {
600 AdjacencyIndex::for_each_outgoing_by_type(tx, node, et, |edge_id| {
601 if let Some(edge) = EdgeStore::get(tx, edge_id)? {
602 neighbors.push((edge.target, edge_id));
603 }
604 Ok(true)
605 })?;
606 }
607 }
608 StepFilter::Any | StepFilter::Custom(_) => {
609 AdjacencyIndex::for_each_outgoing(tx, node, |edge_id| {
610 if let Some(edge) = EdgeStore::get(tx, edge_id)? {
611 if step.filter.matches(&edge) {
612 neighbors.push((edge.target, edge_id));
613 }
614 }
615 Ok(true)
616 })?;
617 }
618 }
619 Ok(())
620 }
621
622 fn add_filtered_incoming<T: Transaction>(
623 &self,
624 tx: &T,
625 node: EntityId,
626 step: &PathStep,
627 neighbors: &mut Vec<(EntityId, EdgeId)>,
628 ) -> GraphResult<()> {
629 match &step.filter {
630 StepFilter::EdgeType(et) => {
631 AdjacencyIndex::for_each_incoming_by_type(tx, node, et, |edge_id| {
632 if let Some(edge) = EdgeStore::get(tx, edge_id)? {
633 neighbors.push((edge.source, edge_id));
634 }
635 Ok(true)
636 })?;
637 }
638 StepFilter::EdgeTypes(types) => {
639 for et in types {
640 AdjacencyIndex::for_each_incoming_by_type(tx, node, et, |edge_id| {
641 if let Some(edge) = EdgeStore::get(tx, edge_id)? {
642 neighbors.push((edge.source, edge_id));
643 }
644 Ok(true)
645 })?;
646 }
647 }
648 StepFilter::Any | StepFilter::Custom(_) => {
649 AdjacencyIndex::for_each_incoming(tx, node, |edge_id| {
650 if let Some(edge) = EdgeStore::get(tx, edge_id)? {
651 if step.filter.matches(&edge) {
652 neighbors.push((edge.source, edge_id));
653 }
654 }
655 Ok(true)
656 })?;
657 }
658 }
659 Ok(())
660 }
661}
662
663pub struct PatternBuilder {
665 pattern: PathPattern,
666}
667
668impl PatternBuilder {
669 pub fn new() -> Self {
671 Self { pattern: PathPattern::new() }
672 }
673
674 pub fn out(mut self, edge_type: impl Into<EdgeType>) -> Self {
676 self.pattern = self.pattern.outgoing(edge_type);
677 self
678 }
679
680 pub fn inc(mut self, edge_type: impl Into<EdgeType>) -> Self {
682 self.pattern = self.pattern.incoming(edge_type);
683 self
684 }
685
686 pub fn rel(mut self, edge_type: impl Into<EdgeType>) -> Self {
688 self.pattern = self.pattern.both(edge_type);
689 self
690 }
691
692 pub fn out_any(mut self) -> Self {
694 self.pattern = self.pattern.add_step(PathStep::any(Direction::Outgoing));
695 self
696 }
697
698 pub fn in_any(mut self) -> Self {
700 self.pattern = self.pattern.add_step(PathStep::any(Direction::Incoming));
701 self
702 }
703
704 pub fn any(mut self) -> Self {
706 self.pattern = self.pattern.add_step(PathStep::any(Direction::Both));
707 self
708 }
709
710 pub fn out_var(mut self, edge_type: impl Into<EdgeType>, min: usize, max: usize) -> Self {
712 self.pattern =
713 self.pattern.add_step(PathStep::outgoing(edge_type).variable_length(min, max));
714 self
715 }
716
717 pub fn in_var(mut self, edge_type: impl Into<EdgeType>, min: usize, max: usize) -> Self {
719 self.pattern =
720 self.pattern.add_step(PathStep::incoming(edge_type).variable_length(min, max));
721 self
722 }
723
724 pub fn limit(mut self, limit: usize) -> Self {
726 self.pattern = self.pattern.with_limit(limit);
727 self
728 }
729
730 pub fn with_cycles(mut self) -> Self {
732 self.pattern = self.pattern.allow_cycles();
733 self
734 }
735
736 pub fn build(self) -> PathPattern {
738 self.pattern
739 }
740}
741
742impl Default for PatternBuilder {
743 fn default() -> Self {
744 Self::new()
745 }
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751
752 #[test]
753 fn step_filter_matches_any() {
754 let filter = StepFilter::Any;
755 let edge = Edge::new(EdgeId::new(1), EntityId::new(1), EntityId::new(2), "TEST");
756 assert!(filter.matches(&edge));
757 }
758
759 #[test]
760 fn step_filter_matches_edge_type() {
761 let filter = StepFilter::edge_type("FRIEND");
762 let friend_edge = Edge::new(EdgeId::new(1), EntityId::new(1), EntityId::new(2), "FRIEND");
763 let work_edge = Edge::new(EdgeId::new(2), EntityId::new(1), EntityId::new(2), "WORKS_AT");
764
765 assert!(filter.matches(&friend_edge));
766 assert!(!filter.matches(&work_edge));
767 }
768
769 #[test]
770 fn step_filter_matches_multiple_types() {
771 let filter = StepFilter::edge_types([EdgeType::new("FRIEND"), EdgeType::new("FOLLOWS")]);
772 let friend = Edge::new(EdgeId::new(1), EntityId::new(1), EntityId::new(2), "FRIEND");
773 let follows = Edge::new(EdgeId::new(2), EntityId::new(1), EntityId::new(2), "FOLLOWS");
774 let blocks = Edge::new(EdgeId::new(3), EntityId::new(1), EntityId::new(2), "BLOCKS");
775
776 assert!(filter.matches(&friend));
777 assert!(filter.matches(&follows));
778 assert!(!filter.matches(&blocks));
779 }
780
781 #[test]
782 fn path_step_variable_length() {
783 let step = PathStep::outgoing("FRIEND").variable_length(1, 3);
784 assert!(step.is_variable_length());
785 assert_eq!(step.min_hops, 1);
786 assert_eq!(step.max_hops, Some(3));
787 }
788
789 #[test]
790 fn path_step_optional() {
791 let step = PathStep::outgoing("FRIEND").optional();
792 assert!(step.is_variable_length());
793 assert_eq!(step.min_hops, 0);
794 assert_eq!(step.max_hops, Some(1));
795 }
796
797 #[test]
798 fn path_step_zero_or_more() {
799 let step = PathStep::outgoing("FRIEND").zero_or_more();
800 assert!(step.is_variable_length());
801 assert_eq!(step.min_hops, 0);
802 assert_eq!(step.max_hops, None);
803 }
804
805 #[test]
806 fn path_pattern_builder() {
807 let pattern = PatternBuilder::new().out("KNOWS").out("WORKS_AT").limit(10).build();
808
809 assert_eq!(pattern.steps().len(), 2);
810 assert_eq!(pattern.limit, Some(10));
811 }
812
813 #[test]
814 fn pattern_match_helpers() {
815 let pm = PatternMatch {
816 nodes: vec![EntityId::new(1), EntityId::new(2), EntityId::new(3)],
817 step_edges: vec![vec![EdgeId::new(10)], vec![EdgeId::new(20)]],
818 };
819
820 assert_eq!(pm.source(), EntityId::new(1));
821 assert_eq!(pm.target(), EntityId::new(3));
822 assert_eq!(pm.length(), 2);
823 assert_eq!(pm.all_edges(), vec![EdgeId::new(10), EdgeId::new(20)]);
824 }
825
826 #[test]
827 fn empty_pattern() {
828 let pattern = PathPattern::new();
829 assert!(pattern.is_empty());
830 }
831}