use super::*;
use crate::graph::core::pattern_matching::PropertyMatcher;
use crate::graph::languages::cypher::parser::parse_cypher;
#[test]
fn test_predicate_pushdown_simple() {
let mut query = parse_cypher("MATCH (n:Person) WHERE n.age = 30 RETURN n").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
assert_eq!(query.clauses.len(), 3); assert!(matches!(&query.clauses[0], Clause::Match(_)));
assert!(matches!(&query.clauses[2], Clause::Return(_)));
if let Clause::Match(m) = &query.clauses[0] {
if let PatternElement::Node(np) = &m.patterns[0].elements[0] {
assert!(np.properties.is_some());
let props = np.properties.as_ref().unwrap();
assert!(props.contains_key("age"));
} else {
panic!("Expected node pattern");
}
}
}
#[test]
fn test_predicate_pushdown_partial() {
let mut query =
parse_cypher("MATCH (n:Person) WHERE n.age = 30 AND n.score > 100 RETURN n").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
assert_eq!(query.clauses.len(), 3);
if let Clause::Match(m) = &query.clauses[0] {
if let PatternElement::Node(np) = &m.patterns[0].elements[0] {
let props = np.properties.as_ref().unwrap();
assert!(matches!(
props.get("age"),
Some(PropertyMatcher::Equals(Value::Int64(30)))
));
assert!(matches!(
props.get("score"),
Some(PropertyMatcher::GreaterThan(Value::Int64(100)))
));
}
}
}
#[test]
fn test_comparison_pushdown() {
let mut query = parse_cypher("MATCH (n:Person) WHERE n.age > 30 RETURN n").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
assert_eq!(query.clauses.len(), 3);
if let Clause::Match(m) = &query.clauses[0] {
if let PatternElement::Node(np) = &m.patterns[0].elements[0] {
let props = np.properties.as_ref().unwrap();
assert!(matches!(
props.get("age"),
Some(PropertyMatcher::GreaterThan(Value::Int64(30)))
));
}
}
}
#[test]
fn test_no_pushdown_for_not_equals() {
let mut query = parse_cypher("MATCH (n:Person) WHERE n.age <> 30 RETURN n").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
assert_eq!(query.clauses.len(), 3); }
#[test]
fn test_predicate_pushdown_parameter() {
let mut query = parse_cypher("MATCH (n:Person) WHERE n.name = $name RETURN n").unwrap();
let graph = DirGraph::new();
let mut params = HashMap::new();
params.insert("name".to_string(), Value::String("Alice".to_string()));
optimize(&mut query, &graph, ¶ms);
assert_eq!(query.clauses.len(), 3);
if let Clause::Match(m) = &query.clauses[0] {
if let PatternElement::Node(np) = &m.patterns[0].elements[0] {
assert!(np.properties.is_some());
let props = np.properties.as_ref().unwrap();
assert!(props.contains_key("name"));
assert!(matches!(
props.get("name"),
Some(PropertyMatcher::Equals(Value::String(s))) if s == "Alice"
));
} else {
panic!("Expected node pattern");
}
}
}
#[test]
fn test_predicate_pushdown_parameter_partial() {
let mut query =
parse_cypher("MATCH (n:Person) WHERE n.name = $name AND n.age > $min_age RETURN n")
.unwrap();
let graph = DirGraph::new();
let mut params = HashMap::new();
params.insert("name".to_string(), Value::String("Alice".to_string()));
params.insert("min_age".to_string(), Value::Int64(25));
optimize(&mut query, &graph, ¶ms);
assert_eq!(query.clauses.len(), 3);
if let Clause::Match(m) = &query.clauses[0] {
if let PatternElement::Node(np) = &m.patterns[0].elements[0] {
let props = np.properties.as_ref().unwrap();
assert!(matches!(
props.get("name"),
Some(PropertyMatcher::Equals(Value::String(s))) if s == "Alice"
));
assert!(matches!(
props.get("age"),
Some(PropertyMatcher::GreaterThan(Value::Int64(25)))
));
}
}
}
#[test]
fn test_comparison_range_merge() {
let mut query =
parse_cypher("MATCH (n:Paper) WHERE n.year >= 2015 AND n.year <= 2022 RETURN n").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
assert_eq!(query.clauses.len(), 3);
if let Clause::Match(m) = &query.clauses[0] {
if let PatternElement::Node(np) = &m.patterns[0].elements[0] {
let props = np.properties.as_ref().unwrap();
assert!(matches!(
props.get("year"),
Some(PropertyMatcher::Range {
lower: Value::Int64(2015),
lower_inclusive: true,
upper: Value::Int64(2022),
upper_inclusive: true,
})
));
}
}
}
#[test]
fn test_correlated_nodeprop_pushdown() {
let mut query =
parse_cypher("MATCH (a:A) MATCH (b:B) WHERE b.x = a.y RETURN a.id, b.id").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let b_match = query
.clauses
.iter()
.filter_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.find(|m| {
matches!(
&m.patterns[0].elements[0],
PatternElement::Node(np) if np.node_type.as_deref() == Some("B")
)
})
.expect("expected second MATCH on B");
if let PatternElement::Node(np) = &b_match.patterns[0].elements[0] {
let props = np.properties.as_ref().expect("expected props on b");
match props.get("x") {
Some(PropertyMatcher::EqualsNodeProp { var, prop }) => {
assert_eq!(var, "a");
assert_eq!(prop, "y");
}
other => panic!("expected EqualsNodeProp on b.x, got {:?}", other),
}
}
}
#[test]
fn test_correlated_nodeprop_reversed_sides() {
let mut query =
parse_cypher("MATCH (a:A) MATCH (b:B) WHERE a.y = b.x RETURN a.id, b.id").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let b_match = query
.clauses
.iter()
.filter_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.find(|m| {
matches!(
&m.patterns[0].elements[0],
PatternElement::Node(np) if np.node_type.as_deref() == Some("B")
)
})
.unwrap();
if let PatternElement::Node(np) = &b_match.patterns[0].elements[0] {
let props = np.properties.as_ref().unwrap();
assert!(matches!(
props.get("x"),
Some(PropertyMatcher::EqualsNodeProp { var, prop })
if var == "a" && prop == "y"
));
}
}
#[test]
fn test_scalar_var_pushdown_from_unwind() {
let mut query =
parse_cypher("UNWIND ['x','y'] AS fname MATCH (s:Strat) WHERE s.title = fname RETURN s.id")
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let s_match = query
.clauses
.iter()
.filter_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.next()
.unwrap();
if let PatternElement::Node(np) = &s_match.patterns[0].elements[0] {
let props = np.properties.as_ref().unwrap();
assert!(matches!(
props.get("title"),
Some(PropertyMatcher::EqualsVar(n)) if n == "fname"
));
}
}
#[test]
fn test_no_pushdown_when_both_vars_in_same_match() {
let mut query = parse_cypher("MATCH (a:A), (b:B) WHERE a.y = b.x RETURN a.id, b.id").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
for clause in &query.clauses {
if let Clause::Match(m) = clause {
for pat in &m.patterns {
for el in &pat.elements {
if let PatternElement::Node(np) = el {
if let Some(props) = &np.properties {
for m in props.values() {
assert!(
!matches!(m, PropertyMatcher::EqualsNodeProp { .. }),
"same-MATCH correlated equality must not be rewritten"
);
}
}
}
}
}
}
}
}
#[test]
fn test_undirected_pattern_reversed_by_selectivity() {
let mut query =
parse_cypher("MATCH (other)-[r]-(p {title: 'X'}) RETURN type(r), count(other)").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let m = query
.clauses
.iter()
.find_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.unwrap();
let first = match &m.patterns[0].elements[0] {
PatternElement::Node(np) => np,
_ => panic!("expected node"),
};
assert_eq!(
first.variable.as_deref(),
Some("p"),
"selective anchor `p` should be the start after reversal"
);
assert!(
first.properties.is_some(),
"start node must carry the title property after reversal"
);
}
#[test]
fn test_undirected_pattern_no_reverse_when_first_is_anchor() {
let mut query =
parse_cypher("MATCH (p {title: 'X'})-[r]-(other) RETURN type(r), count(other)").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let m = query
.clauses
.iter()
.find_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.unwrap();
let first = match &m.patterns[0].elements[0] {
PatternElement::Node(np) => np,
_ => panic!("expected node"),
};
assert_eq!(first.variable.as_deref(), Some("p"));
}
#[test]
fn test_var_length_pattern_reversed_by_selectivity() {
let mut query = parse_cypher("MATCH (other)-[*1..3]-(p {id: 1}) RETURN p, other").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let m = query
.clauses
.iter()
.find_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.unwrap();
let first = match &m.patterns[0].elements[0] {
PatternElement::Node(np) => np,
_ => panic!("expected node"),
};
assert_eq!(
first.variable.as_deref(),
Some("p"),
"var-length patterns should still get start-node optimization"
);
}
#[test]
fn test_var_length_with_path_assignment_not_reversed() {
let mut query = parse_cypher("MATCH path = (other)-[*1..3]-(p {id: 1}) RETURN path").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let m = query
.clauses
.iter()
.find_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.unwrap();
let first = match &m.patterns[0].elements[0] {
PatternElement::Node(np) => np,
_ => panic!("expected node"),
};
assert_eq!(
first.variable.as_deref(),
Some("other"),
"path-bound patterns must not be reversed"
);
}
#[test]
fn test_limit_pushdown_single_match_with_where() {
let mut query =
parse_cypher("MATCH (n:Person) WHERE n.age > 25 RETURN n.name LIMIT 10").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let has_limit = query.clauses.iter().any(|c| matches!(c, Clause::Limit(_)));
assert!(
!has_limit,
"single-MATCH query should have LIMIT pushed into MATCH"
);
let m = query
.clauses
.iter()
.find_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.expect("expected a MATCH clause");
assert_eq!(m.limit_hint, Some(10));
}
#[test]
fn test_multi_match_no_reverse_when_bound_var_first() {
let mut query =
parse_cypher("MATCH (p:Person) MATCH (p)-[:KNOWS]->(c:Company) RETURN p, c").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let matches: Vec<_> = query
.clauses
.iter()
.filter_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.collect();
assert!(matches.len() >= 2, "expected two MATCH clauses");
let second = matches[1];
let first_var = match &second.patterns[0].elements[0] {
PatternElement::Node(np) => np.variable.as_deref(),
_ => None,
};
assert_eq!(
first_var,
Some("p"),
"second MATCH must keep pre-bound `p` as start node, not reverse to `c`"
);
}
#[test]
fn test_multi_match_reorder_prefers_anchored_pattern() {
let mut query = parse_cypher(
"MATCH (p {id: 1}) \
MATCH (p)-[:R1]->(:T1), (p)-[:R2]->({id: 99}) \
RETURN p",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let m2 = query
.clauses
.iter()
.filter_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.nth(1)
.expect("expected second MATCH");
assert_eq!(m2.patterns.len(), 2);
}
#[test]
fn test_limit_pushdown_multi_match_safety() {
let mut query = parse_cypher(
"MATCH (a)-[:R1]->(:T1) \
MATCH (a)-[:R2]->(b) \
MATCH (b)-[:R3]->(c) \
WHERE c.id = 7318 \
RETURN a.id, b.id LIMIT 50",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let has_limit = query.clauses.iter().any(|c| matches!(c, Clause::Limit(_)));
assert!(has_limit, "multi-MATCH query must retain its LIMIT clause");
for clause in &query.clauses {
if let Clause::Match(m) = clause {
assert_eq!(
m.limit_hint, None,
"multi-MATCH clauses must not receive a limit_hint"
);
}
}
}
#[test]
fn test_reorder_match_clauses_picks_rare_edge_first() {
let mut query = parse_cypher(
"MATCH (p)-[:VERY_COMMON]->({id: 1}) \
MATCH (p)-[:RARE]->({id: 2}) \
RETURN p",
)
.unwrap();
let graph = DirGraph::new();
{
let mut cache = graph.edge_type_counts_cache.write().unwrap();
let mut counts = HashMap::new();
counts.insert("VERY_COMMON".to_string(), 1_000_000);
counts.insert("RARE".to_string(), 1_000);
*cache = Some(counts);
}
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let matches: Vec<_> = query
.clauses
.iter()
.filter_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.collect();
assert_eq!(matches.len(), 2, "expected two MATCH clauses preserved");
let first_edge_type = matches[0].patterns[0].elements.iter().find_map(|e| {
if let PatternElement::Edge(ep) = e {
ep.connection_type.clone()
} else {
None
}
});
assert_eq!(
first_edge_type.as_deref(),
Some("RARE"),
"RARE (lower edge-type cost) should be promoted to first MATCH; \
got first edge type = {first_edge_type:?}"
);
}
#[test]
fn test_reorder_match_clauses_skips_when_cache_missing() {
let mut query = parse_cypher(
"MATCH (p)-[:VERY_COMMON]->({id: 1}) \
MATCH (p)-[:RARE]->({id: 2}) \
RETURN p",
)
.unwrap();
let graph = DirGraph::new();
assert!(!graph.has_edge_type_counts_cache());
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let matches: Vec<_> = query
.clauses
.iter()
.filter_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.collect();
assert_eq!(matches.len(), 2);
let first_edge_type = matches[0].patterns[0].elements.iter().find_map(|e| {
if let PatternElement::Edge(ep) = e {
ep.connection_type.clone()
} else {
None
}
});
assert_eq!(first_edge_type.as_deref(), Some("VERY_COMMON"));
assert!(
!graph.has_edge_type_counts_cache(),
"planner must not warm the edge-type-counts cache from the optimization path"
);
}
#[test]
fn test_reorder_match_clauses_requires_id_anchor() {
let mut query = parse_cypher(
"MATCH (p)-[:VERY_COMMON]->(q) \
MATCH (p)-[:RARE]->(r) \
RETURN p",
)
.unwrap();
let graph = DirGraph::new();
{
let mut cache = graph.edge_type_counts_cache.write().unwrap();
let mut counts = HashMap::new();
counts.insert("VERY_COMMON".to_string(), 1_000_000);
counts.insert("RARE".to_string(), 1_000);
*cache = Some(counts);
}
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let matches: Vec<_> = query
.clauses
.iter()
.filter_map(|c| match c {
Clause::Match(m) => Some(m),
_ => None,
})
.collect();
let first_edge_type = matches[0].patterns[0].elements.iter().find_map(|e| {
if let PatternElement::Edge(ep) = e {
ep.connection_type.clone()
} else {
None
}
});
assert_eq!(
first_edge_type.as_deref(),
Some("VERY_COMMON"),
"without id-anchored endpoints the proxy is unreliable; do not reorder"
);
}
#[test]
fn test_fuse_match_return_aggregate_count_distinct() {
let mut query = parse_cypher(
"MATCH (a:Person)-[:KNOWS]->(b:Person) \
RETURN a, count(DISTINCT b) AS friends \
ORDER BY friends DESC LIMIT 10",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let mut found = false;
for clause in &query.clauses {
if let Clause::FusedMatchReturnAggregate {
distinct_count,
top_k,
..
} = clause
{
assert!(*distinct_count, "distinct_count flag must be set");
assert!(
top_k.is_some(),
"ORDER BY count DESC LIMIT 10 must absorb into top_k"
);
found = true;
}
}
assert!(
found,
"FusedMatchReturnAggregate must fire for count(DISTINCT) shape"
);
}
#[test]
fn test_fuse_match_with_aggregate_count_distinct() {
let mut query = parse_cypher(
"MATCH (a:Person)-[:KNOWS]->(b:Person) \
WITH a, count(DISTINCT b) AS friends \
RETURN a, friends",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let mut found = false;
for clause in &query.clauses {
if let Clause::FusedMatchWithAggregate { distinct_count, .. } = clause {
assert!(*distinct_count, "WITH-form distinct_count flag must be set");
found = true;
}
}
assert!(
found,
"FusedMatchWithAggregate must fire for WITH-count-DISTINCT shape"
);
}
#[test]
fn test_count_distinct_unconstrained_group_not_fused() {
let mut query = parse_cypher(
"MATCH (a)-[:R]->(b) \
RETURN b, count(DISTINCT a) AS n \
ORDER BY n DESC LIMIT 10",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let fused_with_distinct = query.clauses.iter().any(|c| {
matches!(
c,
Clause::FusedMatchReturnAggregate {
distinct_count: true,
..
}
)
});
assert!(
!fused_with_distinct,
"untyped group node must skip distinct-count fusion"
);
}
#[test]
fn test_count_distinct_5_element_pattern_not_fused() {
let mut query = parse_cypher(
"MATCH (a:A)-[:R1]->(b)<-[:R2]-(c) \
RETURN a, count(DISTINCT c) AS n \
ORDER BY n DESC LIMIT 10",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let fused_with_distinct = query.clauses.iter().any(|c| {
matches!(
c,
Clause::FusedMatchReturnAggregate {
distinct_count: true,
..
}
)
});
assert!(
!fused_with_distinct,
"5-element distinct-count pattern must not be fused"
);
}
#[test]
fn test_fold_pass_through_with_between_matches() {
let mut query = parse_cypher(
"MATCH (p)-[:T1]->({id: 1}) \
WITH p \
MATCH (p)-[r]->() \
RETURN p.title, count(r) AS d \
ORDER BY d DESC LIMIT 10",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let bare_with_count = query
.clauses
.iter()
.filter(|c| matches!(c, Clause::With(_)))
.count();
assert_eq!(
bare_with_count, 0,
"pass-through WITH must be stripped, and any synthesized WITH \
must be absorbed by aggregate fusion; got query: {:#?}",
query.clauses
);
let has_fused_aggregate = query
.clauses
.iter()
.any(|c| matches!(c, Clause::FusedMatchWithAggregate { .. }));
assert!(
has_fused_aggregate,
"expected the cohort query to land on the fused streaming \
aggregate path; clauses: {:#?}",
query.clauses
);
}
#[test]
fn test_fold_pass_through_with_keeps_useful_with() {
let mut query = parse_cypher("MATCH (p)-[r]->(q) WITH p, r RETURN p, r LIMIT 10").unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let mut renaming =
parse_cypher("MATCH (p)-[r]->(q) WITH p AS person RETURN person LIMIT 10").unwrap();
optimize(&mut renaming, &graph, ¶ms);
let has_with = renaming
.clauses
.iter()
.any(|c| matches!(c, Clause::With(_)));
assert!(
has_with,
"renaming WITH (`p AS person`) must not be folded — it changes scope"
);
}
#[test]
fn test_fold_pass_through_with_skipped_when_orderby_follows() {
let mut query =
parse_cypher("MATCH (p)-[:T]->({id: 1}) WITH p ORDER BY p.title LIMIT 10 RETURN p")
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let has_with = query.clauses.iter().any(|c| matches!(c, Clause::With(_)));
assert!(
has_with,
"WITH followed by ORDER BY/SKIP/LIMIT must not be folded; \
clauses: {:#?}",
query.clauses
);
}
#[test]
fn test_desugar_multi_match_return_aggregate() {
let mut query = parse_cypher(
"MATCH (p)-[:T1]->({id: 1}) \
MATCH (p)-[r]->() \
RETURN p.title, count(r) AS d \
ORDER BY d DESC LIMIT 10",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let landed_on_fused_aggregate = query
.clauses
.iter()
.any(|c| matches!(c, Clause::FusedMatchWithAggregate { .. }));
assert!(
landed_on_fused_aggregate,
"Match-Match-Return-aggregate must desugar and fuse into the \
streaming aggregate path; clauses: {:#?}",
query.clauses
);
}
#[test]
fn test_topk_absorbed_for_property_access_return() {
let mut query = parse_cypher(
"MATCH (p)-[:T1]->({id: 1}) \
MATCH (p)-[r]-(other) \
WHERE NOT (type(r) = 'T2' AND startNode(r) = other) \
RETURN p.title AS name, p.description AS desc, count(r) AS d \
ORDER BY d DESC LIMIT 10",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let topk_absorbed = query
.clauses
.iter()
.any(|c| matches!(c, Clause::FusedMatchWithAggregate { top_k: Some(_), .. }));
assert!(
topk_absorbed,
"ORDER BY DESC LIMIT 10 must be absorbed into \
FusedMatchWithAggregate.top_k when RETURN projects \
properties of the group variable; clauses: {:#?}",
query.clauses
);
}
#[test]
fn test_topk_absorbed_with_explicit_pass_through_with() {
let mut query = parse_cypher(
"MATCH (p)-[:P27]->({id: 20}) \
WITH p \
MATCH (p)-[r]-(other) \
WHERE NOT (type(r) = 'P50' AND startNode(r) = other) \
RETURN p.title AS name, p.description AS desc, count(r) AS connections \
ORDER BY connections DESC LIMIT 10",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let topk_absorbed = query
.clauses
.iter()
.any(|c| matches!(c, Clause::FusedMatchWithAggregate { top_k: Some(_), .. }));
assert!(
topk_absorbed,
"user's exact Q1 shape must absorb top_k; clauses: {:#?}",
query.clauses
);
}
#[test]
fn test_topk_skipped_for_computed_return_expressions() {
let mut query = parse_cypher(
"MATCH (p)-[:T1]->({id: 1}) \
MATCH (p)-[r]-() \
WITH p, count(r) AS total, 1 AS one \
RETURN p.title, total + one AS adjusted \
ORDER BY total DESC LIMIT 10",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let topk_absorbed = query
.clauses
.iter()
.any(|c| matches!(c, Clause::FusedMatchWithAggregate { top_k: Some(_), .. }));
assert!(
!topk_absorbed,
"computed RETURN expressions must not absorb top_k; \
clauses: {:#?}",
query.clauses
);
}
#[test]
fn test_desugar_skips_when_no_aggregate() {
let mut query = parse_cypher(
"MATCH (p)-[:T1]->({id: 1}) \
MATCH (p)-[:T2]->({id: 2}) \
RETURN p.title \
LIMIT 10",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let bare_with_count = query
.clauses
.iter()
.filter(|c| matches!(c, Clause::With(_)))
.count();
assert_eq!(
bare_with_count, 0,
"desugar must not introduce a WITH when RETURN has no aggregate"
);
}
#[test]
fn test_desugar_skips_when_multiple_group_vars() {
let mut query = parse_cypher(
"MATCH (p)-[:T1]->({id: 1}) \
MATCH (p)-[r]->(q) \
RETURN p.title, q.title, count(r) AS d \
ORDER BY d DESC LIMIT 10",
)
.unwrap();
let graph = DirGraph::new();
let params = HashMap::new();
optimize(&mut query, &graph, ¶ms);
let fused_count = query
.clauses
.iter()
.filter(|c| matches!(c, Clause::FusedMatchWithAggregate { .. }))
.count();
assert_eq!(
fused_count, 0,
"multi-group-variable RETURN must not be auto-rewritten"
);
}