use crate::optimizer::heuristic::context::RewriteContext;
use crate::optimizer::heuristic::pattern::Pattern;
use crate::optimizer::heuristic::result::{RewriteError, RewriteResult, TransformResult};
use crate::optimizer::heuristic::rule::RewriteRule;
use crate::planning::plan::core::nodes::base::plan_node_traits::{
MultipleInputNode, SingleInputNode,
};
use crate::planning::plan::core::nodes::join::join_node::{InnerJoinNode, LeftJoinNode};
use crate::planning::plan::core::nodes::operation::project_node::ProjectNode;
use crate::planning::plan::PlanNodeEnum;
use linkrs_core::types::expr::contextual::ContextualExpression;
use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
use linkrs_core::types::expr::visitor::ExpressionVisitor;
use linkrs_core::types::expr::visitor_checkers::VariableContainsChecker;
use linkrs_core::types::expr::visitor_collectors::PropertyCollector;
use linkrs_core::types::expr::ExpressionMeta;
use linkrs_core::types::YieldColumn;
use linkrs_core::Expression;
use std::sync::Arc;
#[derive(Debug)]
pub struct RemoveAppendVerticesBelowJoinRule;
impl RemoveAppendVerticesBelowJoinRule {
pub fn new() -> Self {
Self
}
fn collect_all_property_names(&self, expr: &Expression) -> Vec<String> {
let mut collector = PropertyCollector::new();
ExpressionVisitor::visit(&mut collector, expr);
collector.properties
}
fn is_id_or_joinkey_function(
&self,
expr: &ContextualExpression,
) -> Option<ContextualExpression> {
if let Some(expr_meta) = expr.expression() {
let inner_expr = expr_meta.inner();
match inner_expr {
Expression::Function { name, args }
if (name == "id" || name == "_joinkey") && args.len() == 1 =>
{
let ctx = expr.context().clone();
let meta = ExpressionMeta::new(args[0].as_expr().clone());
let id = ctx.register_expression(meta);
Some(ContextualExpression::new(id, ctx))
}
_ => None,
}
} else {
None
}
}
fn expr_references_alias(&self, expr: &ContextualExpression, alias: &str) -> bool {
if let Some(expr_meta) = expr.expression() {
let inner_expr = expr_meta.inner();
let properties = self.collect_all_property_names(inner_expr);
properties.iter().any(|p| p == alias)
} else {
false
}
}
fn count_alias_references(&self, exprs: &[ContextualExpression], alias: &str) -> usize {
exprs
.iter()
.filter(|e| self.expr_references_alias(e, alias))
.count()
}
fn count_alias_references_in_columns(&self, columns: &[YieldColumn], alias: &str) -> usize {
columns
.iter()
.filter(|c| self.expr_references_alias(&c.expression, alias))
.count()
}
fn find_column_with_alias(&self, columns: &[YieldColumn], alias: &str) -> Option<usize> {
for (idx, col) in columns.iter().enumerate() {
if let Some(expr_meta) = col.expression.expression() {
if let Expression::Variable(var_name) = expr_meta.inner() {
if var_name == alias {
return Some(idx);
}
}
}
}
None
}
fn find_matching_probe_key(
&self,
probe_keys: &[ContextualExpression],
av_node_alias: &str,
) -> Option<usize> {
for (idx, expr) in probe_keys.iter().enumerate() {
if let Some(arg) = self.is_id_or_joinkey_function(expr) {
if self.expr_contains_variable(&arg, av_node_alias) {
return Some(idx);
}
}
}
None
}
fn expr_contains_variable(&self, expr: &ContextualExpression, var_name: &str) -> bool {
if let Some(expr_meta) = expr.expression() {
let inner_expr = expr_meta.inner();
VariableContainsChecker::check(inner_expr, var_name)
} else {
false
}
}
fn create_none_direct_dst_expr(
&self,
edge_alias: &str,
vertex_alias: &str,
) -> ContextualExpression {
let expr = Expression::function(
"none_direct_dst".to_string(),
vec![
Expression::Variable(edge_alias.to_string()),
Expression::Variable(vertex_alias.to_string()),
],
);
let ctx = Arc::new(ExpressionAnalysisContext::new());
let meta = ExpressionMeta::new(expr);
let id = ctx.register_expression(meta);
ContextualExpression::new(id, ctx)
}
fn create_variable_expr(&self, var_name: &str) -> ContextualExpression {
let expr = Expression::Variable(var_name.to_string());
let ctx = Arc::new(ExpressionAnalysisContext::new());
let meta = ExpressionMeta::new(expr);
let id = ctx.register_expression(meta);
ContextualExpression::new(id, ctx)
}
}
impl Default for RemoveAppendVerticesBelowJoinRule {
fn default() -> Self {
Self::new()
}
}
impl RewriteRule for RemoveAppendVerticesBelowJoinRule {
fn name(&self) -> &'static str {
"RemoveAppendVerticesBelowJoinRule"
}
fn pattern(&self) -> Pattern {
Pattern::multi(vec!["InnerJoin", "LeftJoin"])
}
fn apply(
&self,
_ctx: &mut RewriteContext,
node: &PlanNodeEnum,
) -> RewriteResult<Option<TransformResult>> {
let (hash_keys, probe_keys, left_input, right_input) = match node {
PlanNodeEnum::InnerJoin(n) => (
n.hash_keys().to_vec(),
n.probe_keys().to_vec(),
n.left_input().clone(),
n.right_input().clone(),
),
PlanNodeEnum::LeftJoin(n) => (
n.hash_keys().to_vec(),
n.probe_keys().to_vec(),
n.left_input().clone(),
n.right_input().clone(),
),
_ => return Ok(None),
};
let project = match right_input {
PlanNodeEnum::Project(n) => n,
_ => return Ok(None),
};
let project_input = project.input();
let append_vertices = match project_input {
PlanNodeEnum::AppendVertices(n) => n,
_ => return Ok(None),
};
let av_node_alias = match append_vertices.node_alias() {
Some(alias) => alias,
None => return Ok(None),
};
let append_inputs = append_vertices.inputs();
if append_inputs.is_empty() {
return Ok(None);
}
let traverse = match &append_inputs[0] {
PlanNodeEnum::Traverse(n) => n,
_ => return Ok(None),
};
let tv_edge_alias = match traverse.edge_alias() {
Some(alias) => alias,
None => return Ok(None),
};
let _tv_node_alias = match traverse.vertex_alias() {
Some(alias) => alias,
None => return Ok(None),
};
let probe_ref_count = self.count_alias_references(&probe_keys, av_node_alias);
if probe_ref_count > 1 {
return Ok(None);
}
let probe_key_idx = match self.find_matching_probe_key(&probe_keys, av_node_alias) {
Some(idx) => idx,
None => return Ok(None),
};
if probe_key_idx >= hash_keys.len() {
return Ok(None);
}
let corresponding_hash_key = &hash_keys[probe_key_idx];
let probe_key = &probe_keys[probe_key_idx];
if corresponding_hash_key != probe_key {
return Ok(None);
}
let columns = project.columns();
let col_ref_count = self.count_alias_references_in_columns(columns, av_node_alias);
if col_ref_count > 1 {
return Ok(None);
}
let prj_idx = match self.find_column_with_alias(columns, av_node_alias) {
Some(idx) => idx,
None => return Ok(None),
};
let mut new_columns: Vec<YieldColumn> = columns.to_vec();
let none_direct_dst_expr = self.create_none_direct_dst_expr(tv_edge_alias, _tv_node_alias);
new_columns[prj_idx] = YieldColumn {
expression: none_direct_dst_expr,
alias: av_node_alias.clone(),
};
let new_project = ProjectNode::new(append_inputs[0].clone(), new_columns)
.map_err(|e| RewriteError::InvalidPlanStructure(e.to_string()))?;
let mut new_probe_keys: Vec<ContextualExpression> = probe_keys.clone();
new_probe_keys[probe_key_idx] = self.create_variable_expr(av_node_alias);
let new_join: PlanNodeEnum = match node {
PlanNodeEnum::InnerJoin(_) => PlanNodeEnum::InnerJoin(
InnerJoinNode::new(
left_input.clone(),
PlanNodeEnum::Project(new_project),
hash_keys.to_vec(),
new_probe_keys,
)
.map_err(|e| RewriteError::InvalidPlanStructure(e.to_string()))?,
),
PlanNodeEnum::LeftJoin(_) => PlanNodeEnum::LeftJoin(
LeftJoinNode::new(
left_input.clone(),
PlanNodeEnum::Project(new_project),
hash_keys.to_vec(),
new_probe_keys,
)
.map_err(|e| RewriteError::InvalidPlanStructure(e.to_string()))?,
),
_ => unreachable!(),
};
let mut result = TransformResult::new();
result.erase_curr = true;
result.add_new_node(new_join);
Ok(Some(result))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::optimizer::heuristic::rule::RewriteRule;
#[test]
fn test_remove_append_vertices_below_join_rule_name() {
let rule = RemoveAppendVerticesBelowJoinRule::new();
assert_eq!(rule.name(), "RemoveAppendVerticesBelowJoinRule");
}
#[test]
fn test_remove_append_vertices_below_join_rule_pattern() {
let rule = RemoveAppendVerticesBelowJoinRule::new();
let pattern = rule.pattern();
assert!(pattern.node.is_some());
}
}