use crate::optimizer::heuristic::context::RewriteContext;
use crate::optimizer::heuristic::expression_utils::rewrite_contextual_expression;
use crate::optimizer::heuristic::pattern::Pattern;
use crate::optimizer::heuristic::result::{RewriteResult, TransformResult};
use crate::optimizer::heuristic::rule::{MergeRule, RewriteRule};
use crate::planning::plan::core::nodes::base::plan_node_enum::PlanNodeEnum;
use crate::planning::plan::core::nodes::base::plan_node_traits::SingleInputNode;
use crate::planning::plan::core::nodes::operation::project_node::ProjectNode;
use linkrs_core::YieldColumn;
use std::collections::HashMap;
#[derive(Debug)]
pub struct CollapseConsecutiveProjectRule;
impl CollapseConsecutiveProjectRule {
pub fn new() -> Self {
Self
}
fn merge_projects(
&self,
parent_proj: &ProjectNode,
child_proj: &ProjectNode,
ctx: &RewriteContext,
) -> Option<ProjectNode> {
let mut rewrite_map = HashMap::new();
for col in child_proj.columns() {
if !col.alias.is_empty() {
rewrite_map.insert(col.alias.clone(), col.expression.clone());
}
}
let expr_context = ctx.expr_context();
let new_columns: Vec<YieldColumn> = parent_proj
.columns()
.iter()
.map(|col| YieldColumn {
expression: rewrite_contextual_expression(
&col.expression,
&rewrite_map,
expr_context.clone(),
),
alias: col.alias.clone(),
})
.collect();
let child_input = child_proj.input().clone();
ProjectNode::new(child_input, new_columns).ok()
}
}
impl Default for CollapseConsecutiveProjectRule {
fn default() -> Self {
Self::new()
}
}
impl RewriteRule for CollapseConsecutiveProjectRule {
fn name(&self) -> &'static str {
"CollapseConsecutiveProjectRule"
}
fn pattern(&self) -> Pattern {
Pattern::new_with_name("Project").with_dependency_name("Project")
}
fn apply(
&self,
ctx: &mut RewriteContext,
node: &PlanNodeEnum,
) -> RewriteResult<Option<TransformResult>> {
let parent_proj = match node {
PlanNodeEnum::Project(n) => n,
_ => return Ok(None),
};
let child_node = parent_proj.input();
let child_proj = match child_node {
PlanNodeEnum::Project(n) => n,
_ => return Ok(None),
};
if let Some(new_proj) = self.merge_projects(parent_proj, child_proj, ctx) {
let mut result = TransformResult::new();
result.erase_curr = true;
result.add_new_node(PlanNodeEnum::Project(new_proj));
Ok(Some(result))
} else {
Ok(None)
}
}
}
impl MergeRule for CollapseConsecutiveProjectRule {
fn can_merge(&self, parent: &PlanNodeEnum, child: &PlanNodeEnum) -> bool {
parent.is_project() && child.is_project()
}
fn create_merged_node(
&self,
ctx: &mut RewriteContext,
parent: &PlanNodeEnum,
_child: &PlanNodeEnum,
) -> RewriteResult<Option<TransformResult>> {
self.apply(ctx, parent)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::planning::plan::core::nodes::control_flow::start_node::StartNode;
use linkrs_core::types::ContextualExpression;
use linkrs_core::Expression;
#[test]
fn test_rule_name() {
let rule = CollapseConsecutiveProjectRule::new();
assert_eq!(rule.name(), "CollapseConsecutiveProjectRule");
}
#[test]
fn test_rule_pattern() {
let rule = CollapseConsecutiveProjectRule::new();
let pattern = rule.pattern();
assert!(pattern.node.is_some());
}
#[test]
fn test_collapse_consecutive_projects() {
use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
use linkrs_core::types::expr::ExpressionMeta;
use std::sync::Arc;
let start = PlanNodeEnum::Start(StartNode::new());
let expr_ctx = Arc::new(ExpressionAnalysisContext::new());
let a_expr = Expression::Variable("a".to_string());
let a_meta = ExpressionMeta::new(a_expr);
let a_id = expr_ctx.register_expression(a_meta);
let a_ctx_expr = ContextualExpression::new(a_id, expr_ctx.clone());
let b_expr = Expression::Variable("b".to_string());
let b_meta = ExpressionMeta::new(b_expr);
let b_id = expr_ctx.register_expression(b_meta);
let b_ctx_expr = ContextualExpression::new(b_id, expr_ctx.clone());
let child_columns = vec![
YieldColumn {
expression: a_ctx_expr,
alias: "col_a".to_string(),
},
YieldColumn {
expression: b_ctx_expr,
alias: "col_b".to_string(),
},
];
let child_proj =
ProjectNode::new(start, child_columns).expect("Failed to create lower-level Project");
let child_node = PlanNodeEnum::Project(child_proj);
let col_a_expr = Expression::Variable("col_a".to_string());
let col_a_meta = ExpressionMeta::new(col_a_expr);
let col_a_id = expr_ctx.register_expression(col_a_meta);
let col_a_ctx_expr = ContextualExpression::new(col_a_id, expr_ctx);
let parent_columns = vec![YieldColumn {
expression: col_a_ctx_expr,
alias: "result".to_string(),
}];
let parent_proj = ProjectNode::new(child_node, parent_columns)
.expect("Failed to create upper-level Project");
let parent_node = PlanNodeEnum::Project(parent_proj);
let rule = CollapseConsecutiveProjectRule::new();
let mut ctx = RewriteContext::new();
let result = rule
.apply(&mut ctx, &parent_node)
.expect("Failed to apply rule");
assert!(
result.is_some(),
"The consecutive Project nodes should be merged successfully."
);
let transform_result = result.expect("Failed to apply rewrite rule");
assert!(transform_result.erase_curr);
assert_eq!(transform_result.new_nodes.len(), 1);
if let PlanNodeEnum::Project(ref new_proj) = transform_result.new_nodes[0] {
let columns = new_proj.columns();
assert_eq!(columns.len(), 1);
assert_eq!(columns[0].alias, "result");
if let Some(expr_meta) = columns[0].expression.expression() {
if let Expression::Variable(name) = expr_meta.inner() {
assert_eq!(name, "a");
} else {
panic!("The expression should be “Variable”.");
}
} else {
panic!("The expression should exist.");
}
} else {
panic!("The “Project” node");
}
}
}