use crate::optimizer::analysis::RequiredPropertyAnalyzer;
use crate::optimizer::heuristic::pattern::Pattern;
use crate::optimizer::heuristic::result::{RewriteResult, TransformResult};
use crate::optimizer::heuristic::rule::RewriteRule;
use crate::planning::plan::core::nodes::base::plan_node_traits::SingleInputNode;
use crate::planning::plan::PlanNodeEnum;
#[derive(Debug)]
pub struct PushProjectDownAppendVerticesRule;
impl PushProjectDownAppendVerticesRule {
pub fn new() -> Self {
Self
}
}
impl Default for PushProjectDownAppendVerticesRule {
fn default() -> Self {
Self::new()
}
}
impl RewriteRule for PushProjectDownAppendVerticesRule {
fn name(&self) -> &'static str {
"PushProjectDownAppendVerticesRule"
}
fn pattern(&self) -> Pattern {
Pattern::new_with_name("Project")
}
fn apply(
&self,
_ctx: &mut crate::optimizer::heuristic::context::RewriteContext,
node: &PlanNodeEnum,
) -> RewriteResult<Option<TransformResult>> {
let project_node = match node {
PlanNodeEnum::Project(n) => n,
_ => return Ok(None),
};
let mut chain: Vec<PlanNodeEnum> = Vec::new();
let mut current = project_node.input();
let append_node_id = loop {
match current {
PlanNodeEnum::Filter(f) => {
chain.push(current.clone());
current = f.input();
}
PlanNodeEnum::AppendVertices(a) => break a.id(),
_ => return Ok(None),
}
};
let append_node = match current {
PlanNodeEnum::AppendVertices(a) => a,
_ => unreachable!(),
};
let Some(var) = append_node.input_var() else {
return Ok(None);
};
let map = RequiredPropertyAnalyzer::new().analyze(node);
let Some(props) = map.narrowable_properties(append_node_id, var) else {
return Ok(None);
};
let mut narrowed = false;
let vertex_props: Vec<_> = append_node
.vertex_props()
.iter()
.map(|tp| {
let kept: Vec<String> = tp
.props
.iter()
.filter(|p| props.contains(p))
.cloned()
.collect();
if kept.len() != tp.props.len() {
narrowed = true;
}
crate::planning::plan::core::common::TagProp {
tag: tp.tag.clone(),
props: kept,
}
})
.collect();
if !narrowed {
return Ok(None);
}
let mut new_append = append_node.clone();
new_append.set_vertex_props(vertex_props);
let mut input = PlanNodeEnum::AppendVertices(new_append);
for level in chain.into_iter().rev() {
let mut f = match level {
PlanNodeEnum::Filter(f) => f,
_ => unreachable!(),
};
f.set_input(input);
input = PlanNodeEnum::Filter(f);
}
let mut new_project = project_node.clone();
new_project.set_input(input);
let new_node = PlanNodeEnum::Project(new_project);
let mut result = TransformResult::new();
result.add_new_node(new_node);
Ok(Some(result))
}
}
impl crate::optimizer::heuristic::rule::PushDownRule for PushProjectDownAppendVerticesRule {
fn can_push_down(&self, node: &PlanNodeEnum, target: &PlanNodeEnum) -> bool {
matches!(node, PlanNodeEnum::Project(_))
&& matches!(target, PlanNodeEnum::AppendVertices(_))
}
fn push_down(
&self,
ctx: &mut crate::optimizer::heuristic::context::RewriteContext,
node: &PlanNodeEnum,
_target: &PlanNodeEnum,
) -> RewriteResult<Option<TransformResult>> {
self.apply(ctx, node)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::optimizer::heuristic::context::RewriteContext;
use crate::planning::plan::core::common::TagProp;
use crate::planning::plan::core::nodes::operation::project_node::ProjectNode;
use crate::planning::plan::core::nodes::traversal::traversal_node::AppendVerticesNode;
use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
use linkrs_core::types::expr::ExpressionMeta;
use linkrs_core::types::ContextualExpression;
use linkrs_core::{Expression, YieldColumn};
use std::sync::Arc;
fn contextual(expr: Expression) -> ContextualExpression {
let ctx = Arc::new(ExpressionAnalysisContext::new());
let id = ctx.register_expression(ExpressionMeta::new(expr));
ContextualExpression::new(id, ctx)
}
fn prop(var: &str, name: &str) -> Expression {
Expression::Property {
object: Box::new(Expression::Variable(var.to_string())),
property: name.to_string(),
}
}
fn yield_column(expr: Expression, alias: &str) -> YieldColumn {
YieldColumn {
expression: contextual(expr),
alias: alias.to_string(),
}
}
fn append_vertices(var: &str) -> PlanNodeEnum {
let mut node = AppendVerticesNode::new(1, "person");
node.set_input_var(var.to_string());
node.set_vertex_props(vec![TagProp {
tag: "person".to_string(),
props: vec!["age".to_string(), "name".to_string()],
}]);
PlanNodeEnum::AppendVertices(node)
}
#[test]
fn test_rule_name_and_pattern() {
let rule = PushProjectDownAppendVerticesRule::new();
assert_eq!(rule.name(), "PushProjectDownAppendVerticesRule");
assert!(rule.pattern().node.is_some());
}
#[test]
fn test_narrows_append_vertices_vertex_props() {
let rule = PushProjectDownAppendVerticesRule::new();
let project = PlanNodeEnum::Project(
ProjectNode::new(
append_vertices("v"),
vec![yield_column(prop("v", "name"), "name")],
)
.expect("project node"),
);
let result = rule
.apply(&mut RewriteContext::new(), &project)
.expect("rewrite should succeed")
.expect("projection must be narrowed");
let PlanNodeEnum::Project(p) = &result.new_nodes[0] else {
panic!("expected Project preserved");
};
let PlanNodeEnum::AppendVertices(a) = p.input() else {
panic!("expected AppendVertices below Project");
};
let props = a.vertex_props();
assert_eq!(props.len(), 1);
assert_eq!(props[0].props, vec!["name".to_string()]);
}
#[test]
fn test_bare_variable_blocks_narrowing() {
let rule = PushProjectDownAppendVerticesRule::new();
let project = PlanNodeEnum::Project(
ProjectNode::new(
append_vertices("v"),
vec![yield_column(Expression::Variable("v".to_string()), "v")],
)
.expect("project node"),
);
let result = rule
.apply(&mut RewriteContext::new(), &project)
.expect("rewrite should succeed");
assert!(result.is_none(), "bare variable must block narrowing");
}
#[test]
fn test_noop_when_unchanged() {
let rule = PushProjectDownAppendVerticesRule::new();
let mut node = AppendVerticesNode::new(1, "person");
node.set_input_var("v".to_string());
node.set_vertex_props(vec![TagProp {
tag: "person".to_string(),
props: vec!["name".to_string()],
}]);
let project = PlanNodeEnum::Project(
ProjectNode::new(
PlanNodeEnum::AppendVertices(node),
vec![yield_column(prop("v", "name"), "name")],
)
.expect("project node"),
);
let result = rule
.apply(&mut RewriteContext::new(), &project)
.expect("rewrite should succeed");
assert!(result.is_none(), "no-op when already narrowed");
}
}