use super::*;
use crate::graph::languages::cypher::planner;
fn parent_child_graph() -> DirGraph {
let mut graph = DirGraph::new();
let specs = [
(1u32, "s", "Software"),
(2, "a", "Api"),
(3, "d", "Doc"),
(4, "c1", "Software"),
(5, "c2", "Software"),
(6, "c3", "Software"),
(7, "c4", "Api"),
(8, "c5", "Api"),
(9, "c6", "Doc"),
];
let mut idx = Vec::new();
for (id, concept_id, label) in specs {
let node = NodeData::new(
Value::UniqueId(id),
Value::String(concept_id.to_string()),
label.to_string(),
HashMap::from([(
"concept_id".to_string(),
Value::String(concept_id.to_string()),
)]),
&mut graph.interner,
);
let i = graph.graph.add_node(node);
graph
.type_indices
.entry_or_default(label.to_string())
.push(i);
idx.push(i);
}
for (child, parent) in [(3usize, 1usize), (4, 1), (5, 0), (6, 0), (7, 2), (8, 2)] {
let edge = EdgeData::new("CHILD_OF".to_string(), HashMap::new(), &mut graph.interner);
graph.graph.add_edge(idx[child], idx[parent], edge);
}
graph.register_connection_type("CHILD_OF".to_string());
graph
}
fn rows(graph: &DirGraph, query: &str) -> Vec<Vec<Value>> {
let no_params = HashMap::new();
let parsed = parser::parse_cypher(query)
.unwrap_or_else(|e| panic!("query failed to parse: {query}\n error: {e}"));
let plain = CypherExecutor::with_params(graph, &no_params, None)
.execute(&parsed)
.unwrap_or_else(|e| panic!("query failed: {query}\n error: {e}"));
let mut optimized = parsed;
planner::optimize(&mut optimized, graph, &no_params);
let planned = CypherExecutor::with_params(graph, &no_params, None)
.execute(&optimized)
.unwrap_or_else(|e| panic!("optimised query failed: {query}\n error: {e}"));
let sorted = |mut rs: Vec<Vec<Value>>| {
rs.sort_by_key(|r| format!("{r:?}"));
rs
};
let out = sorted(planned.rows);
assert_eq!(
sorted(plain.rows),
out,
"optimised and unoptimised plans disagree for: {query}"
);
out
}
fn expect(graph: &DirGraph, query: &str, want: &[(&str, i64)]) {
let got = rows(graph, query);
let want: Vec<Vec<Value>> = want
.iter()
.map(|(id, k)| vec![Value::String((*id).to_string()), Value::Int64(*k)])
.collect();
assert_eq!(got, want, "wrong rows for: {query}");
}
#[test]
fn label_constrained_histogram_group_keeps_only_matching_parents() {
let g = parent_child_graph();
expect(
&g,
"MATCH (c)-[:CHILD_OF]->(p:Software) WITH p, count(c) AS k \
RETURN p.concept_id AS id, k",
&[("s", 2)],
);
}
#[test]
fn label_constrained_histogram_group_survives_pattern_reversal() {
let g = parent_child_graph();
expect(
&g,
"MATCH (p:Software)<-[:CHILD_OF]-(c) WITH p, count(c) AS k \
RETURN p.concept_id AS id, k",
&[("s", 2)],
);
}
#[test]
fn label_constrained_histogram_group_applies_to_every_count_spelling() {
let g = parent_child_graph();
expect(
&g,
"MATCH (c)-[r:CHILD_OF]->(p:Software) WITH p, count(r) AS k \
RETURN p.concept_id AS id, k",
&[("s", 2)],
);
expect(
&g,
"MATCH (c)-[:CHILD_OF]->(p:Software) WITH p, count(*) AS k \
RETURN p.concept_id AS id, k",
&[("s", 2)],
);
}
#[test]
fn label_constrained_histogram_group_applies_under_limit_and_where() {
let g = parent_child_graph();
expect(
&g,
"MATCH (c)-[:CHILD_OF]->(p:Software) WITH p, count(c) AS k \
RETURN p.concept_id AS id, k ORDER BY k DESC LIMIT 3",
&[("s", 2)],
);
expect(
&g,
"MATCH (c)-[:CHILD_OF]->(p:Software) WITH p, count(c) AS k LIMIT 3 \
RETURN p.concept_id AS id, k",
&[("s", 2)],
);
expect(
&g,
"MATCH (c)-[:CHILD_OF]->(p:Software) WITH p, count(c) AS k WHERE k > 0 \
RETURN p.concept_id AS id, k",
&[("s", 2)],
);
}
#[test]
fn label_alternation_on_the_group_node_keeps_every_branch_and_only_those() {
let g = parent_child_graph();
expect(
&g,
"MATCH (c)-[:CHILD_OF]->(p:Software|Api) WITH p, count(c) AS k \
RETURN p.concept_id AS id, k",
&[("a", 2), ("s", 2)],
);
}
#[test]
fn label_alternation_on_the_source_node_counts_every_branch() {
let g = parent_child_graph();
expect(
&g,
"MATCH (c:Software|Api)-[:CHILD_OF]->(p) WITH p, count(c) AS k \
RETURN p.concept_id AS id, k",
&[("a", 2), ("d", 1), ("s", 2)],
);
}
#[test]
fn two_match_fusion_keeps_the_first_match_row_multiplicity() {
let g = parent_child_graph();
expect(
&g,
"MATCH (c)-[:CHILD_OF]->(p:Software) MATCH (p)<-[r:CHILD_OF]-() \
WITH p, count(r) AS k RETURN p.concept_id AS id, k",
&[("s", 4)],
);
}
#[test]
fn two_match_fusion_multiplicity_applies_with_a_labelled_secondary_pattern() {
let g = parent_child_graph();
expect(
&g,
"MATCH (c)-[:CHILD_OF]->(p) MATCH (p)<-[r:CHILD_OF]-(:Api) \
WITH p, count(r) AS k RETURN p.concept_id AS id, k",
&[("d", 2), ("s", 2)],
);
}
#[test]
fn unaffected_aggregate_shapes_keep_their_answers() {
let g = parent_child_graph();
expect(
&g,
"MATCH (c)-[:CHILD_OF]->(p {concept_id:'s'}) WITH p, count(c) AS k \
RETURN p.concept_id AS id, k",
&[("s", 2)],
);
expect(
&g,
"MATCH (c:Software)-[:CHILD_OF]->(p) WITH p, count(c) AS k \
RETURN p.concept_id AS id, k",
&[("a", 2), ("s", 1)],
);
expect(
&g,
"MATCH (c)-[:CHILD_OF]->(p:Software) RETURN p.concept_id AS id, count(c) AS k",
&[("s", 2)],
);
}