use std::collections::HashSet;
use marsdb_graph::{GraphStore, Txn};
use crate::ast::{
CompareOp, Expr, Literal, NodePattern, Pattern, PropAccess, RelDirection, ReturnExpr,
};
use crate::error::QueryError;
use crate::executor::literal_to_value;
use crate::ir::{ExpandDirection, IndexSeekValue, 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 where_conjuncts = Vec::new();
if let Some(expr) = where_clause {
push_conjuncts(expr.clone(), &mut where_conjuncts);
}
let mut start_only = Vec::new();
where_conjuncts.retain(|c| {
if conjunct_sole_var(c) == Some(start_var.as_str()) {
start_only.push(c.clone());
false
} else {
true
}
});
if let Some(predicate) = rebuild_and(start_only) {
plan = LogicalPlan::Filter {
input: Box::new(plan),
predicate,
};
}
let mut from_var = start_var.clone();
let mut prior_rel_vars: Vec<String> = Vec::new();
let mut prior_edge_sets: Vec<String> = Vec::new();
let mut pattern_bound_vars: HashSet<String> = HashSet::new();
pattern_bound_vars.insert(start_var);
let mut pattern_rel_var_names: HashSet<String> = HashSet::new();
for (rel, node) in &pattern.hops {
let is_repeat = node
.var
.as_ref()
.is_some_and(|v| carried_vars.contains(v) || pattern_bound_vars.contains(v));
let to_var = if is_repeat {
namer.name(&None)
} else {
namer.name(&node.var)
};
if !is_repeat {
if let Some(v) = &node.var {
pattern_bound_vars.insert(v.clone());
}
}
let direction = match rel.direction {
RelDirection::Right => ExpandDirection::Out,
RelDirection::Left => ExpandDirection::In,
RelDirection::Either => ExpandDirection::Either,
};
if let Some(v) = &rel.var {
if pattern_rel_var_names.contains(v) {
return Err(QueryError::Semantic(format!(
"'{v}' is used for two different relationships in the same pattern — a relationship \
variable can't be reused within one MATCH pattern"
)));
}
if !carried_vars.contains(v) {
pattern_rel_var_names.insert(v.clone());
}
}
let rel_is_repeat = rel.var.as_ref().is_some_and(|v| carried_vars.contains(v));
let rel_filter_var = if rel.hop_range.is_none() {
Some(if rel_is_repeat {
namer.name(&None)
} else {
namer.name(&rel.var)
})
} else {
None
};
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_filter_var.clone(),
rel_labels: rel.rel_types.clone(),
direction,
},
Some((min_hops, max_hops)) if rel_is_repeat && !rel.capture_path_segment => {
LogicalPlan::MatchRelList {
input: Box::new(plan),
from_var: from_var.clone(),
to_var: to_var.clone(),
rel_list_var: rel
.var
.clone()
.expect("rel_is_repeat implies rel.var is Some"),
rel_labels: rel.rel_types.clone(),
direction,
min_hops,
max_hops,
}
}
Some((min_hops, max_hops)) => {
let exclude_edge_var = namer.name(&None);
let plan = LogicalPlan::VarExpand {
input: Box::new(plan),
from_var: from_var.clone(),
to_var: to_var.clone(),
rel_labels: rel.rel_types.clone(),
direction,
min_hops,
max_hops,
exclude_edge_vars: prior_rel_vars.clone(),
exclude_edge_sets: prior_edge_sets.clone(),
exclude_edge_var: exclude_edge_var.clone(),
path_segment_var: rel.capture_path_segment.then(|| {
rel.var
.clone()
.expect("name_pattern_for_path always sets rel.var alongside capture_path_segment")
}),
rel_list_var: if rel.capture_path_segment {
rel.rel_list_var.clone()
} else {
rel.var.clone()
},
rel_props: rel.props.clone(),
};
prior_edge_sets.push(exclude_edge_var);
plan
}
};
plan = wrap_labels_and_props(plan, &to_var, node, 0)?;
if let Some(rel_var) = &rel_filter_var {
for (key, expr) in &rel.props {
plan = LogicalPlan::Filter {
input: Box::new(plan),
predicate: pattern_prop_predicate(rel_var, key, expr),
};
}
for prior in &prior_rel_vars {
plan = LogicalPlan::Filter {
input: Box::new(plan),
predicate: Expr::Not(Box::new(Expr::VarEq(rel_var.clone(), prior.clone()))),
};
}
for prior_set in &prior_edge_sets {
plan = LogicalPlan::Filter {
input: Box::new(plan),
predicate: Expr::EdgeNotInSet {
edge_var: rel_var.clone(),
edge_set_var: prior_set.clone(),
},
};
}
if rel_is_repeat {
let original = rel
.var
.clone()
.expect("rel_is_repeat implies rel.var is Some");
plan = LogicalPlan::Filter {
input: Box::new(plan),
predicate: Expr::VarEq(rel_var.clone(), original),
};
}
if rel.hop_range.is_none() {
prior_rel_vars.push(rel_var.clone());
}
}
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(predicate) = rebuild_and(where_conjuncts) {
plan = LogicalPlan::Filter {
input: Box::new(plan),
predicate,
};
}
Ok(plan)
}
fn scan_for(var: &str, node: &NodePattern) -> Result<LogicalPlan, QueryError> {
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,
) -> Result<LogicalPlan, QueryError> {
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, expr) in &node.props {
plan = LogicalPlan::Filter {
input: Box::new(plan),
predicate: pattern_prop_predicate(var, key, expr),
};
}
Ok(plan)
}
fn push_conjuncts(expr: Expr, out: &mut Vec<Expr>) {
match expr {
Expr::And(l, r) => {
push_conjuncts(*l, out);
push_conjuncts(*r, out);
}
other => out.push(other),
}
}
fn conjunct_sole_var(expr: &Expr) -> Option<&str> {
match expr {
Expr::Compare(pa, _, _) | Expr::IsNull(pa) => Some(&pa.var),
Expr::HasLabel(var, _) => Some(var),
Expr::PropCompare(l, _, r) if l.var == r.var => Some(&l.var),
Expr::GeneralCompare(ReturnExpr::Prop(pa), _, other)
if !return_expr_references_var(other, &pa.var) =>
{
Some(&pa.var)
}
Expr::GeneralIsNull(ReturnExpr::Prop(pa)) => Some(&pa.var),
_ => None,
}
}
fn rebuild_and(mut exprs: Vec<Expr>) -> Option<Expr> {
let first = exprs.pop()?;
Some(
exprs
.into_iter()
.fold(first, |acc, e| Expr::And(Box::new(e), Box::new(acc))),
)
}
fn return_expr_references_var(expr: &ReturnExpr, var: &str) -> bool {
match expr {
ReturnExpr::Var(v) => v == var,
ReturnExpr::Prop(pa) => pa.var == var,
ReturnExpr::PropOf(base, _) => return_expr_references_var(base, var),
ReturnExpr::Lit(_) | ReturnExpr::CountStar => false,
ReturnExpr::Call { name, args, .. } => {
name.eq_ignore_ascii_case("rand")
|| args.iter().any(|a| return_expr_references_var(a, var))
}
_ => true,
}
}
fn pattern_prop_predicate(var: &str, key: &str, expr: &ReturnExpr) -> Expr {
let access = PropAccess {
var: var.to_string(),
prop: key.to_string(),
};
match expr {
ReturnExpr::Lit(lit) => Expr::Compare(access, CompareOp::Eq, lit.clone()),
other => Expr::GeneralCompare(ReturnExpr::Prop(access), CompareOp::Eq, other.clone()),
}
}
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) = &rel.rel_list_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()
}
pub fn plan_reversed_pattern(
pattern: &Pattern,
where_clause: &Option<Expr>,
carried_vars: &HashSet<String>,
txn: Txn,
) -> Result<Option<Pattern>, QueryError> {
if pattern.hops.is_empty() {
return Ok(None);
}
if pattern.hops.iter().any(|(rel, _)| rel.hop_range.is_some()) {
return Ok(None);
}
let mut conjuncts = Vec::new();
if let Some(expr) = where_clause {
push_conjuncts(expr.clone(), &mut conjuncts);
}
let end = &pattern.hops.last().expect("hops checked non-empty").1;
let start_cost = endpoint_start_cost(&pattern.start, &conjuncts, carried_vars, txn)?;
let end_cost = endpoint_start_cost(end, &conjuncts, carried_vars, txn)?;
if end_cost < start_cost {
Ok(Some(reverse_pattern(pattern)))
} else {
Ok(None)
}
}
fn endpoint_start_cost(
node: &NodePattern,
conjuncts: &[Expr],
carried_vars: &HashSet<String>,
txn: Txn,
) -> Result<u64, QueryError> {
if node.var.as_ref().is_some_and(|v| carried_vars.contains(v)) {
return Ok(0);
}
let Some(label) = node.labels.first() else {
return Ok(GraphStore::node_count_in_txn(txn)?);
};
let mut cost = GraphStore::label_count_in_txn(txn, label)?;
let mut consider = |prop: &str, lit: &Literal| -> Result<(), QueryError> {
if matches!(lit, Literal::Param(_)) {
return Ok(());
}
if GraphStore::index_def_in_txn(txn, label, prop)?.is_some() {
let count =
GraphStore::index_match_count_in_txn(txn, label, prop, &literal_to_value(lit))?;
cost = cost.min(count);
}
Ok(())
};
for (key, expr) in &node.props {
if let ReturnExpr::Lit(lit) = expr {
consider(key, lit)?;
}
}
if let Some(var) = &node.var {
for c in conjuncts {
if let Expr::Compare(pa, CompareOp::Eq, lit) = c {
if pa.var == *var {
consider(&pa.prop, lit)?;
}
}
}
}
Ok(cost)
}
fn reverse_pattern(pattern: &Pattern) -> Pattern {
let nodes: Vec<&NodePattern> = std::iter::once(&pattern.start)
.chain(pattern.hops.iter().map(|(_, node)| node))
.collect();
let start = (*nodes.last().expect("nodes is never empty")).clone();
let hops = pattern
.hops
.iter()
.enumerate()
.rev()
.map(|(i, (rel, _))| {
let mut rel = rel.clone();
rel.direction = match rel.direction {
RelDirection::Right => RelDirection::Left,
RelDirection::Left => RelDirection::Right,
RelDirection::Either => RelDirection::Either,
};
(rel, nodes[i].clone())
})
.collect();
Pattern { start, hops }
}
pub fn apply_index_seeks(plan: LogicalPlan, txn: Txn) -> Result<LogicalPlan, QueryError> {
Ok(match plan {
LogicalPlan::Filter { .. } => {
let mut node = plan;
let mut candidates = Vec::new();
let base = loop {
match node {
LogicalPlan::Filter { input, predicate } => {
push_conjuncts(predicate, &mut candidates);
node = *input;
}
other => break other,
}
};
let base = apply_index_seeks(base, txn)?;
if let LogicalPlan::NodeByLabelScan { var, label } = &base {
let mut chosen: Option<(usize, u64)> = None;
for (i, c) in candidates.iter().enumerate() {
let Expr::Compare(pa, CompareOp::Eq, lit) = c else {
continue;
};
if pa.var != *var || matches!(lit, Literal::Param(_)) {
continue;
}
if GraphStore::index_def_in_txn(txn, label, &pa.prop)?.is_some() {
let value = literal_to_value(lit);
let count =
GraphStore::index_match_count_in_txn(txn, label, &pa.prop, &value)?;
if chosen.is_none_or(|(_, best)| count < best) {
chosen = Some((i, count));
}
}
}
if let Some((i, _)) = chosen {
let Expr::Compare(pa, _, lit) = candidates.remove(i) else {
unreachable!("chosen index always points at a Compare, checked above")
};
let seek = LogicalPlan::IndexSeek {
var: var.clone(),
label: label.clone(),
prop: pa.prop,
value: IndexSeekValue::Fixed(literal_to_value(&lit)),
};
return Ok(match rebuild_and(candidates) {
Some(predicate) => LogicalPlan::Filter {
input: Box::new(seek),
predicate,
},
None => seek,
});
}
for (i, c) in candidates.iter().enumerate() {
let Expr::GeneralCompare(ReturnExpr::Prop(pa), CompareOp::Eq, other) = c else {
continue;
};
if pa.var != *var {
continue;
}
if return_expr_references_var(other, var) {
continue;
}
if GraphStore::index_def_in_txn(txn, label, &pa.prop)?.is_some() {
let Expr::GeneralCompare(ReturnExpr::Prop(pa), _, value_expr) =
candidates.remove(i)
else {
unreachable!("just matched this index above, shape can't have changed")
};
let seek = LogicalPlan::IndexSeek {
var: var.clone(),
label: label.clone(),
prop: pa.prop,
value: IndexSeekValue::RowExpr(value_expr),
};
return Ok(match rebuild_and(candidates) {
Some(predicate) => LogicalPlan::Filter {
input: Box::new(seek),
predicate,
},
None => seek,
});
}
}
}
match rebuild_and(candidates) {
Some(predicate) => LogicalPlan::Filter {
input: Box::new(base),
predicate,
},
None => base,
}
}
LogicalPlan::Expand {
input,
from_var,
to_var,
rel_var,
rel_labels,
direction,
} => LogicalPlan::Expand {
input: Box::new(apply_index_seeks(*input, txn)?),
from_var,
to_var,
rel_var,
rel_labels,
direction,
},
LogicalPlan::VarExpand {
input,
from_var,
to_var,
rel_labels,
direction,
min_hops,
max_hops,
exclude_edge_vars,
exclude_edge_sets,
exclude_edge_var,
path_segment_var,
rel_list_var,
rel_props,
} => LogicalPlan::VarExpand {
input: Box::new(apply_index_seeks(*input, txn)?),
from_var,
to_var,
rel_labels,
direction,
min_hops,
max_hops,
exclude_edge_vars,
exclude_edge_sets,
exclude_edge_var,
path_segment_var,
rel_list_var,
rel_props,
},
LogicalPlan::MatchRelList {
input,
from_var,
to_var,
rel_list_var,
rel_labels,
direction,
min_hops,
max_hops,
} => LogicalPlan::MatchRelList {
input: Box::new(apply_index_seeks(*input, txn)?),
from_var,
to_var,
rel_list_var,
rel_labels,
direction,
min_hops,
max_hops,
},
leaf @ (LogicalPlan::AllNodesScan { .. }
| LogicalPlan::NodeByLabelScan { .. }
| LogicalPlan::Seed { .. }
| LogicalPlan::IndexSeek { .. }) => leaf,
})
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use marsdb_graph::{GraphStore, PropertyValue, Txn};
use super::*;
use crate::ast::{QueryClause, Statement};
fn pattern_from(cypher: &str) -> crate::ast::Pattern {
part_from(cypher).pattern
}
fn part_from(cypher: &str) -> crate::ast::QueryPart {
let Statement::Match { clauses, .. } = crate::antlr_visitor::parse_antlr(cypher).unwrap()
else {
panic!("expected a Match statement");
};
let QueryClause::Match(part) = clauses.into_iter().next().unwrap() else {
panic!("expected a Match clause");
};
part
}
#[test]
fn fuses_node_pattern_property_into_index_seek_when_an_index_exists() {
let store = GraphStore::open_memory().unwrap();
store.create_index("Person", "email", false).unwrap();
let pattern = pattern_from("MATCH (n:Person {email: 'alice@x.com'}) RETURN n");
let write = store.begin_write().unwrap();
let plan = build_match_plan(&pattern, &None, &Default::default()).unwrap();
let plan = apply_index_seeks(plan, Txn::Write(&write)).unwrap();
match plan {
LogicalPlan::IndexSeek {
var,
label,
prop,
value,
} => {
assert_eq!(var, "n");
assert_eq!(label, "Person");
assert_eq!(prop, "email");
assert_eq!(
value,
IndexSeekValue::Fixed(PropertyValue::String("alice@x.com".to_string()))
);
}
other => panic!("expected an IndexSeek, got {other:?}"),
}
}
#[test]
fn falls_back_to_filter_over_scan_when_no_index_exists() {
let store = GraphStore::open_memory().unwrap();
let pattern = pattern_from("MATCH (n:Person {email: 'alice@x.com'}) RETURN n");
let write = store.begin_write().unwrap();
let plan = build_match_plan(&pattern, &None, &Default::default()).unwrap();
let plan = apply_index_seeks(plan, Txn::Write(&write)).unwrap();
match plan {
LogicalPlan::Filter { input, .. } => {
assert!(matches!(*input, LogicalPlan::NodeByLabelScan { .. }));
}
other => panic!("expected a Filter over a scan, got {other:?}"),
}
}
#[test]
fn fuses_a_where_clause_equality_into_index_seek() {
let store = GraphStore::open_memory().unwrap();
store.create_index("Person", "email", false).unwrap();
let part = part_from("MATCH (n:Person) WHERE n.email = 'alice@x.com' RETURN n");
let write = store.begin_write().unwrap();
let plan =
build_match_plan(&part.pattern, &part.where_clause, &Default::default()).unwrap();
let plan = apply_index_seeks(plan, Txn::Write(&write)).unwrap();
match plan {
LogicalPlan::IndexSeek {
var,
label,
prop,
value,
} => {
assert_eq!(var, "n");
assert_eq!(label, "Person");
assert_eq!(prop, "email");
assert_eq!(
value,
IndexSeekValue::Fixed(PropertyValue::String("alice@x.com".to_string()))
);
}
other => panic!("expected an IndexSeek, got {other:?}"),
}
}
#[test]
fn seeks_one_equality_and_keeps_the_other_conjunct_as_a_residual_filter() {
let store = GraphStore::open_memory().unwrap();
store.create_index("Person", "email", false).unwrap();
let part =
part_from("MATCH (n:Person) WHERE n.email = 'alice@x.com' AND n.age > 35 RETURN n");
let write = store.begin_write().unwrap();
let plan =
build_match_plan(&part.pattern, &part.where_clause, &Default::default()).unwrap();
let plan = apply_index_seeks(plan, Txn::Write(&write)).unwrap();
match plan {
LogicalPlan::Filter { input, predicate } => {
assert!(
matches!(*input, LogicalPlan::IndexSeek { .. }),
"expected the seek underneath"
);
match predicate {
Expr::Compare(pa, CompareOp::Gt, Literal::Int(35)) => {
assert_eq!(pa.prop, "age")
}
other => panic!("expected the residual age > 35 predicate, got {other:?}"),
}
}
other => panic!("expected a residual Filter over an IndexSeek, got {other:?}"),
}
}
fn seed_people(store: &GraphStore, common: usize, rare: usize) {
for i in 0..common {
let mut props = BTreeMap::new();
props.insert("id".to_string(), PropertyValue::Int(i as i64));
store.create_node(&["Common"], props).unwrap();
}
for i in 0..rare {
let mut props = BTreeMap::new();
props.insert("id".to_string(), PropertyValue::Int(i as i64));
store.create_node(&["Rare"], props).unwrap();
}
}
#[test]
fn reverses_when_the_far_endpoint_label_is_smaller() {
let store = GraphStore::open_memory().unwrap();
seed_people(&store, 20, 1);
let pattern = pattern_from("MATCH (a:Common)-[:R]->(b:Rare) RETURN a");
let write = store.begin_write().unwrap();
let reversed =
plan_reversed_pattern(&pattern, &None, &Default::default(), Txn::Write(&write))
.unwrap()
.expect("expected reversal toward the 1-node Rare label");
assert_eq!(reversed.start.var.as_deref(), Some("b"));
assert_eq!(reversed.start.labels, vec!["Rare"]);
let (rel, node) = &reversed.hops[0];
assert_eq!(rel.direction, RelDirection::Left);
assert_eq!(rel.rel_types, vec!["R"]);
assert_eq!(node.var.as_deref(), Some("a"));
}
#[test]
fn keeps_written_order_when_the_start_is_already_cheapest_or_tied() {
let store = GraphStore::open_memory().unwrap();
seed_people(&store, 1, 20);
let write = store.begin_write().unwrap();
let cheaper_start = pattern_from("MATCH (a:Common)-[:R]->(b:Rare) RETURN a");
assert!(plan_reversed_pattern(
&cheaper_start,
&None,
&Default::default(),
Txn::Write(&write)
)
.unwrap()
.is_none());
let tied = pattern_from("MATCH (a:Rare)-[:R]->(b:Rare) RETURN a");
assert!(
plan_reversed_pattern(&tied, &None, &Default::default(), Txn::Write(&write))
.unwrap()
.is_none()
);
}
#[test]
fn reverses_toward_an_indexed_where_equality_on_the_far_endpoint() {
let store = GraphStore::open_memory().unwrap();
seed_people(&store, 20, 20);
store.create_index("Rare", "id", false).unwrap();
let part = part_from("MATCH (a:Rare)-[:R]->(b:Rare) WHERE b.id = 7 RETURN a");
let write = store.begin_write().unwrap();
let reversed = plan_reversed_pattern(
&part.pattern,
&part.where_clause,
&Default::default(),
Txn::Write(&write),
)
.unwrap()
.expect("expected reversal toward the indexed b.id = 7");
assert_eq!(reversed.start.var.as_deref(), Some("b"));
}
#[test]
fn reverses_toward_a_carried_far_endpoint() {
let store = GraphStore::open_memory().unwrap();
seed_people(&store, 20, 1);
let pattern = pattern_from("MATCH (a:Common)-[:R]->(p) RETURN a");
let carried: HashSet<String> = ["p".to_string()].into();
let write = store.begin_write().unwrap();
let reversed = plan_reversed_pattern(&pattern, &None, &carried, Txn::Write(&write))
.unwrap()
.expect("expected reversal toward the carried p");
assert_eq!(reversed.start.var.as_deref(), Some("p"));
}
#[test]
fn never_reverses_a_pattern_containing_a_variable_length_hop() {
let store = GraphStore::open_memory().unwrap();
seed_people(&store, 20, 1);
let pattern = pattern_from("MATCH (a:Common)-[:R*1..2]->(b:Rare) RETURN a");
let write = store.begin_write().unwrap();
assert!(
plan_reversed_pattern(&pattern, &None, &Default::default(), Txn::Write(&write))
.unwrap()
.is_none()
);
}
#[test]
fn multi_hop_reversal_flips_every_hop_and_keeps_inner_nodes_in_order() {
let store = GraphStore::open_memory().unwrap();
seed_people(&store, 20, 1);
let pattern = pattern_from("MATCH (a:Common)-[:X]->(m)<-[:Y]-(b:Rare) RETURN a");
let write = store.begin_write().unwrap();
let reversed =
plan_reversed_pattern(&pattern, &None, &Default::default(), Txn::Write(&write))
.unwrap()
.expect("expected reversal toward Rare");
assert_eq!(reversed.start.var.as_deref(), Some("b"));
assert_eq!(reversed.hops.len(), 2);
assert_eq!(reversed.hops[0].0.rel_types, vec!["Y"]);
assert_eq!(reversed.hops[0].0.direction, RelDirection::Right);
assert_eq!(reversed.hops[0].1.var.as_deref(), Some("m"));
assert_eq!(reversed.hops[1].0.rel_types, vec!["X"]);
assert_eq!(reversed.hops[1].0.direction, RelDirection::Left);
assert_eq!(reversed.hops[1].1.var.as_deref(), Some("a"));
}
#[test]
fn fuses_a_literal_arg_call_equality_into_a_row_expr_index_seek() {
let store = GraphStore::open_memory().unwrap();
store.create_index("Event", "joined", false).unwrap();
let part = part_from("MATCH (n:Event) WHERE n.joined = date('2020-01-10') RETURN n");
let write = store.begin_write().unwrap();
let plan =
build_match_plan(&part.pattern, &part.where_clause, &Default::default()).unwrap();
let plan = apply_index_seeks(plan, Txn::Write(&write)).unwrap();
match plan {
LogicalPlan::IndexSeek {
prop,
value: IndexSeekValue::RowExpr(ReturnExpr::Call { name, .. }),
..
} => {
assert_eq!(prop, "joined");
assert_eq!(name, "date");
}
other => panic!("expected a RowExpr IndexSeek on the call, got {other:?}"),
}
}
#[test]
fn does_not_promote_a_call_whose_argument_references_the_scan_var() {
let store = GraphStore::open_memory().unwrap();
store.create_index("Event", "joined", false).unwrap();
let part = part_from("MATCH (n:Event) WHERE n.joined = date(n.born) RETURN n");
let write = store.begin_write().unwrap();
let plan =
build_match_plan(&part.pattern, &part.where_clause, &Default::default()).unwrap();
let plan = apply_index_seeks(plan, Txn::Write(&write)).unwrap();
match plan {
LogicalPlan::Filter { input, .. } => {
assert!(matches!(*input, LogicalPlan::NodeByLabelScan { .. }));
}
other => panic!("expected a Filter over the scan, got {other:?}"),
}
}
#[test]
fn does_not_promote_a_rand_call() {
let store = GraphStore::open_memory().unwrap();
store.create_index("Event", "score", false).unwrap();
let part = part_from("MATCH (n:Event) WHERE n.score = rand() RETURN n");
let write = store.begin_write().unwrap();
let plan =
build_match_plan(&part.pattern, &part.where_clause, &Default::default()).unwrap();
let plan = apply_index_seeks(plan, Txn::Write(&write)).unwrap();
match plan {
LogicalPlan::Filter { input, .. } => {
assert!(matches!(*input, LogicalPlan::NodeByLabelScan { .. }));
}
other => panic!("expected a Filter over the scan, got {other:?}"),
}
}
#[test]
fn picks_the_more_selective_index_when_multiple_equality_candidates_are_indexed() {
let store = GraphStore::open_memory().unwrap();
store.create_index("Person", "country", false).unwrap();
store.create_index("Person", "email", false).unwrap();
for i in 0..20 {
let mut props = BTreeMap::new();
props.insert(
"country".to_string(),
PropertyValue::String("US".to_string()),
);
props.insert(
"email".to_string(),
PropertyValue::String(format!("user{i}@x.com")),
);
store.create_node(&["Person"], props).unwrap();
}
let part = part_from(
"MATCH (n:Person) WHERE n.country = 'US' AND n.email = 'user7@x.com' RETURN n",
);
let write = store.begin_write().unwrap();
let plan =
build_match_plan(&part.pattern, &part.where_clause, &Default::default()).unwrap();
let plan = apply_index_seeks(plan, Txn::Write(&write)).unwrap();
match plan {
LogicalPlan::Filter { input, predicate } => {
match *input {
LogicalPlan::IndexSeek { prop, value, .. } => {
assert_eq!(prop, "email");
assert_eq!(
value,
IndexSeekValue::Fixed(PropertyValue::String("user7@x.com".to_string()))
);
}
other => panic!("expected the seek underneath, got {other:?}"),
}
match predicate {
Expr::Compare(pa, CompareOp::Eq, Literal::String(s)) => {
assert_eq!(pa.prop, "country");
assert_eq!(s, "US");
}
other => panic!("expected the residual country predicate, got {other:?}"),
}
}
other => panic!("expected a residual Filter over an IndexSeek, got {other:?}"),
}
}
}