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)),
],
)
.unwrap();
let gus = session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Gus".into())),
("age", Value::Int64(25)),
],
)
.unwrap();
let harm = session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Harm".into())),
("age", Value::Int64(35)),
],
)
.unwrap();
let techcorp = session
.create_node_with_props(
&["Company"],
[
("name", Value::String("TechCorp".into())),
("founded", Value::Int64(2010)),
],
)
.unwrap();
let startup = session
.create_node_with_props(
&["Company"],
[
("name", Value::String("Startup".into())),
("founded", Value::Int64(2020)),
],
)
.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");
session.create_edge(harm, startup, "WORKS_AT");
db
}
fn create_chain() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let a = session
.create_node_with_props(&["Node"], [("id", Value::String("A".into()))])
.unwrap();
let b = session
.create_node_with_props(&["Node"], [("id", Value::String("B".into()))])
.unwrap();
let c = session
.create_node_with_props(&["Node"], [("id", Value::String("C".into()))])
.unwrap();
let d = session
.create_node_with_props(&["Node"], [("id", Value::String("D".into()))])
.unwrap();
session.create_edge(a, b, "NEXT");
session.create_edge(b, c, "NEXT");
session.create_edge(c, d, "NEXT");
db
}
fn create_star() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let center = session
.create_node_with_props(&["Hub"], [("id", Value::String("center".into()))])
.unwrap();
for i in 0..5 {
let spoke = session
.create_node_with_props(
&["Spoke"],
[("id", Value::String(format!("spoke_{}", i).into()))],
)
.unwrap();
session.create_edge(center, spoke, "CONNECTS");
}
db
}
fn create_numeric_data() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let prices = [100, 200, 150, 300, 250];
let categories = [
"Electronics",
"Electronics",
"Clothing",
"Electronics",
"Clothing",
];
for (price, category) in prices.iter().zip(categories.iter()) {
session
.create_node_with_props(
&["Product"],
[
("price", Value::Int64(*price)),
("category", Value::String((*category).into())),
],
)
.unwrap();
}
db
}
fn create_tree() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let root = session
.create_node_with_props(
&["TreeNode"],
[
("name", Value::String("root".into())),
("level", Value::Int64(0)),
],
)
.unwrap();
let child1 = session
.create_node_with_props(
&["TreeNode"],
[
("name", Value::String("child1".into())),
("level", Value::Int64(1)),
],
)
.unwrap();
let child2 = session
.create_node_with_props(
&["TreeNode"],
[
("name", Value::String("child2".into())),
("level", Value::Int64(1)),
],
)
.unwrap();
let leaf1 = session
.create_node_with_props(
&["TreeNode"],
[
("name", Value::String("leaf1".into())),
("level", Value::Int64(2)),
],
)
.unwrap();
let leaf2 = session
.create_node_with_props(
&["TreeNode"],
[
("name", Value::String("leaf2".into())),
("level", Value::Int64(2)),
],
)
.unwrap();
session.create_edge(root, child1, "HAS_CHILD");
session.create_edge(root, child2, "HAS_CHILD");
session.create_edge(child1, leaf1, "HAS_CHILD");
session.create_edge(child1, leaf2, "HAS_CHILD");
db
}
#[cfg(feature = "gql")]
mod gql_basic_patterns {
use super::*;
#[test]
fn test_match_all_nodes() {
let db = create_social_network();
let session = db.session();
let result = session.execute("MATCH (n) RETURN n").unwrap();
assert_eq!(
result.row_count(),
5,
"Should find 5 nodes (3 people + 2 companies)"
);
}
#[test]
fn test_match_nodes_by_label() {
let db = create_social_network();
let session = db.session();
let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
assert_eq!(result.row_count(), 3, "Should find 3 Person nodes");
let result = session.execute("MATCH (n:Company) RETURN n").unwrap();
assert_eq!(result.row_count(), 2, "Should find 2 Company nodes");
}
#[test]
fn test_match_with_property_filter() {
let db = create_social_network();
let session = db.session();
let result = session
.execute("MATCH (n:Person) WHERE n.age > 28 RETURN n.name")
.unwrap();
assert_eq!(
result.row_count(),
2,
"Should find 2 people older than 28 (Alix: 30, Harm: 35)"
);
}
#[test]
fn test_match_with_equality_filter() {
let db = create_social_network();
let session = db.session();
let result = session
.execute("MATCH (n:Person) WHERE n.name = \"Alix\" RETURN n")
.unwrap();
assert_eq!(
result.row_count(),
1,
"Should find exactly 1 person named Alix"
);
}
#[test]
fn test_match_relationship_pattern() {
let db = create_social_network();
let session = db.session();
let result = session
.execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name")
.unwrap();
assert_eq!(result.row_count(), 3, "Should find 3 KNOWS relationships");
}
#[test]
fn test_return_specific_properties() {
let db = create_social_network();
let session = db.session();
let result = session
.execute("MATCH (n:Person) RETURN n.name, n.age")
.unwrap();
assert_eq!(result.row_count(), 3);
assert_eq!(result.column_count(), 2);
let names: Vec<&Value> = result.rows().iter().map(|r| &r[0]).collect();
assert!(names.contains(&&Value::String("Alix".into())));
assert!(names.contains(&&Value::String("Gus".into())));
assert!(names.contains(&&Value::String("Harm".into())));
}
#[test]
fn test_empty_result_set() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (n:NonExistent) RETURN n").unwrap();
assert_eq!(
result.row_count(),
0,
"Should return empty result for non-existent label"
);
}
#[test]
fn test_chain_traversal() {
let db = create_chain();
let session = db.session();
let result = session
.execute("MATCH (a:Node)-[:NEXT]->(b:Node) RETURN a.id, b.id")
.unwrap();
assert_eq!(
result.row_count(),
3,
"Should find 3 NEXT edges in chain A->B->C->D"
);
}
#[test]
fn test_star_hub_connections() {
let db = create_star();
let session = db.session();
let result = session
.execute("MATCH (h:Hub)-[:CONNECTS]->(s:Spoke) RETURN s")
.unwrap();
assert_eq!(result.row_count(), 5, "Hub should connect to 5 spokes");
}
}
#[cfg(feature = "gql")]
mod gql_aggregations {
use super::*;
#[test]
fn test_count_all() {
let db = create_social_network();
let session = db.session();
let result = session.execute("MATCH (n:Person) RETURN COUNT(n)").unwrap();
assert_eq!(result.row_count(), 1, "COUNT should return single row");
if let Value::Int64(count) = &result.rows()[0][0] {
assert_eq!(*count, 3, "Should count 3 people");
}
}
#[test]
fn test_count_with_filter() {
let db = create_social_network();
let session = db.session();
let result = session
.execute("MATCH (n:Person) WHERE n.age > 28 RETURN COUNT(n)")
.unwrap();
assert_eq!(result.row_count(), 1);
if let Value::Int64(count) = &result.rows()[0][0] {
assert_eq!(*count, 2, "Should count 2 people older than 28");
}
}
#[test]
fn test_sum() {
let db = create_numeric_data();
let session = db.session();
let result = session
.execute("MATCH (p:Product) RETURN SUM(p.price)")
.unwrap();
assert_eq!(result.row_count(), 1);
match &result.rows()[0][0] {
Value::Int64(sum) => assert_eq!(*sum, 1000, "Sum of all prices should be 1000"),
Value::Float64(sum) => assert!(
(sum - 1000.0).abs() < 0.001,
"Sum of all prices should be 1000"
),
_ => panic!("Expected numeric result for SUM"),
}
}
#[test]
fn test_min_max() {
let db = create_numeric_data();
let session = db.session();
let result = session
.execute("MATCH (p:Product) RETURN MIN(p.price)")
.unwrap();
assert_eq!(result.row_count(), 1);
match &result.rows()[0][0] {
Value::Int64(min) => assert_eq!(*min, 100, "Min price should be 100"),
Value::Null => {} other => panic!("Unexpected value for MIN: {:?}", other),
}
let result = session
.execute("MATCH (p:Product) RETURN MAX(p.price)")
.unwrap();
assert_eq!(result.row_count(), 1);
match &result.rows()[0][0] {
Value::Int64(max) => assert_eq!(*max, 300, "Max price should be 300"),
Value::Null => {} other => panic!("Unexpected value for MAX: {:?}", other),
}
}
#[test]
fn test_count_empty_result() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session
.execute("MATCH (n:NonExistent) RETURN COUNT(n)")
.unwrap();
assert_eq!(result.row_count(), 1);
if let Value::Int64(count) = &result.rows()[0][0] {
assert_eq!(*count, 0, "Count of non-existent nodes should be 0");
}
}
}
#[cfg(feature = "gql")]
mod gql_joins {
use super::*;
#[test]
fn test_two_hop_path() {
let db = create_social_network();
let session = db.session();
let result = session
.execute("MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person) RETURN a.name, b.name, c.name")
.unwrap();
assert_eq!(
result.row_count(),
1,
"Should find exactly one 2-hop path: Alix->Gus->Harm"
);
}
#[test]
fn test_multi_pattern_match() {
let db = create_social_network();
let session = db.session();
let result = session
.execute("MATCH (a:Person)-[:KNOWS]->(b:Person), (a)-[:WORKS_AT]->(c:Company) RETURN a.name, b.name, c.name")
.unwrap();
assert_eq!(
result.row_count(),
3,
"Should find exactly 3 combined patterns"
);
}
#[test]
fn test_tree_parent_child() {
let db = create_tree();
let session = db.session();
let result = session
.execute("MATCH (parent:TreeNode)-[:HAS_CHILD]->(child:TreeNode) RETURN parent.name, child.name")
.unwrap();
assert_eq!(
result.row_count(),
4,
"Should find 4 parent-child relationships"
);
}
#[test]
fn test_chain_full_traversal() {
let db = create_chain();
let session = db.session();
let result = session
.execute("MATCH (a:Node)-[:NEXT]->(b:Node)-[:NEXT]->(c:Node)-[:NEXT]->(d:Node) RETURN a.id, d.id")
.unwrap();
assert_eq!(
result.row_count(),
1,
"Should find exactly one full chain path"
);
}
}
#[cfg(feature = "gql")]
mod gql_mutations {
use super::*;
#[test]
fn test_insert_node() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
let result = session
.execute("MATCH (n:Person) RETURN n.name, n.age")
.unwrap();
assert_eq!(result.row_count(), 1, "Should have 1 Person node");
assert!(
result.rows()[0]
.iter()
.any(|v| *v == Value::String("Alix".into())),
"Should find Alix in result"
);
}
#[test]
fn test_insert_multiple_nodes() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.execute("INSERT (:Person {name: 'Gus'})").unwrap();
session.execute("INSERT (:Person {name: 'Harm'})").unwrap();
let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
assert_eq!(result.row_count(), 3, "Should have 3 Person nodes");
}
#[test]
fn test_transaction_commit() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
session.begin_transaction().unwrap();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.commit().unwrap();
let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
assert_eq!(result.row_count(), 1, "Node should exist after commit");
}
}
#[cfg(feature = "cypher")]
mod cypher_tests {
use super::*;
#[test]
fn test_match_all_nodes() {
let db = create_social_network();
let session = db.session();
let result = session.execute_cypher("MATCH (n) RETURN n").unwrap();
assert_eq!(result.row_count(), 5, "Should find 5 nodes");
}
#[test]
fn test_match_nodes_by_label() {
let db = create_social_network();
let session = db.session();
let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
assert_eq!(result.row_count(), 3, "Should find 3 Person nodes");
}
#[test]
fn test_match_with_property_filter() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher("MATCH (n:Person) WHERE n.age > 28 RETURN n.name")
.unwrap();
assert_eq!(result.row_count(), 2, "Should find 2 people older than 28");
}
#[test]
fn test_match_relationship_pattern() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name")
.unwrap();
assert_eq!(result.row_count(), 3, "Should find 3 KNOWS relationships");
}
#[test]
fn test_count() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher("MATCH (n:Person) RETURN count(n)")
.unwrap();
assert_eq!(result.row_count(), 1);
if let Value::Int64(count) = &result.rows()[0][0] {
assert_eq!(*count, 3);
}
}
#[test]
fn test_create_node() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute_cypher("CREATE (:Person {name: 'Alix', age: 30})")
.unwrap();
let result = session
.execute_cypher("MATCH (n:Person) RETURN n.name, n.age")
.unwrap();
assert_eq!(result.row_count(), 1);
}
#[test]
fn test_two_hop_path() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher("MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person) RETURN a.name, b.name, c.name")
.unwrap();
assert_eq!(
result.row_count(),
1,
"Should find exactly one 2-hop path: Alix->Gus->Harm"
);
}
#[test]
fn test_cypher_exists_subquery_basic() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (n:Person) WHERE EXISTS { MATCH (n)-[:KNOWS]->() } RETURN n.name ORDER BY n.name",
)
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[1][0], Value::String("Gus".into()));
}
#[test]
fn test_cypher_not_exists_subquery() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (n:Person) WHERE NOT EXISTS { MATCH (n)-[:KNOWS]->() } RETURN n.name",
)
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Harm".into()));
}
#[test]
fn test_cypher_exists_with_edge_type_filter() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (n:Person) WHERE EXISTS { MATCH (n)-[:MANAGES]->() } RETURN n.name",
)
.unwrap();
assert_eq!(result.row_count(), 0);
}
#[test]
fn test_cypher_exists_combined_with_predicate() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (n:Person) WHERE EXISTS { MATCH (n)-[:KNOWS]->() } AND n.name = 'Alix' RETURN n.name",
)
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn test_cypher_exists_with_works_at() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (n:Person) WHERE EXISTS { MATCH (n)-[:WORKS_AT]->() } RETURN n.name ORDER BY n.name",
)
.unwrap();
assert_eq!(result.row_count(), 3);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[1][0], Value::String("Gus".into()));
assert_eq!(result.rows()[2][0], Value::String("Harm".into()));
}
#[test]
fn test_cypher_exists_target_side_correlation() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (n:Person) WHERE EXISTS { MATCH ()-[:KNOWS]->(n) } RETURN n.name ORDER BY n.name",
)
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][0], Value::String("Gus".into()));
assert_eq!(result.rows()[1][0], Value::String("Harm".into()));
}
#[test]
fn test_cypher_not_exists_target_side_correlation() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH (n:Person) WHERE NOT EXISTS { MATCH ()-[:KNOWS]->(n) } RETURN n.name",
)
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn test_anon_nodes_with_edge_variable() {
let db = create_social_network();
let session = db.session();
let result =
session.execute_cypher("MATCH (:Person)-[r:KNOWS]->(:Person) RETURN type(r) AS t");
assert!(
result.is_ok(),
"type(r) with anon nodes failed: {:?}",
result.err()
);
assert_eq!(result.unwrap().row_count(), 3);
let result = session.execute_cypher("MATCH (:Person)-[r:KNOWS]->(:Person) RETURN r");
assert!(
result.is_ok(),
"RETURN r with anon nodes failed: {:?}",
result.err()
);
assert_eq!(result.unwrap().row_count(), 3);
let result = session.execute_cypher("MATCH (:Person)-[r:KNOWS]->(:Person) RETURN r.since");
assert!(
result.is_ok(),
"r.since with anon nodes failed: {:?}",
result.err()
);
let result = session.execute_cypher("MATCH (:Person)-[r]->(:Person) RETURN r");
assert!(
result.is_ok(),
"RETURN r (no type) with anon nodes failed: {:?}",
result.err()
);
assert_eq!(result.unwrap().row_count(), 3);
let result = session
.execute_cypher("MATCH (:Person)-[r:KNOWS]->(:Person) WHERE r.since = 2020 RETURN r");
assert!(
result.is_ok(),
"WHERE r.since with anon nodes failed: {:?}",
result.err()
);
}
#[test]
fn test_incoming_edge_target_node_property_filter() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_cypher(
"MATCH ()-[r:KNOWS]->(o:Person {name: \"Harm\"}) RETURN count(r) AS cnt",
)
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(
result.rows()[0][0],
Value::Int64(2),
"Only edges targeting Harm should be counted, not all KNOWS edges"
);
let result = session
.execute_cypher(
"MATCH (o:Person {name: \"Harm\"})<-[r:KNOWS]-() RETURN count(r) AS cnt",
)
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(
result.rows()[0][0],
Value::Int64(2),
"Reversed arrow should match the same 2 edges"
);
let result = session
.execute_cypher("MATCH ()-[r]->(o:Person {name: \"Harm\"}) RETURN count(r) AS cnt")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(
result.rows()[0][0],
Value::Int64(2),
"Untyped edge should still filter by target node property"
);
}
}
#[cfg(feature = "gremlin")]
mod gremlin_tests {
use super::*;
#[test]
fn test_gremlin_parser() {
let db = create_social_network();
let session = db.session();
let result = session.execute_gremlin("g.V()");
assert!(
result.is_ok(),
"Gremlin g.V() should parse and execute: {result:?}"
);
}
#[test]
fn test_v_all_nodes() {
let db = create_social_network();
let session = db.session();
let result = session.execute_gremlin("g.V()").unwrap();
assert_eq!(result.row_count(), 5, "Should find 5 vertices");
}
#[test]
fn test_v_has_label() {
let db = create_social_network();
let session = db.session();
let result = session.execute_gremlin("g.V().hasLabel('Person')").unwrap();
assert_eq!(result.row_count(), 3, "Should find 3 Person vertices");
}
#[test]
fn test_v_has_property() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_gremlin("g.V().hasLabel('Person').has('age', gt(28))")
.unwrap();
assert_eq!(result.row_count(), 2, "Should find 2 people with age > 28");
}
#[test]
fn test_out_step() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_gremlin("g.V().hasLabel('Person').out('KNOWS')")
.unwrap();
assert_eq!(result.row_count(), 3, "Should find 3 outgoing KNOWS edges");
}
#[test]
fn test_values_step() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_gremlin("g.V().hasLabel('Person').values('name')")
.unwrap();
assert_eq!(result.row_count(), 3, "Should return 3 names");
}
#[test]
fn test_limit_step() {
let db = create_social_network();
let session = db.session();
let result = session.execute_gremlin("g.V().limit(2)").unwrap();
assert_eq!(result.row_count(), 2, "Should limit to 2 results");
}
#[test]
fn test_count() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_gremlin("g.V().hasLabel('Person').count()")
.unwrap();
assert_eq!(result.row_count(), 1);
if let Value::Int64(count) = &result.rows()[0][0] {
assert_eq!(*count, 3);
}
}
#[test]
fn test_sum() {
let db = create_numeric_data();
let session = db.session();
let result = session
.execute_gremlin("g.V().hasLabel('Product').values('price').sum()")
.unwrap();
assert_eq!(result.row_count(), 1);
if let Value::Int64(sum) = &result.rows()[0][0] {
assert_eq!(*sum, 1000);
}
}
#[test]
fn test_two_hop_traversal() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_gremlin("g.V().hasLabel('Person').out('KNOWS').out('KNOWS')")
.unwrap();
assert_eq!(
result.row_count(),
1,
"Should find exactly one friend-of-friend: Harm via Gus"
);
}
}
#[cfg(feature = "graphql")]
mod graphql_tests {
use super::*;
#[test]
fn test_graphql_parser() {
let db = create_social_network();
let session = db.session();
let result = session.execute_graphql("query { person { id } }");
assert!(
result.is_ok(),
"GraphQL query should parse and execute: {result:?}"
);
}
#[test]
fn test_query_all_by_type() {
let db = create_social_network();
let session = db.session();
let result = session.execute_graphql("query { person { id } }").unwrap();
assert_eq!(result.row_count(), 3, "Should find 3 Person nodes");
}
#[test]
fn test_query_with_field_selection() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_graphql("query { person { name age } }")
.unwrap();
assert_eq!(result.row_count(), 3);
}
#[test]
fn test_query_with_filter() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_graphql("query { person(filter: { age_gt: 28 }) { name } }")
.unwrap();
assert_eq!(result.row_count(), 2, "Should find 2 people with age > 28");
}
#[test]
fn test_nested_query() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_graphql("query { person { name knows { name } } }")
.unwrap();
assert_eq!(
result.row_count(),
3,
"Should return one row per Person node"
);
}
}
#[cfg(feature = "gql")]
mod gql_direction_tests {
use super::*;
fn create_directed_graph() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let a = session
.create_node_with_props(&["User"], [("name", Value::String("A".into()))])
.unwrap();
let b = session
.create_node_with_props(&["User"], [("name", Value::String("B".into()))])
.unwrap();
let c = session
.create_node_with_props(&["User"], [("name", Value::String("C".into()))])
.unwrap();
let d = session
.create_node_with_props(&["User"], [("name", Value::String("D".into()))])
.unwrap();
session.create_edge(a, b, "FOLLOWS"); session.create_edge(b, c, "FOLLOWS"); session.create_edge(d, b, "FOLLOWS");
db
}
#[test]
fn test_outgoing_edges() {
let db = create_directed_graph();
let session = db.session();
let result = session
.execute("MATCH (a:User {name: \"A\"})-[:FOLLOWS]->(b) RETURN b.name")
.unwrap();
assert_eq!(result.row_count(), 1, "A should follow 1 person");
assert_eq!(result.rows()[0][0], Value::String("B".into()));
let result = session
.execute("MATCH (b:User {name: \"B\"})-[:FOLLOWS]->(c) RETURN c.name")
.unwrap();
assert_eq!(result.row_count(), 1, "B should follow 1 person");
assert_eq!(result.rows()[0][0], Value::String("C".into()));
let result = session
.execute("MATCH (d:User {name: \"D\"})-[:FOLLOWS]->(x) RETURN x.name")
.unwrap();
assert_eq!(result.row_count(), 1, "D should follow 1 person");
let result = session
.execute("MATCH (c:User {name: \"C\"})-[:FOLLOWS]->(x) RETURN x")
.unwrap();
assert_eq!(result.row_count(), 0, "C should follow nobody");
}
#[test]
fn test_incoming_edges() {
let db = create_directed_graph();
let session = db.session();
let result = session
.execute("MATCH (b:User {name: \"B\"})<-[:FOLLOWS]-(x) RETURN x.name")
.unwrap();
assert_eq!(result.row_count(), 2, "B should be followed by 2 people");
let names: std::collections::HashSet<_> = result
.rows()
.iter()
.filter_map(|r| {
if let Value::String(s) = &r[0] {
Some(s.as_ref())
} else {
None
}
})
.collect();
assert!(names.contains("A"), "A should follow B");
assert!(names.contains("D"), "D should follow B");
let result = session
.execute("MATCH (c:User {name: \"C\"})<-[:FOLLOWS]-(x) RETURN x.name")
.unwrap();
assert_eq!(result.row_count(), 1, "C should be followed by 1 person");
assert_eq!(result.rows()[0][0], Value::String("B".into()));
let result = session
.execute("MATCH (a:User {name: \"A\"})<-[:FOLLOWS]-(x) RETURN x")
.unwrap();
assert_eq!(result.row_count(), 0, "A should have no followers");
}
#[test]
fn test_bidirectional_edges() {
let db = create_directed_graph();
let session = db.session();
let result = session
.execute("MATCH (b:User {name: \"B\"})-[:FOLLOWS]-(x) RETURN x.name")
.unwrap();
assert_eq!(
result.row_count(),
3,
"B should have 3 total FOLLOWS connections"
);
}
#[test]
fn test_chain_traversal_incoming() {
let db = create_chain();
let session = db.session();
let result = session
.execute("MATCH (d:Node {id: \"D\"})<-[:NEXT]-(c) RETURN c.id")
.unwrap();
assert_eq!(result.row_count(), 1, "D has 1 predecessor");
assert_eq!(result.rows()[0][0], Value::String("C".into()));
let result = session
.execute("MATCH (a:Node {id: \"A\"})<-[:NEXT]-(x) RETURN x")
.unwrap();
assert_eq!(result.row_count(), 0, "A has no predecessors");
}
#[test]
fn test_tree_parent_child() {
let db = create_tree();
let session = db.session();
let result = session
.execute("MATCH (r:TreeNode {name: \"root\"})-[:HAS_CHILD]->(c) RETURN c.name")
.unwrap();
assert_eq!(result.row_count(), 2, "root should have 2 children");
let result = session
.execute("MATCH (l:TreeNode {name: \"leaf1\"})<-[:HAS_CHILD]-(p) RETURN p.name")
.unwrap();
assert_eq!(result.row_count(), 1, "leaf1 should have 1 parent");
assert_eq!(result.rows()[0][0], Value::String("child1".into()));
}
#[test]
fn test_social_network_followers() {
let db = create_social_network();
let session = db.session();
let result = session
.execute("MATCH (c:Person {name: \"Harm\"})<-[:KNOWS]-(x) RETURN x.name")
.unwrap();
assert_eq!(result.row_count(), 2, "Harm should be known by 2 people");
let result = session
.execute("MATCH (b:Person {name: \"Gus\"})<-[:KNOWS]-(x) RETURN x.name")
.unwrap();
assert_eq!(result.row_count(), 1, "Gus should be known by 1 person");
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
}
}
#[cfg(all(feature = "gql", feature = "cypher"))]
mod cross_language_consistency_gql_cypher {
use super::*;
#[test]
fn test_node_count_consistency() {
let db = create_social_network();
let session = db.session();
let gql_result = session.execute("MATCH (n) RETURN n").unwrap();
let cypher_result = session.execute_cypher("MATCH (n) RETURN n").unwrap();
assert_eq!(
gql_result.row_count(),
cypher_result.row_count(),
"GQL and Cypher should return same node count"
);
}
#[test]
fn test_label_filter_consistency() {
let db = create_social_network();
let session = db.session();
let gql_result = session.execute("MATCH (n:Person) RETURN n").unwrap();
let cypher_result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
assert_eq!(
gql_result.row_count(),
cypher_result.row_count(),
"GQL and Cypher should return same Person count"
);
}
#[test]
fn test_relationship_traversal_consistency() {
let db = create_social_network();
let session = db.session();
let gql_result = session
.execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
.unwrap();
let cypher_result = session
.execute_cypher("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
.unwrap();
assert_eq!(
gql_result.row_count(),
cypher_result.row_count(),
"GQL and Cypher should return same relationship count"
);
}
#[test]
fn test_count_aggregation_consistency() {
let db = create_social_network();
let session = db.session();
let gql_result = session.execute("MATCH (n:Person) RETURN COUNT(n)").unwrap();
let cypher_result = session
.execute_cypher("MATCH (n:Person) RETURN count(n)")
.unwrap();
let gql_count = match &gql_result.rows()[0][0] {
Value::Int64(c) => *c,
_ => panic!("Expected Int64"),
};
let cypher_count = match &cypher_result.rows()[0][0] {
Value::Int64(c) => *c,
_ => panic!("Expected Int64"),
};
assert_eq!(gql_count, 3, "GQL count should be 3");
assert_eq!(cypher_count, 3, "Cypher count should be 3");
}
#[test]
fn test_sum_aggregation_consistency() {
let db = create_numeric_data();
let session = db.session();
let gql_result = session
.execute("MATCH (p:Product) RETURN SUM(p.price)")
.unwrap();
let cypher_result = session
.execute_cypher("MATCH (p:Product) RETURN sum(p.price)")
.unwrap();
let gql_sum = match &gql_result.rows()[0][0] {
Value::Int64(s) => *s,
_ => panic!("Expected Int64"),
};
let cypher_sum = match &cypher_result.rows()[0][0] {
Value::Int64(s) => *s,
_ => panic!("Expected Int64"),
};
assert_eq!(gql_sum, 1000, "GQL sum should be 1000");
assert_eq!(cypher_sum, 1000, "Cypher sum should be 1000");
}
}
#[cfg(all(feature = "gql", feature = "cypher", feature = "gremlin"))]
mod cross_language_consistency_all {
use super::*;
#[test]
fn test_node_count_with_gremlin() {
let db = create_social_network();
let session = db.session();
let gql_result = session.execute("MATCH (n) RETURN n").unwrap();
let gremlin_result = session.execute_gremlin("g.V()").unwrap();
assert_eq!(
gql_result.row_count(),
gremlin_result.row_count(),
"GQL and Gremlin should return same node count"
);
}
#[test]
fn test_label_filter_with_gremlin() {
let db = create_social_network();
let session = db.session();
let gql_result = session.execute("MATCH (n:Person) RETURN n").unwrap();
let gremlin_result = session.execute_gremlin("g.V().hasLabel('Person')").unwrap();
assert_eq!(
gql_result.row_count(),
gremlin_result.row_count(),
"GQL and Gremlin should return same Person count"
);
}
}
#[cfg(all(feature = "gql", feature = "cypher"))]
mod cross_language_mutations {
use super::*;
#[test]
fn test_insert_read_consistency() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
let result = session
.execute_cypher("MATCH (n:Person) RETURN n.name, n.age")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn test_create_read_consistency() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute_cypher("CREATE (:Person {name: 'Gus', age: 25})")
.unwrap();
let result = session
.execute("MATCH (n:Person) RETURN n.name, n.age")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Gus".into()));
}
#[test]
fn test_mixed_mutations() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session
.execute_cypher("CREATE (:Person {name: 'Gus'})")
.unwrap();
let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
assert_eq!(
result.row_count(),
2,
"Should have 2 nodes from both insert methods"
);
}
}
mod gql_in_operator {
use super::*;
fn setup_db() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
session
.execute("INSERT (:Person {name: 'Gus', age: 25})")
.unwrap();
session
.execute("INSERT (:Person {name: 'Harm', age: 35})")
.unwrap();
db
}
#[test]
fn test_in_string_list() {
let db = setup_db();
let session = db.session();
let result = session
.execute(
"MATCH (n:Person) WHERE n.name IN ['Alix', 'Gus'] RETURN n.name ORDER BY n.name",
)
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[1][0], Value::String("Gus".into()));
}
#[test]
fn test_in_integer_list() {
let db = setup_db();
let session = db.session();
let result = session
.execute("MATCH (n:Person) WHERE n.age IN [25, 35] RETURN n.name ORDER BY n.name")
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][0], Value::String("Gus".into()));
assert_eq!(result.rows()[1][0], Value::String("Harm".into()));
}
#[test]
fn test_in_single_element() {
let db = setup_db();
let session = db.session();
let result = session
.execute("MATCH (n:Person) WHERE n.name IN ['Harm'] RETURN n.name")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Harm".into()));
}
#[test]
fn test_in_no_match() {
let db = setup_db();
let session = db.session();
let result = session
.execute("MATCH (n:Person) WHERE n.name IN ['Dave', 'Eve'] RETURN n.name")
.unwrap();
assert_eq!(result.row_count(), 0);
}
#[test]
fn test_string_with_escaped_quote() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute(r"INSERT (:Person {name: 'O\'Brien'})")
.unwrap();
let result = session.execute("MATCH (n:Person) RETURN n.name").unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("O'Brien".into()));
}
}
#[cfg(feature = "gql")]
mod parameterized_queries {
use super::*;
use std::collections::HashMap;
fn setup_db() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
session
.execute("INSERT (:Person {name: 'Gus', age: 25})")
.unwrap();
session
.execute("INSERT (:Person {name: 'Harm', age: 35})")
.unwrap();
db
}
#[test]
fn test_execute_with_params_string() {
let db = setup_db();
let session = db.session();
let mut params = HashMap::new();
params.insert("name".to_string(), Value::String("Alix".into()));
let result = session
.execute_with_params(
"MATCH (n:Person) WHERE n.name = $name RETURN n.name",
params,
)
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn test_execute_with_params_integer() {
let db = setup_db();
let session = db.session();
let mut params = HashMap::new();
params.insert("min_age".to_string(), Value::Int64(28));
let result = session
.execute_with_params(
"MATCH (n:Person) WHERE n.age > $min_age RETURN n.name ORDER BY n.name",
params,
)
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[1][0], Value::String("Harm".into()));
}
#[test]
fn test_execute_with_multiple_params() {
let db = setup_db();
let session = db.session();
let mut params = HashMap::new();
params.insert("min_age".to_string(), Value::Int64(24));
params.insert("max_age".to_string(), Value::Int64(31));
let result = session
.execute_with_params(
"MATCH (n:Person) WHERE n.age >= $min_age AND n.age <= $max_age RETURN n.name ORDER BY n.name",
params,
)
.unwrap();
assert_eq!(result.row_count(), 2);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[1][0], Value::String("Gus".into()));
}
#[test]
fn test_db_level_execute_with_params() {
let db = setup_db();
let mut params = HashMap::new();
params.insert("name".to_string(), Value::String("Gus".into()));
let result = db
.execute_with_params("MATCH (n:Person) WHERE n.name = $name RETURN n.age", params)
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(25));
}
}
#[cfg(feature = "gremlin")]
mod gremlin_db_execute {
use super::*;
#[test]
fn test_db_execute_gremlin() {
let db = create_social_network();
let result = db.execute_gremlin("g.V().hasLabel('Person')").unwrap();
assert_eq!(result.row_count(), 3);
}
#[test]
fn test_db_execute_gremlin_with_params() {
let db = create_social_network();
let params = std::collections::HashMap::new();
let result = db
.execute_gremlin_with_params("g.V().hasLabel('Person').count()", params)
.unwrap();
assert_eq!(result.row_count(), 1);
}
}
#[cfg(feature = "graphql")]
mod graphql_db_execute {
use super::*;
#[test]
fn test_db_execute_graphql() {
let db = create_social_network();
let result = db.execute_graphql("query { person { name } }").unwrap();
assert_eq!(result.row_count(), 3);
}
#[test]
fn test_db_execute_graphql_with_params() {
let db = create_social_network();
let params = std::collections::HashMap::new();
let result = db
.execute_graphql_with_params("query { person { name } }", params)
.unwrap();
assert_eq!(result.row_count(), 3);
}
}
#[cfg(feature = "cypher")]
mod cypher_db_execute {
use super::*;
use std::collections::HashMap;
#[test]
fn test_db_execute_cypher() {
let db = create_social_network();
let result = db.execute_cypher("MATCH (p:Person) RETURN p").unwrap();
assert_eq!(result.row_count(), 3);
}
#[test]
fn test_db_execute_cypher_with_params() {
let db = create_social_network();
let mut params = HashMap::new();
params.insert("name".to_string(), Value::String("Alix".into()));
let result = db
.execute_cypher_with_params(
"MATCH (p:Person) WHERE p.name = $name RETURN p.name",
params,
)
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn test_cypher_exists_as_alias() {
let db = create_social_network();
let result = db
.execute_cypher("MATCH (n:Person) WHERE n.name = 'Alix' RETURN count(n) as exists")
.unwrap();
assert_eq!(result.row_count(), 1);
}
#[test]
fn test_cypher_multiple_match_clauses() {
let db = create_social_network();
let result = db
.execute_cypher(
"MATCH (a:Person) WHERE a.name = 'Alix' \
MATCH (b:Person) WHERE b.name = 'Gus' \
RETURN a.name, b.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("Gus".into()));
}
#[test]
fn test_cypher_multiple_match_with_create() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(
&["Person"],
[
("id", Value::String("src".into())),
("name", Value::String("Src".into())),
],
)
.unwrap();
session
.create_node_with_props(
&["Person"],
[
("id", Value::String("dst".into())),
("name", Value::String("Dst".into())),
],
)
.unwrap();
let mut params = HashMap::new();
params.insert("src_id".to_string(), Value::String("src".into()));
params.insert("dst_id".to_string(), Value::String("dst".into()));
let result = db
.execute_cypher_with_params(
"MATCH (src:Person) WHERE src.id = $src_id \
MATCH (dst:Person) WHERE dst.id = $dst_id \
CREATE (src)-[r:KNOWS]->(dst) \
RETURN src.name, dst.name",
params,
)
.unwrap();
assert_eq!(result.row_count(), 1);
}
#[test]
fn test_cypher_merge_relationship() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(
&["Person"],
[
("id", Value::String("a".into())),
("name", Value::String("Alix".into())),
],
)
.unwrap();
session
.create_node_with_props(
&["Person"],
[
("id", Value::String("b".into())),
("name", Value::String("Gus".into())),
],
)
.unwrap();
let result = db
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
MERGE (a)-[r:KNOWS]->(b) \
RETURN r",
)
.unwrap();
assert_eq!(result.row_count(), 1);
let result2 = db
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
MERGE (a)-[r:KNOWS]->(b) \
RETURN r",
)
.unwrap();
assert_eq!(result2.row_count(), 1);
}
#[test]
fn test_cypher_merge_relationship_with_properties() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(&["Person"], [("name", Value::String("Alix".into()))])
.unwrap();
session
.create_node_with_props(&["Person"], [("name", Value::String("Gus".into()))])
.unwrap();
let result = db
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
MERGE (a)-[r:KNOWS {id: 'edge1'}]->(b) \
RETURN r",
)
.unwrap();
assert_eq!(result.row_count(), 1);
let result2 = db
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
MERGE (a)-[r:KNOWS {id: 'edge1'}]->(b) \
RETURN r",
)
.unwrap();
assert_eq!(result2.row_count(), 1);
}
#[test]
fn test_cypher_merge_relationship_then_set() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(&["Person"], [("name", Value::String("Alix".into()))])
.unwrap();
session
.create_node_with_props(&["Person"], [("name", Value::String("Gus".into()))])
.unwrap();
let result = db
.execute_cypher(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
MERGE (a)-[r:KNOWS]->(b) \
SET r.weight = 5 \
RETURN r",
)
.unwrap();
assert_eq!(result.row_count(), 1);
}
#[test]
fn test_cypher_multi_match_with_merge_and_set() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(&["Node"], [("id", Value::String("src".into()))])
.unwrap();
session
.create_node_with_props(&["Node"], [("id", Value::String("dst".into()))])
.unwrap();
let mut params = HashMap::new();
params.insert("src_id".to_string(), Value::String("src".into()));
params.insert("dst_id".to_string(), Value::String("dst".into()));
params.insert("edge_id".to_string(), Value::String("e1".into()));
params.insert("props".to_string(), Value::String("{}".into()));
let result = db
.execute_cypher_with_params(
"MATCH (src:Node) WHERE src.id = $src_id \
MATCH (dst:Node) WHERE dst.id = $dst_id \
MERGE (src)-[r:INHERITS {id: $edge_id}]->(dst) \
SET r.properties_json = $props \
RETURN r",
params,
)
.unwrap();
assert_eq!(result.row_count(), 1);
}
#[test]
fn test_cypher_set_node_property() {
let db = GrafeoDB::new_in_memory();
db.execute_cypher("CREATE (n:Person {name: 'Alix'})")
.unwrap();
db.execute_cypher("MATCH (n:Person) WHERE n.name = 'Alix' SET n.age = 30")
.unwrap();
let result = db
.execute_cypher("MATCH (n:Person) WHERE n.name = 'Alix' RETURN n.age")
.unwrap();
assert_eq!(result.row_count(), 1);
let age = &result.rows()[0][0];
assert_eq!(age, &Value::Int64(30));
}
#[test]
fn test_cypher_set_labels() {
let db = GrafeoDB::new_in_memory();
db.execute_cypher("CREATE (n:Person {name: 'Alix'})")
.unwrap();
db.execute_cypher("MATCH (n:Person) WHERE n.name = 'Alix' SET n:Employee")
.unwrap();
let result = db
.execute_cypher("MATCH (n:Employee) RETURN n.name")
.unwrap();
assert_eq!(result.row_count(), 1);
let name = &result.rows()[0][0];
assert_eq!(name, &Value::String("Alix".into()));
}
#[test]
fn test_cypher_delete_node() {
let db = GrafeoDB::new_in_memory();
db.execute_cypher("CREATE (n:Temp {name: 'ToDelete'})")
.unwrap();
let before = db.execute_cypher("MATCH (n:Temp) RETURN n").unwrap();
assert_eq!(before.row_count(), 1);
db.execute_cypher("MATCH (n:Temp) WHERE n.name = 'ToDelete' DETACH DELETE n")
.unwrap();
let after = db.execute_cypher("MATCH (n:Temp) RETURN n").unwrap();
assert_eq!(after.row_count(), 0);
}
#[test]
fn test_cypher_detach_delete_with_edges() {
let db = GrafeoDB::new_in_memory();
db.execute_cypher("CREATE (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'})")
.unwrap();
db.execute_cypher(
"MATCH (a:Person), (b:Person) WHERE a.name = 'Alix' AND b.name = 'Gus' \
CREATE (a)-[:KNOWS]->(b)",
)
.unwrap();
db.execute_cypher("MATCH (n:Person) WHERE n.name = 'Alix' DETACH DELETE n")
.unwrap();
let result = db.execute_cypher("MATCH (n:Person) RETURN n.name").unwrap();
assert_eq!(result.row_count(), 1);
let name = &result.rows()[0][0];
assert_eq!(name, &Value::String("Gus".into()));
}
#[test]
fn test_cypher_delete_edge_variable() {
let db = GrafeoDB::new_in_memory();
db.execute_cypher("CREATE (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'})")
.unwrap();
db.execute_cypher(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
CREATE (a)-[:KNOWS]->(b)",
)
.unwrap();
let before = db
.execute_cypher("MATCH (a)-[e:KNOWS]->(b) RETURN e")
.unwrap();
assert_eq!(before.row_count(), 1, "Edge should exist before delete");
db.execute_cypher("MATCH (a)-[e:KNOWS]->(b) DELETE e")
.unwrap();
let edges_after = db
.execute_cypher("MATCH (a)-[e:KNOWS]->(b) RETURN e")
.unwrap();
assert_eq!(edges_after.row_count(), 0, "Edge should be deleted");
let nodes_after = db.execute_cypher("MATCH (n:Person) RETURN n.name").unwrap();
assert_eq!(nodes_after.row_count(), 2, "Both nodes should remain");
}
#[test]
fn test_cypher_remove_property() {
let db = GrafeoDB::new_in_memory();
db.execute_cypher("CREATE (n:Person {name: 'Alix', age: 30})")
.unwrap();
db.execute_cypher("MATCH (n:Person) WHERE n.name = 'Alix' REMOVE n.age")
.unwrap();
let result = db
.execute_cypher("MATCH (n:Person) WHERE n.name = 'Alix' RETURN n.age")
.unwrap();
assert_eq!(result.row_count(), 1);
let age = &result.rows()[0][0];
assert_eq!(age, &Value::Null);
}
#[test]
fn test_cypher_remove_label() {
let db = GrafeoDB::new_in_memory();
db.execute_cypher("CREATE (n:Person:Employee {name: 'Alix'})")
.unwrap();
db.execute_cypher("MATCH (n:Person) WHERE n.name = 'Alix' REMOVE n:Employee")
.unwrap();
let result = db.execute_cypher("MATCH (n:Employee) RETURN n").unwrap();
assert_eq!(result.row_count(), 0);
let result = db.execute_cypher("MATCH (n:Person) RETURN n.name").unwrap();
assert_eq!(result.row_count(), 1);
}
#[test]
fn test_cypher_return_count_gt_zero() {
let db = GrafeoDB::new_in_memory();
db.execute_cypher("CREATE (n:Person {name: 'Alix'})")
.unwrap();
db.execute_cypher("CREATE (n:Person {name: 'Gus'})")
.unwrap();
let result = db
.execute_cypher("MATCH (n:Person) RETURN count(n) > 0 AS exists")
.unwrap();
assert_eq!(result.row_count(), 1);
let exists = &result.rows()[0][0];
assert_eq!(exists, &Value::Bool(true));
}
#[test]
fn test_cypher_return_count_gt_zero_empty() {
let db = GrafeoDB::new_in_memory();
let result = db
.execute_cypher("MATCH (n:Ghost) RETURN count(n) > 0 AS exists")
.unwrap();
assert_eq!(result.row_count(), 1);
let exists = &result.rows()[0][0];
assert_eq!(exists, &Value::Bool(false));
}
}
#[cfg(feature = "sql-pgq")]
mod sql_pgq_db_execute {
use super::*;
#[test]
fn test_db_execute_sql() {
let db = create_social_network();
let result = db
.execute_sql(
"SELECT p.name FROM GRAPH_TABLE (MATCH (p:Person) COLUMNS (p.name AS name))",
)
.unwrap();
assert_eq!(result.row_count(), 3);
}
#[test]
fn test_db_execute_sql_with_params() {
let db = create_social_network();
let params = std::collections::HashMap::new();
let result = db
.execute_sql_with_params(
"SELECT p.name FROM GRAPH_TABLE (MATCH (p:Person) COLUMNS (p.name AS name))",
params,
)
.unwrap();
assert_eq!(result.row_count(), 3);
}
}
#[cfg(feature = "gql")]
mod profile_tests {
use grafeo_common::types::Value;
use grafeo_engine::GrafeoDB;
#[test]
fn gql_profile_returns_single_profile_column() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
session
.execute("INSERT (:Person {name: 'Gus', age: 25})")
.unwrap();
let result = session
.execute("PROFILE MATCH (n:Person) RETURN n.name")
.unwrap();
assert_eq!(result.columns, vec!["profile"]);
assert_eq!(result.row_count(), 1);
let profile_text = match &result.rows()[0][0] {
Value::String(s) => s.to_string(),
other => panic!("Expected String, got {other:?}"),
};
assert!(
profile_text.contains("Return") || profile_text.contains("Project"),
"Profile should contain Return or Project operator, got: {profile_text}"
);
assert!(
profile_text.contains("Scan"),
"Profile should contain a scan operator, got: {profile_text}"
);
assert!(
profile_text.contains("rows="),
"Profile should contain row counts, got: {profile_text}"
);
assert!(
profile_text.contains("time="),
"Profile should contain timing, got: {profile_text}"
);
assert!(
profile_text.contains("Total time:"),
"Profile should contain total time, got: {profile_text}"
);
}
#[test]
fn gql_profile_row_counts_are_accurate() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
for i in 0..5 {
session
.execute(&format!("INSERT (:Person {{name: 'P{i}', age: {i}0}})"))
.unwrap();
}
let result = session
.execute("PROFILE MATCH (n:Person) RETURN n.name")
.unwrap();
let profile_text = match &result.rows()[0][0] {
Value::String(s) => s.to_string(),
other => panic!("Expected String, got {other:?}"),
};
assert!(
profile_text.contains("rows=5"),
"Profile should show 5 rows for 5 Person nodes, got: {profile_text}"
);
}
#[cfg(feature = "cypher")]
#[test]
fn cypher_profile_works() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let result = session
.execute_cypher("PROFILE MATCH (n:Person) RETURN n.name")
.unwrap();
assert_eq!(result.columns, vec!["profile"]);
assert_eq!(result.row_count(), 1);
let profile_text = match &result.rows()[0][0] {
Value::String(s) => s.to_string(),
other => panic!("Expected String, got {other:?}"),
};
assert!(
profile_text.contains("rows="),
"Cypher PROFILE should work, got: {profile_text}"
);
}
}
#[cfg(feature = "sql-pgq")]
mod gql_conformance_edge_cases {
use super::*;
#[test]
fn filter_where_clause() {
let db = create_social_network();
let session = db.session();
let result = session
.execute(
"MATCH (a:Person)-[:KNOWS]->(b:Person) \
FILTER WHERE a.age > 28 \
RETURN a.name AS name ORDER BY name",
)
.unwrap();
assert!(result.row_count() > 0);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn post_edge_quantifier_bounded() {
let db = create_chain();
let session = db.session();
let result = session
.execute(
"MATCH (a:Node)-[:NEXT]->{1,2}(b:Node) \
RETURN a.id AS src, b.id AS dst ORDER BY src, dst",
)
.unwrap();
assert_eq!(result.row_count(), 5);
}
#[test]
fn post_edge_quantifier_plus() {
let db = create_chain();
let session = db.session();
let result = session
.execute(
"MATCH (a:Node {id: 'A'})-[:NEXT]->{1,}(b:Node) \
RETURN b.id AS dst ORDER BY dst",
)
.unwrap();
assert_eq!(result.row_count(), 3);
}
#[test]
fn select_from_match_statement() {
let db = create_social_network();
let session = db.session();
let result = session
.execute("SELECT a.name AS name FROM MATCH (a:Person) ORDER BY name")
.unwrap();
assert_eq!(result.row_count(), 3);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn nullif_in_return() {
let db = create_social_network();
let session = db.session();
let result = session
.execute(
"MATCH (a:Person) \
RETURN a.name AS name, nullIf(a.name, 'Gus') AS filtered \
ORDER BY name",
)
.unwrap();
assert_eq!(result.rows()[0][1], Value::String("Alix".into()));
assert!(result.rows()[1][1].is_null());
assert_eq!(result.rows()[2][1], Value::String("Harm".into()));
}
}
mod sql_pgq_correctness {
use super::*;
#[test]
fn sql_pgq_matches_gql_for_node_query() {
let db = create_social_network();
let session = db.session();
let gql = session
.execute("MATCH (a:Person) RETURN a.name AS name ORDER BY name")
.unwrap();
let sql = session
.execute_sql(
"SELECT * FROM GRAPH_TABLE (
MATCH (a:Person)
COLUMNS (a.name AS name)
) ORDER BY name",
)
.unwrap();
assert_eq!(gql.row_count(), sql.row_count());
for (g, s) in gql.rows().iter().zip(sql.rows().iter()) {
assert_eq!(g[0], s[0], "GQL and SQL/PGQ should return same names");
}
}
#[test]
fn sql_pgq_inner_where_matches_gql_where() {
let db = create_social_network();
let session = db.session();
let gql = session
.execute(
"MATCH (a:Person)-[:KNOWS]->(b:Person) \
WHERE a.age > 28 \
RETURN a.name AS source, b.name AS target \
ORDER BY source, target",
)
.unwrap();
let sql = session
.execute_sql(
"SELECT * FROM GRAPH_TABLE (
MATCH (a:Person)-[:KNOWS]->(b:Person)
WHERE a.age > 28
COLUMNS (a.name AS source, b.name AS target)
) ORDER BY source, target",
)
.unwrap();
assert_eq!(
gql.row_count(),
sql.row_count(),
"GQL: {}, SQL/PGQ: {}",
gql.row_count(),
sql.row_count()
);
for (g, s) in gql.rows().iter().zip(sql.rows().iter()) {
assert_eq!(g, s);
}
}
#[test]
fn sql_pgq_group_by_aggregate_correctness() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_sql(
"SELECT source, COUNT(*) AS cnt FROM GRAPH_TABLE (
MATCH (a:Person)-[:KNOWS]->(b:Person)
COLUMNS (a.name AS source)
) GROUP BY source ORDER BY source",
)
.unwrap();
assert_eq!(result.row_count(), 2);
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));
}
#[test]
fn sql_pgq_select_distinct_correctness() {
let db = create_social_network();
let session = db.session();
let all = session
.execute_sql(
"SELECT source FROM GRAPH_TABLE (
MATCH (a:Person)-[:KNOWS]->(b:Person)
COLUMNS (a.name AS source)
)",
)
.unwrap();
let distinct = session
.execute_sql(
"SELECT DISTINCT source FROM GRAPH_TABLE (
MATCH (a:Person)-[:KNOWS]->(b:Person)
COLUMNS (a.name AS source)
)",
)
.unwrap();
assert_eq!(all.row_count(), 3, "3 KNOWS edges total");
assert_eq!(distinct.row_count(), 2, "2 distinct source names");
}
#[test]
fn sql_pgq_nullif_in_columns() {
let db = create_social_network();
let session = db.session();
let result = session
.execute_sql(
"SELECT * FROM GRAPH_TABLE (
MATCH (a:Person)
COLUMNS (a.name AS name, nullIf(a.name, 'Alix') AS filtered)
) ORDER BY name",
)
.unwrap();
assert_eq!(result.row_count(), 3);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert!(result.rows()[0][1].is_null());
assert_eq!(result.rows()[1][0], Value::String("Gus".into()));
assert_eq!(result.rows()[1][1], Value::String("Gus".into()));
}
}