use std::collections::HashSet;
use crate::ast::{CompareOp, Expr, NodePattern, Pattern, PropAccess, RelDirection};
use crate::error::QueryError;
use crate::ir::{ExpandDirection, LogicalPlan};
struct VarNamer {
next: usize,
}
impl VarNamer {
fn new() -> Self {
Self { next: 0 }
}
fn name(&mut self, given: &Option<String>) -> String {
match given {
Some(v) => v.clone(),
None => {
let n = format!("__anon{}", self.next);
self.next += 1;
n
}
}
}
}
pub fn build_match_plan(
pattern: &Pattern,
where_clause: &Option<Expr>,
carried_vars: &HashSet<String>,
) -> Result<LogicalPlan, QueryError> {
let mut namer = VarNamer::new();
let start_var = namer.name(&pattern.start.var);
let mut plan = if carried_vars.contains(&start_var) {
wrap_labels_and_props(LogicalPlan::Seed { var: start_var.clone() }, &start_var, &pattern.start, 0)
} else {
scan_for(&start_var, &pattern.start)
};
let mut from_var = start_var;
for (rel, node) in &pattern.hops {
let is_repeat = node.var.as_ref().is_some_and(|v| carried_vars.contains(v));
let to_var = if is_repeat { namer.name(&None) } else { namer.name(&node.var) };
let direction = match rel.direction {
RelDirection::Right => ExpandDirection::Out,
RelDirection::Left => ExpandDirection::In,
RelDirection::Either => ExpandDirection::Either,
};
plan = match rel.hop_range {
None => LogicalPlan::Expand {
input: Box::new(plan),
from_var: from_var.clone(),
to_var: to_var.clone(),
rel_var: rel.var.clone(),
rel_label: rel.rel_type.clone(),
direction,
},
Some((min_hops, max_hops)) => {
if rel.var.is_some() {
return Err(QueryError::Parse(
"binding a variable name to a variable-length relationship (e.g. \
[r:TYPE*1..3]) isn't supported — omit the variable name"
.into(),
));
}
LogicalPlan::VarExpand {
input: Box::new(plan),
from_var: from_var.clone(),
to_var: to_var.clone(),
rel_label: rel.rel_type.clone(),
direction,
min_hops,
max_hops,
}
}
};
plan = wrap_labels_and_props(plan, &to_var, node, 0);
if is_repeat {
let original = node.var.clone().expect("is_repeat implies node.var is Some");
plan = LogicalPlan::Filter {
input: Box::new(plan),
predicate: Expr::VarEq(to_var.clone(), original),
};
}
from_var = to_var;
}
if let Some(expr) = where_clause {
plan = LogicalPlan::Filter {
input: Box::new(plan),
predicate: expr.clone(),
};
}
Ok(plan)
}
fn scan_for(var: &str, node: &NodePattern) -> LogicalPlan {
let base = match node.labels.first() {
Some(label) => LogicalPlan::NodeByLabelScan {
var: var.to_string(),
label: label.clone(),
},
None => LogicalPlan::AllNodesScan { var: var.to_string() },
};
wrap_labels_and_props(base, var, node, 1)
}
fn wrap_labels_and_props(plan: LogicalPlan, var: &str, node: &NodePattern, skip: usize) -> LogicalPlan {
let mut plan = plan;
for label in node.labels.iter().skip(skip) {
plan = LogicalPlan::Filter {
input: Box::new(plan),
predicate: Expr::HasLabel(var.to_string(), label.clone()),
};
}
for (key, lit) in &node.props {
let predicate = Expr::Compare(
PropAccess {
var: var.to_string(),
prop: key.clone(),
},
CompareOp::Eq,
lit.clone(),
);
plan = LogicalPlan::Filter {
input: Box::new(plan),
predicate,
};
}
plan
}
pub fn pattern_all_vars(pattern: &Pattern) -> HashSet<String> {
let mut vars = HashSet::new();
if let Some(v) = &pattern.start.var {
vars.insert(v.clone());
}
for (rel, node) in &pattern.hops {
if let Some(v) = &rel.var {
vars.insert(v.clone());
}
if let Some(v) = &node.var {
vars.insert(v.clone());
}
}
vars
}
pub fn pattern_new_vars(pattern: &Pattern, carried_vars: &HashSet<String>) -> HashSet<String> {
pattern_all_vars(pattern)
.into_iter()
.filter(|v| !carried_vars.contains(v))
.collect()
}