use super::{Clause, CypherQuery, Expression, PassCtx};
use crate::graph::core::pattern_matching::PatternElement;
use crate::graph::schema::DirGraph;
use std::collections::HashSet;
pub(super) fn pass_mark_fast_var_length_paths(query: &mut CypherQuery, _ctx: &PassCtx) {
mark_fast_var_length_paths(query)
}
pub(super) fn pass_mark_disjoint_fixed_trails(query: &mut CypherQuery, _ctx: &PassCtx) {
mark_disjoint_fixed_trails(query)
}
pub(super) fn pass_mark_skip_target_type_check(query: &mut CypherQuery, ctx: &PassCtx) {
mark_skip_target_type_check(query, ctx.graph)
}
fn mark_fast_var_length_paths(query: &mut CypherQuery) {
if !downstream_is_dedup_safe(query) {
return;
}
for clause in &mut query.clauses {
let mc = match clause {
Clause::Match(mc) | Clause::OptionalMatch(mc) => mc,
_ => continue,
};
if !mc.path_assignments.is_empty() {
continue;
}
for pattern in &mut mc.patterns {
for element in &mut pattern.elements {
if let PatternElement::Edge(ep) = element {
if ep.var_length.is_some() && ep.variable.is_none() {
ep.needs_path_info = false;
}
}
}
}
}
}
fn mark_disjoint_fixed_trails(query: &mut CypherQuery) {
for clause in &mut query.clauses {
let mc = match clause {
Clause::Match(mc) | Clause::OptionalMatch(mc) => mc,
_ => continue,
};
if !mc.path_assignments.is_empty() || mc.patterns.len() != 1 {
continue;
}
let pattern = &mut mc.patterns[0];
if !fixed_edge_types_are_pairwise_disjoint(pattern) {
continue;
}
for element in &mut pattern.elements {
if let PatternElement::Edge(edge) = element {
edge.needs_path_info = false;
}
}
}
}
fn fixed_edge_types_are_pairwise_disjoint(
pattern: &crate::graph::core::pattern_matching::Pattern,
) -> bool {
let mut seen = HashSet::new();
let mut edge_count = 0usize;
for element in &pattern.elements {
let PatternElement::Edge(edge) = element else {
continue;
};
edge_count += 1;
if edge.var_length.is_some() {
return false;
}
if let Some(types) = &edge.connection_types {
if types.is_empty() || types.iter().any(|ty| !seen.insert(ty.as_str())) {
return false;
}
} else if let Some(ty) = &edge.connection_type {
if !seen.insert(ty.as_str()) {
return false;
}
} else {
return false;
}
}
edge_count > 0
}
fn downstream_is_dedup_safe(query: &CypherQuery) -> bool {
for clause in &query.clauses {
match clause {
Clause::Return(r) => {
if r.distinct {
return true;
}
let all_agg_distinct = !r.items.is_empty()
&& r.items
.iter()
.all(|item| is_distinct_safe_aggregate(&item.expression));
return all_agg_distinct;
}
Clause::With(w) => {
if w.distinct {
return true;
}
let all_agg_distinct = !w.items.is_empty()
&& w.items
.iter()
.all(|item| is_distinct_safe_aggregate(&item.expression));
return all_agg_distinct;
}
_ => continue,
}
}
false
}
fn is_distinct_safe_aggregate(expr: &Expression) -> bool {
if let Expression::FunctionCall {
name,
args: _,
distinct,
} = expr
{
let nm = name.to_lowercase();
if matches!(nm.as_str(), "min" | "max") {
return true;
}
if *distinct && matches!(nm.as_str(), "count" | "collect") {
return true;
}
}
false
}
fn mark_skip_target_type_check(query: &mut CypherQuery, graph: &DirGraph) {
use crate::graph::core::pattern_matching::EdgeDirection;
for clause in &mut query.clauses {
let mc = match clause {
Clause::Match(mc) | Clause::OptionalMatch(mc) => mc,
_ => continue,
};
for pattern in &mut mc.patterns {
let elements = &mut pattern.elements;
let len = elements.len();
for i in 0..len {
if i + 2 >= len {
break;
}
let (conn_type, direction, target_node_type) = {
let edge = match &elements[i + 1] {
PatternElement::Edge(ep) => ep,
_ => continue,
};
let target = match &elements[i + 2] {
PatternElement::Node(np) => np,
_ => continue,
};
if !target.extra_labels.is_empty() {
continue;
}
match (&edge.connection_type, edge.direction, &target.node_type) {
(Some(ct), dir, Some(nt)) => (ct.clone(), dir, nt.clone()),
_ => continue,
}
};
if let Some(info) = graph.connection_type_metadata.get(&conn_type) {
let guaranteed = match direction {
EdgeDirection::Outgoing => {
info.target_types.len() == 1
&& info.target_types.contains(&target_node_type)
}
EdgeDirection::Incoming => {
info.source_types.len() == 1
&& info.source_types.contains(&target_node_type)
}
EdgeDirection::Both => false, };
if guaranteed {
if let PatternElement::Edge(ep) = &mut elements[i + 1] {
ep.skip_target_type_check = true;
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::fixed_edge_types_are_pairwise_disjoint;
use crate::graph::core::pattern_matching::parse_pattern;
#[test]
fn disjoint_fixed_edge_types_need_no_trail() {
let pattern = parse_pattern("(a)-[:JUDGED_BY]-(b)-[:CITES]->(c)").unwrap();
assert!(fixed_edge_types_are_pairwise_disjoint(&pattern));
let single = parse_pattern("(a)-[:CITES]->(b)").unwrap();
assert!(fixed_edge_types_are_pairwise_disjoint(&single));
}
#[test]
fn overlapping_or_unbounded_edge_types_keep_trail() {
for text in [
"(a)-[:CITES]->(b)-[:CITES]->(c)",
"(a)-[:CITES|REFERS_TO]->(b)-[:REFERS_TO]->(c)",
"(a)-->(b)-[:CITES]->(c)",
"(a)-[:CITES*1..2]->(b)-[:REFERS_TO]->(c)",
] {
let pattern = parse_pattern(text).unwrap();
assert!(!fixed_edge_types_are_pairwise_disjoint(&pattern), "{text}");
}
}
}