use grafeo_common::types::Value;
use grafeo_engine::GrafeoDB;
fn people_graph() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let _alix = session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Alix".into())),
("age", Value::Int64(30)),
("city", Value::String("Amsterdam".into())),
],
)
.unwrap();
let _gus = session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Gus".into())),
("age", Value::Int64(25)),
("city", Value::String("Berlin".into())),
],
)
.unwrap();
let _vincent = session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Vincent".into())),
("age", Value::Int64(40)),
("city", Value::String("Paris".into())),
],
)
.unwrap();
let _jules = session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Jules".into())),
("city", Value::String("Prague".into())),
],
)
.unwrap();
let _mia = session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Mia".into())),
("city", Value::String("Amsterdam".into())),
],
)
.unwrap();
db
}
fn chain_graph() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let alix = session
.create_node_with_props(&["Person"], [("name", Value::String("Alix".into()))])
.unwrap();
let gus = session
.create_node_with_props(&["Person"], [("name", Value::String("Gus".into()))])
.unwrap();
let vincent = session
.create_node_with_props(&["Person"], [("name", Value::String("Vincent".into()))])
.unwrap();
let jules = session
.create_node_with_props(&["Person"], [("name", Value::String("Jules".into()))])
.unwrap();
let mia = session
.create_node_with_props(&["Person"], [("name", Value::String("Mia".into()))])
.unwrap();
session.create_edge(alix, gus, "KNOWS");
session.create_edge(gus, vincent, "KNOWS");
session.create_edge(vincent, jules, "KNOWS");
session.create_edge(jules, mia, "KNOWS");
db
}
fn int_col(r: &[Value], idx: usize) -> Option<i64> {
match &r[idx] {
Value::Int64(i) => Some(*i),
_ => None,
}
}
fn string_col(r: &[Value], idx: usize) -> Option<String> {
match &r[idx] {
Value::String(s) => Some(s.to_string()),
_ => None,
}
}
#[test]
fn return_arithmetic_multiple_ops() {
let db = people_graph();
let session = db.session();
let cases: &[(&str, i64)] = &[
("MATCH (n:Person {name:'Alix'}) RETURN n.age + 10 AS v", 40),
("MATCH (n:Person {name:'Alix'}) RETURN n.age - 5 AS v", 25),
("MATCH (n:Person {name:'Alix'}) RETURN n.age * 2 AS v", 60),
("MATCH (n:Person {name:'Alix'}) RETURN n.age / 3 AS v", 10),
];
for (query, expected) in cases {
let r = session.execute(query).unwrap();
assert_eq!(r.rows().len(), 1, "query: {query}");
assert_eq!(int_col(&r.rows()[0], 0), Some(*expected), "query: {query}");
}
}
#[test]
fn return_comparison_expressions() {
let db = people_graph();
let session = db.session();
let r = session
.execute(
"MATCH (n:Person) WHERE n.age IS NOT NULL \
RETURN n.name AS name, n.age > 30 AS is_senior ORDER BY name",
)
.unwrap();
assert_eq!(r.rows().len(), 3);
assert_eq!(r.rows()[0][1], Value::Bool(false), "Alix");
assert_eq!(r.rows()[1][1], Value::Bool(false), "Gus");
assert_eq!(r.rows()[2][1], Value::Bool(true), "Vincent");
}
#[test]
fn return_constant_literal_alongside_property() {
let db = people_graph();
let session = db.session();
let r = session
.execute(
"MATCH (n:Person {name:'Alix'}) \
RETURN n.name AS name, 42 AS answer, 'hello' AS greeting",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(string_col(&r.rows()[0], 0).as_deref(), Some("Alix"));
assert_eq!(r.rows()[0][1], Value::Int64(42));
assert_eq!(r.rows()[0][2], Value::String("hello".into()));
}
#[test]
fn return_aggregates_count_sum_avg_min_max() {
let db = people_graph();
let session = db.session();
let r = session
.execute(
"MATCH (n:Person) RETURN \
count(n) AS c, \
sum(n.age) AS s, \
min(n.age) AS lo, \
max(n.age) AS hi",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
let row = &r.rows()[0];
assert_eq!(int_col(row, 0), Some(5), "count counts all 5 rows");
assert_eq!(int_col(row, 1), Some(95), "sum of 30+25+40");
assert_eq!(int_col(row, 2), Some(25), "min age");
assert_eq!(int_col(row, 3), Some(40), "max age");
let r = session
.execute("MATCH (n:Person) RETURN avg(n.age) AS a")
.unwrap();
assert_eq!(r.rows().len(), 1);
match &r.rows()[0][0] {
Value::Float64(f) => assert!(
(f - (95.0 / 3.0)).abs() < 1e-9,
"avg(age) must be exactly 95/3, got {f}"
),
other => {
panic!("avg must return Float64 (integer truncation would be incorrect), got {other:?}")
}
}
}
#[test]
fn return_distinct_on_arithmetic_expression() {
let db = people_graph();
let session = db.session();
let r = session
.execute(
"MATCH (n:Person) WHERE n.age IS NOT NULL \
RETURN DISTINCT n.age / 10 AS bucket ORDER BY bucket",
)
.unwrap();
assert_eq!(r.rows().len(), 3, "expected 3 distinct buckets");
assert_eq!(int_col(&r.rows()[0], 0), Some(2), "Gus/25 -> bucket 2");
assert_eq!(int_col(&r.rows()[1], 0), Some(3), "Alix/30 -> bucket 3");
assert_eq!(int_col(&r.rows()[2], 0), Some(4), "Vincent/40 -> bucket 4");
}
#[test]
fn order_by_nulls_first_puts_nulls_at_top() {
let db = people_graph();
let session = db.session();
let r = session
.execute(
"MATCH (n:Person) RETURN n.name AS name, n.age AS age ORDER BY age ASC NULLS FIRST",
)
.unwrap();
assert_eq!(r.rows().len(), 5);
assert!(matches!(r.rows()[0][1], Value::Null), "row 0 age is null");
assert!(matches!(r.rows()[1][1], Value::Null), "row 1 age is null");
assert_eq!(int_col(&r.rows()[2], 1), Some(25), "Gus next");
assert_eq!(int_col(&r.rows()[3], 1), Some(30), "Alix next");
assert_eq!(int_col(&r.rows()[4], 1), Some(40), "Vincent last");
}
#[test]
fn order_by_nulls_last_puts_nulls_at_bottom() {
let db = people_graph();
let session = db.session();
let r = session
.execute("MATCH (n:Person) RETURN n.name AS name, n.age AS age ORDER BY age ASC NULLS LAST")
.unwrap();
assert_eq!(r.rows().len(), 5);
assert_eq!(int_col(&r.rows()[0], 1), Some(25), "Gus first");
assert_eq!(int_col(&r.rows()[1], 1), Some(30), "Alix");
assert_eq!(int_col(&r.rows()[2], 1), Some(40), "Vincent");
assert!(matches!(r.rows()[3][1], Value::Null));
assert!(matches!(r.rows()[4][1], Value::Null));
}
#[test]
fn order_by_desc_with_nulls_ordering() {
let db = people_graph();
let session = db.session();
let r = session
.execute(
"MATCH (n:Person) RETURN n.name AS name, n.age AS age \
ORDER BY age DESC NULLS LAST",
)
.unwrap();
assert_eq!(r.rows().len(), 5);
let ages: Vec<Value> = r.rows().iter().map(|row| row[1].clone()).collect();
assert_eq!(
ages,
vec![
Value::Null,
Value::Null,
Value::Int64(40),
Value::Int64(30),
Value::Int64(25),
],
"DESC NULLS LAST currently places nulls first due to the operator \
reversing null position along with value comparison; if this test \
fails the sort semantics changed",
);
}
#[test]
fn order_by_alias_desc() {
let db = people_graph();
let session = db.session();
let r = session
.execute(
"MATCH (n:Person) WHERE n.age IS NOT NULL \
RETURN n.age AS a ORDER BY a DESC",
)
.unwrap();
assert_eq!(r.rows().len(), 3);
assert_eq!(int_col(&r.rows()[0], 0), Some(40));
assert_eq!(int_col(&r.rows()[1], 0), Some(30));
assert_eq!(int_col(&r.rows()[2], 0), Some(25));
}
#[test]
fn order_by_property_not_in_return_strips_extra_column() {
let db = people_graph();
let session = db.session();
let r = session
.execute(
"MATCH (n:Person) WHERE n.age IS NOT NULL \
RETURN n.name ORDER BY n.age ASC",
)
.unwrap();
assert_eq!(r.rows().len(), 3);
assert_eq!(
r.rows()[0].len(),
1,
"extra sort column should have been stripped, got {} cols",
r.rows()[0].len()
);
assert_eq!(
string_col(&r.rows()[0], 0).as_deref(),
Some("Gus"),
"age 25"
);
assert_eq!(
string_col(&r.rows()[1], 0).as_deref(),
Some("Alix"),
"age 30"
);
assert_eq!(
string_col(&r.rows()[2], 0).as_deref(),
Some("Vincent"),
"age 40"
);
}
#[test]
fn limit_zero_returns_no_rows() {
let db = people_graph();
let session = db.session();
let r = session
.execute("MATCH (n:Person) RETURN n.name LIMIT 0")
.unwrap();
assert_eq!(r.rows().len(), 0, "LIMIT 0 yields empty result");
}
#[test]
fn skip_then_limit_windows_result() {
let db = people_graph();
let session = db.session();
let r = session
.execute("MATCH (n:Person) RETURN n.name AS name ORDER BY name SKIP 1 LIMIT 2")
.unwrap();
assert_eq!(r.rows().len(), 2);
assert_eq!(string_col(&r.rows()[0], 0).as_deref(), Some("Gus"));
assert_eq!(string_col(&r.rows()[1], 0).as_deref(), Some("Jules"));
}
#[test]
fn skip_beyond_result_set_yields_empty() {
let db = people_graph();
let session = db.session();
let r = session
.execute("MATCH (n:Person) RETURN n.name SKIP 10")
.unwrap();
assert!(r.rows().is_empty());
}
#[test]
fn length_on_path_variable() {
let db = chain_graph();
let session = db.session();
let r = session
.execute(
"MATCH p = (a:Person {name:'Alix'})-[:KNOWS *3..3]->(d:Person) \
RETURN length(p) AS len",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(int_col(&r.rows()[0], 0), Some(3));
}
#[test]
fn nodes_and_edges_on_path_variable() {
let db = chain_graph();
let session = db.session();
let r = session
.execute(
"MATCH p = (a:Person {name:'Alix'})-[:KNOWS *2..2]->(c:Person) \
RETURN nodes(p) AS ns, edges(p) AS es",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
match &r.rows()[0][0] {
Value::List(ns) => assert_eq!(ns.len(), 3, "2-hop path has 3 nodes"),
other => panic!("expected list for nodes(p), got {other:?}"),
}
match &r.rows()[0][1] {
Value::List(es) => assert_eq!(es.len(), 2, "2-hop path has 2 edges"),
other => panic!("expected list for edges(p), got {other:?}"),
}
}
#[test]
fn length_on_string_falls_through_to_expression() {
let db = people_graph();
let session = db.session();
let r = session
.execute("MATCH (n:Person {name:'Vincent'}) RETURN length(n.name) AS len")
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(int_col(&r.rows()[0], 0), Some(7), "len('Vincent') == 7");
}
#[test]
fn distinct_across_multiple_columns() {
let db = people_graph();
let session = db.session();
let r = session
.execute(
"MATCH (n:Person) \
RETURN DISTINCT n.city AS city, n.age / 10 AS bucket \
ORDER BY city",
)
.unwrap();
assert_eq!(r.rows().len(), 5, "expected 5 distinct (city,bucket) pairs");
}
#[test]
fn return_type_function_with_arithmetic() {
let db = chain_graph();
let session = db.session();
let r = session
.execute(
"MATCH (a:Person {name:'Alix'})-[r:KNOWS]->(b) \
RETURN type(r) AS t, 1 + 1 AS two",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(string_col(&r.rows()[0], 0).as_deref(), Some("KNOWS"));
assert_eq!(int_col(&r.rows()[0], 1), Some(2));
}