mod test_helpers;
use test_helpers::TestDb;
fn db_with_data() -> TestDb {
let db = TestDb::new();
db.run("CREATE (a:User {name: 'Alice', age: 30, dept: 'eng'})");
db.run("CREATE (b:User {name: 'Bob', age: 25, dept: 'eng'})");
db.run("CREATE (c:User {name: 'Carol', age: 35, dept: 'sales'})");
db.run("CREATE (d:User {name: 'Dave', age: 40, dept: 'sales'})");
db
}
#[test]
fn count_all_nodes() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN count(n) AS c");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["c"], 4);
}
#[test]
fn count_empty_result() {
let db = TestDb::new();
let rows = db.run("MATCH (n:User) RETURN count(n) AS c");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["c"], 0);
}
#[test]
fn count_with_filter() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) WHERE n.age > 30 RETURN count(n) AS c");
assert_eq!(rows[0]["c"], 2);
}
#[test]
fn count_no_matching_rows() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) WHERE n.age > 100 RETURN count(n) AS c");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["c"], 0);
}
#[test]
fn count_distinct() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN count(DISTINCT n.dept) AS c");
assert_eq!(rows[0]["c"], 2);
}
#[test]
fn count_distinct_departments() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run("MATCH (p:Person) RETURN count(DISTINCT p.dept) AS depts");
assert_eq!(rows[0]["depts"], 2);
}
#[test]
fn count_distinct_cities() {
let db = TestDb::new();
db.seed_org_graph();
let rows =
db.run("MATCH (p:Person)-[:LIVES_IN]->(c:City) RETURN count(DISTINCT c.name) AS cities");
assert_eq!(rows[0]["cities"], 3);
}
#[test]
fn count_distinct_with_grouping() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person)-[:LIVES_IN]->(c:City) \
RETURN p.dept AS dept, count(DISTINCT c.name) AS cities ORDER BY p.dept",
);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["cities"], 2);
assert_eq!(rows[1]["cities"], 2);
}
#[test]
fn sum_property() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN sum(n.age) AS total");
assert_eq!(rows[0]["total"], 130);
}
#[test]
fn avg_property() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN avg(n.age) AS average");
let avg = rows[0]["average"].as_f64().unwrap();
assert!((avg - 32.5).abs() < 0.01);
}
#[test]
fn min_property() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN min(n.age) AS youngest");
assert_eq!(rows[0]["youngest"], 25);
}
#[test]
fn max_property() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN max(n.age) AS oldest");
assert_eq!(rows[0]["oldest"], 40);
}
#[test]
fn min_max_age_org() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run("MATCH (p:Person) RETURN min(p.age) AS youngest, max(p.age) AS oldest");
assert_eq!(rows[0]["youngest"], 26);
assert_eq!(rows[0]["oldest"], 50);
}
#[test]
fn sum_project_budgets() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run("MATCH (p:Project) RETURN sum(p.budget) AS total");
assert_eq!(rows[0]["total"], 150000);
}
#[test]
fn collect_property() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN collect(n.name) AS names");
let names = rows[0]["names"].as_array().unwrap();
assert_eq!(names.len(), 4);
}
#[test]
fn collect_names_per_project() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person)-[:ASSIGNED_TO]->(proj:Project) \
RETURN proj.name AS project, collect(p.name) AS members ORDER BY proj.name",
);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["project"], "Alpha");
assert_eq!(rows[0]["members"].as_array().unwrap().len(), 2);
}
#[test]
fn count_grouped_by_property() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN n.dept AS dept, count(n) AS c");
assert_eq!(rows.len(), 2);
for row in &rows {
let dept = row["dept"].as_str().unwrap();
let count = row["c"].as_i64().unwrap();
match dept {
"eng" => assert_eq!(count, 2),
"sales" => assert_eq!(count, 2),
_ => panic!("unexpected dept: {dept}"),
}
}
}
#[test]
fn count_employees_per_department() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run("MATCH (p:Person) RETURN p.dept AS dept, count(p) AS cnt ORDER BY p.dept");
assert_eq!(rows.len(), 2);
for row in &rows {
match row["dept"].as_str().unwrap() {
"Engineering" => assert_eq!(row["cnt"], 4),
"Marketing" => assert_eq!(row["cnt"], 2),
other => panic!("unexpected dept: {other}"),
}
}
}
#[test]
fn avg_age_per_department() {
let db = TestDb::new();
db.seed_org_graph();
let rows =
db.run("MATCH (p:Person) RETURN p.dept AS dept, avg(p.age) AS avg_age ORDER BY p.dept");
assert_eq!(rows.len(), 2);
for row in &rows {
let avg = row["avg_age"].as_f64().unwrap();
match row["dept"].as_str().unwrap() {
"Engineering" => assert!((avg - 34.75).abs() < 0.01),
"Marketing" => assert!((avg - 36.5).abs() < 0.01),
other => panic!("unexpected dept: {other}"),
}
}
}
#[test]
fn having_filter_on_grouped_count() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person) \
WITH p.dept AS dept, count(p) AS cnt \
WHERE cnt > 2 \
RETURN dept, cnt",
);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["dept"], "Engineering");
assert_eq!(rows[0]["cnt"], 4);
}
#[test]
fn count_outgoing_relationships_per_person() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run("MATCH (p:Person {name:'Frank'})-[r]->(x) RETURN count(r) AS rels");
assert_eq!(rows[0]["rels"], 5);
}
#[test]
fn aggregation_on_empty_match() {
let db = TestDb::new();
db.seed_org_graph();
let rows =
db.run("MATCH (p:Person) WHERE p.age > 100 RETURN count(p) AS cnt, sum(p.age) AS total");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["cnt"], 0);
assert!(rows[0]["total"].is_null() || rows[0]["total"] == 0);
}
#[test]
fn multiple_aggregates_same_query() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person) RETURN count(p) AS cnt, min(p.age) AS youngest, max(p.age) AS oldest, avg(p.age) AS avg_age",
);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["cnt"], 6);
assert_eq!(rows[0]["youngest"], 26);
assert_eq!(rows[0]["oldest"], 50);
}
#[test]
fn count_star_counts_all_rows() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN count(*) AS c");
assert_eq!(rows[0]["c"], 4);
}
#[test]
fn count_star_with_no_matches() {
let db = TestDb::new();
let rows = db.run("MATCH (n:User) RETURN count(*) AS c");
assert_eq!(rows[0]["c"], 0);
}
#[test]
fn count_star_includes_null_property_rows() {
let db = TestDb::new();
db.run("CREATE (:Item {name: 'A', score: 10})");
db.run("CREATE (:Item {name: 'B'})");
db.run("CREATE (:Item {name: 'C', score: 20})");
let rows = db.run("MATCH (i:Item) RETURN count(*) AS c");
assert_eq!(rows[0]["c"], 3);
let rows = db.run("MATCH (i:Item) RETURN count(i.score) AS c");
assert_eq!(rows[0]["c"], 2);
}
#[test]
fn count_star_no_match_clause() {
let db = TestDb::new();
let rows = db.run("RETURN count(*) AS c");
assert_eq!(rows[0]["c"], 1);
}
#[test]
fn count_star_grouped() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN n.dept AS dept, count(*) AS c ORDER BY n.dept");
assert_eq!(rows.len(), 2);
for row in &rows {
assert_eq!(row["c"], 2);
}
}
#[test]
fn min_on_strings() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN min(n.name) AS first_name");
assert_eq!(rows[0]["first_name"], "Alice");
}
#[test]
fn max_on_strings() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN max(n.name) AS last_name");
assert_eq!(rows[0]["last_name"], "Dave");
}
#[test]
fn count_skips_null_properties() {
let db = TestDb::new();
db.run("CREATE (:Item {name: 'A', score: 10})");
db.run("CREATE (:Item {name: 'B'})");
db.run("CREATE (:Item {name: 'C', score: 20})");
let rows = db.run("MATCH (i:Item) RETURN count(i.score) AS cnt");
assert_eq!(rows[0]["cnt"], 2);
}
#[test]
fn sum_skips_null_properties() {
let db = TestDb::new();
db.run("CREATE (:Item {name: 'A', score: 10})");
db.run("CREATE (:Item {name: 'B'})");
db.run("CREATE (:Item {name: 'C', score: 20})");
let rows = db.run("MATCH (i:Item) RETURN sum(i.score) AS total");
assert_eq!(rows[0]["total"], 30);
}
#[test]
fn avg_skips_null_properties() {
let db = TestDb::new();
db.run("CREATE (:Item {name: 'A', score: 10})");
db.run("CREATE (:Item {name: 'B'})");
db.run("CREATE (:Item {name: 'C', score: 20})");
let rows = db.run("MATCH (i:Item) RETURN avg(i.score) AS average");
let avg = rows[0]["average"].as_f64().unwrap();
assert!((avg - 15.0).abs() < 0.01);
}
#[test]
fn avg_returns_float_for_integers() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN avg(n.age) AS average");
assert!(rows[0]["average"].is_f64());
}
#[test]
fn min_max_with_single_row() {
let db = TestDb::new();
db.run("CREATE (:Solo {val: 42})");
let rows = db.run("MATCH (n:Solo) RETURN min(n.val) AS lo, max(n.val) AS hi");
assert_eq!(rows[0]["lo"], 42);
assert_eq!(rows[0]["hi"], 42);
}
#[test]
fn sum_of_computed_expression() {
let db = TestDb::new();
db.run("CREATE (:Item {price: 10, qty: 3})");
db.run("CREATE (:Item {price: 20, qty: 2})");
db.run("CREATE (:Item {price: 5, qty: 10})");
let rows = db.run("MATCH (i:Item) RETURN sum(i.price * i.qty) AS revenue");
assert_eq!(rows[0]["revenue"], 120);
}
#[test]
fn multiple_aggregates_grouped() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person) \
RETURN p.dept AS dept, count(p) AS cnt, min(p.age) AS youngest, max(p.age) AS oldest \
ORDER BY p.dept",
);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["dept"], "Engineering");
assert_eq!(rows[0]["cnt"], 4);
assert_eq!(rows[0]["youngest"], 26);
assert_eq!(rows[0]["oldest"], 50);
}
#[test]
fn count_relationships_grouped_by_type() {
let db = TestDb::new();
db.seed_org_graph();
let rows =
db.run("MATCH (a)-[r]->(b) RETURN type(r) AS rel_type, count(r) AS cnt ORDER BY type(r)");
assert!(rows.len() >= 4);
}
#[test]
fn collect_distinct_values() {
let db = db_with_data();
let rows = db.run("MATCH (n:User) RETURN collect(DISTINCT n.dept) AS depts");
let depts = rows[0]["depts"].as_array().unwrap();
assert_eq!(depts.len(), 2);
}
#[test]
fn avg_rating_per_movie() {
let db = TestDb::new();
db.seed_recommendation_graph();
let rows = db.run(
"MATCH (v:Viewer)-[r:RATED]->(m:Movie) \
RETURN m.title AS title, avg(r.score) AS avg_score, count(v) AS reviewers \
ORDER BY m.title",
);
assert_eq!(rows.len(), 4);
assert_eq!(rows[0]["title"], "Amelie");
let amelie_avg = rows[0]["avg_score"].as_f64().unwrap();
assert!((amelie_avg - 3.5).abs() < 0.01);
}
#[test]
fn total_ratings_per_viewer() {
let db = TestDb::new();
db.seed_recommendation_graph();
let rows = db.run(
"MATCH (v:Viewer)-[r:RATED]->(m:Movie) \
RETURN v.name AS viewer, count(r) AS ratings ORDER BY v.name",
);
assert_eq!(rows[0]["viewer"], "Alice");
assert_eq!(rows[0]["ratings"], 3);
assert_eq!(rows[1]["viewer"], "Bob");
assert_eq!(rows[1]["ratings"], 2);
assert_eq!(rows[2]["viewer"], "Carol");
assert_eq!(rows[2]["ratings"], 2);
}
#[test]
fn count_dependencies_per_package() {
let db = TestDb::new();
db.seed_dependency_graph();
let rows = db.run(
"MATCH (p:Package)-[d:DEPENDS_ON]->(dep:Package) \
RETURN p.name AS pkg, count(d) AS deps ORDER BY p.name",
);
assert_eq!(rows.len(), 4);
assert_eq!(rows[0]["pkg"], "app");
assert_eq!(rows[0]["deps"], 3);
}
#[test]
fn rich_social_count_friends_per_person() {
let db = TestDb::new();
db.seed_rich_social_graph();
let rows = db.run(
"MATCH (p:Person)-[:KNOWS]->(friend:Person) \
RETURN p.name AS name, count(friend) AS friends ORDER BY p.name",
);
assert_eq!(rows.len(), 5);
assert_eq!(rows[0]["name"], "Alice");
assert_eq!(rows[0]["friends"], 2);
assert_eq!(rows[1]["name"], "Bob");
assert_eq!(rows[1]["friends"], 2);
assert_eq!(rows[2]["name"], "Carol");
assert_eq!(rows[2]["friends"], 1);
}
#[test]
fn rich_social_avg_friendship_strength() {
let db = TestDb::new();
db.seed_rich_social_graph();
let rows = db.run("MATCH (a:Person)-[k:KNOWS]->(b:Person) RETURN avg(k.strength) AS avg_str");
let avg = rows[0]["avg_str"].as_f64().unwrap();
assert!((avg - 5.0).abs() < 0.01);
}
#[test]
fn rich_social_people_with_most_interests() {
let db = TestDb::new();
db.seed_rich_social_graph();
let rows = db.run(
"MATCH (p:Person)-[:INTERESTED_IN]->(i:Interest) \
RETURN p.name AS name, count(i) AS interest_count \
ORDER BY count(i) DESC, p.name ASC",
);
assert_eq!(rows.len(), 6);
assert_eq!(rows[5]["name"], "Frank");
assert_eq!(rows[5]["interest_count"], 1);
}
#[test]
fn rich_social_collect_interest_names_per_person() {
let db = TestDb::new();
db.seed_rich_social_graph();
let rows = db.run(
"MATCH (p:Person {name:'Alice'})-[:INTERESTED_IN]->(i:Interest) \
RETURN p.name AS name, collect(i.name) AS interests",
);
assert_eq!(rows.len(), 1);
let interests = rows[0]["interests"].as_array().unwrap();
assert_eq!(interests.len(), 2);
}
#[test]
fn knowledge_graph_count_entities_per_type() {
let db = TestDb::new();
db.seed_knowledge_graph();
let rows = db.run("MATCH (e:Entity) RETURN e.type AS etype, count(e) AS cnt ORDER BY e.type");
assert_eq!(rows.len(), 4);
assert_eq!(rows[0]["etype"], "award");
assert_eq!(rows[0]["cnt"], 1);
assert_eq!(rows[1]["etype"], "field");
assert_eq!(rows[1]["cnt"], 3);
assert_eq!(rows[2]["etype"], "person");
assert_eq!(rows[2]["cnt"], 2);
assert_eq!(rows[3]["etype"], "theory");
assert_eq!(rows[3]["cnt"], 2);
}
#[test]
fn knowledge_graph_count_relationships_per_entity() {
let db = TestDb::new();
db.seed_knowledge_graph();
let rows = db.run("MATCH (e:Entity {name:'Albert Einstein'})-[r]->(x) RETURN count(r) AS rels");
assert_eq!(rows[0]["rels"], 9);
}
#[test]
fn knowledge_graph_collect_document_titles_by_author() {
let db = TestDb::new();
db.seed_knowledge_graph();
let rows = db.run(
"MATCH (e:Entity {type:'person'})-[:AUTHORED]->(d:Document) \
RETURN e.name AS author, collect(d.title) AS docs ORDER BY e.name",
);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["author"], "Albert Einstein");
let docs = rows[0]["docs"].as_array().unwrap();
assert_eq!(docs.len(), 2);
}
#[test]
fn knowledge_graph_count_people_who_received_nobel() {
let db = TestDb::new();
db.seed_knowledge_graph();
let rows = db.run(
"MATCH (e:Entity)-[:RECEIVED]->(a:Entity {name:'Nobel Prize'}) RETURN count(e) AS winners",
);
assert_eq!(rows[0]["winners"], 2);
}
#[test]
fn count_distinct_on_computed_expression() {
let db = TestDb::new();
db.run("CREATE (:Num {val: 10})");
db.run("CREATE (:Num {val: 20})");
db.run("CREATE (:Num {val: 10})");
db.run("CREATE (:Num {val: 30})");
let rows = db.run("MATCH (n:Num) RETURN count(DISTINCT n.val * 2) AS cnt");
assert_eq!(rows[0]["cnt"], 3);
}
#[test]
fn sum_on_empty_result_set() {
let db = TestDb::new();
let rows = db.run("MATCH (n:Nothing) RETURN sum(n.val) AS total");
assert_eq!(rows.len(), 1);
assert!(rows[0]["total"].is_null() || rows[0]["total"] == 0);
}
#[test]
fn multiple_aggregates_with_multiple_grouping_keys() {
let db = TestDb::new();
db.seed_rich_social_graph();
let rows = db.run(
"MATCH (p:Person)-[r:INTERESTED_IN]->(i:Interest) \
RETURN p.city AS city, r.level AS level, count(p) AS cnt \
ORDER BY p.city ASC, r.level ASC",
);
assert!(rows.len() >= 3);
}
#[test]
fn aggregation_after_varlen_traversal() {
let db = TestDb::new();
db.seed_dependency_graph();
let rows = db.run(
"MATCH (src:Package {name:'app'})-[:DEPENDS_ON*]->(dep:Package) \
RETURN count(dep) AS total_paths",
);
assert_eq!(rows[0]["total_paths"], 8);
}
#[test]
fn count_distinct_transitive_dependencies() {
let db = TestDb::new();
db.seed_dependency_graph();
let rows = db.run(
"MATCH (src:Package {name:'app'})-[:DEPENDS_ON*]->(dep:Package) \
RETURN count(DISTINCT dep.name) AS unique_deps",
);
assert_eq!(rows[0]["unique_deps"], 5);
}
#[test]
fn percentile_cont_function() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run("MATCH (p:Person) RETURN percentileCont(p.age, 0.5) AS median_age");
assert!(!rows.is_empty());
}
#[test]
fn percentile_disc_function() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run("MATCH (p:Person) RETURN percentileDisc(p.age, 0.5) AS median_age");
assert!(!rows.is_empty());
}
#[test]
fn stdev_function() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run("MATCH (p:Person) RETURN stDev(p.age) AS sd");
let sd = rows[0]["sd"].as_f64().unwrap();
assert!(sd > 0.0);
}
#[test]
fn collect_ordering_guarantee() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person) \
WITH p ORDER BY p.name ASC \
RETURN collect(p.name) AS names",
);
let names = rows[0]["names"].as_array().unwrap();
assert_eq!(names[0], "Alice");
assert_eq!(names[5], "Frank");
}
#[test]
fn aggregation_in_case_expression() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person) \
RETURN p.dept AS dept, \
CASE WHEN count(p) > 3 THEN 'large' ELSE 'small' END AS size",
);
assert!(!rows.is_empty());
}
#[test]
fn agg_multiple_aggregates_single_query() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person)-[:WORKS_AT]->(c:Company) \
RETURN count(p) AS total, min(p.age) AS youngest, max(p.age) AS oldest, \
collect(p.name) AS names",
);
assert_eq!(rows.len(), 1);
assert!(rows[0]["total"].as_i64().unwrap() >= 5);
assert!(rows[0]["youngest"].as_i64().unwrap() < rows[0]["oldest"].as_i64().unwrap());
assert!(!rows[0]["names"].as_array().unwrap().is_empty());
}
#[test]
fn agg_group_by_with_having_equivalent() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person) \
WITH p.dept AS dept, count(p) AS cnt \
WHERE cnt > 2 \
RETURN dept, cnt ORDER BY dept",
);
assert!(!rows.is_empty());
for row in &rows {
assert!(row["cnt"].as_i64().unwrap() > 2);
}
}
#[test]
fn agg_count_distinct_values() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run("MATCH (p:Person) RETURN count(DISTINCT p.dept) AS dept_count");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["dept_count"], 2); }
#[test]
fn agg_sum_with_null_values() {
let db = TestDb::new();
db.run("CREATE (:Val {x: 10})");
db.run("CREATE (:Val {x: 20})");
db.run("CREATE (:Val {})"); let rows = db.run("MATCH (v:Val) RETURN sum(v.x) AS total");
assert_eq!(rows[0]["total"], 30); }
#[test]
fn agg_avg_with_null_skip() {
let db = TestDb::new();
db.run("CREATE (:Score {v: 10})");
db.run("CREATE (:Score {v: 20})");
db.run("CREATE (:Score {})"); let rows = db.run("MATCH (s:Score) RETURN avg(s.v) AS mean");
let mean = rows[0]["mean"].as_f64().unwrap();
assert!((mean - 15.0).abs() < 0.001); }
#[test]
fn agg_collect_preserves_order_with_order_by() {
let db = TestDb::new();
db.run("CREATE (:O {name: 'c', rank: 3})");
db.run("CREATE (:O {name: 'a', rank: 1})");
db.run("CREATE (:O {name: 'b', rank: 2})");
let rows = db.run(
"MATCH (o:O) \
WITH o ORDER BY o.rank \
RETURN collect(o.name) AS names",
);
let names = rows[0]["names"].as_array().unwrap();
assert_eq!(names[0], "a");
assert_eq!(names[1], "b");
assert_eq!(names[2], "c");
}
#[test]
fn agg_min_max_on_strings() {
let db = TestDb::new();
db.run("CREATE (:W {v: 'banana'})");
db.run("CREATE (:W {v: 'apple'})");
db.run("CREATE (:W {v: 'cherry'})");
let rows = db.run("MATCH (w:W) RETURN min(w.v) AS lo, max(w.v) AS hi");
assert_eq!(rows[0]["lo"], "apple");
assert_eq!(rows[0]["hi"], "cherry");
}
#[test]
fn agg_count_star_vs_count_property() {
let db = TestDb::new();
db.run("CREATE (:T {v: 1})");
db.run("CREATE (:T {v: 2})");
db.run("CREATE (:T {})"); let rows = db.run("MATCH (t:T) RETURN count(*) AS all_rows, count(t.v) AS non_null");
assert_eq!(rows[0]["all_rows"], 3);
assert_eq!(rows[0]["non_null"], 2);
}
#[test]
fn agg_nested_grouping_with_unwind() {
let db = TestDb::new();
db.run("CREATE (:Tag {category: 'A', items: [1, 2, 3]})");
db.run("CREATE (:Tag {category: 'A', items: [4, 5]})");
db.run("CREATE (:Tag {category: 'B', items: [10]})");
let rows = db.run(
"MATCH (t:Tag) \
UNWIND t.items AS item \
RETURN t.category AS cat, count(item) AS cnt, sum(item) AS total \
ORDER BY cat",
);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["cat"], "A");
assert_eq!(rows[0]["cnt"], 5);
assert_eq!(rows[0]["total"], 15);
assert_eq!(rows[1]["cat"], "B");
assert_eq!(rows[1]["cnt"], 1);
}
#[test]
fn agg_empty_group_returns_zero() {
let db = TestDb::new();
let rows = db.run("MATCH (n:NoSuchLabel) RETURN count(n) AS cnt");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["cnt"], 0);
}
#[test]
#[ignore = "pending implementation"]
fn agg_percentile_disc() {
let db = TestDb::new();
for i in 1..=10 {
db.run(&format!("CREATE (:V {{x: {}}})", i));
}
let rows = db.run("MATCH (v:V) RETURN percentileDisc(v.x, 0.5) AS median");
let m = rows[0]["median"].as_i64().unwrap();
assert!(m == 5 || m == 6);
}
#[test]
fn agg_count_with_optional_match_nulls() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person) \
OPTIONAL MATCH (p)-[:MANAGES]->(sub:Person) \
RETURN p.name AS name, count(sub) AS subs \
ORDER BY subs DESC, p.name ASC",
);
assert_eq!(rows.len(), 6);
let frank = rows.iter().find(|r| r["name"] == "Frank").unwrap();
assert_eq!(frank["subs"], 3);
let carol = rows.iter().find(|r| r["name"] == "Carol").unwrap();
assert_eq!(carol["subs"], 1);
let zero_count = rows.iter().filter(|r| r["subs"] == 0).count();
assert_eq!(zero_count, 4);
}
#[test]
fn agg_collect_with_optional_match_filters_nulls() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person) \
OPTIONAL MATCH (p)-[:ASSIGNED_TO]->(proj:Project) \
RETURN p.name AS name, collect(proj.name) AS projects \
ORDER BY p.name",
);
let alice = rows.iter().find(|r| r["name"] == "Alice").unwrap();
assert_eq!(alice["projects"].as_array().unwrap().len(), 1);
assert_eq!(alice["projects"].as_array().unwrap()[0], "Alpha");
let bob = rows.iter().find(|r| r["name"] == "Bob").unwrap();
assert_eq!(bob["projects"].as_array().unwrap().len(), 1);
let dave = rows.iter().find(|r| r["name"] == "Dave").unwrap();
let dave_projects = dave["projects"].as_array().unwrap();
assert!(dave_projects.len() <= 1);
}
#[test]
fn agg_two_level_count_of_counts() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person)-[:LIVES_IN]->(c:City) \
WITH c.name AS city, count(p) AS pop \
RETURN count(city) AS num_cities, sum(pop) AS total_residents, max(pop) AS biggest_city",
);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["num_cities"], 3);
assert_eq!(rows[0]["total_residents"], 6);
assert_eq!(rows[0]["biggest_city"], 3); }
#[test]
fn agg_pipeline_filter_then_reaggregate() {
let db = TestDb::new();
db.seed_recommendation_graph();
let rows = db.run(
"MATCH (v:Viewer)-[r:RATED]->(m:Movie) \
WITH m.title AS movie, avg(r.score) AS avg_score \
WHERE avg_score >= 4.0 \
RETURN count(movie) AS highly_rated_count",
);
assert_eq!(rows[0]["highly_rated_count"], 2);
}
#[test]
fn agg_count_distinct_over_varlen() {
let db = TestDb::new();
db.seed_dependency_graph();
let rows = db.run(
"MATCH (p:Package)-[:DEPENDS_ON*]->(dep:Package) \
RETURN p.name AS pkg, count(DISTINCT dep.name) AS unique_deps \
ORDER BY unique_deps DESC",
);
assert_eq!(rows[0]["pkg"], "app");
assert_eq!(rows[0]["unique_deps"], 5);
}
#[test]
fn agg_all_same_values() {
let db = TestDb::new();
for _ in 0..5 {
db.run("CREATE (:Same {x: 42})");
}
let rows = db.run("MATCH (s:Same) RETURN min(s.x) AS lo, max(s.x) AS hi, avg(s.x) AS avg, count(DISTINCT s.x) AS uniq");
assert_eq!(rows[0]["lo"], 42);
assert_eq!(rows[0]["hi"], 42);
let avg = rows[0]["avg"].as_f64().unwrap();
assert!((avg - 42.0).abs() < 0.01);
assert_eq!(rows[0]["uniq"], 1);
}
#[test]
fn agg_all_null_values() {
let db = TestDb::new();
db.run("CREATE (:Nil {x: 1})");
db.run("MATCH (n:Nil) SET n.x = null");
db.run("CREATE (:Nil {})");
db.run("CREATE (:Nil {})");
let rows = db.run("MATCH (n:Nil) RETURN count(n.x) AS cnt, sum(n.x) AS total");
assert_eq!(rows[0]["cnt"], 0);
assert!(rows[0]["total"].is_null() || rows[0]["total"] == 0);
}
#[test]
fn agg_group_by_two_keys() {
let db = TestDb::new();
db.seed_org_graph();
let rows = db.run(
"MATCH (p:Person)-[:LIVES_IN]->(c:City) \
RETURN p.dept AS dept, c.name AS city, count(p) AS cnt \
ORDER BY dept, city",
);
assert!(rows.len() >= 4);
for row in &rows {
assert!(row["cnt"].as_i64().unwrap() >= 1);
}
}
#[test]
fn agg_collect_then_size() {
let db = TestDb::new();
db.seed_rich_social_graph();
let rows = db.run(
"MATCH (p:Person)-[:INTERESTED_IN]->(i:Interest) \
WITH p.name AS name, collect(i.name) AS interests \
RETURN name, size(interests) AS cnt ORDER BY cnt DESC, name ASC",
);
assert_eq!(rows.len(), 6);
assert_eq!(rows[5]["name"], "Frank");
assert_eq!(rows[5]["cnt"], 1);
}
#[test]
#[ignore = "pending implementation"]
fn agg_stdev_population() {
let db = TestDb::new();
for i in 1..=10 {
db.run(&format!("CREATE (:V {{x: {}}})", i));
}
let _rows = db.run("MATCH (v:V) RETURN stDevP(v.x) AS sd");
}
#[test]
#[ignore = "pending implementation"]
fn agg_having_without_with() {
let db = TestDb::new();
db.seed_org_graph();
let _rows = db.run(
"MATCH (p:Person) \
RETURN p.dept AS dept, count(p) AS cnt \
HAVING cnt > 2",
);
}