linkrs_query/optimizer/heuristic/join_optimization/
join_to_expand.rs1use crate::optimizer::heuristic::context::RewriteContext;
25use crate::optimizer::heuristic::pattern::Pattern;
26use crate::optimizer::heuristic::result::{RewriteResult, TransformResult};
27use crate::optimizer::heuristic::rule::RewriteRule;
28use crate::planning::plan::core::nodes::base::plan_node_traits::MultipleInputNode;
29use crate::planning::plan::core::nodes::join::join_node::InnerJoinNode;
30use crate::planning::plan::core::nodes::traversal::traversal_node::ExpandAllNode;
31use crate::planning::plan::PlanNodeEnum;
32use linkrs_core::types::expr::contextual::ContextualExpression;
33use linkrs_core::types::expr::visitor::ExpressionVisitor;
34use linkrs_core::types::expr::visitor_collectors::VariableCollector;
35
36#[derive(Debug)]
38pub struct JoinToExpandRule;
39
40impl JoinToExpandRule {
41 pub fn new() -> Self {
42 Self
43 }
44
45 fn extract_join_key_variable(&self, expr: &ContextualExpression) -> Option<String> {
46 if let Some(expr_meta) = expr.expression() {
47 let mut collector = VariableCollector::new();
48 collector.visit(expr_meta.inner());
49 collector.variables.into_iter().next()
50 } else {
51 None
52 }
53 }
54
55 fn analyze_join_condition(&self, probe_keys: &[ContextualExpression]) -> Option<String> {
56 if probe_keys.len() != 1 {
57 return None;
58 }
59
60 self.extract_join_key_variable(&probe_keys[0])
61 }
62
63 fn determine_direction(&self, _edge_var: &str, join_key: &str) -> Option<&'static str> {
64 if join_key.ends_with("._src") || join_key.contains("src") {
65 Some("OUT")
66 } else if join_key.ends_with("._dst") || join_key.contains("dst") {
67 Some("IN")
68 } else {
69 None
70 }
71 }
72
73 fn apply_to_inner_join(&self, join: &InnerJoinNode) -> RewriteResult<Option<TransformResult>> {
74 let left = join.left_input();
75 let right = join.right_input();
76
77 let (scan_vertices, scan_edges, vertex_on_left) = match (left, right) {
78 (PlanNodeEnum::ScanVertices(v), PlanNodeEnum::ScanEdges(e)) => (v, e, true),
79 (PlanNodeEnum::ScanEdges(e), PlanNodeEnum::ScanVertices(v)) => (v, e, false),
80 _ => return Ok(None),
81 };
82
83 let probe_keys = if vertex_on_left {
84 join.probe_keys()
85 } else {
86 join.hash_keys()
87 };
88
89 let probe_var = match self.analyze_join_condition(probe_keys) {
90 Some(var) => var,
91 None => return Ok(None),
92 };
93
94 let direction = self.determine_direction(&probe_var, &probe_var);
95 let direction = match direction {
96 Some(d) => d,
97 None => return Ok(None),
98 };
99
100 let edge_types = scan_edges
101 .edge_type()
102 .map(|et| vec![et])
103 .unwrap_or_default();
104
105 let mut expand_all = ExpandAllNode::new(scan_vertices.space_id(), edge_types, direction);
106
107 expand_all.add_input(left.clone());
108
109 let mut result = TransformResult::new();
110 result.erase_curr = true;
111 result.add_new_node(PlanNodeEnum::ExpandAll(expand_all));
112
113 Ok(Some(result))
114 }
115}
116
117impl Default for JoinToExpandRule {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123impl RewriteRule for JoinToExpandRule {
124 fn name(&self) -> &'static str {
125 "JoinToExpandRule"
126 }
127
128 fn pattern(&self) -> Pattern {
129 Pattern::new_with_name("InnerJoin")
130 }
131
132 fn apply(
133 &self,
134 _ctx: &mut RewriteContext,
135 node: &PlanNodeEnum,
136 ) -> RewriteResult<Option<TransformResult>> {
137 match node {
138 PlanNodeEnum::InnerJoin(join) => self.apply_to_inner_join(join),
139 _ => Ok(None),
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_rule_name() {
150 let rule = JoinToExpandRule::new();
151 assert_eq!(rule.name(), "JoinToExpandRule");
152 }
153
154 #[test]
155 fn test_rule_pattern() {
156 let rule = JoinToExpandRule::new();
157 let pattern = rule.pattern();
158 assert!(pattern.node.is_some());
159 }
160}