#![allow(clippy::cast_possible_truncation)]
use grafeo_common::types::Value;
use grafeo_engine::GrafeoDB;
fn stats_graph() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for (name, x, y) in [
("Alix", 1.0, 2.0),
("Gus", 2.0, 4.0),
("Vincent", 3.0, 6.0),
("Jules", 4.0, 8.0),
("Mia", 5.0, 10.0),
] {
session
.create_node_with_props(
&["Data"],
[
("name", Value::String(name.into())),
("x", Value::Float64(x)),
("y", Value::Float64(y)),
("score", Value::Int64(x as i64 * 10)),
],
)
.unwrap();
}
db
}
#[test]
fn test_wrapped_aggregate_count_gt_zero() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN count(d) > 0 AS has_data")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Bool(true));
}
#[test]
fn test_wrapped_aggregate_sum_minus_literal() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN sum(d.score) - 10 AS adjusted")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(140));
}
#[test]
fn test_wrapped_aggregate_not_count() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN NOT (count(d) > 100) AS few")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Bool(true));
}
#[test]
fn test_group_concat_default_separator() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN group_concat(d.name) AS names")
.unwrap();
assert_eq!(r.rows().len(), 1);
if let Value::String(names) = &r.rows()[0][0] {
assert_eq!(names.split(' ').count(), 5);
} else {
panic!("expected string, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn test_group_concat_custom_separator() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN group_concat(d.name, ';') AS names")
.unwrap();
assert_eq!(r.rows().len(), 1);
if let Value::String(names) = &r.rows()[0][0] {
assert!(names.contains(';'), "expected semicolons: {names}");
assert_eq!(names.split(';').count(), 5);
} else {
panic!("expected string, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn test_listagg_default_comma() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN listagg(d.name) AS names")
.unwrap();
assert_eq!(r.rows().len(), 1);
if let Value::String(names) = &r.rows()[0][0] {
assert!(names.contains(','), "expected commas: {names}");
} else {
panic!("expected string, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn test_sample_returns_one_value() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN sample(d.name) AS picked")
.unwrap();
assert_eq!(r.rows().len(), 1);
if let Value::String(name) = &r.rows()[0][0] {
assert!(
["Alix", "Gus", "Vincent", "Jules", "Mia"].contains(&name.as_str()),
"unexpected: {name}"
);
} else {
panic!("expected string, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn test_collect_to_list() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN collect(d.score) AS scores")
.unwrap();
assert_eq!(r.rows().len(), 1);
if let Value::List(items) = &r.rows()[0][0] {
assert_eq!(items.len(), 5);
} else {
panic!("expected list, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn test_percentile_disc_with_integer_param() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN percentile_disc(d.score, 1) AS p100")
.unwrap();
assert_eq!(r.rows().len(), 1);
match &r.rows()[0][0] {
Value::Int64(v) => assert_eq!(*v, 50),
Value::Float64(v) => assert!((*v - 50.0).abs() < 0.01),
other => panic!("expected numeric, got {other:?}"),
}
}
#[test]
fn test_percentile_cont_zero() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN percentile_cont(d.score, 0) AS p0")
.unwrap();
assert_eq!(r.rows().len(), 1);
match &r.rows()[0][0] {
Value::Float64(v) => assert!((*v - 10.0).abs() < 0.01),
Value::Int64(v) => assert_eq!(*v, 10),
other => panic!("expected numeric, got {other:?}"),
}
}
#[test]
fn test_stdev_and_stdevp() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN stdev(d.score) AS s, stdevp(d.score) AS sp")
.unwrap();
assert_eq!(r.rows().len(), 1);
if let Value::Float64(s) = r.rows()[0][0] {
assert!(s > 0.0);
}
if let Value::Float64(sp) = r.rows()[0][1] {
assert!(sp > 0.0);
}
}
#[test]
fn test_variance_and_variance_pop() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN variance(d.score) AS v, var_pop(d.score) AS vp")
.unwrap();
assert_eq!(r.rows().len(), 1);
if let Value::Float64(v) = r.rows()[0][0] {
assert!(v > 0.0);
}
}
#[test]
fn test_mixed_aggregates_with_group_by() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for (name, city, score) in [
("Alix", "Amsterdam", 80),
("Gus", "Amsterdam", 90),
("Vincent", "Berlin", 70),
("Jules", "Berlin", 85),
] {
session
.create_node_with_props(
&["Person"],
[
("name", Value::String(name.into())),
("city", Value::String(city.into())),
("score", Value::Int64(score)),
],
)
.unwrap();
}
let r = session
.execute(
"MATCH (p:Person) \
RETURN p.city AS city, count(p) AS cnt, min(p.score) AS lo, max(p.score) AS hi",
)
.unwrap();
assert_eq!(r.rows().len(), 2);
assert_eq!(r.rows()[0][1], Value::Int64(2));
assert_eq!(r.rows()[1][1], Value::Int64(2));
}
#[test]
fn test_order_by_alias_after_aggregation() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for (name, city) in [
("Alix", "Berlin"),
("Gus", "Amsterdam"),
("Vincent", "Berlin"),
("Jules", "Amsterdam"),
("Mia", "Paris"),
] {
session
.create_node_with_props(
&["Person"],
[
("name", Value::String(name.into())),
("city", Value::String(city.into())),
],
)
.unwrap();
}
let r = session
.execute("MATCH (p:Person) RETURN p.city AS city, count(p) AS cnt ORDER BY city")
.unwrap();
assert_eq!(r.rows().len(), 3);
assert_eq!(r.rows()[0][0], Value::String("Amsterdam".into()));
assert_eq!(r.rows()[1][0], Value::String("Berlin".into()));
assert_eq!(r.rows()[2][0], Value::String("Paris".into()));
}
#[test]
fn test_order_by_aggregate_alias_desc() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for (name, city) in [
("Alix", "Berlin"),
("Gus", "Amsterdam"),
("Vincent", "Berlin"),
("Jules", "Amsterdam"),
("Mia", "Paris"),
] {
session
.create_node_with_props(
&["Person"],
[
("name", Value::String(name.into())),
("city", Value::String(city.into())),
],
)
.unwrap();
}
let r = session
.execute("MATCH (p:Person) RETURN p.city AS city, count(p) AS cnt ORDER BY cnt DESC")
.unwrap();
assert_eq!(r.rows().len(), 3);
assert_eq!(r.rows()[0][1], Value::Int64(2));
assert_eq!(r.rows()[1][1], Value::Int64(2));
assert_eq!(r.rows()[2][1], Value::Int64(1));
assert_eq!(r.rows()[2][0], Value::String("Paris".into()));
}
#[test]
fn test_nullif_expression() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN nullif(d.score, 10) AS v ORDER BY d.x")
.unwrap();
assert_eq!(r.rows()[0][0], Value::Null);
assert_eq!(r.rows()[1][0], Value::Int64(20));
}
#[test]
fn test_list_predicate_all() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
s.create_node_with_props(&["Flag"], [("v", Value::Int64(1))])
.unwrap();
let r = s
.execute("MATCH (f:Flag) WHERE all(x IN [2, 4, 6] WHERE x % 2 = 0) RETURN f.v AS v")
.unwrap();
assert_eq!(r.rows().len(), 1);
}
#[test]
fn test_list_predicate_any() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
s.create_node_with_props(&["Flag"], [("v", Value::Int64(1))])
.unwrap();
let r = s
.execute("MATCH (f:Flag) WHERE any(x IN [1, 2, 3] WHERE x > 2) RETURN f.v AS v")
.unwrap();
assert_eq!(r.rows().len(), 1);
}
#[test]
fn test_wrapped_aggregate_literal_plus_count() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN 100 + count(d) AS total")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(105));
}
#[test]
fn test_count_distinct() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for city in ["Amsterdam", "Amsterdam", "Berlin", "Berlin", "Paris"] {
session
.create_node_with_props(&["City"], [("name", Value::String(city.into()))])
.unwrap();
}
let r = session
.execute("MATCH (c:City) RETURN count(DISTINCT c.name) AS unique_cities")
.unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(3));
}
#[test]
fn test_count_expression_non_null() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(&["Item"], [("val", Value::Int64(1))])
.unwrap();
session
.create_node_with_props(&["Item"], [("val", Value::Null)])
.unwrap();
session
.create_node_with_props(&["Item"], [("val", Value::Int64(3))])
.unwrap();
let r = session
.execute("MATCH (i:Item) RETURN count(i.val) AS cnt")
.unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(2));
}
#[test]
fn test_having_filters_groups() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for (name, city) in [
("Alix", "Amsterdam"),
("Gus", "Amsterdam"),
("Vincent", "Berlin"),
("Jules", "Berlin"),
("Mia", "Paris"),
] {
session
.create_node_with_props(
&["Person"],
[
("name", Value::String(name.into())),
("city", Value::String(city.into())),
],
)
.unwrap();
}
let r = session
.execute(
"MATCH (p:Person) \
RETURN p.city AS city, count(p) AS cnt \
ORDER BY city \
HAVING cnt > 1",
)
.unwrap();
assert_eq!(
r.rows().len(),
2,
"HAVING should filter groups with cnt <= 1"
);
assert_eq!(r.rows()[0][0], Value::String("Amsterdam".into()));
assert_eq!(r.rows()[0][1], Value::Int64(2));
assert_eq!(r.rows()[1][0], Value::String("Berlin".into()));
assert_eq!(r.rows()[1][1], Value::Int64(2));
}
#[test]
fn test_having_no_matching_groups() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for city in ["Amsterdam", "Berlin", "Paris"] {
session
.create_node_with_props(&["City"], [("name", Value::String(city.into()))])
.unwrap();
}
let r = session
.execute(
"MATCH (c:City) \
RETURN c.name AS name, count(c) AS cnt \
HAVING cnt > 10",
)
.unwrap();
assert_eq!(r.rows().len(), 0, "No groups should pass HAVING cnt > 10");
}
#[test]
fn test_having_with_sum_aggregate() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for (name, dept, salary) in [
("Alix", "Engineering", 90),
("Gus", "Engineering", 80),
("Vincent", "Sales", 50),
("Jules", "Sales", 60),
("Mia", "Marketing", 70),
] {
session
.create_node_with_props(
&["Employee"],
[
("name", Value::String(name.into())),
("dept", Value::String(dept.into())),
("salary", Value::Int64(salary)),
],
)
.unwrap();
}
let r = session
.execute(
"MATCH (e:Employee) \
RETURN e.dept AS dept, sum(e.salary) AS total \
ORDER BY dept \
HAVING total > 100",
)
.unwrap();
assert_eq!(
r.rows().len(),
2,
"Only depts with total > 100 should appear"
);
assert_eq!(r.rows()[0][0], Value::String("Engineering".into()));
assert_eq!(r.rows()[1][0], Value::String("Sales".into()));
}
#[test]
fn count_on_empty_result_returns_zero() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let r = session
.execute("MATCH (n:NonExistent) RETURN count(n) AS cnt")
.unwrap();
assert_eq!(
r.rows().len(),
1,
"Global COUNT should always return one row"
);
assert_eq!(r.rows()[0][0], Value::Int64(0));
}
#[test]
fn sum_on_empty_result_returns_null() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let r = session
.execute("MATCH (n:NonExistent) RETURN sum(n.x) AS total")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Null);
}
#[test]
fn avg_on_empty_result_returns_null() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let r = session
.execute("MATCH (n:NonExistent) RETURN avg(n.x) AS average")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Null);
}
#[test]
fn min_max_on_empty_result_returns_null() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let r = session
.execute("MATCH (n:NonExistent) RETURN min(n.x) AS lo, max(n.x) AS hi")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Null);
assert_eq!(r.rows()[0][1], Value::Null);
}
#[test]
fn test_case_wrapping_aggregate() {
let db = stats_graph();
let s = db.session();
let r = s
.execute(
"MATCH (d:Data) \
RETURN CASE WHEN count(d) > 3 THEN 'many' ELSE 'few' END AS label",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::String("many".into()));
}
#[test]
fn test_case_wrapping_aggregate_else_branch() {
let db = stats_graph();
let s = db.session();
let r = s
.execute(
"MATCH (d:Data) \
RETURN CASE WHEN sum(d.score) > 9999 THEN 'huge' ELSE 'normal' END AS label",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::String("normal".into()));
}
#[test]
fn test_case_aggregate_in_then_branch() {
let db = stats_graph();
let s = db.session();
let r = s
.execute(
"MATCH (d:Data) \
RETURN CASE WHEN true THEN count(d) ELSE 0 END AS cnt",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(5));
}
#[cfg(feature = "cypher")]
#[test]
fn test_cypher_case_when_aggregate_condition_true() {
let db = stats_graph();
let s = db.session();
let r = s
.execute_cypher(
"MATCH (d:Data) \
RETURN CASE WHEN sum(d.score) > 0 THEN 'positive' ELSE 'zero' END AS label",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::String("positive".into()));
}
#[cfg(feature = "cypher")]
#[test]
fn test_cypher_case_then_aggregate_value() {
let db = stats_graph();
let s = db.session();
let r = s
.execute_cypher(
"MATCH (d:Data) \
RETURN CASE WHEN true THEN sum(d.score) ELSE -1 END AS total",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(150));
}
#[cfg(feature = "cypher")]
#[test]
fn test_cypher_case_else_aggregate_division() {
let db = stats_graph();
let s = db.session();
let r = s
.execute_cypher(
"MATCH (d:Data) \
RETURN CASE WHEN false THEN -1 ELSE sum(d.score) / count(d) END AS avg_score",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(30));
}
#[test]
fn test_non_aggregate_function_wrapping_aggregate() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN abs(sum(d.score)) AS total")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(150));
}
#[test]
fn test_tostring_wrapping_count() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN toString(count(d)) AS cnt_str")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::String("5".into()));
}
#[test]
fn test_covar_samp_empty_returns_null() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s
.execute("MATCH (p:NonExistent) RETURN COVAR_SAMP(p.y, p.x) AS cov")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Null);
}
#[test]
fn test_corr_empty_returns_null() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s
.execute("MATCH (p:NonExistent) RETURN CORR(p.y, p.x) AS r")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Null);
}
#[test]
fn test_regr_slope_empty_returns_null() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s
.execute("MATCH (p:NonExistent) RETURN REGR_SLOPE(p.y, p.x) AS slope")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Null);
}
#[test]
fn test_regr_intercept_empty_returns_null() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s
.execute("MATCH (p:NonExistent) RETURN REGR_INTERCEPT(p.y, p.x) AS intercept")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Null);
}
#[test]
fn test_regr_r2_empty_returns_null() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s
.execute("MATCH (p:NonExistent) RETURN REGR_R2(p.y, p.x) AS r2")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Null);
}
#[test]
fn test_regr_sxx_syy_sxy_empty_returns_null() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s
.execute(
"MATCH (p:NonExistent) \
RETURN REGR_SXX(p.y, p.x) AS sxx, REGR_SYY(p.y, p.x) AS syy, REGR_SXY(p.y, p.x) AS sxy",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Null);
assert_eq!(r.rows()[0][1], Value::Null);
assert_eq!(r.rows()[0][2], Value::Null);
}
#[test]
fn test_regr_avgx_avgy_empty_returns_null() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s
.execute(
"MATCH (p:NonExistent) \
RETURN REGR_AVGX(p.y, p.x) AS ax, REGR_AVGY(p.y, p.x) AS ay",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Null);
assert_eq!(r.rows()[0][1], Value::Null);
}
#[test]
fn test_regr_count_empty_returns_zero() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s
.execute("MATCH (p:NonExistent) RETURN REGR_COUNT(p.y, p.x) AS cnt")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(0));
}
#[test]
fn test_group_concat_distinct() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for city in ["Amsterdam", "Berlin", "Amsterdam", "Berlin", "Paris"] {
session
.create_node_with_props(&["City"], [("name", Value::String(city.into()))])
.unwrap();
}
let r = session
.execute("MATCH (c:City) RETURN group_concat(DISTINCT c.name) AS names")
.unwrap();
assert_eq!(r.rows().len(), 1);
if let Value::String(names) = &r.rows()[0][0] {
assert_eq!(
names.split(' ').count(),
3,
"expected 3 unique cities: {names}"
);
} else {
panic!("expected string, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn test_group_concat_distinct_with_separator() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for city in ["Amsterdam", "Berlin", "Amsterdam", "Berlin", "Paris"] {
session
.create_node_with_props(&["City"], [("name", Value::String(city.into()))])
.unwrap();
}
let r = session
.execute("MATCH (c:City) RETURN group_concat(DISTINCT c.name, '|') AS names")
.unwrap();
assert_eq!(r.rows().len(), 1);
if let Value::String(names) = &r.rows()[0][0] {
assert_eq!(
names.split('|').count(),
3,
"expected 3 unique cities: {names}"
);
} else {
panic!("expected string, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn test_sum_distinct_mixed_int_float() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(&["Val"], [("v", Value::Int64(10))])
.unwrap();
session
.create_node_with_props(&["Val"], [("v", Value::Int64(10))])
.unwrap(); session
.create_node_with_props(&["Val"], [("v", Value::Float64(20.5))])
.unwrap();
session
.create_node_with_props(&["Val"], [("v", Value::Float64(20.5))])
.unwrap();
let r = session
.execute("MATCH (n:Val) RETURN sum(DISTINCT n.v) AS total")
.unwrap();
assert_eq!(r.rows().len(), 1);
match &r.rows()[0][0] {
Value::Float64(f) => assert!((*f - 30.5).abs() < 0.01, "expected 30.5, got {f}"),
other => panic!("expected Float64, got {other:?}"),
}
}
#[test]
fn test_wrapped_two_aggregates_in_binary() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN sum(d.score) + count(d) AS combo")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(155));
}
#[test]
fn test_wrapped_aggregate_multiply() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN avg(d.score) * 2 AS doubled")
.unwrap();
assert_eq!(r.rows().len(), 1);
match &r.rows()[0][0] {
Value::Float64(f) => assert!((*f - 60.0).abs() < 0.01, "expected 60.0, got {f}"),
Value::Int64(v) => assert_eq!(*v, 60),
other => panic!("expected numeric, got {other:?}"),
}
}
#[test]
fn test_covar_samp_single_row_returns_null() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
s.create_node_with_props(
&["Point"],
[("x", Value::Float64(1.0)), ("y", Value::Float64(2.0))],
)
.unwrap();
let r = s
.execute("MATCH (p:Point) RETURN COVAR_SAMP(p.y, p.x) AS cov")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Null);
}
#[test]
fn test_covar_pop_single_row_returns_zero() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
s.create_node_with_props(
&["Point"],
[("x", Value::Float64(1.0)), ("y", Value::Float64(2.0))],
)
.unwrap();
let r = s
.execute("MATCH (p:Point) RETURN COVAR_POP(p.y, p.x) AS cov")
.unwrap();
assert_eq!(r.rows().len(), 1);
match &r.rows()[0][0] {
Value::Float64(f) => assert!(f.abs() < 1e-10, "expected 0.0, got {f}"),
other => panic!("expected Float64, got {other:?}"),
}
}
#[test]
fn test_optional_match_with_count_aggregate() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
s.create_node_with_props(&["Person"], [("name", Value::String("Alix".into()))])
.unwrap();
s.create_node_with_props(&["Person"], [("name", Value::String("Gus".into()))])
.unwrap();
s.execute(
"MATCH (a:Person {name: 'Alix'}) MATCH (b:Person {name: 'Gus'}) INSERT (a)-[:KNOWS]->(b)",
)
.unwrap();
let r = s
.execute(
"MATCH (p:Person) \
OPTIONAL MATCH (p)-[:KNOWS]->(f:Person) \
RETURN p.name AS name, count(f) AS friend_count \
ORDER BY name",
)
.unwrap();
assert_eq!(r.rows().len(), 2);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
assert_eq!(r.rows()[0][1], Value::Int64(1));
assert_eq!(r.rows()[1][0], Value::String("Gus".into()));
assert_eq!(r.rows()[1][1], Value::Int64(0));
}
#[test]
fn test_percentile_cont_interpolation() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
for v in [10, 20, 30, 40] {
s.create_node_with_props(&["Item"], [("v", Value::Int64(v))])
.unwrap();
}
let r = s
.execute("MATCH (i:Item) RETURN percentile_cont(i.v, 0.5) AS median")
.unwrap();
assert_eq!(r.rows().len(), 1);
match &r.rows()[0][0] {
Value::Float64(f) => assert!((*f - 25.0).abs() < 0.01, "expected 25.0, got {f}"),
other => panic!("expected Float64, got {other:?}"),
}
}
#[test]
fn test_percentile_cont_at_boundary() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
for v in [10, 20, 30, 40, 50] {
s.create_node_with_props(&["Item"], [("v", Value::Int64(v))])
.unwrap();
}
let r = s
.execute("MATCH (i:Item) RETURN percentile_cont(i.v, 0.25) AS p25")
.unwrap();
assert_eq!(r.rows().len(), 1);
match &r.rows()[0][0] {
Value::Float64(f) => assert!((*f - 20.0).abs() < 0.01, "expected 20.0, got {f}"),
other => panic!("expected Float64, got {other:?}"),
}
}
#[test]
fn test_wrapped_aggregate_unary_negation() {
let db = stats_graph();
let s = db.session();
let r = s
.execute("MATCH (d:Data) RETURN -(count(d)) AS neg")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(-5));
}
#[test]
fn test_collect_distinct() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
for city in ["Amsterdam", "Berlin", "Amsterdam", "Berlin", "Paris"] {
s.create_node_with_props(&["City"], [("name", Value::String(city.into()))])
.unwrap();
}
let r = s
.execute("MATCH (c:City) RETURN collect(DISTINCT c.name) AS names")
.unwrap();
assert_eq!(r.rows().len(), 1);
if let Value::List(items) = &r.rows()[0][0] {
assert_eq!(
items.len(),
3,
"expected 3 unique cities, got {}",
items.len()
);
} else {
panic!("expected list, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn test_avg_distinct() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
for v in [10, 10, 20, 20, 30] {
s.create_node_with_props(&["Val"], [("v", Value::Int64(v))])
.unwrap();
}
let r = s
.execute("MATCH (n:Val) RETURN avg(DISTINCT n.v) AS a")
.unwrap();
assert_eq!(r.rows().len(), 1);
match &r.rows()[0][0] {
Value::Float64(f) => assert!((*f - 20.0).abs() < 0.01, "expected 20.0, got {f}"),
other => panic!("expected Float64, got {other:?}"),
}
}