use grafeo_common::types::Value;
use grafeo_engine::GrafeoDB;
use grafeo_engine::database::QueryResult;
fn db() -> GrafeoDB {
GrafeoDB::new_in_memory()
}
mod merge_unwind {
use super::*;
#[test]
fn unwind_merge_returns_one_row_per_input() {
let db = db();
let s = db.session();
let r = s
.execute("UNWIND [1, 1, 1] AS i MERGE (:Item {val: i}) RETURN i")
.unwrap();
assert_eq!(r.row_count(), 3, "MERGE must emit one row per UNWIND input");
}
#[test]
fn unwind_merge_creates_single_node() {
let db = db();
let s = db.session();
s.execute("UNWIND [1, 1, 1] AS i MERGE (:Item {val: i})")
.unwrap();
let r = s.execute("MATCH (n:Item) RETURN count(n) AS cnt").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(1),
"MERGE with duplicate values should create only one node"
);
}
#[test]
fn unwind_merge_distinct_values_create_multiple() {
let db = db();
let s = db.session();
s.execute("UNWIND [1, 2, 3] AS i MERGE (:Item {val: i})")
.unwrap();
let r = s.execute("MATCH (n:Item) RETURN count(n) AS cnt").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(3),
"MERGE with distinct values should create three nodes"
);
}
#[test]
fn unwind_merge_mixed_creates_and_matches() {
let db = db();
let s = db.session();
s.execute("INSERT (:Item {val: 2})").unwrap();
s.execute("UNWIND [1, 2, 3] AS i MERGE (:Item {val: i})")
.unwrap();
let r = s.execute("MATCH (n:Item) RETURN count(n) AS cnt").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(3),
"MERGE should create 2 new nodes and match 1 existing"
);
}
#[test]
fn unwind_merge_on_create_on_match_set() {
let db = db();
let s = db.session();
s.execute("INSERT (:Item {val: 1, status: 'old'})").unwrap();
s.execute(
"UNWIND [1, 2] AS i \
MERGE (n:Item {val: i}) \
ON CREATE SET n.status = 'new' \
ON MATCH SET n.status = 'updated'",
)
.unwrap();
let r = s
.execute("MATCH (n:Item) RETURN n.val, n.status ORDER BY n.val")
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(r.rows()[0][0], Value::Int64(1));
assert_eq!(r.rows()[0][1], Value::String("updated".into()));
assert_eq!(r.rows()[1][0], Value::Int64(2));
assert_eq!(r.rows()[1][1], Value::String("new".into()));
}
}
mod merge_composite_keys {
use super::*;
#[test]
fn merge_two_property_key_no_duplicate() {
let db = db();
let s = db.session();
s.execute("MERGE (:City {name: 'Amsterdam', country: 'NL'})")
.unwrap();
s.execute("MERGE (:City {name: 'Amsterdam', country: 'NL'})")
.unwrap();
let r = s.execute("MATCH (n:City) RETURN count(n) AS cnt").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(1),
"Identical composite key should not create duplicate"
);
}
#[test]
fn merge_partial_match_creates_new() {
let db = db();
let s = db.session();
s.execute("MERGE (:City {name: 'Amsterdam', country: 'NL'})")
.unwrap();
s.execute("MERGE (:City {name: 'Amsterdam', country: 'US'})")
.unwrap();
let r = s.execute("MATCH (n:City) RETURN count(n) AS cnt").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(2),
"Partial composite match should create a new node"
);
}
#[test]
fn merge_three_property_key() {
let db = db();
let s = db.session();
s.execute("MERGE (:Place {city: 'Berlin', country: 'DE', district: 'Mitte'})")
.unwrap();
s.execute("MERGE (:Place {city: 'Berlin', country: 'DE', district: 'Mitte'})")
.unwrap();
s.execute("MERGE (:Place {city: 'Berlin', country: 'DE', district: 'Kreuzberg'})")
.unwrap();
let r = s.execute("MATCH (n:Place) RETURN count(n) AS cnt").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(2),
"Three-property MERGE: identical creates 1, different district creates 2"
);
}
}
mod relationship_isomorphism {
use super::*;
#[test]
fn two_hop_pattern_no_edge_reuse() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {name: 'Alix'})-[:R]->(:N {name: 'Gus'})")
.unwrap();
s.execute(
"MATCH (a:N {name: 'Gus'}), (b:N {name: 'Alix'}) \
CREATE (a)-[:R]->(n:N {name: 'Vincent'})-[:R]->(b)",
)
.unwrap();
let r = s
.execute(
"MATCH (a:N)-[r1:R]->(b:N)-[r2:R]->(c:N) \
RETURN count(*) AS cnt",
)
.unwrap();
let cnt = &r.rows()[0][0];
assert_eq!(
*cnt,
Value::Int64(3),
"Triangle should yield exactly 3 two-hop paths"
);
}
#[test]
fn single_edge_not_matched_twice_in_pattern() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {name: 'Alix'})-[:R]->(:N {name: 'Gus'})")
.unwrap();
let r = s
.execute(
"MATCH (a:N)-[r1:R]->(b:N)-[r2:R]->(c:N) \
RETURN count(*) AS cnt",
)
.unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(0),
"Single edge cannot satisfy a two-hop pattern"
);
}
}
mod optional_match_order {
use super::*;
#[test]
fn swapped_optional_match_same_results() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})-[:KNOWS]->(:Person {name: 'Gus'})")
.unwrap();
s.execute("INSERT (:Person {name: 'Alix'})-[:WORKS_AT]->(:Company {name: 'Acme'})")
.unwrap();
let r1 = s
.execute(
"MATCH (p:Person {name: 'Alix'}) \
OPTIONAL MATCH (p)-[:KNOWS]->(friend:Person) \
OPTIONAL MATCH (p)-[:WORKS_AT]->(co:Company) \
RETURN p.name, friend.name, co.name",
)
.unwrap();
let r2 = s
.execute(
"MATCH (p:Person {name: 'Alix'}) \
OPTIONAL MATCH (p)-[:WORKS_AT]->(co:Company) \
OPTIONAL MATCH (p)-[:KNOWS]->(friend:Person) \
RETURN p.name, friend.name, co.name",
)
.unwrap();
assert_eq!(
r1.row_count(),
r2.row_count(),
"Swapping OPTIONAL MATCH order must not change row count"
);
}
}
mod aggregation_in_subquery {
use super::*;
#[test]
fn count_inside_call_subquery() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
s.execute("INSERT (:Person {name: 'Vincent'})").unwrap();
let r = s
.execute("CALL { MATCH (n:Person) RETURN count(n) AS cnt } RETURN cnt")
.unwrap();
assert_eq!(r.row_count(), 1, "Aggregation in CALL should return 1 row");
assert_eq!(r.rows()[0][0], Value::Int64(3));
}
}
mod collect_order {
use super::*;
#[test]
fn collect_returns_all_elements() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Vincent'})").unwrap();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
s.execute("INSERT (:Person {name: 'Jules'})").unwrap();
s.execute("INSERT (:Person {name: 'Mia'})").unwrap();
let r = s
.execute("MATCH (p:Person) RETURN collect(p.name) AS names")
.unwrap();
assert_eq!(r.row_count(), 1);
if let Value::List(names) = &r.rows()[0][0] {
assert_eq!(names.len(), 5, "COLLECT must gather all 5 names");
let name_strs: Vec<String> = names
.iter()
.filter_map(|v| match v {
Value::String(s) => Some(s.to_string()),
_ => None,
})
.collect();
let mut sorted = name_strs.clone();
sorted.sort();
assert_eq!(sorted, vec!["Alix", "Gus", "Jules", "Mia", "Vincent"],);
} else {
panic!("Expected List, got {:?}", r.rows()[0][0]);
}
}
}
mod group_by_expression_order {
use super::*;
fn string_column(result: &QueryResult, col: usize) -> Vec<String> {
result
.rows()
.iter()
.map(|row| match &row[col] {
Value::String(s) => s.to_string(),
other => format!("{other:?}"),
})
.collect()
}
#[test]
fn return_column_order_does_not_affect_group_by() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix', city: 'Amsterdam'})")
.unwrap();
s.execute("INSERT (:Person {name: 'Gus', city: 'Amsterdam'})")
.unwrap();
s.execute("INSERT (:Person {name: 'Vincent', city: 'Berlin'})")
.unwrap();
let r1 = s
.execute(
"MATCH (p:Person) \
RETURN p.city AS city, count(p) AS cnt \
ORDER BY city",
)
.unwrap();
let r2 = s
.execute(
"MATCH (p:Person) \
RETURN count(p) AS cnt, p.city AS city \
ORDER BY city",
)
.unwrap();
assert_eq!(r1.row_count(), r2.row_count(), "Same grouping, same rows");
let cities_1 = string_column(&r1, 0);
let cities_2 = string_column(&r2, 1);
assert_eq!(cities_1, cities_2, "City grouping should be identical");
}
}
mod where_filter_and_projection {
use super::*;
#[test]
fn where_is_not_null_preserves_return_value() {
let db = db();
let s = db.session();
s.execute("INSERT (:Sensor {name: 'Temp', reading: 42.5})")
.unwrap();
s.execute("INSERT (:Sensor {name: 'Humidity'})").unwrap();
let r = s
.execute(
"MATCH (s:Sensor) \
WHERE s.reading IS NOT NULL \
RETURN s.name, s.reading",
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Temp".into()));
assert_eq!(
r.rows()[0][1],
Value::Float64(42.5),
"WHERE IS NOT NULL must not replace the returned value with boolean"
);
}
}
mod sum_overflow {
use super::*;
#[test]
fn sum_large_floats_returns_infinity() {
let db = db();
let s = db.session();
let r = s
.execute(
"UNWIND [1.7976931348623157e308, 1.7976931348623157e308] AS val \
RETURN SUM(val) AS total",
)
.unwrap();
assert_eq!(r.row_count(), 1);
match &r.rows()[0][0] {
Value::Float64(f) => {
assert!(
f.is_infinite() && f.is_sign_positive(),
"SUM of two f64::MAX should be +Infinity, got {f}"
);
}
other => panic!("Expected Float64, got {:?}", other),
}
}
}
mod unicode_emoji {
use super::*;
#[test]
fn emoji_property_value_roundtrip() {
let db = db();
let s = db.session();
s.execute("INSERT (:Tag {symbol: '\u{1F389}', name: 'party'})") .unwrap();
let r = s.execute("MATCH (t:Tag) RETURN t.symbol, t.name").unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("\u{1F389}".into()));
assert_eq!(r.rows()[0][1], Value::String("party".into()));
}
#[test]
fn cjk_property_value_roundtrip() {
let db = db();
let s = db.session();
s.execute("INSERT (:City {name: '\u{6771}\u{4EAC}'})") .unwrap();
let r = s.execute("MATCH (c:City) RETURN c.name").unwrap();
assert_eq!(r.rows()[0][0], Value::String("\u{6771}\u{4EAC}".into()));
}
#[test]
fn combining_diacritics_roundtrip() {
let db = db();
let s = db.session();
s.execute("INSERT (:Word {text: 'calf\u{0065}\u{0301}'})")
.unwrap();
let r = s.execute("MATCH (w:Word) RETURN w.text").unwrap();
assert_eq!(r.rows()[0][0], Value::String("calf\u{0065}\u{0301}".into()));
}
}
mod self_loop {
use super::*;
#[test]
fn create_and_match_self_loop() {
let db = db();
let s = db.session();
s.execute("INSERT (a:Node {name: 'Alix'})-[:SELF]->(a)")
.unwrap();
let r = s
.execute("MATCH (a:Node)-[r:SELF]->(a) RETURN a.name")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn self_loop_not_counted_as_two_edges() {
let db = db();
let s = db.session();
s.execute("INSERT (a:Node {name: 'Alix'})-[:SELF]->(a)")
.unwrap();
let r = s
.execute("MATCH (:Node)-[r:SELF]->() RETURN count(r) AS cnt")
.unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(1),
"Self-loop should be counted exactly once"
);
}
}
mod deleted_node_access {
use super::*;
#[test]
fn delete_then_match_in_same_session() {
let db = db();
let s = db.session();
s.execute("INSERT (:Temp {name: 'ephemeral'})").unwrap();
s.execute("MATCH (n:Temp) DELETE n").unwrap();
let r = s.execute("MATCH (n:Temp) RETURN n.name").unwrap();
assert_eq!(
r.row_count(),
0,
"Deleted node should not be visible in subsequent MATCH"
);
}
#[test]
fn detach_delete_clears_edges() {
let db = db();
let s = db.session();
s.execute("INSERT (:A {name: 'a'})-[:R]->(:B {name: 'b'})")
.unwrap();
s.execute("MATCH (a:A) DETACH DELETE a").unwrap();
let r = s
.execute("MATCH ()-[r:R]->() RETURN count(r) AS cnt")
.unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(0),
"DETACH DELETE should remove all connected edges"
);
}
}
mod nested_properties {
use super::*;
use grafeo_common::types::PropertyKey;
use std::collections::{BTreeMap, HashMap};
#[test]
fn nested_map_roundtrip() {
let db = db();
let s = db.session();
let mut params = HashMap::new();
let mut inner_map = BTreeMap::new();
inner_map.insert(PropertyKey::new("c"), Value::Int64(42));
let inner = Value::Map(inner_map.into());
let mut mid_map = BTreeMap::new();
mid_map.insert(PropertyKey::new("b"), inner);
let mid = Value::Map(mid_map.into());
params.insert("meta".to_string(), mid);
s.execute_with_params("INSERT (:Data {meta: $meta})", params)
.unwrap();
let r = s.execute("MATCH (d:Data) RETURN d.meta").unwrap();
assert_eq!(r.row_count(), 1);
if let Value::Map(outer) = &r.rows()[0][0] {
let b = outer.get(&PropertyKey::new("b")).expect("Missing key 'b'");
if let Value::Map(inner_result) = b {
assert_eq!(
inner_result.get(&PropertyKey::new("c")),
Some(&Value::Int64(42))
);
} else {
panic!("Expected inner Map, got {:?}", b);
}
} else {
panic!("Expected Map, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn heterogeneous_list_roundtrip() {
let db = db();
let s = db.session();
let mut params = HashMap::new();
params.insert(
"items".to_string(),
Value::List(
vec![
Value::Int64(1),
Value::String("two".into()),
Value::Bool(true),
Value::Null,
]
.into(),
),
);
s.execute_with_params("INSERT (:Data {items: $items})", params)
.unwrap();
let r = s.execute("MATCH (d:Data) RETURN d.items").unwrap();
if let Value::List(items) = &r.rows()[0][0] {
assert_eq!(items.len(), 4);
assert_eq!(items[0], Value::Int64(1));
assert_eq!(items[1], Value::String("two".into()));
assert_eq!(items[2], Value::Bool(true));
assert_eq!(items[3], Value::Null);
} else {
panic!("Expected List, got {:?}", r.rows()[0][0]);
}
}
}
mod constant_folding {
use super::*;
#[test]
fn where_constant_false_returns_empty() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
let r = s.execute("MATCH (n:N) WHERE 1 = 2 RETURN n").unwrap();
assert_eq!(
r.row_count(),
0,
"WHERE 1 = 2 should short-circuit to empty"
);
}
#[test]
fn where_constant_true_returns_all() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
s.execute("INSERT (:N {val: 2})").unwrap();
let r = s.execute("MATCH (n:N) WHERE 1 = 1 RETURN n.val").unwrap();
assert_eq!(r.row_count(), 2, "WHERE 1 = 1 should return all rows");
}
}
mod cyclic_traversal {
use super::*;
#[test]
fn variable_length_path_in_triangle_terminates() {
let db = db();
let s = db.session();
s.execute(
"INSERT (a:N {name: 'Alix'})-[:R]->(b:N {name: 'Gus'})-[:R]->(c:N {name: 'Vincent'})-[:R]->(a)",
)
.unwrap();
let r = s
.execute(
"MATCH (a:N {name: 'Alix'})-[:R*1..5]->(b:N) \
RETURN b.name",
)
.unwrap();
assert_eq!(
r.row_count(),
5,
"Expected exactly 5 paths (one per hop 1..=5), got {}",
r.row_count()
);
}
}
mod concurrent_merge {
use super::*;
#[test]
#[ignore = "stress test: run locally before releases"]
fn concurrent_merge_no_duplicates() {
let db = std::sync::Arc::new(db());
let handles: Vec<_> = (0..10)
.map(|_| {
let db = std::sync::Arc::clone(&db);
std::thread::spawn(move || {
let s = db.session();
s.execute("MERGE (:Singleton {key: 'only_one'})").unwrap();
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let s = db.session();
let r = s
.execute("MATCH (n:Singleton) RETURN count(n) AS cnt")
.unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(1),
"Concurrent MERGE should produce exactly 1 node"
);
}
}
mod merge_null_node_reference {
use super::*;
#[test]
fn merge_relationship_with_null_source_errors() {
let db = db();
let s = db.session();
let result = s.execute(
"OPTIONAL MATCH (n:NonExistent) \
MERGE (n)-[:R]->(m:Target {name: 'Alix'})",
);
assert!(
result.is_err(),
"MERGE with NULL node reference should error, got: {result:?}"
);
}
#[test]
fn merge_relationship_with_null_target_errors() {
let db = db();
let s = db.session();
s.execute("INSERT (:Source {name: 'Gus'})").unwrap();
let result = s.execute(
"MATCH (a:Source {name: 'Gus'}) \
OPTIONAL MATCH (b:NonExistent) \
MERGE (a)-[:R]->(b)",
);
assert!(
result.is_err(),
"MERGE with NULL target node reference should error"
);
}
#[test]
fn merge_with_valid_optional_match_succeeds() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Vincent'})").unwrap();
let result = s.execute(
"MATCH (n:Person {name: 'Vincent'}) \
MERGE (n)-[:KNOWS]->(m:Person {name: 'Jules'})",
);
assert!(
result.is_ok(),
"MERGE with non-null node should succeed: {result:?}"
);
let check = s
.execute(
"MATCH (v:Person {name: 'Vincent'})-[:KNOWS]->(j:Person {name: 'Jules'}) RETURN j.name",
)
.unwrap();
assert_eq!(check.row_count(), 1);
assert_eq!(check.rows()[0][0], Value::String("Jules".into()));
}
#[test]
fn standalone_merge_unaffected() {
let db = db();
let s = db.session();
let result = s
.execute("MERGE (:Person {name: 'Mia'}) RETURN 1 AS ok")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(1));
}
}
mod distinct_edges {
use super::*;
#[test]
fn distinct_on_edges_deduplicates_correctly() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
s.execute(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
INSERT (a)-[:KNOWS]->(b)",
)
.unwrap();
let result = s
.execute(
"MATCH (a:Person), (b:Person) \
MATCH (a)-[e:KNOWS]->(b) \
RETURN DISTINCT e",
)
.unwrap();
assert_eq!(
result.rows().len(),
1,
"DISTINCT e should return exactly one row, got: {}",
result.rows().len()
);
}
#[test]
fn distinct_on_edges_multiple_edge_types() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Vincent'})").unwrap();
s.execute("INSERT (:Person {name: 'Jules'})").unwrap();
s.execute("INSERT (:Person {name: 'Mia'})").unwrap();
s.execute(
"MATCH (a:Person {name: 'Vincent'}), (b:Person {name: 'Jules'}) \
INSERT (a)-[:KNOWS]->(b)",
)
.unwrap();
s.execute(
"MATCH (a:Person {name: 'Jules'}), (b:Person {name: 'Mia'}) \
INSERT (a)-[:KNOWS]->(b)",
)
.unwrap();
let result = s
.execute("MATCH ()-[e:KNOWS]->() RETURN DISTINCT e")
.unwrap();
assert_eq!(
result.rows().len(),
2,
"DISTINCT should return both edges, got: {}",
result.rows().len()
);
}
}
mod call_block_scope {
use super::*;
#[test]
fn sibling_call_block_outputs_are_in_scope_for_return() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
s.execute("INSERT (:Person {name: 'Gus', age: 25})")
.unwrap();
let result = s.execute(
"CALL { MATCH (a:Person {name: 'Alix'}) RETURN a.age AS age_a } \
CALL { MATCH (b:Person {name: 'Gus'}) RETURN b.age AS age_b } \
RETURN age_a, age_b",
);
let result = result.expect("sibling CALL outputs should be accessible in outer RETURN");
assert_eq!(result.rows()[0][0], Value::Int64(30)); assert_eq!(result.rows()[0][1], Value::Int64(25)); }
#[test]
fn internal_variable_from_first_call_is_not_visible_in_second_call() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
let result = s.execute(
"CALL { MATCH (a:Person) RETURN a.name AS name_a } \
CALL { MATCH (b:Person) WHERE b.name = a.name RETURN b } \
RETURN name_a",
);
assert!(
result.is_err(),
"second CALL referencing internal var of first CALL should fail, got: {result:?}"
);
}
#[test]
fn same_variable_name_in_sibling_calls_is_independent() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Company {name: 'TechCorp'})").unwrap();
let result = s
.execute(
"CALL { MATCH (n:Person) RETURN n.name AS person_name } \
CALL { MATCH (n:Company) RETURN n.name AS company_name } \
RETURN person_name, company_name",
)
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[0][1], Value::String("TechCorp".into()));
}
#[test]
fn sum_inside_call_subquery_returns_single_row() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
s.execute("INSERT (:Person {name: 'Gus', age: 25})")
.unwrap();
s.execute("INSERT (:Person {name: 'Vincent', age: 40})")
.unwrap();
let result = s
.execute("CALL { MATCH (n:Person) RETURN sum(n.age) AS total } RETURN total")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(95));
}
}
#[cfg(feature = "cypher")]
mod unwind_merge_set {
use super::*;
#[test]
fn unwind_merge_set_property_from_map() {
let db = db();
let s = db.session();
s.execute_cypher(
"UNWIND [{qn: 'test://foo', name: 'Foo'}, {qn: 'test://bar', name: 'Bar'}] AS item \
MERGE (x:Test {qn: item.qn}) \
SET x.name = item.name",
)
.unwrap();
let result = s
.execute("MATCH (n:Test) RETURN n.qn AS qn, n.name AS name ORDER BY qn")
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][0], Value::String("test://bar".into()));
assert_eq!(
result.rows()[0][1],
Value::String("Bar".into()),
"SET x.name = item.name should resolve item.name from UNWIND map (#172)"
);
assert_eq!(result.rows()[1][0], Value::String("test://foo".into()));
assert_eq!(result.rows()[1][1], Value::String("Foo".into()));
}
#[test]
fn unwind_merge_set_map_merge() {
let db = db();
let s = db.session();
s.execute_cypher(
"UNWIND [{qn: 'test://baz', name: 'Baz', kind: 'module'}] AS item \
MERGE (x:Test {qn: item.qn}) \
SET x += item",
)
.unwrap();
let result = s
.execute("MATCH (n:Test {qn: 'test://baz'}) RETURN n.name AS name, n.kind AS kind")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(
result.rows()[0][0],
Value::String("Baz".into()),
"SET x += item should merge all map properties (#172)"
);
assert_eq!(result.rows()[0][1], Value::String("module".into()));
}
}
#[cfg(feature = "cypher")]
mod issue_187_labels_type_aggregation {
use super::*;
#[test]
fn labels_with_count() {
let db = db();
let s = db.session();
s.execute_cypher(
"CREATE (:Class {name: 'A'}), (:Class {name: 'B'}), (:Method {name: 'C'})",
)
.unwrap();
let result = s
.execute_cypher(
"MATCH (n) RETURN labels(n)[0] AS label, count(n) AS cnt ORDER BY label",
)
.unwrap();
assert_eq!(
result.row_count(),
2,
"Should have two groups: Class and Method"
);
assert_eq!(result.rows()[0][0], Value::String("Class".into()));
assert_eq!(result.rows()[0][1], Value::Int64(2));
assert_eq!(result.rows()[1][0], Value::String("Method".into()));
assert_eq!(result.rows()[1][1], Value::Int64(1));
}
#[test]
fn type_with_count() {
let db = db();
let s = db.session();
s.execute_cypher(
"CREATE (a:A), (b:B), (c:C), (a)-[:CALLS]->(b), (a)-[:CALLS]->(c), (b)-[:IMPORTS]->(c)",
)
.unwrap();
let result = s
.execute_cypher(
"MATCH ()-[r]->() RETURN type(r) AS edge_type, count(r) AS cnt ORDER BY edge_type",
)
.unwrap();
assert_eq!(
result.row_count(),
2,
"Should have two groups: CALLS and IMPORTS"
);
assert_eq!(result.rows()[0][0], Value::String("CALLS".into()));
assert_eq!(result.rows()[0][1], Value::Int64(2));
assert_eq!(result.rows()[1][0], Value::String("IMPORTS".into()));
assert_eq!(result.rows()[1][1], Value::Int64(1));
}
#[test]
fn labels_without_index_access() {
let db = db();
let s = db.session();
s.execute_cypher("CREATE (:Foo), (:Bar)").unwrap();
let result = s
.execute_cypher("MATCH (n) RETURN labels(n) AS lbls, count(n) AS cnt ORDER BY lbls")
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][1], Value::Int64(1));
assert_eq!(result.rows()[1][1], Value::Int64(1));
}
#[test]
fn order_by_labels() {
let db = db();
let s = db.session();
s.execute_cypher("CREATE (:Zebra {name: 'Z'}), (:Apple {name: 'A'})")
.unwrap();
let result = s
.execute_cypher("MATCH (n) RETURN n.name ORDER BY labels(n)[0]")
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][0], Value::String("A".into()));
assert_eq!(result.rows()[1][0], Value::String("Z".into()));
}
#[test]
fn order_by_type() {
let db = db();
let s = db.session();
s.execute_cypher("CREATE (a:X), (b:Y), (c:Z), (a)-[:BETA]->(b), (a)-[:ALPHA]->(c)")
.unwrap();
let result = s
.execute_cypher("MATCH ()-[r]->() RETURN type(r) AS t ORDER BY t")
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][0], Value::String("ALPHA".into()));
assert_eq!(result.rows()[1][0], Value::String("BETA".into()));
}
#[test]
fn gql_labels_with_count() {
let db = db();
let s = db.session();
s.execute(
"INSERT (:Engineer {name: 'Vincent'}), (:Engineer {name: 'Jules'}), (:Designer {name: 'Mia'})",
)
.unwrap();
let result = s
.execute("MATCH (n) RETURN labels(n)[0] AS label, count(n) AS cnt ORDER BY label")
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][0], Value::String("Designer".into()));
assert_eq!(result.rows()[0][1], Value::Int64(1));
assert_eq!(result.rows()[1][0], Value::String("Engineer".into()));
assert_eq!(result.rows()[1][1], Value::Int64(2));
}
#[test]
fn gql_order_by_labels() {
let db = db();
let s = db.session();
s.execute("INSERT (:Zebra {name: 'Z'}), (:Apple {name: 'A'})")
.unwrap();
let result = s
.execute("MATCH (n) RETURN n.name ORDER BY labels(n)[0]")
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][0], Value::String("A".into()));
assert_eq!(result.rows()[1][0], Value::String("Z".into()));
}
}
#[cfg(feature = "cypher")]
mod issue_187_extended {
use super::*;
#[test]
fn labels_with_sum() {
let db = db();
let s = db.session();
s.execute_cypher("CREATE (:Alpha {val: 10}), (:Alpha {val: 20}), (:Beta {val: 5})")
.unwrap();
let result = s
.execute_cypher(
"MATCH (n) RETURN labels(n)[0] AS label, sum(n.val) AS total ORDER BY label",
)
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][0], Value::String("Alpha".into()));
assert_eq!(result.rows()[0][1], Value::Int64(30));
assert_eq!(result.rows()[1][0], Value::String("Beta".into()));
assert_eq!(result.rows()[1][1], Value::Int64(5));
}
#[test]
fn type_with_collect() {
let db = db();
let s = db.session();
s.execute_cypher(
"CREATE (a:X), (b:Y), (c:Z), (a)-[:LIKES]->(b), (a)-[:LIKES]->(c), (b)-[:KNOWS]->(c)",
)
.unwrap();
let result = s
.execute_cypher("MATCH ()-[r]->() RETURN type(r) AS t, collect(r) AS items ORDER BY t")
.unwrap();
assert_eq!(result.row_count(), 2, "Two edge types: KNOWS and LIKES");
}
#[test]
fn multi_label_node_group_by() {
let db = db();
let s = db.session();
s.execute_cypher(
"CREATE (:A:B {name: 'one'}), (:A:B {name: 'two'}), (:C:D {name: 'three'})",
)
.unwrap();
let result = s
.execute_cypher("MATCH (n) RETURN labels(n) AS lbls, count(n) AS cnt")
.unwrap();
assert_eq!(
result.row_count(),
2,
"Expected exactly 2 rows (one per distinct label-set), got {}: {:?}",
result.row_count(),
result.rows()
);
}
#[test]
fn order_by_type_descending() {
let db = db();
let s = db.session();
s.execute_cypher(
"CREATE (a:X), (b:Y), (c:Z), (a)-[:ALPHA]->(b), (a)-[:GAMMA]->(c), (b)-[:BETA]->(c)",
)
.unwrap();
let result = s
.execute_cypher("MATCH ()-[r]->() RETURN type(r) AS t ORDER BY t DESC")
.unwrap();
assert_eq!(result.row_count(), 3);
assert_eq!(result.rows()[0][0], Value::String("GAMMA".into()));
assert_eq!(result.rows()[1][0], Value::String("BETA".into()));
assert_eq!(result.rows()[2][0], Value::String("ALPHA".into()));
}
#[test]
fn order_by_labels_with_limit() {
let db = db();
let s = db.session();
s.execute_cypher("CREATE (:Zebra {name: 'Z'}), (:Apple {name: 'A'}), (:Mango {name: 'M'})")
.unwrap();
let result = s
.execute_cypher("MATCH (n) RETURN n.name ORDER BY labels(n)[0] LIMIT 1")
.unwrap();
assert_eq!(
result.row_count(),
1,
"LIMIT 1 should return exactly one row"
);
}
#[test]
fn group_by_and_order_by_both_complex() {
let db = db();
let s = db.session();
s.execute_cypher(
"CREATE (:Engineer {name: 'Vincent'}), (:Engineer {name: 'Jules'}), (:Designer {name: 'Mia'})",
)
.unwrap();
let result = s
.execute_cypher(
"MATCH (n) RETURN labels(n)[0] AS lbl, count(n) AS cnt ORDER BY labels(n)[0]",
)
.unwrap();
assert_eq!(
result.row_count(),
2,
"Should produce exactly 2 rows (Designer, Engineer), got {}",
result.row_count()
);
}
#[test]
fn empty_result_group_by_labels() {
let db = db();
let s = db.session();
let result = s
.execute_cypher("MATCH (n:NonExistent) RETURN labels(n)[0] AS lbl, count(n) AS cnt")
.unwrap();
assert_eq!(
result.row_count(),
0,
"No matching nodes should yield zero rows"
);
}
}
mod issue_187_gql_type_with_count {
use super::*;
#[test]
fn gql_type_with_count() {
let db = db();
let s = db.session();
s.execute("INSERT (:A)-[:FOLLOWS]->(:B)").unwrap();
s.execute("INSERT (:C)-[:FOLLOWS]->(:D)").unwrap();
s.execute("INSERT (:E)-[:BLOCKS]->(:F)").unwrap();
let result = s
.execute("MATCH ()-[r]->() RETURN type(r) AS t, count(r) AS cnt ORDER BY t")
.unwrap();
assert_eq!(result.row_count(), 2, "Two edge types: BLOCKS and FOLLOWS");
assert_eq!(result.rows()[0][0], Value::String("BLOCKS".into()));
assert_eq!(result.rows()[0][1], Value::Int64(1));
assert_eq!(result.rows()[1][0], Value::String("FOLLOWS".into()));
assert_eq!(result.rows()[1][1], Value::Int64(2));
}
}
mod integer_overflow {
use super::*;
#[test]
fn i64_max_plus_one_overflows() {
let db = db();
let s = db.session();
let r = s.execute("RETURN 9223372036854775807 + 1 AS r").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Null,
"i64::MAX + 1 should return NULL (overflow)"
);
}
#[test]
fn i64_min_minus_one_overflows() {
let db = db();
let s = db.session();
let result = s.execute("RETURN -9223372036854775808 - 1 AS r");
match result {
Err(_) => {} Ok(r) => {
assert_ne!(
r.rows()[0][0],
Value::Int64(i64::MAX),
"Must not silently wrap i64::MIN - 1 to i64::MAX"
);
}
}
}
#[test]
fn multiplication_intermediate_overflow() {
let db = db();
let s = db.session();
let r = s
.execute("RETURN 100 * 1000000000 * 100000000 AS r")
.unwrap();
assert_eq!(
r.rows()[0][0],
Value::Null,
"Intermediate multiplication overflow should return NULL"
);
}
}
mod variable_length_path_enumeration {
use super::*;
#[test]
fn diamond_two_hop_enumerates_all_paths() {
let db = db();
let s = db.session();
s.execute(
"INSERT (a:N {name: 'Alix'})-[:R]->(b1:N {name: 'Gus'}), \
(a)-[:R]->(b2:N {name: 'Vincent'}), \
(b1)-[:R]->(c:N {name: 'Jules'}), \
(b2)-[:R]->(c)",
)
.unwrap();
let r = s
.execute(
"MATCH (a:N {name: 'Alix'})-[:R*2]->(c:N {name: 'Jules'}) \
RETURN c.name",
)
.unwrap();
assert_eq!(
r.row_count(),
2,
"Diamond graph must yield 2 distinct 2-hop paths, got {}",
r.row_count()
);
}
#[test]
fn single_hop_variable_length_matches_direct_edges() {
let db = db();
let s = db.session();
s.execute(
"INSERT (a:N {name: 'Alix'})-[:R]->(b:N {name: 'Gus'})-[:R]->(c:N {name: 'Vincent'})",
)
.unwrap();
let r = s
.execute("MATCH (a:N {name: 'Alix'})-[:R*1..1]->(b) RETURN b.name")
.unwrap();
assert_eq!(
r.row_count(),
1,
"1..1 hop should match exactly 1 direct neighbor"
);
assert_eq!(r.rows()[0][0], Value::String("Gus".into()));
}
#[test]
fn variable_length_path_respects_max_hops() {
let db = db();
let s = db.session();
s.execute(
"INSERT (:N {name: 'a'})-[:R]->(:N {name: 'b'})-[:R]->(:N {name: 'c'})-[:R]->(:N {name: 'd'})-[:R]->(:N {name: 'e'})",
)
.unwrap();
let r = s
.execute("MATCH (a:N {name: 'a'})-[:R*1..2]->(b) RETURN b.name ORDER BY b.name")
.unwrap();
assert_eq!(
r.row_count(),
2,
"1..2 hops from 'a' should reach 'b' and 'c'"
);
}
}
mod exists_subquery {
use super::*;
#[test]
fn exists_returns_false_for_missing_pattern() {
let db = db();
let s = db.session();
s.execute("INSERT (:User {name: 'Alix'})").unwrap();
let r = s
.execute(
"MATCH (u:User) \
RETURN EXISTS { MATCH (u)<-[:AUTH]-(:Identity) } AS has_id",
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::Bool(false),
"EXISTS must return false when no matching pattern exists"
);
}
#[test]
fn exists_returns_true_for_present_pattern() {
let db = db();
let s = db.session();
s.execute("INSERT (:User {name: 'Alix'})<-[:AUTH]-(:Identity {provider: 'github'})")
.unwrap();
let r = s
.execute(
"MATCH (u:User) \
RETURN EXISTS { MATCH (u)<-[:AUTH]-(:Identity) } AS has_id",
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::Bool(true),
"EXISTS must return true when matching pattern exists"
);
}
#[test]
fn exists_with_label_filter_no_false_positive() {
let db = db();
let s = db.session();
s.execute("INSERT (:User {name: 'Gus'})<-[:FOLLOWS]-(:Bot {name: 'bot1'})")
.unwrap();
let r = s
.execute(
"MATCH (u:User) \
RETURN EXISTS { MATCH (u)<-[:AUTH]-(:Identity) } AS has_id",
)
.unwrap();
assert_eq!(r.rows()[0][0], Value::Bool(false));
}
}
mod unwind_null {
use super::*;
#[test]
fn unwind_null_produces_zero_rows() {
let db = db();
let s = db.session();
let r = s.execute("UNWIND NULL AS x RETURN x").unwrap();
assert_eq!(r.row_count(), 0, "UNWIND NULL must produce zero rows");
}
#[test]
fn unwind_empty_list_produces_zero_rows() {
let db = db();
let s = db.session();
let r = s.execute("UNWIND [] AS x RETURN x").unwrap();
assert_eq!(
r.row_count(),
0,
"UNWIND of empty list must produce zero rows"
);
}
#[test]
fn unwind_null_does_not_execute_writes() {
let db = db();
let s = db.session();
let _ = s.execute("UNWIND NULL AS x INSERT (:Ghost {val: x})");
let r = s.execute("MATCH (n:Ghost) RETURN count(n) AS cnt").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(0),
"No nodes should be created when UNWIND produces zero rows"
);
}
}
mod null_predicate_semantics {
use super::*;
#[test]
fn where_equality_with_null_returns_empty() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
let r = s.execute("MATCH (n:N) WHERE 1 = NULL RETURN n").unwrap();
assert_eq!(
r.row_count(),
0,
"WHERE 1 = NULL is UNKNOWN, should filter all rows"
);
}
#[test]
fn where_in_empty_list_returns_empty() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
let r = s.execute("MATCH (n:N) WHERE 1 IN [] RETURN n").unwrap();
assert_eq!(
r.row_count(),
0,
"WHERE 1 IN [] is false, should filter all rows"
);
}
#[test]
fn null_not_equal_to_null() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
let r = s.execute("MATCH (n:N) WHERE NULL = NULL RETURN n").unwrap();
assert_eq!(
r.row_count(),
0,
"NULL = NULL is UNKNOWN (not TRUE), should filter all rows"
);
}
#[test]
fn null_is_null_returns_true() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
let r = s
.execute("MATCH (n:N) WHERE NULL IS NULL RETURN n.val")
.unwrap();
assert_eq!(
r.row_count(),
1,
"NULL IS NULL is TRUE, should pass all rows"
);
}
}
mod predicate_rewriting {
use super::*;
#[test]
fn not_is_null_on_comparison_result() {
let db = db();
let s = db.session();
s.execute("INSERT (:X {k1: 100})-[:R]->(:Y {k2: 34})")
.unwrap();
let r = s
.execute(
"MATCH (x:X)-[:R]->(y:Y) \
WHERE NOT ((x.k1 = y.k2) IS NULL) \
RETURN x.k1, y.k2",
)
.unwrap();
assert_eq!(
r.row_count(),
1,
"NOT((100 = 34) IS NULL) should be TRUE, row must pass filter"
);
}
#[test]
fn not_is_null_with_null_property() {
let db = db();
let s = db.session();
s.execute("INSERT (:X {k1: 100})-[:R]->(:Y {name: 'test'})")
.unwrap();
let r = s
.execute(
"MATCH (x:X)-[:R]->(y:Y) \
WHERE NOT ((x.k1 = y.k2) IS NULL) \
RETURN x.k1",
)
.unwrap();
assert_eq!(
r.row_count(),
0,
"NOT((100 = NULL) IS NULL) should be FALSE, row must be filtered"
);
}
}
mod double_delete {
use super::*;
#[test]
fn delete_same_node_twice_no_phantom() {
let db = db();
let s = db.session();
s.execute("INSERT (:Temp {name: 'Alix'})").unwrap();
s.execute("MATCH (n:Temp) DELETE n").unwrap();
let r2 = s.execute("MATCH (n:Temp) DELETE n");
if r2.is_err() {
}
let check = s.execute("MATCH (n) RETURN count(n) AS cnt").unwrap();
assert_eq!(
check.rows()[0][0],
Value::Int64(0),
"No phantom nodes should remain after double delete"
);
}
#[test]
fn detach_delete_then_verify_edges_clean() {
let db = db();
let s = db.session();
s.execute("INSERT (:A {name: 'a'})-[:R]->(:B {name: 'b'})-[:S]->(:C {name: 'c'})")
.unwrap();
s.execute("MATCH (n:A) DETACH DELETE n").unwrap();
s.execute("MATCH (n:B) DETACH DELETE n").unwrap();
s.execute("MATCH (n:C) DETACH DELETE n").unwrap();
let nodes = s.execute("MATCH (n) RETURN count(n) AS cnt").unwrap();
let edges = s
.execute("MATCH ()-[r]->() RETURN count(r) AS cnt")
.unwrap();
assert_eq!(
nodes.rows()[0][0],
Value::Int64(0),
"All nodes should be gone"
);
assert_eq!(
edges.rows()[0][0],
Value::Int64(0),
"All edges should be gone"
);
}
}
mod optional_match_cartesian {
use super::*;
#[test]
fn optional_match_rebinds_with_cartesian() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {name: 'Alix'})").unwrap();
let r = s.execute(
"MATCH (n:N), (n1:N) \
OPTIONAL MATCH (n)-[:R]->(n1) \
RETURN n.name, n1.name",
);
assert!(
r.is_ok(),
"OPTIONAL MATCH with Cartesian should not crash: {r:?}"
);
}
#[test]
fn optional_match_no_match_produces_nulls() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let r = s
.execute(
"MATCH (p:Person {name: 'Alix'}) \
OPTIONAL MATCH (p)-[:WORKS_AT]->(c:Company) \
RETURN p.name, c.name",
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
assert_eq!(
r.rows()[0][1],
Value::Null,
"Unmatched OPTIONAL MATCH variable should be NULL"
);
}
}
mod where_filter_on_traversal {
use super::*;
#[test]
fn filter_on_destination_property() {
let db = db();
let s = db.session();
s.execute(
"INSERT (:Person {name: 'Alix'})-[:LIVES_IN]->(:City {name: 'Amsterdam', pop: 900000})",
)
.unwrap();
s.execute(
"INSERT (:Person {name: 'Gus'})-[:LIVES_IN]->(:City {name: 'Berlin', pop: 3700000})",
)
.unwrap();
let r = s
.execute(
"MATCH (p:Person)-[:LIVES_IN]->(c:City) \
WHERE c.pop > 1000000 \
RETURN p.name, c.name",
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Gus".into()));
assert_eq!(r.rows()[0][1], Value::String("Berlin".into()));
}
#[test]
fn filter_on_edge_property() {
let db = db();
let s = db.session();
s.execute(
"INSERT (:Person {name: 'Alix'})-[:KNOWS {since: 2020}]->(:Person {name: 'Gus'})",
)
.unwrap();
s.execute(
"INSERT (:Person {name: 'Vincent'})-[:KNOWS {since: 2024}]->(:Person {name: 'Jules'})",
)
.unwrap();
let r = s
.execute(
"MATCH (a:Person)-[k:KNOWS]->(b:Person) \
WHERE k.since >= 2023 \
RETURN a.name, b.name",
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Vincent".into()));
assert_eq!(r.rows()[0][1], Value::String("Jules".into()));
}
#[test]
fn filter_on_both_endpoints() {
let db = db();
let s = db.session();
s.execute(
"INSERT (:Person {name: 'Alix', age: 30})-[:KNOWS]->(:Person {name: 'Gus', age: 25})",
)
.unwrap();
s.execute("INSERT (:Person {name: 'Vincent', age: 40})-[:KNOWS]->(:Person {name: 'Jules', age: 35})")
.unwrap();
let r = s
.execute(
"MATCH (a:Person)-[:KNOWS]->(b:Person) \
WHERE a.age > 35 AND b.age > 30 \
RETURN a.name, b.name",
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Vincent".into()));
assert_eq!(r.rows()[0][1], Value::String("Jules".into()));
}
}
mod empty_aggregation {
use super::*;
#[test]
fn count_on_empty_returns_zero() {
let db = db();
let s = db.session();
let r = s
.execute("MATCH (n:NonExistent) RETURN count(n) AS cnt")
.unwrap();
assert_eq!(
r.row_count(),
1,
"Aggregation over empty set should return 1 row"
);
assert_eq!(r.rows()[0][0], Value::Int64(0));
}
#[test]
fn sum_on_empty_returns_null_or_zero() {
let db = db();
let s = db.session();
let r = s
.execute("MATCH (n:NonExistent) RETURN sum(n.val) AS total")
.unwrap();
assert_eq!(r.row_count(), 1);
assert!(
r.rows()[0][0] == Value::Null || r.rows()[0][0] == Value::Int64(0),
"SUM over empty set should be NULL or 0, got {:?}",
r.rows()[0][0]
);
}
#[test]
fn avg_on_empty_returns_null() {
let db = db();
let s = db.session();
let r = s
.execute("MATCH (n:NonExistent) RETURN avg(n.val) AS average")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::Null,
"AVG over empty set should return NULL"
);
}
#[test]
fn min_max_on_empty_returns_null() {
let db = db();
let s = db.session();
let r = s
.execute("MATCH (n:NonExistent) RETURN min(n.val) AS lo, max(n.val) AS hi")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::Null,
"MIN over empty set should be NULL"
);
assert_eq!(
r.rows()[0][1],
Value::Null,
"MAX over empty set should be NULL"
);
}
}
mod relationship_direction {
use super::*;
#[test]
fn backward_arrow_matches_correct_direction() {
let db = db();
let s = db.session();
s.execute("INSERT (:A {name: 'Alix'})-[:FOLLOWS]->(:B {name: 'Gus'})")
.unwrap();
let fwd = s
.execute("MATCH (a:A)-[:FOLLOWS]->(b:B) RETURN a.name, b.name")
.unwrap();
assert_eq!(fwd.row_count(), 1);
let bwd = s
.execute("MATCH (b:B)<-[:FOLLOWS]-(a:A) RETURN a.name, b.name")
.unwrap();
assert_eq!(
bwd.row_count(),
1,
"Backward arrow should match the same edge"
);
let wrong = s
.execute("MATCH (a:A)<-[:FOLLOWS]-(b:B) RETURN a.name")
.unwrap();
assert_eq!(
wrong.row_count(),
0,
"Reversed direction should not match A<-B when edge is A->B"
);
}
#[test]
fn undirected_match_finds_both_directions() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {name: 'Alix'})-[:KNOWS]->(:N {name: 'Gus'})")
.unwrap();
let r = s
.execute("MATCH (a:N)-[:KNOWS]-(b:N) RETURN a.name, b.name ORDER BY a.name")
.unwrap();
assert_eq!(
r.row_count(),
2,
"Undirected pattern should match edge in both directions"
);
}
}
mod merge_edge_patterns {
use super::*;
#[test]
fn merge_relationship_creates_when_missing() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
s.execute(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
MERGE (a)-[:KNOWS]->(b)",
)
.unwrap();
let r = s
.execute("MATCH (:Person)-[k:KNOWS]->(:Person) RETURN count(k) AS cnt")
.unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(1));
}
#[test]
fn merge_relationship_does_not_duplicate() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})-[:KNOWS]->(:Person {name: 'Gus'})")
.unwrap();
s.execute(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
MERGE (a)-[:KNOWS]->(b)",
)
.unwrap();
let r = s
.execute("MATCH (:Person)-[k:KNOWS]->(:Person) RETURN count(k) AS cnt")
.unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(1),
"MERGE should not create duplicate relationship"
);
}
}
mod delete_reinsert {
use super::*;
#[test]
fn delete_all_then_reinsert_same_label() {
let db = db();
let s = db.session();
s.execute("INSERT (:Item {val: 1})").unwrap();
s.execute("INSERT (:Item {val: 2})").unwrap();
s.execute("MATCH (n:Item) DELETE n").unwrap();
let r = s.execute("MATCH (n:Item) RETURN count(n) AS cnt").unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(0));
s.execute("INSERT (:Item {val: 10})").unwrap();
s.execute("INSERT (:Item {val: 20})").unwrap();
let r2 = s
.execute("MATCH (n:Item) RETURN n.val ORDER BY n.val")
.unwrap();
assert_eq!(r2.row_count(), 2);
assert_eq!(r2.rows()[0][0], Value::Int64(10));
assert_eq!(r2.rows()[1][0], Value::Int64(20));
}
#[test]
fn detach_delete_then_reinsert_with_edges() {
let db = db();
let s = db.session();
s.execute("INSERT (:A {name: 'a'})-[:R]->(:B {name: 'b'})")
.unwrap();
s.execute("MATCH (n) DETACH DELETE n").unwrap();
s.execute("INSERT (:A {name: 'x'})-[:R]->(:B {name: 'y'})")
.unwrap();
let nodes = s.execute("MATCH (n) RETURN count(n) AS cnt").unwrap();
let edges = s
.execute("MATCH ()-[r]->() RETURN count(r) AS cnt")
.unwrap();
assert_eq!(nodes.rows()[0][0], Value::Int64(2));
assert_eq!(edges.rows()[0][0], Value::Int64(1));
}
}
mod multi_label {
use super::*;
#[test]
fn node_with_multiple_labels_matched_by_any() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person:Employee {name: 'Alix'})")
.unwrap();
let r1 = s.execute("MATCH (n:Person) RETURN n.name").unwrap();
assert_eq!(r1.row_count(), 1, "Should match by first label");
let r2 = s.execute("MATCH (n:Employee) RETURN n.name").unwrap();
assert_eq!(r2.row_count(), 1, "Should match by second label");
}
#[test]
fn labels_function_returns_all_labels() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person:Employee:Manager {name: 'Alix'})")
.unwrap();
let r = s
.execute("MATCH (n:Person) RETURN labels(n) AS lbls")
.unwrap();
assert_eq!(r.row_count(), 1);
if let Value::List(labels) = &r.rows()[0][0] {
assert_eq!(labels.len(), 3, "Should have 3 labels");
let label_strs: Vec<String> = labels
.iter()
.filter_map(|v| match v {
Value::String(s) => Some(s.to_string()),
_ => None,
})
.collect();
assert!(label_strs.contains(&"Person".to_string()));
assert!(label_strs.contains(&"Employee".to_string()));
assert!(label_strs.contains(&"Manager".to_string()));
} else {
panic!("Expected List, got {:?}", r.rows()[0][0]);
}
}
}
mod property_type_edge_cases {
use super::*;
#[test]
fn integer_float_comparison() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {ival: 42, fval: 42.0})").unwrap();
let r = s
.execute("MATCH (n:N) WHERE n.ival = n.fval RETURN n.ival")
.unwrap();
assert_eq!(
r.row_count(),
1,
"Integer 42 should equal float 42.0 in comparison"
);
}
#[test]
fn missing_property_is_null() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {name: 'Alix'})").unwrap();
let r = s
.execute("MATCH (n:N) RETURN n.nonexistent AS val")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::Null,
"Accessing a missing property should return NULL"
);
}
#[test]
fn empty_string_is_not_null() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {name: ''})").unwrap();
let r = s
.execute("MATCH (n:N) WHERE n.name IS NOT NULL RETURN n.name")
.unwrap();
assert_eq!(r.row_count(), 1, "Empty string is a valid value, not NULL");
assert_eq!(r.rows()[0][0], Value::String("".into()));
}
#[test]
fn zero_is_not_null() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 0})").unwrap();
let r = s
.execute("MATCH (n:N) WHERE n.val IS NOT NULL RETURN n.val")
.unwrap();
assert_eq!(r.row_count(), 1, "Integer 0 is a valid value, not NULL");
assert_eq!(r.rows()[0][0], Value::Int64(0));
}
#[test]
fn boolean_false_is_not_null() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {flag: false})").unwrap();
let r = s
.execute("MATCH (n:N) WHERE n.flag IS NOT NULL RETURN n.flag")
.unwrap();
assert_eq!(r.row_count(), 1, "Boolean false is a valid value, not NULL");
assert_eq!(r.rows()[0][0], Value::Bool(false));
}
}
mod avg_large_integers {
use super::*;
#[test]
fn avg_of_identical_large_values_equals_itself() {
let db = db();
let s = db.session();
s.execute("INSERT (:M {val: 389916982198384})").unwrap();
s.execute("INSERT (:M {val: 389916982198384})").unwrap();
s.execute("INSERT (:M {val: 389916982198384})").unwrap();
let r = s.execute("MATCH (m:M) RETURN avg(m.val) AS a").unwrap();
assert_eq!(r.row_count(), 1);
match &r.rows()[0][0] {
Value::Float64(f) => {
let expected = 389_916_982_198_384.0_f64;
let diff = (f - expected).abs();
assert!(
diff < 1.0,
"AVG of identical large values should equal the value itself, got {f}"
);
}
Value::Int64(v) => {
assert_eq!(
*v, 389_916_982_198_384,
"AVG of identical values should equal the value"
);
}
other => panic!("Expected numeric, got {:?}", other),
}
}
}
mod list_comparison {
use super::*;
#[test]
fn empty_list_not_equal_to_list_with_null() {
let db = db();
let s = db.session();
let r = s.execute("RETURN [] = [NULL] AS eq").unwrap();
assert_eq!(r.row_count(), 1);
assert_ne!(
r.rows()[0][0],
Value::Bool(true),
"Empty list must not equal a list containing NULL"
);
}
#[test]
fn empty_list_equals_empty_list() {
let db = db();
let s = db.session();
let r = s.execute("RETURN [] = [] AS eq").unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::Bool(true));
}
#[test]
fn list_equality_same_elements() {
let db = db();
let s = db.session();
let r = s.execute("RETURN [1, 2, 3] = [1, 2, 3] AS eq").unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::Bool(true));
}
#[test]
fn list_equality_different_elements() {
let db = db();
let s = db.session();
let r = s.execute("RETURN [1, 2] = [1, 3] AS eq").unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::Bool(false));
}
}
mod optional_match_aggregation {
use super::*;
#[test]
fn count_after_optional_match_preserves_all_rows() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})-[:FRIEND]->(:Person {name: 'Gus'})")
.unwrap();
s.execute("INSERT (:Person {name: 'Vincent'})").unwrap();
let r = s
.execute(
"MATCH (p:Person) \
OPTIONAL MATCH (p)-[:FRIEND]->(f:Person) \
RETURN p.name, count(f) AS fc \
ORDER BY p.name",
)
.unwrap();
assert_eq!(
r.row_count(),
3,
"All 3 persons should appear, even those without friends"
);
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));
assert_eq!(r.rows()[2][0], Value::String("Vincent".into()));
assert_eq!(r.rows()[2][1], Value::Int64(0));
}
}
mod union_correctness {
use super::*;
#[test]
fn basic_union_deduplicates() {
let db = db();
let s = db.session();
let r = s.execute("RETURN 1 AS x UNION RETURN 1 AS x").unwrap();
assert_eq!(r.row_count(), 1, "UNION should deduplicate identical rows");
}
#[test]
fn union_all_preserves_duplicates() {
let db = db();
let s = db.session();
let r = s.execute("RETURN 1 AS x UNION ALL RETURN 1 AS x").unwrap();
assert_eq!(r.row_count(), 2, "UNION ALL should preserve duplicates");
}
#[test]
fn union_different_values() {
let db = db();
let s = db.session();
let r = s.execute("RETURN 1 AS x UNION RETURN 2 AS x").unwrap();
assert_eq!(r.row_count(), 2);
}
}
mod or_condition {
use super::*;
#[test]
fn or_in_where_matches_either_branch() {
let db = db();
let s = db.session();
s.execute("INSERT (:Item {prop: 'A'})").unwrap();
s.execute("INSERT (:Item {prop: 'B'})").unwrap();
s.execute("INSERT (:Item {prop: 'C'})").unwrap();
let r = s
.execute(
"MATCH (n:Item) WHERE n.prop = 'A' OR n.prop = 'B' RETURN n.prop ORDER BY n.prop",
)
.unwrap();
assert_eq!(r.row_count(), 2, "OR should match both A and B");
assert_eq!(r.rows()[0][0], Value::String("A".into()));
assert_eq!(r.rows()[1][0], Value::String("B".into()));
}
#[test]
fn or_with_and_precedence() {
let db = db();
let s = db.session();
s.execute("INSERT (:Item {a: 1, b: 2})").unwrap();
s.execute("INSERT (:Item {a: 3, b: 4})").unwrap();
s.execute("INSERT (:Item {a: 5, b: 6})").unwrap();
let r = s
.execute(
"MATCH (n:Item) \
WHERE (n.a = 1 AND n.b = 2) OR (n.a = 5 AND n.b = 6) \
RETURN n.a ORDER BY n.a",
)
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(r.rows()[0][0], Value::Int64(1));
assert_eq!(r.rows()[1][0], Value::Int64(5));
}
#[test]
fn not_condition_inverts_filter() {
let db = db();
let s = db.session();
s.execute("INSERT (:Item {val: 1})").unwrap();
s.execute("INSERT (:Item {val: 2})").unwrap();
s.execute("INSERT (:Item {val: 3})").unwrap();
let r = s
.execute("MATCH (n:Item) WHERE NOT n.val = 2 RETURN n.val ORDER BY n.val")
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(r.rows()[0][0], Value::Int64(1));
assert_eq!(r.rows()[1][0], Value::Int64(3));
}
}
mod type_coercion_string_bool {
use super::*;
#[test]
fn string_false_not_equal_to_boolean_false() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: false})").unwrap();
s.execute("INSERT (:N {val: 'false'})").unwrap();
let r = s
.execute("MATCH (n:N) WHERE n.val <> 'false' RETURN n.val")
.unwrap();
assert_eq!(
r.row_count(),
1,
"Boolean false is not equal to string 'false'"
);
assert_eq!(r.rows()[0][0], Value::Bool(false));
}
#[test]
fn string_true_not_equal_to_boolean_true() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: true})").unwrap();
s.execute("INSERT (:N {val: 'true'})").unwrap();
let r = s
.execute("MATCH (n:N) WHERE n.val <> 'true' RETURN n.val")
.unwrap();
assert_eq!(
r.row_count(),
1,
"Boolean true is not equal to string 'true'"
);
assert_eq!(r.rows()[0][0], Value::Bool(true));
}
}
mod self_loop_variable_length {
use super::*;
#[test]
fn self_loop_varlength_terminates() {
let db = db();
let s = db.session();
s.execute("INSERT (a:N {name: 'Alix'})-[:LOOP]->(a)")
.unwrap();
s.execute(
"MATCH (a:N {name: 'Alix'}) \
INSERT (a)-[:R]->(b:N {name: 'Gus'})-[:R]->(c:N {name: 'Vincent'})",
)
.unwrap();
let r = s.execute("MATCH (a:N {name: 'Alix'})-[:R*1..3]->(b) RETURN b.name");
assert!(
r.is_ok(),
"Variable-length expansion with self-loop in graph must terminate: {r:?}"
);
}
#[test]
fn optional_match_varlength_with_self_loop_returns_null_or_paths() {
let db = db();
let s = db.session();
s.execute("INSERT (a:N {name: 'solo'})-[:LOOP]->(a)")
.unwrap();
let r = s.execute(
"MATCH (a:N {name: 'solo'}) \
OPTIONAL MATCH (a)-[:R*3..5]->(b) \
RETURN b.name",
);
assert!(
r.is_ok(),
"OPTIONAL MATCH with self-loop must not OOM: {r:?}"
);
if let Ok(result) = r {
assert_eq!(
result.row_count(),
1,
"OPTIONAL MATCH should still return 1 row"
);
assert_eq!(
result.rows()[0][0],
Value::Null,
"No matching :R paths from solo node, should be NULL"
);
}
}
}
mod in_operator {
use super::*;
#[test]
fn in_basic_membership() {
let db = db();
let s = db.session();
let r = s.execute("RETURN 2 IN [1, 2, 3] AS found").unwrap();
assert_eq!(r.rows()[0][0], Value::Bool(true));
}
#[test]
fn in_not_found() {
let db = db();
let s = db.session();
let r = s.execute("RETURN 4 IN [1, 2, 3] AS found").unwrap();
assert_eq!(r.rows()[0][0], Value::Bool(false));
}
#[test]
fn in_empty_list() {
let db = db();
let s = db.session();
let r = s.execute("RETURN 1 IN [] AS found").unwrap();
assert_eq!(r.rows()[0][0], Value::Bool(false));
}
#[test]
fn in_with_null_element() {
let db = db();
let s = db.session();
let r = s.execute("RETURN 1 IN [1, NULL] AS found").unwrap();
assert_eq!(r.rows()[0][0], Value::Bool(true));
}
#[test]
fn in_with_null_not_found() {
let db = db();
let s = db.session();
let r = s.execute("RETURN 2 IN [1, NULL] AS found").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Null,
"x IN [y, NULL] where x != y should be NULL (unknown)"
);
}
}
mod order_by_nulls {
use super::*;
#[test]
fn order_by_with_some_null_properties() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {name: 'Alix', score: 90})").unwrap();
s.execute("INSERT (:N {name: 'Gus'})").unwrap(); s.execute("INSERT (:N {name: 'Vincent', score: 80})")
.unwrap();
let r = s
.execute("MATCH (n:N) RETURN n.name, n.score ORDER BY n.score")
.unwrap();
assert_eq!(
r.row_count(),
3,
"ORDER BY on partial property should still return all rows"
);
assert_eq!(
r.rows()[2][1],
Value::Null,
"NULL score should sort to the end"
);
}
}
mod merge_after_delete {
use super::*;
#[test]
fn merge_does_not_match_deleted_node() {
let db = db();
let s = db.session();
s.execute("INSERT (:Singleton {key: 'only'})").unwrap();
s.execute("MATCH (n:Singleton) DELETE n").unwrap();
s.execute("MERGE (:Singleton {key: 'only'})").unwrap();
let r = s
.execute("MATCH (n:Singleton) RETURN count(n) AS cnt")
.unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(1),
"MERGE after delete should create exactly 1 new node"
);
}
}
mod label_intersection {
use super::*;
#[test]
fn same_variable_multiple_labels_across_match() {
let db = db();
let s = db.session();
s.execute("INSERT (:Worker:Senior {name: 'Vincent'})")
.unwrap();
s.execute("INSERT (:Worker:Junior {name: 'Jules'})")
.unwrap();
s.execute(
"MATCH (a:Worker {name: 'Vincent'}), (b:Worker {name: 'Jules'}) \
INSERT (a)-[:MANAGES]->(b)",
)
.unwrap();
let r = s
.execute(
"MATCH (n:Worker)-[:MANAGES]->() \
MATCH (n:Senior) \
RETURN n.name",
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::String("Vincent".into()),
"Only Vincent has both :Worker and :Senior"
);
}
#[test]
fn label_intersection_filters_non_matching() {
let db = db();
let s = db.session();
s.execute("INSERT (:A:B {name: 'both'})").unwrap();
s.execute("INSERT (:A {name: 'only_a'})").unwrap();
s.execute("INSERT (:B {name: 'only_b'})").unwrap();
let r = s.execute("MATCH (n:A) MATCH (n:B) RETURN n.name").unwrap();
assert_eq!(
r.row_count(),
1,
"Only the node with both :A and :B should match"
);
assert_eq!(r.rows()[0][0], Value::String("both".into()));
}
}
mod is_not_null_precedence {
use super::*;
#[test]
fn boolean_expression_is_not_null() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
let r = s
.execute("MATCH (n:N) WHERE (1 = 1) IS NOT NULL RETURN n.val")
.unwrap();
assert_eq!(
r.row_count(),
1,
"(1=1) IS NOT NULL should be TRUE, returning the row"
);
}
#[test]
fn property_comparison_is_not_null() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {a: 10, b: 20})").unwrap();
let r = s
.execute("MATCH (n:N) WHERE (n.a < n.b) IS NOT NULL RETURN n.a")
.unwrap();
assert_eq!(r.row_count(), 1);
}
}
mod quantifier_functions {
use super::*;
#[test]
fn any_on_empty_list_is_false() {
let db = db();
let s = db.session();
let r = s
.execute("RETURN any(x IN [] WHERE x > 0) AS result")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::Bool(false),
"any() on empty list should be FALSE per spec"
);
}
#[test]
fn all_on_empty_list_is_true() {
let db = db();
let s = db.session();
let r = s
.execute("RETURN all(x IN [] WHERE x > 0) AS result")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::Bool(true),
"all() on empty list should be TRUE per spec (vacuous truth)"
);
}
#[test]
fn none_on_empty_list_is_true() {
let db = db();
let s = db.session();
let r = s
.execute("RETURN none(x IN [] WHERE x > 0) AS result")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::Bool(true),
"none() on empty list should be TRUE (no elements violate)"
);
}
}
mod string_escapes {
use super::*;
#[test]
fn newline_escape_in_property() {
let db = db();
let s = db.session();
s.execute(r#"INSERT (:Entry {text: "line1\nline2"})"#)
.unwrap();
let r = s.execute("MATCH (e:Entry) RETURN e.text").unwrap();
assert_eq!(r.row_count(), 1);
if let Value::String(s) = &r.rows()[0][0] {
assert!(
s.contains('\n'),
"String should contain actual newline, got: {:?}",
s
);
} else {
panic!("Expected String, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn tab_escape_in_property() {
let db = db();
let s = db.session();
s.execute(r#"INSERT (:Entry {text: "col1\tcol2"})"#)
.unwrap();
let r = s.execute("MATCH (e:Entry) RETURN e.text").unwrap();
assert_eq!(r.row_count(), 1);
if let Value::String(s) = &r.rows()[0][0] {
assert!(
s.contains('\t'),
"String should contain actual tab, got: {:?}",
s
);
} else {
panic!("Expected String, got {:?}", r.rows()[0][0]);
}
}
}
mod null_join_semantics {
use super::*;
#[test]
fn null_equality_in_cross_match_where() {
let db = db();
let s = db.session();
s.execute("INSERT (:Account {svc: 'A', name: 'Alix'})")
.unwrap();
s.execute("INSERT (:Account {svc: 'B', name: 'Alix'})")
.unwrap();
s.execute("INSERT (:Account {svc: 'A'})").unwrap();
s.execute("INSERT (:Account {svc: 'B'})").unwrap();
let r = s
.execute(
"MATCH (a:Account {svc: 'A'}) \
MATCH (b:Account {svc: 'B'}) \
WHERE a.name = b.name \
RETURN a.name",
)
.unwrap();
assert_eq!(
r.row_count(),
1,
"Only 'Alix'='Alix' should match, NULL=NULL must not"
);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
}
}
mod null_function_arguments {
use super::*;
#[test]
fn type_of_null_relationship_returns_null() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {name: 'Alix'})").unwrap();
let r = s
.execute(
"MATCH (n:N) \
OPTIONAL MATCH (n)-[r:NONEXISTENT]->() \
RETURN type(r) AS t",
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::Null,
"type() of NULL relationship should return NULL"
);
}
}
mod delete_recreate_edge {
use super::*;
#[test]
fn replace_edge_in_sequence() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})-[:LIKES]->(:Fruit {name: 'apple'})")
.unwrap();
s.execute("INSERT (:Fruit {name: 'banana'})").unwrap();
s.execute("MATCH (p:Person {name: 'Alix'})-[r:LIKES]->() DELETE r")
.unwrap();
s.execute(
"MATCH (p:Person {name: 'Alix'}), (f:Fruit {name: 'banana'}) \
INSERT (p)-[:LIKES]->(f)",
)
.unwrap();
let r = s
.execute(
"MATCH (p:Person {name: 'Alix'})-[:LIKES]->(f:Fruit) \
RETURN f.name",
)
.unwrap();
assert_eq!(
r.row_count(),
1,
"Should have exactly 1 LIKES edge after replace"
);
assert_eq!(r.rows()[0][0], Value::String("banana".into()));
}
}
mod limit_with_order {
use super::*;
#[test]
fn limit_respected_with_order_by() {
let db = db();
let s = db.session();
for i in 0..20 {
s.execute(&format!("INSERT (:Item {{seq: {i}}})")).unwrap();
}
let r = s
.execute("MATCH (n:Item) RETURN n.seq ORDER BY n.seq LIMIT 5")
.unwrap();
assert_eq!(
r.row_count(),
5,
"LIMIT 5 with ORDER BY should return exactly 5"
);
assert_eq!(r.rows()[0][0], Value::Int64(0));
assert_eq!(r.rows()[4][0], Value::Int64(4));
}
#[test]
fn limit_without_order_by() {
let db = db();
let s = db.session();
for i in 0..20 {
s.execute(&format!("INSERT (:Item {{seq: {i}}})")).unwrap();
}
let r = s.execute("MATCH (n:Item) RETURN n.seq LIMIT 5").unwrap();
assert_eq!(
r.row_count(),
5,
"LIMIT 5 without ORDER BY should return exactly 5"
);
}
#[test]
fn limit_larger_than_result_set() {
let db = db();
let s = db.session();
s.execute("INSERT (:Item {seq: 1})").unwrap();
s.execute("INSERT (:Item {seq: 2})").unwrap();
let r = s
.execute("MATCH (n:Item) RETURN n.seq ORDER BY n.seq LIMIT 100")
.unwrap();
assert_eq!(
r.row_count(),
2,
"LIMIT larger than result set returns all rows"
);
}
}
mod idempotent_set {
use super::*;
#[test]
fn set_property_to_same_value_is_noop() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {code: 'X', note: 'initial'})")
.unwrap();
s.execute("MATCH (n:N {code: 'X'}) SET n.code = 'X', n.note = 'updated'")
.unwrap();
let r = s.execute("MATCH (n:N {code: 'X'}) RETURN n.note").unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::String("updated".into()),
"SET with unchanged code should still update note"
);
}
}
mod float_precision {
use super::*;
#[test]
fn float64_full_precision_roundtrip() {
let db = db();
let s = db.session();
s.execute("INSERT (:M {val: 0.123456789012345})").unwrap();
let r = s.execute("MATCH (m:M) RETURN m.val").unwrap();
assert_eq!(r.row_count(), 1);
if let Value::Float64(f) = r.rows()[0][0] {
let diff = (f - 0.123456789012345_f64).abs();
assert!(
diff < 1e-15,
"Float64 must preserve full precision, got {f} (diff {diff})"
);
} else {
panic!("Expected Float64, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn repeated_edge_updates_no_stale_values() {
let db = db();
let s = db.session();
s.execute("INSERT (:Host {name: 'h1'})-[:NESTS]->(:Nest {name: 'n1'})")
.unwrap();
s.execute("INSERT (:Nest {name: 'n2'})").unwrap();
s.execute("INSERT (:Nest {name: 'n3'})").unwrap();
s.execute("MATCH (:Host {name: 'h1'})-[r:NESTS]->() DELETE r")
.unwrap();
s.execute("MATCH (h:Host {name: 'h1'}), (n:Nest {name: 'n2'}) INSERT (h)-[:NESTS]->(n)")
.unwrap();
s.execute("MATCH (:Host {name: 'h1'})-[r:NESTS]->() DELETE r")
.unwrap();
s.execute("MATCH (h:Host {name: 'h1'}), (n:Nest {name: 'n3'}) INSERT (h)-[:NESTS]->(n)")
.unwrap();
let r = s
.execute("MATCH (:Host {name: 'h1'})-[:NESTS]->(n) RETURN n.name")
.unwrap();
assert_eq!(
r.row_count(),
1,
"Should have exactly 1 NESTS edge after 2 updates"
);
assert_eq!(r.rows()[0][0], Value::String("n3".into()));
}
}
mod null_grouping_key {
use super::*;
#[test]
fn null_column_does_not_collapse_groups() {
let db = db();
let s = db.session();
s.execute("INSERT (:Q {status: 'ok'})").unwrap();
s.execute("INSERT (:Q {status: 'ok'})").unwrap();
s.execute("INSERT (:Q {status: 'reject'})").unwrap();
let r = s
.execute(
"MATCH (q:Q) \
RETURN q.status AS status, q.extra AS extra, count(*) AS cnt \
ORDER BY status",
)
.unwrap();
assert!(
r.row_count() >= 2,
"NULL in one grouping column must not collapse distinct values in another, got {} rows",
r.row_count()
);
}
}
mod inequality_missing_property {
use super::*;
#[test]
fn neq_does_not_match_missing_property() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
s.execute("INSERT (:Person {age: 30})").unwrap();
let r = s
.execute("MATCH (n:Person) WHERE n.name <> 'Alix' RETURN n.name ORDER BY n.name")
.unwrap();
assert_eq!(
r.row_count(),
1,
"<> must not match nodes missing the property (NULL <> x is UNKNOWN)"
);
assert_eq!(r.rows()[0][0], Value::String("Gus".into()));
}
}
mod merge_batch_composite_dedup {
use super::*;
#[test]
fn unwind_merge_composite_key_deduplicates() {
let db = db();
let s = db.session();
s.execute("UNWIND [1, 2, 1, 3, 2] AS i MERGE (:Item {a: i})")
.unwrap();
let r = s.execute("MATCH (n:Item) RETURN count(n) AS cnt").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(3),
"Duplicate values in UNWIND should MERGE into distinct nodes"
);
}
}
mod mixed_aggregate_non_aggregate {
use super::*;
#[test]
fn mixing_aggregate_and_bare_column_without_group_by() {
let db = db();
let s = db.session();
s.execute("INSERT (:P {n: 'a'})").unwrap();
s.execute("INSERT (:P {n: 'b'})").unwrap();
let r = s.execute("MATCH (p:P) RETURN p.n, count(p) AS cnt");
match r {
Err(_) => {} Ok(result) => {
assert!(
result.row_count() >= 1,
"If not an error, must return grouped result, not empty"
);
for row in result.rows() {
assert_ne!(
row[0],
Value::Null,
"Non-aggregate column must not silently become NULL"
);
}
}
}
}
}
mod order_by_aliased_property {
use super::*;
#[test]
fn order_by_original_expression_after_alias() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})-[:WORKS_FOR]->(:Company {name: 'Acme'})")
.unwrap();
s.execute("INSERT (:Person {name: 'Gus'})-[:WORKS_FOR]->(:Company {name: 'Beta'})")
.unwrap();
let r = s
.execute(
"MATCH (e:Person)-[:WORKS_FOR]->(c:Company) \
RETURN e.name AS employee, c.name AS company \
ORDER BY employee",
)
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
assert_eq!(r.rows()[1][0], Value::String("Gus".into()));
}
#[test]
fn order_by_alias_with_optional_match() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})-[:WORKS_FOR]->(:Company {name: 'Acme'})")
.unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
let r = s
.execute(
"MATCH (e:Person) \
OPTIONAL MATCH (e)-[:WORKS_FOR]->(c:Company) \
RETURN e.name AS employee, c.name AS company \
ORDER BY employee",
)
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
assert_eq!(r.rows()[0][1], Value::String("Acme".into()));
assert_eq!(r.rows()[1][0], Value::String("Gus".into()));
assert_eq!(r.rows()[1][1], Value::Null);
}
}
mod order_by_relationship_traversal_218 {
use super::*;
#[test]
fn order_by_property_matching_return_item() {
let db = db();
let s = db.session();
s.execute_cypher("CREATE (:Method {name: 'foo', qn: 'java://A:foo()'})")
.unwrap();
s.execute_cypher("CREATE (:Method {name: 'bar', qn: 'java://B:bar()'})")
.unwrap();
s.execute_cypher("CREATE (:Method {name: 'baz', qn: 'java://C:baz()'})")
.unwrap();
s.execute_cypher(
"MATCH (a:Method {name: 'foo'}), (b:Method {name: 'bar'}) CREATE (a)-[:CALLS]->(b)",
)
.unwrap();
s.execute_cypher(
"MATCH (a:Method {name: 'baz'}), (b:Method {name: 'bar'}) CREATE (a)-[:CALLS]->(b)",
)
.unwrap();
let r = s
.execute_cypher(
"MATCH (caller)-[:CALLS]->(target) \
WHERE target.name = 'bar' \
RETURN caller.name AS caller, caller.qn AS caller_qn",
)
.unwrap();
assert_eq!(r.row_count(), 2);
for row in r.rows() {
assert!(
matches!(&row[0], Value::String(s) if s == "foo" || s == "baz"),
"caller should be a string, got {:?}",
row[0]
);
assert!(
matches!(&row[1], Value::String(_)),
"caller_qn should be a string, got {:?}",
row[1]
);
}
let r = s
.execute_cypher(
"MATCH (caller)-[:CALLS]->(target) \
WHERE target.name = 'bar' \
RETURN caller.name AS caller, caller.qn AS caller_qn \
ORDER BY caller.name",
)
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(
r.columns.len(),
2,
"should have exactly 2 columns, got {:?}",
r.columns
);
assert_eq!(r.rows()[0][0], Value::String("baz".into()));
assert_eq!(r.rows()[0][1], Value::String("java://C:baz()".into()));
assert_eq!(r.rows()[1][0], Value::String("foo".into()));
assert_eq!(r.rows()[1][1], Value::String("java://A:foo()".into()));
}
#[test]
fn order_by_desc_with_relationship_traversal() {
let db = db();
let s = db.session();
s.execute_cypher("CREATE (:Method {name: 'foo', qn: 'java://A:foo()'})")
.unwrap();
s.execute_cypher("CREATE (:Method {name: 'bar', qn: 'java://B:bar()'})")
.unwrap();
s.execute_cypher("CREATE (:Method {name: 'baz', qn: 'java://C:baz()'})")
.unwrap();
s.execute_cypher(
"MATCH (a:Method {name: 'foo'}), (b:Method {name: 'bar'}) CREATE (a)-[:CALLS]->(b)",
)
.unwrap();
s.execute_cypher(
"MATCH (a:Method {name: 'baz'}), (b:Method {name: 'bar'}) CREATE (a)-[:CALLS]->(b)",
)
.unwrap();
let r = s
.execute_cypher(
"MATCH (caller)-[:CALLS]->(target) \
WHERE target.name = 'bar' \
RETURN caller.name AS caller, caller.qn AS caller_qn \
ORDER BY caller.name DESC",
)
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(r.rows()[0][0], Value::String("foo".into()));
assert_eq!(r.rows()[1][0], Value::String("baz".into()));
}
}
mod call_subquery_scope {
use super::*;
#[test]
fn call_subquery_preserves_outer_variable() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
s.execute("INSERT (:Person {name: 'Vincent'})").unwrap();
let r = s.execute(
"MATCH (p:Person) \
CALL { \
WITH p \
OPTIONAL MATCH (p)-[:ACTED_IN]->(m) \
RETURN count(m) AS movie_count \
} \
RETURN p.name, movie_count \
ORDER BY p.name",
);
match r {
Ok(result) => {
assert_eq!(
result.row_count(),
3,
"CALL subquery must preserve all outer rows, got {}",
result.row_count()
);
for row in result.rows() {
assert_eq!(row[1], Value::Int64(0));
}
}
Err(_) => {
}
}
}
}
mod edge_properties_in_path {
use super::*;
#[test]
fn edge_properties_preserved_in_traversal() {
let db = db();
let s = db.session();
s.execute(
"INSERT (:Place {name: 'France'})-[:IS_IN {category: 'continent'}]->(:Place {name: 'Europe'})",
)
.unwrap();
s.execute(
"INSERT (:Place {name: 'Europe'})-[:IS_IN {category: 'planet'}]->(:Place {name: 'World'})",
)
.unwrap();
let r = s
.execute(
"MATCH (:Place {name: 'France'})-[r:IS_IN]->(:Place {name: 'Europe'}) \
RETURN r.category",
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::String("continent".into()),
"Edge property must be preserved"
);
}
#[test]
fn multiple_edge_properties_in_chain() {
let db = db();
let s = db.session();
s.execute(
"INSERT (:A {name: 'a'})-[:R {weight: 10}]->(:B {name: 'b'})-[:R {weight: 20}]->(:C {name: 'c'})",
)
.unwrap();
let r = s
.execute(
"MATCH (:A)-[r1:R]->(:B)-[r2:R]->(:C) \
RETURN r1.weight, r2.weight",
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(10));
assert_eq!(r.rows()[0][1], Value::Int64(20));
}
}
mod varlength_vs_explicit {
use super::*;
#[test]
fn two_hop_varlength_equals_explicit() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {id: 1})-[:R]->(:N {id: 2})-[:R]->(:N {id: 3})")
.unwrap();
let explicit = s
.execute(
"MATCH (a:N {id: 1})-[:R]->(mid)-[:R]->(c) \
RETURN count(*) AS cnt",
)
.unwrap();
let varlength = s
.execute(
"MATCH (a:N {id: 1})-[:R*2]->(c) \
RETURN count(*) AS cnt",
)
.unwrap();
assert_eq!(
explicit.rows()[0][0],
varlength.rows()[0][0],
"Variable-length *2 must match explicit two-hop count"
);
}
}
mod case_when_null_aggregate {
use super::*;
#[test]
fn case_when_with_count_zero_and_null_max() {
let db = db();
let s = db.session();
s.execute("INSERT (:Column {name: 'todo'})").unwrap();
let r = s.execute(
"MATCH (col:Column {name: 'todo'}) \
OPTIONAL MATCH (c:Card)-[:IN_COL]->(col) \
WITH col, count(c) AS cc, max(c.pos) AS mp \
RETURN CASE WHEN cc = 0 THEN 0 ELSE mp + 1 END AS next_pos",
);
match r {
Ok(result) => {
assert_eq!(result.row_count(), 1);
assert_eq!(
result.rows()[0][0],
Value::Int64(0),
"When count is 0, CASE should return 0"
);
}
Err(_) => {
}
}
}
}
mod chained_or_and {
use super::*;
#[test]
fn chained_or_not_flattened() {
let db = db();
let s = db.session();
s.execute("INSERT (:T {a: true, b: true, c: true, d: true})")
.unwrap();
s.execute("INSERT (:T {a: true, b: false, c: true, d: false})")
.unwrap();
s.execute("INSERT (:T {a: false, b: true, c: false, d: true})")
.unwrap();
s.execute("INSERT (:T {a: false, b: false, c: true, d: false})")
.unwrap();
let r = s
.execute(
"MATCH (n:T) \
WHERE (n.a = true OR n.b = true) AND (n.c = false OR n.d = true) \
RETURN count(*) AS cnt",
)
.unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(2),
"(A OR B) AND (C OR D) must not flatten to (A OR B OR C OR D)"
);
}
}
mod substring_indexing {
use super::*;
#[test]
fn gql_substring_extracts_correctly() {
let db = db();
let s = db.session();
let r = s.execute("RETURN substring('hello world', 0, 5) AS sub");
match r {
Ok(result) => {
assert_eq!(result.row_count(), 1);
if let Value::String(s) = &result.rows()[0][0] {
assert!(
!s.is_empty(),
"substring should not return empty for valid input"
);
}
}
Err(_) => {} }
}
}
mod property_key_prefix {
use super::*;
#[test]
fn property_keys_with_shared_prefix() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {hello: 'world', hel: 'wor'})")
.unwrap();
let r1 = s.execute("MATCH (n:N) RETURN n.hel").unwrap();
assert_eq!(r1.rows()[0][0], Value::String("wor".into()));
let r2 = s.execute("MATCH (n:N) RETURN n.hello").unwrap();
assert_eq!(r2.rows()[0][0], Value::String("world".into()));
}
#[test]
fn filter_on_prefix_property() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {abc: 1, abcdef: 2, ab: 3})").unwrap();
let r = s
.execute("MATCH (n:N) WHERE n.abc = 1 RETURN n.abcdef, n.ab")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(2));
assert_eq!(r.rows()[0][1], Value::Int64(3));
}
}
mod property_type_overwrite {
use super::*;
#[test]
fn overwrite_bool_with_string() {
let db = db();
let s = db.session();
s.execute("INSERT (:Item {flag: true})").unwrap();
s.execute("MATCH (n:Item) SET n.flag = 'yes'").unwrap();
let r = s.execute("MATCH (n:Item) RETURN n.flag").unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(
r.rows()[0][0],
Value::String("yes".into()),
"Property type should be overwritable from bool to string"
);
}
#[test]
fn overwrite_int_with_string() {
let db = db();
let s = db.session();
s.execute("INSERT (:Item {val: 42})").unwrap();
s.execute("MATCH (n:Item) SET n.val = 'forty-two'").unwrap();
let r = s.execute("MATCH (n:Item) RETURN n.val").unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("forty-two".into()));
}
#[test]
fn overwrite_string_with_int() {
let db = db();
let s = db.session();
s.execute("INSERT (:Item {val: 'hello'})").unwrap();
s.execute("MATCH (n:Item) SET n.val = 99").unwrap();
let r = s.execute("MATCH (n:Item) RETURN n.val").unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(99));
}
}
mod escaped_quotes {
use super::*;
#[test]
fn double_quotes_in_single_quoted_string() {
let db = db();
let s = db.session();
s.execute(r#"INSERT (:Book {title: 'The "Problem" of Knowledge'})"#)
.unwrap();
let r = s.execute("MATCH (b:Book) RETURN b.title").unwrap();
assert_eq!(r.row_count(), 1);
if let Value::String(title) = &r.rows()[0][0] {
assert!(
title.contains('"'),
"Double quotes should be preserved in property value, got: {title}"
);
} else {
panic!("Expected String, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn single_quotes_in_double_quoted_string() {
let db = db();
let s = db.session();
s.execute(r#"INSERT (:Book {title: "It's a Test"})"#)
.unwrap();
let r = s.execute("MATCH (b:Book) RETURN b.title").unwrap();
assert_eq!(r.row_count(), 1);
if let Value::String(title) = &r.rows()[0][0] {
assert!(
title.contains('\''),
"Single quotes should be preserved, got: {title}"
);
} else {
panic!("Expected String, got {:?}", r.rows()[0][0]);
}
}
}
mod phantom_relationships {
use super::*;
#[test]
fn detach_delete_one_node_preserves_unrelated_edges() {
let db = db();
let s = db.session();
s.execute("INSERT (:A {id: 1})-[:R]->(:B {id: 1})").unwrap();
s.execute("INSERT (:A {id: 2})-[:R]->(:B {id: 2})").unwrap();
s.execute("MATCH (a:A {id: 1}) DETACH DELETE a").unwrap();
let r = s
.execute("MATCH ()-[r:R]->() RETURN count(r) AS cnt")
.unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(1),
"Unrelated edge chain should survive DETACH DELETE of another node"
);
}
}
mod detach_delete_return {
use super::*;
#[test]
fn delete_then_count_returns_zero() {
let db = db();
let s = db.session();
s.execute("INSERT (:Ghost {name: 'Alix', age: 30})")
.unwrap();
s.execute("MATCH (n:Ghost) DETACH DELETE n").unwrap();
let r = s.execute("MATCH (n:Ghost) RETURN count(n) AS cnt").unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(0));
}
}
mod multi_statement {
use super::*;
#[test]
fn sequential_statements_all_execute() {
let db = db();
let s = db.session();
s.execute("INSERT (:Item {val: 1})").unwrap();
s.execute("INSERT (:Item {val: 2})").unwrap();
s.execute("INSERT (:Item {val: 3})").unwrap();
let r = s.execute("MATCH (n:Item) RETURN count(n) AS cnt").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(3),
"All sequential statements should execute"
);
}
}
mod backtick_identifiers {
use super::*;
#[test]
fn backtick_label_matches_plain_label() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let r = s.execute("MATCH (n:`Person`) RETURN n.name");
match r {
Ok(result) => {
assert_eq!(
result.row_count(),
1,
"Backtick-quoted :Person should match plain :Person"
);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
}
Err(_) => {
}
}
}
}
mod skip_limit {
use super::*;
#[test]
fn skip_skips_first_n_rows() {
let db = db();
let s = db.session();
for i in 0..5 {
s.execute(&format!("INSERT (:Item {{seq: {i}}})")).unwrap();
}
let r = s
.execute("MATCH (n:Item) RETURN n.seq ORDER BY n.seq SKIP 2")
.unwrap();
assert_eq!(r.row_count(), 3);
assert_eq!(r.rows()[0][0], Value::Int64(2));
}
#[test]
fn skip_plus_limit() {
let db = db();
let s = db.session();
for i in 0..10 {
s.execute(&format!("INSERT (:Item {{seq: {i}}})")).unwrap();
}
let r = s
.execute("MATCH (n:Item) RETURN n.seq ORDER BY n.seq SKIP 3 LIMIT 2")
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(r.rows()[0][0], Value::Int64(3));
assert_eq!(r.rows()[1][0], Value::Int64(4));
}
#[test]
fn skip_beyond_result_set() {
let db = db();
let s = db.session();
s.execute("INSERT (:Item {seq: 1})").unwrap();
s.execute("INSERT (:Item {seq: 2})").unwrap();
let r = s.execute("MATCH (n:Item) RETURN n.seq SKIP 100").unwrap();
assert_eq!(r.row_count(), 0, "SKIP past end should return empty");
}
}
mod order_by_mixed_types {
use super::*;
#[test]
fn order_by_heterogeneous_property_does_not_crash() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
s.execute("INSERT (:N {val: 'hello'})").unwrap();
s.execute("INSERT (:N {val: true})").unwrap();
let r = s.execute("MATCH (n:N) RETURN n.val ORDER BY n.val");
assert!(
r.is_ok(),
"ORDER BY with mixed types should not crash: {r:?}"
);
if let Ok(result) = r {
assert_eq!(result.row_count(), 3, "All 3 rows should be returned");
}
}
}
mod return_distinct {
use super::*;
#[test]
fn distinct_removes_exact_duplicates() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
s.execute("INSERT (:N {val: 1})").unwrap();
s.execute("INSERT (:N {val: 2})").unwrap();
let r = s
.execute("MATCH (n:N) RETURN DISTINCT n.val ORDER BY n.val")
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(r.rows()[0][0], Value::Int64(1));
assert_eq!(r.rows()[1][0], Value::Int64(2));
}
#[test]
fn distinct_on_null_values() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
s.execute("INSERT (:N {val: 1})").unwrap();
s.execute("INSERT (:N)").unwrap(); s.execute("INSERT (:N)").unwrap();
let r = s.execute("MATCH (n:N) RETURN DISTINCT n.val").unwrap();
assert_eq!(
r.row_count(),
2,
"DISTINCT should collapse duplicate NULLs into one"
);
}
}
mod with_clause {
use super::*;
#[test]
fn with_renames_variable() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
let r = s
.execute(
"MATCH (p:Person) \
WITH p.name AS person_name, p.age AS person_age \
RETURN person_name, person_age",
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
assert_eq!(r.rows()[0][1], Value::Int64(30));
}
#[test]
fn with_filters_rows() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
s.execute("INSERT (:N {val: 2})").unwrap();
s.execute("INSERT (:N {val: 3})").unwrap();
let r = s
.execute(
"MATCH (n:N) \
WITH n WHERE n.val > 1 \
RETURN n.val ORDER BY n.val",
)
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(r.rows()[0][0], Value::Int64(2));
assert_eq!(r.rows()[1][0], Value::Int64(3));
}
}
mod multi_match_create_edge {
use super::*;
#[test]
fn match_match_create_relationship() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
s.execute(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
INSERT (a)-[:KNOWS]->(b)",
)
.unwrap();
let nodes = s
.execute("MATCH (n:Person) RETURN count(n) AS cnt")
.unwrap();
let edges = s
.execute("MATCH ()-[r:KNOWS]->() RETURN count(r) AS cnt")
.unwrap();
assert_eq!(
nodes.rows()[0][0],
Value::Int64(2),
"Should still have 2 nodes"
);
assert_eq!(edges.rows()[0][0], Value::Int64(1), "Should have 1 edge");
}
#[test]
fn match_match_create_multiple_edges() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
s.execute("INSERT (:Person {name: 'Vincent'})").unwrap();
s.execute(
"MATCH (a:Person {name: 'Alix'}), (b:Person) \
WHERE b.name <> 'Alix' \
INSERT (a)-[:KNOWS]->(b)",
)
.unwrap();
let edges = s
.execute("MATCH (:Person {name: 'Alix'})-[r:KNOWS]->() RETURN count(r) AS cnt")
.unwrap();
assert_eq!(edges.rows()[0][0], Value::Int64(2));
}
}
mod negative_numerics {
use super::*;
#[test]
fn insert_negative_integer() {
let db = db();
let s = db.session();
s.execute("INSERT (:Location {lat: -33, lon: 151})")
.unwrap();
let r = s.execute("MATCH (l:Location) RETURN l.lat, l.lon").unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(-33));
assert_eq!(r.rows()[0][1], Value::Int64(151));
}
#[test]
fn insert_negative_float() {
let db = db();
let s = db.session();
s.execute("INSERT (:Location {lat: -33.8688, lon: 151.2093})")
.unwrap();
let r = s.execute("MATCH (l:Location) RETURN l.lat, l.lon").unwrap();
if let Value::Float64(lat) = r.rows()[0][0] {
assert!(lat < 0.0, "Negative latitude must be preserved");
assert!((lat - (-33.8688)).abs() < 0.001);
} else {
panic!("Expected Float64 for lat, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn filter_on_negative_value() {
let db = db();
let s = db.session();
s.execute("INSERT (:Loc {lat: -33.0})").unwrap();
s.execute("INSERT (:Loc {lat: 48.8})").unwrap();
let r = s
.execute("MATCH (l:Loc) WHERE l.lat < 0 RETURN l.lat")
.unwrap();
assert_eq!(r.row_count(), 1, "Only one location has negative latitude");
}
#[test]
fn merge_with_negative_property() {
let db = db();
let s = db.session();
s.execute("MERGE (:Temp {val: -42})").unwrap();
s.execute("MERGE (:Temp {val: -42})").unwrap();
let r = s.execute("MATCH (n:Temp) RETURN count(n) AS cnt").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(1),
"MERGE with negative value should deduplicate"
);
}
}
mod count_variants {
use super::*;
#[test]
fn count_star_after_insert() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
s.execute("INSERT (:N {val: 2})").unwrap();
let r = s.execute("MATCH (n:N) RETURN count(*) AS cnt").unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(2));
}
#[test]
fn count_variable_after_insert() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
s.execute("INSERT (:N {val: 2})").unwrap();
let r = s.execute("MATCH (n:N) RETURN count(n) AS cnt").unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(2));
}
#[test]
fn count_star_equals_count_variable() {
let db = db();
let s = db.session();
s.execute("INSERT (:N {val: 1})").unwrap();
s.execute("INSERT (:N {val: 2})").unwrap();
s.execute("INSERT (:N {val: 3})").unwrap();
let r1 = s.execute("MATCH (n:N) RETURN count(*) AS cnt").unwrap();
let r2 = s.execute("MATCH (n:N) RETURN count(n) AS cnt").unwrap();
assert_eq!(
r1.rows()[0][0],
r2.rows()[0][0],
"count(*) and count(n) must agree"
);
}
}
mod batch_upsert {
use super::*;
#[test]
fn unwind_merge_set_property() {
let db = db();
let s = db.session();
s.execute(
"UNWIND [1, 2, 3] AS i \
MERGE (n:Item {key: i}) \
SET n.updated = true",
)
.unwrap();
let r = s
.execute("MATCH (n:Item) WHERE n.updated = true RETURN count(n) AS cnt")
.unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(3));
}
#[test]
fn unwind_merge_second_pass_updates() {
let db = db();
let s = db.session();
s.execute("UNWIND [1, 2, 3] AS i MERGE (n:Item {key: i}) SET n.ver = 1")
.unwrap();
s.execute("UNWIND [1, 2, 3] AS i MERGE (n:Item {key: i}) SET n.ver = 2")
.unwrap();
let r = s.execute("MATCH (n:Item) RETURN count(n) AS cnt").unwrap();
assert_eq!(
r.rows()[0][0],
Value::Int64(3),
"No duplicates from second MERGE"
);
let r2 = s
.execute("MATCH (n:Item) WHERE n.ver = 2 RETURN count(n) AS cnt")
.unwrap();
assert_eq!(
r2.rows()[0][0],
Value::Int64(3),
"All nodes should have ver=2 after second pass"
);
}
}
mod aggregation_with_functions {
use super::*;
#[test]
fn group_by_labels_with_count() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
s.execute("INSERT (:City {name: 'Amsterdam'})").unwrap();
let r = s
.execute("MATCH (n) RETURN labels(n)[0] AS label, count(n) AS cnt ORDER BY label")
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(r.rows()[0][0], Value::String("City".into()));
assert_eq!(r.rows()[0][1], Value::Int64(1));
assert_eq!(r.rows()[1][0], Value::String("Person".into()));
assert_eq!(r.rows()[1][1], Value::Int64(2));
}
#[test]
fn group_by_type_with_count() {
let db = db();
let s = db.session();
s.execute("INSERT (:A)-[:FOLLOWS]->(:B)").unwrap();
s.execute("INSERT (:C)-[:FOLLOWS]->(:D)").unwrap();
s.execute("INSERT (:E)-[:BLOCKS]->(:F)").unwrap();
let r = s
.execute("MATCH ()-[r]->() RETURN type(r) AS t, count(r) AS cnt ORDER BY t")
.unwrap();
assert_eq!(r.row_count(), 2);
}
}
mod persistence_roundtrip {
use super::*;
use grafeo_engine::GrafeoDB;
#[test]
fn in_memory_data_survives_session() {
let db = GrafeoDB::new_in_memory();
let s1 = db.session();
s1.execute("INSERT (:Persist {key: 'test', val: 42})")
.unwrap();
let s2 = db.session();
let r = s2
.execute("MATCH (n:Persist {key: 'test'}) RETURN n.val")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(42));
}
}
mod order_by_synthetic_columns {
use super::*;
#[test]
fn order_by_labels_does_not_leak_expr_column() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
let r = s
.execute("MATCH (n:Person) RETURN n.name ORDER BY labels(n)[0]")
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(
r.columns.len(),
1,
"Expected 1 column (n.name), got {}: {:?}",
r.columns.len(),
r.columns
);
for col in &r.columns {
assert!(
!col.starts_with("__expr"),
"Synthetic __expr column leaked into results: {col}"
);
}
}
#[test]
fn order_by_arithmetic_does_not_leak_expr_column() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
s.execute("INSERT (:Person {name: 'Gus', age: 25})")
.unwrap();
let r = s
.execute("MATCH (n:Person) RETURN n.name, n.age ORDER BY n.age + 1")
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(
r.columns.len(),
2,
"Expected 2 columns (n.name, n.age), got {}: {:?}",
r.columns.len(),
r.columns
);
for col in &r.columns {
assert!(
!col.starts_with("__expr"),
"Synthetic __expr column leaked into results: {col}"
);
}
}
#[test]
fn order_by_type_on_edges_does_not_leak() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})-[:KNOWS]->(:Person {name: 'Gus'})")
.unwrap();
let r = s
.execute("MATCH (a:Person)-[r]->(b:Person) RETURN a.name, b.name ORDER BY type(r)")
.unwrap();
assert_eq!(r.row_count(), 1);
for col in &r.columns {
assert!(
!col.starts_with("__expr"),
"Synthetic __expr column leaked into results: {col}"
);
}
}
}
mod group_by_list_keys {
use super::*;
#[test]
fn group_by_labels_multi_label_nodes() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person:Employee {name: 'Gus'})")
.unwrap();
s.execute("INSERT (:Person:Employee {name: 'Jules'})")
.unwrap();
s.execute("INSERT (:Company {name: 'GrafeoDB'})").unwrap();
let r = s
.execute("MATCH (n) RETURN labels(n) AS lbl, count(*) AS cnt ORDER BY cnt DESC")
.unwrap();
assert_eq!(
r.row_count(),
3,
"GROUP BY labels(n) should produce 3 distinct groups, got {}: {:?}",
r.row_count(),
r.rows()
);
}
#[test]
fn group_by_labels_single_label_nodes() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("INSERT (:Person {name: 'Gus'})").unwrap();
s.execute("INSERT (:Company {name: 'GrafeoDB'})").unwrap();
let r = s
.execute("MATCH (n) RETURN labels(n) AS lbl, count(*) AS cnt ORDER BY cnt DESC")
.unwrap();
assert_eq!(
r.row_count(),
2,
"GROUP BY labels(n) should produce 2 distinct groups, got {}: {:?}",
r.row_count(),
r.rows()
);
}
#[test]
fn group_by_date_valued_property() {
let db = db();
let s = db.session();
s.execute("INSERT (:Event {name: 'A', day: date('2024-06-15')})")
.unwrap();
s.execute("INSERT (:Event {name: 'B', day: date('2024-06-15')})")
.unwrap();
s.execute("INSERT (:Event {name: 'C', day: date('2024-12-25')})")
.unwrap();
let r = s
.execute("MATCH (e:Event) RETURN e.day AS day, count(*) AS cnt ORDER BY cnt DESC")
.unwrap();
assert_eq!(
r.row_count(),
2,
"GROUP BY date should produce 2 distinct groups, got {}: {:?}",
r.row_count(),
r.rows()
);
}
}