use crate::optimizer::heuristic::context::RewriteContext;
use crate::optimizer::heuristic::pattern::Pattern;
use crate::optimizer::heuristic::result::{RewriteResult, TransformResult};
use crate::optimizer::heuristic::rule::{EliminationRule, RewriteRule};
use crate::planning::plan::core::nodes::base::plan_node_traits::SingleInputNode;
use crate::planning::plan::core::nodes::operation::project_node::ProjectNode;
use crate::planning::plan::PlanNodeEnum;
use linkrs_core::Expression;
use std::collections::HashSet;
#[derive(Debug)]
pub struct RemoveNoopProjectRule {
allowed_child_types: HashSet<&'static str>,
}
impl RemoveNoopProjectRule {
pub fn new() -> Self {
let mut allowed_child_types = HashSet::new();
allowed_child_types.insert("GetNeighbors");
allowed_child_types.insert("GetVertices");
allowed_child_types.insert("GetEdges");
allowed_child_types.insert("Traverse");
allowed_child_types.insert("AppendVertices");
allowed_child_types.insert("IndexScan");
allowed_child_types.insert("ScanVertices");
allowed_child_types.insert("ScanEdges");
allowed_child_types.insert("Union");
allowed_child_types.insert("Project");
allowed_child_types.insert("Unwind");
allowed_child_types.insert("Sort");
allowed_child_types.insert("TopN");
allowed_child_types.insert("Sample");
allowed_child_types.insert("Aggregate");
allowed_child_types.insert("Assign");
allowed_child_types.insert("InnerJoin");
allowed_child_types.insert("InnerJoin");
allowed_child_types.insert("LeftJoin");
allowed_child_types.insert("CrossJoin");
allowed_child_types.insert("DataCollect");
allowed_child_types.insert("Argument");
Self {
allowed_child_types,
}
}
fn is_allowed_child_type(&self, node: &PlanNodeEnum) -> bool {
self.allowed_child_types.contains(node.name())
}
fn child_widens_layout(child: &PlanNodeEnum) -> bool {
match child {
PlanNodeEnum::ScanVertices(node) => !node.projected_properties().is_empty(),
PlanNodeEnum::ScanEdges(node) => !node.projected_properties().is_empty(),
PlanNodeEnum::GetVertices(node) => !node.projected_properties().is_empty(),
PlanNodeEnum::GetNeighbors(node) => !node.projected_properties().is_empty(),
PlanNodeEnum::IndexScan(node) => !node.return_columns().is_empty(),
_ => false,
}
}
fn is_noop_projection(&self, project: &ProjectNode, child_col_names: &[String]) -> bool {
let proj_col_names = project.col_names();
if proj_col_names.len() != child_col_names.len() {
return false;
}
let columns = project.columns();
for (i, col) in columns.iter().enumerate() {
let expr = &col.expression;
if let Some(expr_meta) = expr.expression() {
let inner_expr = expr_meta.inner();
match inner_expr {
Expression::Variable(var_name) => {
if var_name != &proj_col_names[i] {
return false;
}
}
Expression::Property { property, .. } => {
if property != &proj_col_names[i] {
return false;
}
}
_ => {
return false;
}
}
} else {
return false;
}
if proj_col_names[i] != child_col_names[i] {
return false;
}
}
true
}
}
impl Default for RemoveNoopProjectRule {
fn default() -> Self {
Self::new()
}
}
impl RewriteRule for RemoveNoopProjectRule {
fn name(&self) -> &'static str {
"RemoveNoopProjectRule"
}
fn pattern(&self) -> Pattern {
Pattern::new_with_name("Project")
}
fn apply(
&self,
_ctx: &mut RewriteContext,
node: &PlanNodeEnum,
) -> RewriteResult<Option<TransformResult>> {
let project = match node {
PlanNodeEnum::Project(n) => n,
_ => return Ok(None),
};
let input = project.input();
if !self.is_allowed_child_type(input) {
return Ok(None);
}
if Self::child_widens_layout(input) {
return Ok(None);
}
let child_col_names = input.col_names();
if !self.is_noop_projection(project, child_col_names) {
return Ok(None);
}
let mut result = TransformResult::new();
result.erase_curr = true;
result.add_new_node(input.clone());
Ok(Some(result))
}
}
impl EliminationRule for RemoveNoopProjectRule {
fn can_eliminate(&self, node: &PlanNodeEnum) -> bool {
match node {
PlanNodeEnum::Project(n) => {
let input = n.input();
if !self.is_allowed_child_type(input) {
return false;
}
if Self::child_widens_layout(input) {
return false;
}
self.is_noop_projection(n, input.col_names())
}
_ => false,
}
}
fn eliminate(
&self,
ctx: &mut RewriteContext,
node: &PlanNodeEnum,
) -> RewriteResult<Option<TransformResult>> {
self.apply(ctx, node)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::optimizer::heuristic::rule::RewriteRule;
use crate::planning::plan::core::nodes::access::{IndexScanNode, ScanType};
use crate::planning::plan::core::nodes::{ProjectNode, ScanVerticesNode};
use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
use linkrs_core::types::ContextualExpression;
use linkrs_core::{Expression, YieldColumn};
use std::sync::Arc;
fn create_yield_column(expr: Expression, alias: &str) -> YieldColumn {
let ctx = Arc::new(ExpressionAnalysisContext::new());
let expr_meta = linkrs_core::types::expr::ExpressionMeta::new(expr);
let id = ctx.register_expression(expr_meta);
let ctx_expr = ContextualExpression::new(id, ctx);
YieldColumn {
expression: ctx_expr,
alias: alias.to_string(),
}
}
fn noop_project(scan: PlanNodeEnum) -> PlanNodeEnum {
let columns = vec![create_yield_column(
Expression::Variable("p".to_string()),
"p",
)];
let project = ProjectNode::new(scan, columns).expect("Failed to create ProjectNode");
PlanNodeEnum::Project(project)
}
#[test]
fn test_remove_noop_project_rule_name() {
let rule = RemoveNoopProjectRule::new();
assert_eq!(rule.name(), "RemoveNoopProjectRule");
}
#[test]
fn test_remove_noop_project_rule_pattern() {
let rule = RemoveNoopProjectRule::new();
let pattern = rule.pattern();
assert!(pattern.node.is_some());
}
#[test]
fn test_is_allowed_child_type() {
let rule = RemoveNoopProjectRule::new();
let start_node =
crate::planning::plan::core::nodes::control_flow::start_node::StartNode::new();
assert!(!rule.is_allowed_child_type(&PlanNodeEnum::Start(start_node.clone())));
}
#[test]
fn test_noop_project_removed_over_plain_scan() {
let rule = RemoveNoopProjectRule::new();
let mut ctx = RewriteContext::new();
let mut scan_node = ScanVerticesNode::new(1, "default");
scan_node.set_col_names(vec!["p".to_string()]);
let project = noop_project(PlanNodeEnum::ScanVertices(scan_node));
let result = rule
.apply(&mut ctx, &project)
.expect("Failed to apply rule");
assert!(result.is_some());
assert!(result.expect("transform should exist").erase_curr);
}
#[test]
fn test_project_kept_over_scan_with_flat_property_columns() {
let rule = RemoveNoopProjectRule::new();
let mut ctx = RewriteContext::new();
let mut scan_node = ScanVerticesNode::new(1, "default");
scan_node.set_col_names(vec!["p".to_string()]);
scan_node.set_projected_properties(vec!["age".to_string()]);
let project = noop_project(PlanNodeEnum::ScanVertices(scan_node));
let result = rule
.apply(&mut ctx, &project)
.expect("Failed to apply rule");
assert!(
result.is_none(),
"project must survive a widened scan layout"
);
assert!(!rule.can_eliminate(&project));
}
#[test]
fn test_project_kept_over_covering_index_scan() {
let rule = RemoveNoopProjectRule::new();
let mut ctx = RewriteContext::new();
let mut index_scan = IndexScanNode::new(
1,
1,
1,
"person_idx".to_string(),
"person".to_string(),
ScanType::Unique,
);
index_scan.set_col_names(vec!["p".to_string()]);
index_scan.set_return_columns(vec!["name".to_string()]);
let project = noop_project(PlanNodeEnum::IndexScan(index_scan));
let result = rule
.apply(&mut ctx, &project)
.expect("Failed to apply rule");
assert!(
result.is_none(),
"project must survive a covering index scan"
);
}
}