use super::super::ast::*;
use crate::graph::core::pattern_matching::{EdgePattern, NodePattern, Pattern, PatternElement};
const MAX_LOWERED_PATTERN_HOPS: usize = 2;
pub(super) fn lower_fixed_var_length_hops(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() {
continue;
}
for pattern in &mut mc.patterns {
lower_pattern(pattern);
}
}
}
fn lower_pattern(pattern: &mut Pattern) -> bool {
let Some(total_hops) = lowered_hop_count(pattern) else {
return false;
};
if total_hops > MAX_LOWERED_PATTERN_HOPS {
return false;
}
let mut lowered: Vec<PatternElement> = Vec::with_capacity(pattern.elements.len() + total_hops);
for element in &pattern.elements {
let PatternElement::Edge(edge) = element else {
lowered.push(element.clone());
continue;
};
match lowering_hops(edge) {
Some(k) => push_lowered_hops(&mut lowered, edge, k),
None => lowered.push(element.clone()),
}
}
pattern.elements = lowered;
true
}
fn lowered_hop_count(pattern: &Pattern) -> Option<usize> {
let mut total = 0usize;
let mut eligible = false;
for element in &pattern.elements {
let PatternElement::Edge(edge) = element else {
continue;
};
match lowering_hops(edge) {
Some(k) => {
eligible = true;
total += k;
}
None => total += 1,
}
}
eligible.then_some(total)
}
fn push_lowered_hops(lowered: &mut Vec<PatternElement>, edge: &EdgePattern, k: usize) {
let mut hop = edge.clone();
hop.var_length = None;
hop.needs_path_info = true;
hop.skip_target_type_check = false;
for i in 0..k {
lowered.push(PatternElement::Edge(hop.clone()));
if i + 1 < k {
lowered.push(PatternElement::Node(anonymous_intermediate()));
}
}
}
fn anonymous_intermediate() -> NodePattern {
NodePattern {
variable: None,
node_type: None,
extra_labels: Vec::new(),
properties: None,
label_params: Vec::new(),
}
}
fn lowering_hops(edge: &EdgePattern) -> Option<usize> {
let (min, max) = edge.var_length?;
if min != max || min == 0 || min > MAX_LOWERED_PATTERN_HOPS {
return None;
}
if edge.variable.is_some() || edge.edge_filter.is_some() || !edge.type_params.is_empty() {
return None;
}
Some(min)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::core::pattern_matching::{parse_pattern, EdgeDirection};
type HopShape = (Option<String>, Option<(usize, usize)>, EdgeDirection);
fn hops(pattern: &Pattern) -> Vec<HopShape> {
pattern
.elements
.iter()
.filter_map(|element| match element {
PatternElement::Edge(edge) => Some((
edge.connection_type.clone(),
edge.var_length,
edge.direction,
)),
_ => None,
})
.collect()
}
fn lowered(text: &str) -> (Pattern, bool) {
let mut pattern = parse_pattern(text).unwrap_or_else(|e| panic!("{text}: {e}"));
let changed = lower_pattern(&mut pattern);
(pattern, changed)
}
#[test]
fn two_hop_segment_becomes_two_fixed_hops_with_an_anonymous_intermediate() {
let (pattern, changed) = lowered("(a:N)-[:R*2..2]->(b:N)");
assert!(changed);
assert_eq!(pattern.elements.len(), 5);
assert_eq!(
hops(&pattern),
vec![
(Some("R".to_string()), None, EdgeDirection::Outgoing),
(Some("R".to_string()), None, EdgeDirection::Outgoing),
]
);
let PatternElement::Node(mid) = &pattern.elements[2] else {
panic!("element 2 is not a node: {:?}", pattern.elements[2]);
};
assert!(mid.variable.is_none());
assert!(mid.node_type.is_none());
assert!(mid.properties.is_none());
assert!(mid.extra_labels.is_empty());
}
#[test]
fn lowered_hops_keep_trail_tracking_and_drop_the_stale_type_hint() {
let (pattern, _) = lowered("(a:N)-[:R*2..2]->(b:N)");
for element in &pattern.elements {
if let PatternElement::Edge(edge) = element {
assert!(edge.needs_path_info, "lowered hop lost its trail");
assert!(!edge.skip_target_type_check);
}
}
}
#[test]
fn direction_and_alternation_and_properties_replicate_onto_every_hop() {
let (undirected, _) = lowered("(a:N)-[:R*2..2]-(b:N)");
assert_eq!(
hops(&undirected).iter().map(|h| h.2).collect::<Vec<_>>(),
vec![EdgeDirection::Both, EdgeDirection::Both]
);
let (incoming, _) = lowered("(a:N)<-[:R*2..2]-(b:N)");
assert_eq!(
hops(&incoming).iter().map(|h| h.2).collect::<Vec<_>>(),
vec![EdgeDirection::Incoming, EdgeDirection::Incoming]
);
let (alternation, _) = lowered("(a:N)-[:A|B*2..2]->(b:N)");
for element in &alternation.elements {
if let PatternElement::Edge(edge) = element {
assert_eq!(
edge.connection_types.as_deref(),
Some(["A".to_string(), "B".to_string()].as_slice())
);
}
}
}
#[test]
fn ranges_zero_hops_and_bound_relationship_variables_are_left_alone() {
for text in [
"(a:N)-[:R*1..3]->(b:N)",
"(a:N)-[:R*0..0]->(b:N)",
"(a:N)-[:R*2..3]->(b:N)",
"(a:N)-[r:R*2..2]->(b:N)",
"(a:N)-[:R]->(b:N)",
] {
let (pattern, changed) = lowered(text);
assert!(!changed, "{text} was rewritten");
assert_eq!(hops(&pattern).len(), 1, "{text}");
}
}
#[test]
fn the_hop_ceiling_is_two() {
let (two, changed) = lowered("(a:N)-[:R*2..2]->(b:N)");
assert!(changed);
assert_eq!(hops(&two).len(), 2);
let (three, changed) = lowered("(a:N)-[:R*3..3]->(b:N)");
assert!(!changed);
assert_eq!(
hops(&three),
vec![(Some("R".to_string()), Some((3, 3)), EdgeDirection::Outgoing)]
);
}
#[test]
fn the_ceiling_counts_the_whole_pattern_not_one_segment() {
let (fits, changed) = lowered("(a:N)-[:A*1..1]->(b:N)-[:B*1..1]->(c:N)");
assert!(changed);
assert_eq!(hops(&fits).len(), 2);
let (over, changed) = lowered("(a:N)-[:A*1..1]->(b:N)-[:B*2..2]->(c:N)");
assert!(!changed);
assert_eq!(hops(&over).len(), 2);
let (mixed, changed) = lowered("(a:N)-[:A]->(b:N)-[:B*1..1]->(c:N)");
assert!(changed);
assert_eq!(hops(&mixed).len(), 2);
let (mixed_over, changed) = lowered("(a:N)-[:A]->(b:N)-[:B*2..2]->(c:N)");
assert!(!changed);
assert_eq!(hops(&mixed_over).len(), 2);
}
#[test]
fn a_non_lowerable_segment_does_not_block_its_neighbour() {
let (pattern, changed) = lowered("(a:N)-[:A*1..1]->(b:N)-[:B*1..3]->(c:N)");
assert!(changed);
assert_eq!(
hops(&pattern),
vec![
(Some("A".to_string()), None, EdgeDirection::Outgoing),
(Some("B".to_string()), Some((1, 3)), EdgeDirection::Outgoing),
]
);
}
#[test]
fn a_lowered_same_type_segment_is_never_marked_disjoint() {
for text in [
"(a:N)-[:R*2..2]->(b:N)",
"(a:N)-[:R*2..2]-(b:N)",
"(a:N)-[:A|B*2..2]->(b:N)",
"(a:N)-[*2..2]->(b:N)",
] {
let (pattern, changed) = lowered(text);
assert!(changed, "{text}");
assert!(
!super::super::annotations::fixed_edge_types_are_pairwise_disjoint(&pattern),
"{text} would have its trail bookkeeping removed"
);
}
let (single, _) = lowered("(a:N)-[:R*1..1]->(b:N)");
assert!(super::super::annotations::fixed_edge_types_are_pairwise_disjoint(&single));
}
}