use grafeo_common::types::Value;
use grafeo_engine::GrafeoDB;
fn create_social_network() -> 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("NYC".into())),
],
)
.unwrap();
let gus = session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Gus".into())),
("age", Value::Int64(25)),
("city", Value::String("NYC".into())),
],
)
.unwrap();
let harm = session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Harm".into())),
("age", Value::Int64(35)),
("city", Value::String("London".into())),
],
)
.unwrap();
let techcorp = session
.create_node_with_props(&["Company"], [("name", Value::String("TechCorp".into()))])
.unwrap();
session.create_edge(alix, gus, "KNOWS");
session.create_edge(alix, harm, "KNOWS");
session.create_edge(gus, harm, "KNOWS");
session.create_edge(alix, techcorp, "WORKS_AT");
session.create_edge(gus, techcorp, "WORKS_AT");
db
}
#[test]
fn test_unwind_literal_list() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("UNWIND [1, 2, 3] AS x RETURN x").unwrap();
assert_eq!(result.rows().len(), 3);
let values: Vec<&Value> = result.rows().iter().map(|r| &r[0]).collect();
assert!(values.contains(&&Value::Int64(1)));
assert!(values.contains(&&Value::Int64(2)));
assert!(values.contains(&&Value::Int64(3)));
}
#[test]
fn test_unwind_after_match() {
let db = create_social_network();
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Alix'}) UNWIND [10, 20] AS x RETURN n.name, x")
.unwrap();
assert_eq!(result.rows().len(), 2);
for row in result.rows() {
assert_eq!(row[0], Value::String("Alix".into()));
}
}
#[test]
fn test_unwind_with_strings() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session
.execute("UNWIND ['a', 'b', 'c'] AS letter RETURN letter")
.unwrap();
assert_eq!(result.rows().len(), 3);
let values: Vec<&Value> = result.rows().iter().map(|r| &r[0]).collect();
assert!(values.contains(&&Value::String("a".into())));
assert!(values.contains(&&Value::String("b".into())));
assert!(values.contains(&&Value::String("c".into())));
}
#[cfg(feature = "cypher")]
#[test]
fn test_unwind_create() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute_cypher("UNWIND [1, 2, 3] AS x CREATE (:Number {val: x})")
.unwrap();
let result = session
.execute("MATCH (n:Number) RETURN n.val ORDER BY n.val")
.unwrap();
assert_eq!(result.rows().len(), 3);
assert_eq!(result.rows()[0][0], Value::Int64(1));
assert_eq!(result.rows()[1][0], Value::Int64(2));
assert_eq!(result.rows()[2][0], Value::Int64(3));
}
#[test]
fn test_unwind_create_map_property_access() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute(
"UNWIND [{id: 'u1', name: 'Gus'}, {id: 'u2', name: 'Harm'}] AS props \
CREATE (:Test {id: props.id, name: props.name})",
)
.unwrap();
let result = session
.execute("MATCH (n:Test) RETURN n.id AS id, n.name AS name ORDER BY n.id")
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][0], Value::String("u1".into()));
assert_eq!(result.rows()[0][1], Value::String("Gus".into()));
assert_eq!(result.rows()[1][0], Value::String("u2".into()));
assert_eq!(result.rows()[1][1], Value::String("Harm".into()));
}
#[test]
fn test_unwind_param_create_map_property_access() {
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::sync::Arc;
use grafeo_common::types::PropertyKey;
let db = GrafeoDB::new_in_memory();
let session = db.session();
let nodes = Value::List(Arc::from(vec![
Value::Map(Arc::new(BTreeMap::from([
(PropertyKey::new("id"), Value::String("u1".into())),
(PropertyKey::new("name"), Value::String("Gus".into())),
]))),
Value::Map(Arc::new(BTreeMap::from([
(PropertyKey::new("id"), Value::String("u2".into())),
(PropertyKey::new("name"), Value::String("Harm".into())),
]))),
]));
let params = HashMap::from([("nodes".to_string(), nodes)]);
session
.execute_with_params(
"UNWIND $nodes AS props CREATE (:Test {id: props.id, name: props.name})",
params,
)
.unwrap();
let result = session
.execute("MATCH (n:Test) RETURN n.id AS id, n.name AS name ORDER BY n.id")
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][0], Value::String("u1".into()));
assert_eq!(result.rows()[0][1], Value::String("Gus".into()));
assert_eq!(result.rows()[1][0], Value::String("u2".into()));
assert_eq!(result.rows()[1][1], Value::String("Harm".into()));
}
#[test]
fn test_for_with_ordinality() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session
.execute("FOR x IN [10, 20, 30] WITH ORDINALITY i RETURN x, i")
.unwrap();
assert_eq!(result.rows().len(), 3);
let mut pairs: Vec<(i64, i64)> = result
.rows()
.iter()
.map(|r| {
let x = match &r[0] {
Value::Int64(v) => *v,
_ => panic!("Expected Int64 for x"),
};
let i = match &r[1] {
Value::Int64(v) => *v,
_ => panic!("Expected Int64 for i"),
};
(x, i)
})
.collect();
pairs.sort_unstable();
assert_eq!(pairs, vec![(10, 1), (20, 2), (30, 3)]);
}
#[test]
fn test_for_with_offset() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session
.execute("FOR x IN [10, 20, 30] WITH OFFSET idx RETURN x, idx")
.unwrap();
assert_eq!(result.rows().len(), 3);
let mut pairs: Vec<(i64, i64)> = result
.rows()
.iter()
.map(|r| {
let x = match &r[0] {
Value::Int64(v) => *v,
_ => panic!("Expected Int64 for x"),
};
let idx = match &r[1] {
Value::Int64(v) => *v,
_ => panic!("Expected Int64 for idx"),
};
(x, idx)
})
.collect();
pairs.sort_unstable();
assert_eq!(pairs, vec![(10, 0), (20, 1), (30, 2)]);
}
#[test]
fn test_for_without_ordinality_or_offset() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("FOR x IN [1, 2, 3] RETURN x").unwrap();
assert_eq!(result.rows().len(), 3);
}
#[test]
fn test_merge_creates_when_not_exists() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session
.execute("MERGE (n:Animal {species: 'Cat'}) RETURN n.species")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Cat".into()));
assert_eq!(db.node_count(), 1);
}
#[test]
fn test_merge_matches_existing() {
let db = create_social_network();
let session = db.session();
let before = db.node_count();
let result = session
.execute("MERGE (n:Person {name: 'Alix'}) RETURN n.name")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(db.node_count(), before);
}
#[test]
fn test_merge_on_create_set() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session
.execute(
"MERGE (n:Person {name: 'NewGuy'}) ON CREATE SET n.created = true RETURN n.name, n.created",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("NewGuy".into()));
assert_eq!(result.rows()[0][1], Value::Bool(true));
}
#[test]
fn test_merge_on_match_set() {
let db = create_social_network();
let session = db.session();
let result = session
.execute(
"MERGE (n:Person {name: 'Alix'}) ON MATCH SET n.found = true RETURN n.name, n.found",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[0][1], Value::Bool(true));
}
#[test]
fn test_create_node_with_list_property() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("CREATE (:Tag {names: ['rust', 'graph', 'db']})")
.unwrap();
let result = session.execute("MATCH (t:Tag) RETURN t.names").unwrap();
assert_eq!(result.rows().len(), 1);
match &result.rows()[0][0] {
Value::List(items) => assert_eq!(items.len(), 3),
other => panic!("expected list, got {:?}", other),
}
}
#[cfg(feature = "cypher")]
#[test]
fn test_create_edge_named_variable() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
CREATE (a)-[r:LIKES]->(b) RETURN type(r)",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("LIKES".into()));
}
#[cfg(feature = "cypher")]
#[test]
fn test_create_edge_anonymous() {
let db = create_social_network();
let session = db.session();
let edges_before = db.edge_count();
session
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
CREATE (a)-[:LIKES]->(b)",
)
.unwrap();
assert_eq!(db.edge_count(), edges_before + 1);
}
#[test]
fn test_create_path_with_new_nodes() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let paris = session
.create_node_with_props(&["City"], [("name", Value::String("Paris".into()))])
.unwrap();
let france = session
.create_node_with_props(&["Country"], [("name", Value::String("France".into()))])
.unwrap();
session.create_edge(paris, france, "IN");
assert_eq!(db.node_count(), 2);
assert_eq!(db.edge_count(), 1);
let result = session
.execute("MATCH (c:City)-[:IN]->(co:Country) RETURN c.name, co.name")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Paris".into()));
assert_eq!(result.rows()[0][1], Value::String("France".into()));
}
#[test]
fn test_delete_node_by_match() {
let db = create_social_network();
let session = db.session();
let before = db.node_count();
session
.execute("MATCH (n:Person) WHERE n.name = 'Harm' DETACH DELETE n")
.unwrap();
assert!(db.node_count() < before);
}
#[test]
fn test_delete_node_reduces_count() {
let db = create_social_network();
let before = db.node_count();
let session = db.session();
session
.execute("MATCH (n:Company {name: 'TechCorp'}) DETACH DELETE n")
.unwrap();
assert_eq!(db.node_count(), before - 1);
}
#[test]
fn test_detach_delete_all_nodes() {
let db = create_social_network();
let session = db.session();
session.execute("MATCH (n) DETACH DELETE n").unwrap();
assert_eq!(db.node_count(), 0);
assert_eq!(db.edge_count(), 0);
}
#[test]
fn test_set_property_literal() {
let db = create_social_network();
let session = db.session();
session
.execute("MATCH (n:Person {name: 'Alix'}) SET n.age = 31")
.unwrap();
let result = session
.execute("MATCH (n:Person {name: 'Alix'}) RETURN n.age")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(31));
}
#[test]
fn test_set_property_string() {
let db = create_social_network();
let session = db.session();
session
.execute("MATCH (n:Person {name: 'Gus'}) SET n.city = 'Berlin'")
.unwrap();
let result = session
.execute("MATCH (n:Person {name: 'Gus'}) RETURN n.city")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Berlin".into()));
}
#[test]
fn test_add_label_via_set() {
let db = create_social_network();
let session = db.session();
session
.execute("MATCH (n:Person {name: 'Alix'}) SET n:Employee")
.unwrap();
let result = session.execute("MATCH (n:Employee) RETURN n.name").unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn test_remove_label() {
let db = create_social_network();
let session = db.session();
session
.execute("MATCH (n:Person {name: 'Alix'}) SET n:Temp")
.unwrap();
let result = session.execute("MATCH (n:Temp) RETURN n.name").unwrap();
assert_eq!(result.rows().len(), 1);
session
.execute("MATCH (n:Person {name: 'Alix'}) REMOVE n:Temp")
.unwrap();
let result = session.execute("MATCH (n:Temp) RETURN n.name").unwrap();
assert!(result.rows().is_empty());
}
#[test]
fn test_set_label_preserves_variable_binding() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let r = session
.execute("MATCH (n:Person {name: 'Alix'}) SET n:Employee RETURN n.name")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn test_remove_label_preserves_variable_binding() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:Person:Employee {name: 'Alix'})")
.unwrap();
let r = session
.execute("MATCH (n:Person {name: 'Alix'}) REMOVE n:Employee RETURN n.name")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn test_count_star_after_set_label() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:Node {id: '1'})").unwrap();
session.execute("INSERT (:Node {id: '2'})").unwrap();
session.execute("INSERT (:Node {id: '3'})").unwrap();
let r = session
.execute("MATCH (n:Node) SET n:Tagged RETURN count(*) AS cnt")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(3));
}
#[test]
fn test_set_label_then_set_property() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session
.execute("MATCH (n:Person {name: 'Alix'}) SET n:Employee SET n.role = 'Engineer'")
.unwrap();
let r = session
.execute("MATCH (n:Employee {name: 'Alix'}) RETURN n.role")
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Engineer".into()));
}
#[test]
#[cfg(feature = "cypher")]
fn test_match_create_edge_no_phantom_nodes() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute_cypher("CREATE (:Person {name: 'Alix'})")
.unwrap();
session
.execute_cypher("CREATE (:Person {name: 'Gus'})")
.unwrap();
let before = session
.execute_cypher("MATCH (n) RETURN count(n) AS cnt")
.unwrap();
let count_before = match &before.rows()[0][0] {
Value::Int64(n) => *n,
other => panic!("expected Int64, got {other:?}"),
};
session
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) CREATE (a)-[:KNOWS]->(b)",
)
.unwrap();
let after = session
.execute_cypher("MATCH (n) RETURN count(n) AS cnt")
.unwrap();
assert_eq!(
after.rows()[0][0],
Value::Int64(count_before),
"phantom nodes created"
);
let edges = session
.execute_cypher("MATCH ()-[r:KNOWS]->() RETURN count(r) AS cnt")
.unwrap();
assert_eq!(edges.rows()[0][0], Value::Int64(1));
}
#[test]
fn test_optional_match_with_results() {
let db = create_social_network();
let session = db.session();
let result = session
.execute(
"MATCH (a:Person {name: 'Alix'}) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
RETURN a.name, c.name",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[0][1], Value::String("TechCorp".into()));
}
#[test]
fn test_optional_match_null_when_missing() {
let db = create_social_network();
let session = db.session();
let result = session
.execute(
"MATCH (a:Person {name: 'Harm'}) \
OPTIONAL MATCH (a)-[:MANAGES]->(c:Company) \
RETURN a, c",
)
.unwrap();
assert_eq!(
result.rows().len(),
1,
"OPTIONAL MATCH should return 1 row even when right side has no matches"
);
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_where_right_side_preserves_left() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
WHERE c.name = 'TechCorp' \
RETURN a.name, c.name \
ORDER BY a.name",
)
.unwrap();
assert_eq!(
result.rows().len(),
3,
"All persons should be preserved; WHERE on right side should not filter left rows"
);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[0][1], Value::String("TechCorp".into()));
assert_eq!(result.rows()[1][0], Value::String("Gus".into()));
assert_eq!(result.rows()[1][1], Value::String("TechCorp".into()));
assert_eq!(result.rows()[2][0], Value::String("Harm".into()));
assert_eq!(result.rows()[2][1], Value::Null);
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_where_left_side_filters_correctly() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
WHERE a.city = 'NYC' \
RETURN a.name, c.name \
ORDER BY a.name",
)
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[1][0], Value::String("Gus".into()));
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_where_mixed_predicates() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
WHERE a.city = 'NYC' AND c.name = 'TechCorp' \
RETURN a.name, c.name \
ORDER BY a.name",
)
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[0][1], Value::String("TechCorp".into()));
assert_eq!(result.rows()[1][0], Value::String("Gus".into()));
assert_eq!(result.rows()[1][1], Value::String("TechCorp".into()));
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_where_right_property_no_match() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
WHERE c.name = 'NonExistent' \
RETURN a.name, c.name \
ORDER BY a.name",
)
.unwrap();
assert_eq!(
result.rows().len(),
3,
"All persons preserved with NULL when no right match passes the filter"
);
for row in result.rows() {
assert_eq!(row[1], Value::Null);
}
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_where_cross_side_predicate() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:KNOWS]->(b:Person) \
WHERE b.age > a.age \
RETURN a.name, b.name \
ORDER BY a.name, b.name",
)
.unwrap();
assert!(
result.rows().len() >= 3,
"Expected at least 3 rows: Alix+Harm, Gus+Harm, Harm+null, got {}",
result.rows().len()
);
let harm_rows: Vec<_> = result
.rows()
.iter()
.filter(|r| r[0] == Value::String("Harm".into()))
.collect();
assert!(
!harm_rows.is_empty(),
"Harm should appear (no outgoing KNOWS edges, left join preserves)"
);
assert_eq!(
harm_rows[0][1],
Value::Null,
"Harm has no outgoing KNOWS edges, right side should be NULL"
);
}
#[test]
fn test_optional_match_gql_where_pushdown() {
let db = create_social_network();
let session = db.session();
let result = session
.execute(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
WHERE c.name = 'TechCorp' \
RETURN a.name, c.name \
ORDER BY a.name",
)
.unwrap();
assert_eq!(
result.rows().len(),
3,
"GQL: all persons preserved with right-side WHERE pushdown"
);
assert_eq!(result.rows()[2][0], Value::String("Harm".into()));
assert_eq!(result.rows()[2][1], Value::Null);
}
#[cfg(feature = "cypher")]
#[test]
fn test_chained_optional_matches() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person {name: 'Harm'}) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
OPTIONAL MATCH (a)-[:KNOWS]->(b:Person) \
RETURN a.name, c.name, b.name",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Harm".into()));
assert_eq!(result.rows()[0][1], Value::Null);
assert_eq!(result.rows()[0][2], Value::Null);
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_is_null_check() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:MANAGES]->(c) \
RETURN a.name, c IS NULL AS no_manages \
ORDER BY a.name",
)
.unwrap();
assert_eq!(result.rows().len(), 3);
for row in result.rows() {
assert_eq!(row[1], Value::Bool(true));
}
}
#[cfg(feature = "cypher")]
#[test]
fn test_chained_independent_optional_matches_mixed() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
OPTIONAL MATCH (a)-[:MANAGES]->(m) \
RETURN a.name, c.name, m",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[0][1], Value::String("TechCorp".into()));
assert_eq!(result.rows()[0][2], Value::Null);
}
#[cfg(feature = "cypher")]
#[test]
fn test_chained_dependent_optional_matches() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person {name: 'Harm'}) \
OPTIONAL MATCH (a)-[:KNOWS]->(b:Person) \
OPTIONAL MATCH (b)-[:WORKS_AT]->(c:Company) \
RETURN a.name, b.name, c.name",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Harm".into()));
assert_eq!(result.rows()[0][1], Value::Null);
assert_eq!(result.rows()[0][2], Value::Null);
}
#[cfg(feature = "cypher")]
#[test]
fn test_chained_dependent_optional_partial_match() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}) \
OPTIONAL MATCH (a)-[:KNOWS]->(b:Person) \
OPTIONAL MATCH (b)-[:WORKS_AT]->(c:Company) \
RETURN a.name, b.name, c.name \
ORDER BY b.name",
)
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][1], Value::String("Gus".into()));
assert_eq!(result.rows()[0][2], Value::String("TechCorp".into()));
assert_eq!(result.rows()[1][1], Value::String("Harm".into()));
assert_eq!(result.rows()[1][2], Value::Null);
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_with_node_label_filter() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute_cypher(
"CREATE (a:Person {name: 'Alix'}), \
(b:Person:Developer {name: 'Gus'}), \
(c:Person {name: 'Harm'}), \
(a)-[:KNOWS]->(b), \
(a)-[:KNOWS]->(c)",
)
.unwrap();
let result = session
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}) \
OPTIONAL MATCH (a)-[:KNOWS]->(m:Developer) \
RETURN a.name, m.name",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][1], Value::String("Gus".into()));
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_with_vle() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute_cypher(
"CREATE (a:Person {name: 'Alix'}), \
(b:Person {name: 'Gus'}), \
(c:Person {name: 'Harm'}), \
(d:Person {name: 'Jules'}), \
(a)-[:KNOWS]->(b), \
(b)-[:KNOWS]->(c)",
)
.unwrap();
let result = session
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}) \
OPTIONAL MATCH (a)-[:KNOWS*1..2]->(m:Person) \
RETURN a.name, m.name \
ORDER BY m.name",
)
.unwrap();
assert!(
result.rows().len() >= 2,
"Should find at least Gus and Harm via VLE, got {}",
result.rows().len()
);
let names: Vec<_> = result
.rows()
.iter()
.filter_map(|r| match &r[1] {
Value::String(s) => Some(s.as_str().to_owned()),
_ => None,
})
.collect();
assert!(names.contains(&"Gus".to_owned()));
assert!(names.contains(&"Harm".to_owned()));
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_with_vle_no_path() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute_cypher(
"CREATE (a:Person {name: 'Alix'}), \
(b:Person {name: 'Gus'})",
)
.unwrap();
let result = session
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}) \
OPTIONAL MATCH (a)-[:KNOWS*1..3]->(m:Person) \
RETURN a.name, m.name",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[0][1], Value::Null);
}
#[test]
fn test_chained_independent_optional_gql() {
let db = create_social_network();
let session = db.session();
let result = session
.execute(
"MATCH (a:Person {name: 'Alix'}) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
OPTIONAL MATCH (a)-[:MANAGES]->(m) \
RETURN a.name, c.name, m",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[0][1], Value::String("TechCorp".into()));
assert_eq!(result.rows()[0][2], Value::Null);
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_is_not_null() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
RETURN a.name, c IS NOT NULL AS has_job \
ORDER BY a.name",
)
.unwrap();
assert_eq!(result.rows().len(), 3);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[0][1], Value::Bool(true));
assert_eq!(result.rows()[1][0], Value::String("Gus".into()));
assert_eq!(result.rows()[1][1], Value::Bool(true));
assert_eq!(result.rows()[2][0], Value::String("Harm".into()));
assert_eq!(result.rows()[2][1], Value::Bool(false));
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_coalesce() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
RETURN a.name, COALESCE(c.name, 'unemployed') AS employer \
ORDER BY a.name",
)
.unwrap();
assert_eq!(result.rows().len(), 3);
assert_eq!(result.rows()[0][1], Value::String("TechCorp".into()));
assert_eq!(result.rows()[1][1], Value::String("TechCorp".into()));
assert_eq!(result.rows()[2][1], Value::String("unemployed".into()));
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_case_when() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
RETURN a.name, \
CASE WHEN c IS NOT NULL THEN c.name ELSE 'none' END AS employer \
ORDER BY a.name",
)
.unwrap();
assert_eq!(result.rows().len(), 3);
assert_eq!(result.rows()[0][1], Value::String("TechCorp".into()));
assert_eq!(result.rows()[1][1], Value::String("TechCorp".into()));
assert_eq!(result.rows()[2][1], Value::String("none".into()));
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_count_star_vs_count_expr() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
RETURN COUNT(*) AS total, COUNT(c.name) AS with_job",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(3));
assert_eq!(result.rows()[0][1], Value::Int64(2));
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_collect_skips_nulls() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
RETURN COLLECT(c.name) AS employers",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
match &result.rows()[0][0] {
Value::List(list) => {
assert_eq!(
list.len(),
2,
"COLLECT should have 2 items (no NULLs), got {:?}",
list
);
for item in list.iter() {
assert_ne!(*item, Value::Null, "COLLECT should not contain NULL values");
}
}
other => panic!("Expected List from COLLECT, got {:?}", other),
}
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_sum_skips_nulls() {
let db = create_social_network();
let session = db.session();
session
.execute_cypher("MATCH (c:Company {name: 'TechCorp'}) SET c.headcount = 100")
.unwrap();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company) \
RETURN SUM(c.headcount) AS total_headcount",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(200));
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_count_per_group() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:KNOWS]->(b:Person) \
RETURN a.name AS person, COUNT(b.name) AS friend_count \
ORDER BY person",
)
.unwrap();
assert_eq!(result.rows().len(), 3);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[0][1], Value::Int64(2));
assert_eq!(result.rows()[1][0], Value::String("Gus".into()));
assert_eq!(result.rows()[1][1], Value::Int64(1));
assert_eq!(result.rows()[2][0], Value::String("Harm".into()));
assert_eq!(result.rows()[2][1], Value::Int64(0));
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_boolean_null_comparison() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute_cypher(
"CREATE (a:Person {name: 'Alix', active: true}), \
(b:Person {name: 'Gus'}), \
(a)-[:KNOWS]->(b)",
)
.unwrap();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:KNOWS]->(m:Person) \
WHERE m.active = true \
RETURN a.name, m.name \
ORDER BY a.name",
)
.unwrap();
assert_eq!(
result.rows().len(),
2,
"Both persons preserved by left join"
);
for row in result.rows() {
assert_eq!(
row[1],
Value::Null,
"No match for active=true, right side should be NULL"
);
}
}
#[cfg(feature = "cypher")]
#[test]
fn test_optional_match_where_cross_predicate_preserves_null_rows() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute_cypher(
"CREATE (a:Person {name: 'Alix', age: 30}), \
(b:Person {name: 'Gus', age: 25}), \
(c:Person {name: 'Harm', age: 40}), \
(a)-[:KNOWS]->(b)",
)
.unwrap();
let result = session
.execute_cypher(
"MATCH (a:Person) \
OPTIONAL MATCH (a)-[:KNOWS]->(b:Person) \
WHERE b.age < a.age \
RETURN a.name, b.name \
ORDER BY a.name",
)
.unwrap();
assert_eq!(
result.rows().len(),
3,
"Cross-predicate WHERE must not eliminate unmatched left rows, got {} rows",
result.rows().len()
);
let alix_row = result
.rows()
.iter()
.find(|r| r[0] == Value::String("Alix".into()))
.expect("Alix row missing");
assert_eq!(
alix_row[1],
Value::String("Gus".into()),
"Alix->Gus should match (25 < 30)"
);
let gus_row = result
.rows()
.iter()
.find(|r| r[0] == Value::String("Gus".into()))
.expect("Gus row missing");
assert_eq!(
gus_row[1],
Value::Null,
"Gus has no KNOWS, right side must be NULL"
);
let harm_row = result
.rows()
.iter()
.find(|r| r[0] == Value::String("Harm".into()))
.expect("Harm row missing");
assert_eq!(
harm_row[1],
Value::Null,
"Harm has no KNOWS, right side must be NULL"
);
}
#[cfg(feature = "algos")]
#[test]
fn test_call_list_procedures() {
let db = create_social_network();
let session = db.session();
let result = session.execute("CALL grafeo.procedures()").unwrap();
assert!(!result.rows().is_empty());
}
#[cfg(feature = "algos")]
#[test]
fn test_call_degree_centrality() {
let db = create_social_network();
let session = db.session();
let result = session.execute("CALL grafeo.degree_centrality()").unwrap();
assert!(!result.rows().is_empty());
}
#[cfg(feature = "algos")]
#[test]
fn test_call_procedure_with_yield() {
let db = create_social_network();
let session = db.session();
let result = session
.execute("CALL grafeo.pagerank() YIELD node_id, score RETURN node_id, score")
.unwrap();
assert!(!result.rows().is_empty());
assert!(result.columns.len() >= 2);
}
#[test]
fn test_gql_remove_property() {
let db = create_social_network();
let session = db.session();
session
.execute("MATCH (n:Person {name: 'Alix'}) SET n.temp = 'delete_me'")
.unwrap();
let before = session
.execute("MATCH (n:Person {name: 'Alix'}) RETURN n.temp")
.unwrap();
assert_eq!(before.rows()[0][0], Value::String("delete_me".into()));
session
.execute("MATCH (n:Person {name: 'Alix'}) REMOVE n.temp")
.unwrap();
let after = session
.execute("MATCH (n:Person {name: 'Alix'}) RETURN n.temp")
.unwrap();
assert_eq!(after.rows()[0][0], Value::Null);
}
#[test]
fn test_gql_set_map_merge() {
let db = create_social_network();
let session = db.session();
session
.execute(
"MATCH (n:Person {name: 'Alix'}) SET n += {email: 'alix@example.com', active: true}",
)
.unwrap();
let result = session
.execute("MATCH (n:Person {name: 'Alix'}) RETURN n.email, n.active, n.name")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(
result.rows()[0][0],
Value::String("alix@example.com".into())
);
assert_eq!(result.rows()[0][1], Value::Bool(true));
assert_eq!(result.rows()[0][2], Value::String("Alix".into()));
}
#[test]
fn test_gql_set_multiple_labels() {
let db = create_social_network();
let session = db.session();
session
.execute("MATCH (n:Person {name: 'Gus'}) SET n:Employee:Developer")
.unwrap();
let emp = session.execute("MATCH (n:Employee) RETURN n.name").unwrap();
let dev = session
.execute("MATCH (n:Developer) RETURN n.name")
.unwrap();
assert_eq!(emp.rows().len(), 1);
assert_eq!(dev.rows().len(), 1);
assert_eq!(emp.rows()[0][0], Value::String("Gus".into()));
}
#[test]
fn test_gql_merge_with_match_input() {
let db = create_social_network();
let session = db.session();
session
.execute(
"MATCH (n:Person {name: 'Alix'}) \
MERGE (n)-[:FOLLOWS]->(t:Trend {name: 'Rust'})",
)
.unwrap();
let result = session.execute("MATCH (t:Trend) RETURN t.name").unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Rust".into()));
}
#[test]
fn test_gql_merge_in_ordered_clauses() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("MERGE (n:Config {key: 'theme'}) ON CREATE SET n.value = 'dark'")
.unwrap();
session
.execute("MERGE (n:Config {key: 'theme'}) ON MATCH SET n.value = 'light'")
.unwrap();
let result = session
.execute("MATCH (n:Config) RETURN n.key, n.value")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][1], Value::String("light".into()));
}
#[test]
fn test_gql_match_create_edge_ordered() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:City {name: 'Prague'})").unwrap();
session
.execute("INSERT (:Country {name: 'Czechia'})")
.unwrap();
session
.execute(
"MATCH (c:City {name: 'Prague'}), (co:Country {name: 'Czechia'}) \
CREATE (c)-[:IN]->(co)",
)
.unwrap();
let result = session
.execute("MATCH (c:City)-[:IN]->(co:Country) RETURN c.name, co.name")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Prague".into()));
assert_eq!(result.rows()[0][1], Value::String("Czechia".into()));
}
#[test]
fn test_gql_match_detach_delete_ordered() {
let db = create_social_network();
let session = db.session();
let before = db.node_count();
session
.execute("MATCH (n:Company {name: 'TechCorp'}) DETACH DELETE n")
.unwrap();
assert_eq!(db.node_count(), before - 1);
}
#[test]
fn test_gql_for_in_ordered_clauses() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session
.execute("FOR x IN [100, 200, 300] WITH ORDINALITY idx RETURN x, idx ORDER BY x")
.unwrap();
assert_eq!(result.rows().len(), 3);
assert_eq!(result.rows()[0][0], Value::Int64(100));
assert_eq!(result.rows()[0][1], Value::Int64(1));
assert_eq!(result.rows()[2][0], Value::Int64(300));
assert_eq!(result.rows()[2][1], Value::Int64(3));
}
#[test]
fn test_gql_ordered_create_delete() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:Temp {name: 'ephemeral'})")
.unwrap();
assert_eq!(db.node_count(), 1);
session.execute("MATCH (n:Temp) DETACH DELETE n").unwrap();
assert_eq!(db.node_count(), 0);
}
#[test]
fn test_traits_create_with_props_convenience() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let node = session
.create_node_with_props(
&["Widget"],
[
("color", Value::String("blue".into())),
("weight", Value::Int64(42)),
],
)
.unwrap();
let result = session
.execute("MATCH (w:Widget) RETURN w.color, w.weight")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("blue".into()));
assert_eq!(result.rows()[0][1], Value::Int64(42));
let other = session
.create_node_with_props(&["Box"], [("size", Value::Int64(10))])
.unwrap();
session.create_edge(node, other, "FITS_IN");
let edge_result = session
.execute("MATCH (w:Widget)-[:FITS_IN]->(b:Box) RETURN w.color, b.size")
.unwrap();
assert_eq!(edge_result.rows().len(), 1);
}
#[cfg(feature = "cypher")]
mod cypher_mutations {
use super::*;
#[cfg(feature = "cypher")]
#[test]
fn test_merge_relationship_creates() {
let db = create_social_network();
let session = db.session();
let edges_before = db.edge_count();
session
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Harm'}) \
MERGE (a)-[:LIKES]->(b)",
)
.unwrap();
assert_eq!(db.edge_count(), edges_before + 1);
}
#[cfg(feature = "cypher")]
#[test]
fn test_merge_relationship_matches() {
let db = create_social_network();
let session = db.session();
let edges_before = db.edge_count();
session
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
MERGE (a)-[:KNOWS]->(b)",
)
.unwrap();
assert_eq!(db.edge_count(), edges_before);
}
#[cfg(feature = "cypher")]
#[test]
fn test_merge_relationship_on_create() {
let db = create_social_network();
let session = db.session();
session
.execute_cypher(
"MATCH (a:Person {name: 'Gus'}), (b:Person {name: 'Alix'}) \
MERGE (a)-[r:MENTORS]->(b) ON CREATE SET r.since = 2025",
)
.unwrap();
let result = session
.execute_cypher(
"MATCH (a:Person {name: 'Gus'})-[r:MENTORS]->(b:Person {name: 'Alix'}) \
RETURN r.since",
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(2025));
}
#[cfg(feature = "cypher")]
#[test]
fn test_delete_without_detach_connected_node() {
let db = create_social_network();
let session = db.session();
let result = session.execute_cypher("MATCH (n:Person {name: 'Alix'}) DELETE n");
assert!(
result.is_err(),
"DELETE without DETACH on connected node should fail"
);
}
#[cfg(feature = "cypher")]
#[test]
fn test_unwind_standalone_create() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute_cypher(
"UNWIND [{name: 'Alix', age: 30}, {name: 'Gus', age: 25}] AS props \
CREATE (n:Person) SET n.name = props.name, n.age = props.age",
)
.unwrap();
let result = session
.execute("MATCH (n:Person) RETURN n.name ORDER BY n.name")
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[1][0], Value::String("Gus".into()));
}
#[cfg(feature = "cypher")]
#[test]
fn test_set_map_replace() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:Item {name: 'Widget', price: 10, color: 'red'})")
.unwrap();
session
.execute_cypher("MATCH (n:Item) SET n = {name: 'Gadget', price: 20}")
.unwrap();
let result = session
.execute("MATCH (n:Item) RETURN n.name, n.price, n.color")
.unwrap();
assert_eq!(result.rows()[0][0], Value::String("Gadget".into()));
assert_eq!(result.rows()[0][1], Value::Int64(20));
assert_eq!(result.rows()[0][2], Value::Null);
}
#[cfg(feature = "cypher")]
#[test]
fn test_set_map_merge() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:Item {name: 'Widget', price: 10})")
.unwrap();
session
.execute_cypher("MATCH (n:Item) SET n += {color: 'blue', price: 15}")
.unwrap();
let result = session
.execute("MATCH (n:Item) RETURN n.name, n.price, n.color")
.unwrap();
assert_eq!(result.rows()[0][0], Value::String("Widget".into()));
assert_eq!(result.rows()[0][1], Value::Int64(15));
assert_eq!(result.rows()[0][2], Value::String("blue".into()));
}
#[cfg(feature = "cypher")]
#[test]
fn test_foreach_set_property() {
let db = create_social_network();
let session = db.session();
session
.execute_cypher(
"MATCH (n:Person) \
FOREACH (val IN [1] | SET n.tagged = true)",
)
.unwrap();
let result = session
.execute("MATCH (n:Person) WHERE n.tagged = true RETURN n.name")
.unwrap();
assert_eq!(result.rows().len(), 3);
}
#[cfg(feature = "cypher")]
#[test]
fn test_multi_pattern_shared_vars() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (n:Person)-[:KNOWS]->(m:Person), (n)-[:WORKS_AT]->(c:Company) \
RETURN DISTINCT n.name, c.name \
ORDER BY n.name",
)
.unwrap();
assert!(!result.rows().is_empty());
for row in result.rows() {
assert_eq!(row[1], Value::String("TechCorp".into()));
}
}
#[cfg(feature = "cypher")]
#[test]
fn test_multi_pattern_no_shared_vars() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:A {val: 1})").unwrap();
session.execute("INSERT (:B {val: 2})").unwrap();
let result = session
.execute_cypher("MATCH (a:A), (b:B) RETURN a.val, b.val")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(1));
assert_eq!(result.rows()[0][1], Value::Int64(2));
}
}