1use crate::optimizer::heuristic::context::RewriteContext;
42use crate::optimizer::heuristic::pattern::Pattern;
43use crate::optimizer::heuristic::result::{RewriteResult, TransformResult};
44use crate::optimizer::heuristic::rule::RewriteRule;
45use crate::planning::plan::core::nodes::base::plan_node_traits::SingleInputNode;
46use crate::planning::plan::core::nodes::operation::filter_node::FilterNode;
47use crate::planning::plan::PlanNodeEnum;
48use linkrs_core::types::expr::contextual::ContextualExpression;
49use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
50use linkrs_core::types::expr::visitor::ExpressionVisitor;
51use linkrs_core::types::expr::visitor_collectors::VariableCollector;
52use linkrs_core::types::expr::ExpressionMeta;
53use linkrs_core::types::operators::BinaryOperator;
54use linkrs_core::Expression;
55use std::sync::Arc;
56
57#[derive(Debug)]
59pub struct ExtractEquiJoinKeysRule;
60
61impl ExtractEquiJoinKeysRule {
62 pub fn new() -> Self {
64 Self
65 }
66
67 fn flatten_conjuncts(expr: &Expression, out: &mut Vec<Expression>) {
69 match expr {
70 Expression::Binary {
71 op: BinaryOperator::And,
72 left,
73 right,
74 } => {
75 Self::flatten_conjuncts(left, out);
76 Self::flatten_conjuncts(right, out);
77 }
78 _ => out.push(expr.clone()),
79 }
80 }
81
82 fn referenced_variables(expr: &Expression) -> Vec<String> {
84 let mut collector = VariableCollector::new();
85 collector.visit(expr);
86 collector.variables
87 }
88
89 fn is_left_side(vars: &[String], left_cols: &[String], right_cols: &[String]) -> Option<bool> {
96 if vars.is_empty() {
97 return None;
98 }
99 let in_left = vars.iter().all(|v| left_cols.contains(v));
100 let in_right = vars.iter().all(|v| right_cols.contains(v));
101 match (in_left, in_right) {
102 (true, false) => Some(true),
103 (false, true) => Some(false),
104 _ => None,
105 }
106 }
107
108 fn register_key(
110 expr: Expression,
111 ctx: &Arc<ExpressionAnalysisContext>,
112 ) -> ContextualExpression {
113 let id = ctx.register_expression(ExpressionMeta::new(expr));
114 ContextualExpression::new(id, ctx.clone())
115 }
116
117 fn combine_residual(conjuncts: Vec<Expression>) -> Option<Expression> {
120 let mut iter = conjuncts.into_iter();
121 let first = iter.next()?;
122 Some(iter.fold(first, |acc, next| Expression::Binary {
123 left: Box::new(acc),
124 op: BinaryOperator::And,
125 right: Box::new(next),
126 }))
127 }
128}
129
130impl Default for ExtractEquiJoinKeysRule {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136impl RewriteRule for ExtractEquiJoinKeysRule {
137 fn name(&self) -> &'static str {
138 "ExtractEquiJoinKeysRule"
139 }
140
141 fn pattern(&self) -> Pattern {
142 Pattern::new_with_name("Filter").with_dependency_name("InnerJoin")
143 }
144
145 fn apply(
146 &self,
147 _ctx: &mut RewriteContext,
148 node: &PlanNodeEnum,
149 ) -> RewriteResult<Option<TransformResult>> {
150 let filter_node = match node {
151 PlanNodeEnum::Filter(n) => n,
152 _ => return Ok(None),
153 };
154 let join = match filter_node.input() {
155 PlanNodeEnum::InnerJoin(n) => n,
156 _ => return Ok(None),
157 };
158 if !join.hash_keys().is_empty() || !join.probe_keys().is_empty() {
160 return Ok(None);
161 }
162
163 let condition = filter_node.condition();
164 let filter_expr = match condition.expression() {
165 Some(meta) => meta.inner().clone(),
166 None => return Ok(None),
167 };
168 let expr_ctx = condition.context().clone();
169
170 let left_cols = join.left_input().col_names().to_vec();
171 let right_cols = join.right_input().col_names().to_vec();
172
173 let mut conjuncts = Vec::new();
174 Self::flatten_conjuncts(&filter_expr, &mut conjuncts);
175
176 let mut hash_keys = Vec::new();
177 let mut probe_keys = Vec::new();
178 let mut residual = Vec::new();
179
180 for conjunct in conjuncts {
181 let sides = match &conjunct {
185 Expression::Binary {
186 op: BinaryOperator::Equal,
187 left,
188 right,
189 } => {
190 let left_is_left = Self::is_left_side(
191 &Self::referenced_variables(left),
192 &left_cols,
193 &right_cols,
194 );
195 let right_is_left = Self::is_left_side(
196 &Self::referenced_variables(right),
197 &left_cols,
198 &right_cols,
199 );
200 match (left_is_left, right_is_left) {
201 (Some(true), Some(false)) => Some((left, right)),
202 (Some(false), Some(true)) => Some((right, left)),
203 _ => None,
204 }
205 }
206 _ => None,
207 };
208 match sides {
209 Some((probe_side, hash_side)) => {
210 probe_keys.push(Self::register_key((**probe_side).clone(), &expr_ctx));
211 hash_keys.push(Self::register_key((**hash_side).clone(), &expr_ctx));
212 }
213 None => residual.push(conjunct),
214 }
215 }
216
217 if hash_keys.is_empty() {
218 return Ok(None);
219 }
220
221 let mut new_join = join.clone();
222 new_join.set_hash_keys(hash_keys);
223 new_join.set_probe_keys(probe_keys);
224 let new_join_enum = PlanNodeEnum::InnerJoin(new_join);
225
226 let mut result = TransformResult::new();
227 result.erase_curr = true;
228 if residual.is_empty() {
229 result.add_new_node(new_join_enum);
230 } else {
231 let residual_expr = match Self::combine_residual(residual) {
232 Some(expr) => expr,
233 None => {
234 result.add_new_node(new_join_enum);
235 return Ok(Some(result));
236 }
237 };
238 let residual_ctx_expr = Self::register_key(residual_expr, &expr_ctx);
239 let new_filter = FilterNode::new(new_join_enum, residual_ctx_expr).map_err(|e| {
240 crate::optimizer::heuristic::result::RewriteError::rewrite_failed(format!(
241 "Failed to create residual FilterNode: {:?}",
242 e
243 ))
244 })?;
245 result.add_new_node(PlanNodeEnum::Filter(new_filter));
246 }
247 Ok(Some(result))
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use crate::planning::plan::core::nodes::access::graph_scan_node::ScanVerticesNode;
255 use crate::planning::plan::core::nodes::join::join_node::InnerJoinNode;
256 use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
257
258 fn scan_with_cols(space: &str, cols: &[&str]) -> PlanNodeEnum {
259 let mut scan = ScanVerticesNode::new(1, space);
260 scan.set_col_names(cols.iter().map(|s| s.to_string()).collect());
261 PlanNodeEnum::ScanVertices(scan)
262 }
263
264 fn contextual(expr: Expression, ctx: &Arc<ExpressionAnalysisContext>) -> ContextualExpression {
265 let id = ctx.register_expression(ExpressionMeta::new(expr));
266 ContextualExpression::new(id, ctx.clone())
267 }
268
269 fn equality_filter(
270 input: PlanNodeEnum,
271 left: Expression,
272 right: Expression,
273 ctx: &Arc<ExpressionAnalysisContext>,
274 ) -> PlanNodeEnum {
275 let cond = Expression::Binary {
276 left: Box::new(left),
277 op: BinaryOperator::Equal,
278 right: Box::new(right),
279 };
280 let filter = FilterNode::new(input, contextual(cond, ctx)).expect("filter");
281 PlanNodeEnum::Filter(filter)
282 }
283
284 fn prop(var: &str, property: &str) -> Expression {
285 Expression::Property {
286 object: Box::new(Expression::variable(var)),
287 property: property.to_string(),
288 }
289 }
290
291 #[test]
292 fn test_rule_name() {
293 let rule = ExtractEquiJoinKeysRule::new();
294 assert_eq!(rule.name(), "ExtractEquiJoinKeysRule");
295 }
296
297 #[test]
298 fn test_rule_pattern() {
299 let rule = ExtractEquiJoinKeysRule::new();
300 let pattern = rule.pattern();
301 assert!(pattern.node.is_some());
302 }
303
304 #[test]
305 fn test_skips_join_with_existing_keys() {
306 let ctx = Arc::new(ExpressionAnalysisContext::new());
307 let left = scan_with_cols("s", &["a"]);
308 let right = scan_with_cols("s", &["b"]);
309 let key = contextual(Expression::variable("a"), &ctx);
310 let join =
311 InnerJoinNode::new(left, right, vec![key.clone()], vec![key]).expect("join with keys");
312 let node = equality_filter(
313 PlanNodeEnum::InnerJoin(join),
314 prop("a", "value"),
315 prop("b", "value"),
316 &ctx,
317 );
318 let rule = ExtractEquiJoinKeysRule::new();
319 let mut rewrite_ctx = RewriteContext::new();
320 let out = rule.apply(&mut rewrite_ctx, &node).expect("apply");
321 assert!(out.is_none(), "joins with keys must be left alone");
322 }
323
324 #[test]
325 fn test_extracts_cross_side_equality() {
326 let ctx = Arc::new(ExpressionAnalysisContext::new());
327 let left = scan_with_cols("s", &["a"]);
328 let right = scan_with_cols("s", &["b"]);
329 let join = InnerJoinNode::new(left, right, vec![], vec![]).expect("keyless join");
330 let node = equality_filter(
331 PlanNodeEnum::InnerJoin(join),
332 prop("a", "value"),
333 prop("b", "value"),
334 &ctx,
335 );
336 let rule = ExtractEquiJoinKeysRule::new();
337 let mut rewrite_ctx = RewriteContext::new();
338 let out = rule
339 .apply(&mut rewrite_ctx, &node)
340 .expect("apply")
341 .expect("must fire");
342 assert!(out.erase_curr);
343 assert_eq!(out.new_nodes.len(), 1);
344 match &out.new_nodes[0] {
345 PlanNodeEnum::InnerJoin(j) => {
346 assert_eq!(j.hash_keys().len(), 1);
347 assert_eq!(j.probe_keys().len(), 1);
348 let hash_vars = ExtractEquiJoinKeysRule::referenced_variables(
351 j.hash_keys()[0].expression().expect("hash").inner(),
352 );
353 let probe_vars = ExtractEquiJoinKeysRule::referenced_variables(
354 j.probe_keys()[0].expression().expect("probe").inner(),
355 );
356 assert_eq!(hash_vars, vec!["b".to_string()]);
357 assert_eq!(probe_vars, vec!["a".to_string()]);
358 }
359 other => panic!("expected keyed InnerJoin, got {:?}", other.type_name()),
360 }
361 }
362
363 #[test]
364 fn test_extracts_mirrored_equality() {
365 let ctx = Arc::new(ExpressionAnalysisContext::new());
366 let left = scan_with_cols("s", &["a"]);
367 let right = scan_with_cols("s", &["b"]);
368 let join = InnerJoinNode::new(left, right, vec![], vec![]).expect("keyless join");
369 let node = equality_filter(
371 PlanNodeEnum::InnerJoin(join),
372 prop("b", "value"),
373 prop("a", "value"),
374 &ctx,
375 );
376 let rule = ExtractEquiJoinKeysRule::new();
377 let mut rewrite_ctx = RewriteContext::new();
378 let out = rule
379 .apply(&mut rewrite_ctx, &node)
380 .expect("apply")
381 .expect("must fire");
382 match &out.new_nodes[0] {
383 PlanNodeEnum::InnerJoin(j) => {
384 let hash_vars = ExtractEquiJoinKeysRule::referenced_variables(
385 j.hash_keys()[0].expression().expect("hash").inner(),
386 );
387 let probe_vars = ExtractEquiJoinKeysRule::referenced_variables(
388 j.probe_keys()[0].expression().expect("probe").inner(),
389 );
390 assert_eq!(hash_vars, vec!["b".to_string()]);
391 assert_eq!(probe_vars, vec!["a".to_string()]);
392 }
393 other => panic!("expected keyed InnerJoin, got {:?}", other.type_name()),
394 }
395 }
396
397 #[test]
398 fn test_keeps_residual_filter() {
399 let ctx = Arc::new(ExpressionAnalysisContext::new());
400 let left = scan_with_cols("s", &["a"]);
401 let right = scan_with_cols("s", &["b"]);
402 let join = InnerJoinNode::new(left, right, vec![], vec![]).expect("keyless join");
403 let equality = Expression::Binary {
404 left: Box::new(prop("a", "value")),
405 op: BinaryOperator::Equal,
406 right: Box::new(prop("b", "value")),
407 };
408 let range = Expression::Binary {
409 left: Box::new(prop("a", "value")),
410 op: BinaryOperator::LessThan,
411 right: Box::new(Expression::int(200)),
412 };
413 let cond = Expression::Binary {
414 left: Box::new(equality),
415 op: BinaryOperator::And,
416 right: Box::new(range),
417 };
418 let filter =
419 FilterNode::new(PlanNodeEnum::InnerJoin(join), contextual(cond, &ctx)).expect("filter");
420 let node = PlanNodeEnum::Filter(filter);
421 let rule = ExtractEquiJoinKeysRule::new();
422 let mut rewrite_ctx = RewriteContext::new();
423 let out = rule
424 .apply(&mut rewrite_ctx, &node)
425 .expect("apply")
426 .expect("must fire");
427 assert_eq!(out.new_nodes.len(), 1);
428 match &out.new_nodes[0] {
429 PlanNodeEnum::Filter(f) => match f.input() {
430 PlanNodeEnum::InnerJoin(j) => {
431 assert_eq!(j.hash_keys().len(), 1);
432 assert_eq!(j.probe_keys().len(), 1);
433 }
434 other => panic!(
435 "residual filter must sit above keyed join, got {:?}",
436 other.type_name()
437 ),
438 },
439 other => panic!("expected residual Filter, got {:?}", other.type_name()),
440 }
441 }
442
443 #[test]
444 fn test_ignores_single_side_predicate() {
445 let ctx = Arc::new(ExpressionAnalysisContext::new());
446 let left = scan_with_cols("s", &["a"]);
447 let right = scan_with_cols("s", &["b"]);
448 let join = InnerJoinNode::new(left, right, vec![], vec![]).expect("keyless join");
449 let range = Expression::Binary {
450 left: Box::new(prop("a", "value")),
451 op: BinaryOperator::LessThan,
452 right: Box::new(Expression::int(200)),
453 };
454 let filter = FilterNode::new(PlanNodeEnum::InnerJoin(join), contextual(range, &ctx))
455 .expect("filter");
456 let node = PlanNodeEnum::Filter(filter);
457 let rule = ExtractEquiJoinKeysRule::new();
458 let mut rewrite_ctx = RewriteContext::new();
459 let out = rule.apply(&mut rewrite_ctx, &node).expect("apply");
460 assert!(out.is_none(), "single-side predicates are not join keys");
461 }
462}