use grafeo_common::types::{EpochId, Value};
use grafeo_engine::GrafeoDB;
fn setup() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Alix".into())),
("age", Value::Int64(30)),
],
)
.unwrap();
session
.create_node_with_props(
&["Person"],
[
("name", Value::String("Gus".into())),
("age", Value::Int64(25)),
],
)
.unwrap();
db
}
#[test]
fn test_session_set_and_get_parameter() {
let db = setup();
let session = db.session();
session.set_parameter("threshold", Value::Int64(42));
let val = session.get_parameter("threshold");
assert_eq!(val, Some(Value::Int64(42)));
assert_eq!(session.get_parameter("missing"), None);
}
#[test]
fn test_reset_session_clears_state() {
let db = setup();
let session = db.session();
session.set_parameter("key", Value::String("val".into()));
session.reset_session();
assert_eq!(session.get_parameter("key"), None);
}
#[test]
fn test_set_time_zone_direct() {
let db = setup();
let session = db.session();
session.set_time_zone("Europe/Amsterdam");
}
#[test]
fn test_graph_model_default() {
let db = setup();
let session = db.session();
let model = session.graph_model();
assert_eq!(format!("{model:?}"), "Lpg");
}
#[test]
fn test_viewing_epoch_lifecycle() {
let db = setup();
let session = db.session();
assert_eq!(session.viewing_epoch(), None);
session.set_viewing_epoch(EpochId::new(1));
assert_eq!(session.viewing_epoch(), Some(EpochId::new(1)));
session.clear_viewing_epoch();
assert_eq!(session.viewing_epoch(), None);
}
#[test]
fn test_execute_at_epoch() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.create_node_with_props(&["Item"], [("name", Value::String("original".into()))])
.unwrap();
let epoch = db.current_epoch();
let r = session
.execute_at_epoch("MATCH (i:Item) RETURN i.name AS name", epoch)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert!(matches!(&r.rows()[0][0], Value::String(_)));
}
#[test]
fn test_savepoint_outside_transaction_fails() {
let db = setup();
let session = db.session();
let result = session.savepoint("sp");
let err = result.unwrap_err().to_string();
assert!(
err.contains("transaction") || err.contains("savepoint"),
"error should mention transaction context, got: {err}"
);
}
#[test]
fn test_release_savepoint_via_api() {
let db = setup();
let mut session = db.session();
session.begin_transaction().unwrap();
session.savepoint("sp1").unwrap();
session
.execute("MATCH (p:Person {name: 'Alix'}) SET p.age = 99")
.unwrap();
session.release_savepoint("sp1").unwrap();
let result = session.rollback_to_savepoint("sp1");
let err = result.unwrap_err().to_string();
assert!(
err.contains("savepoint") || err.contains("sp1") || err.contains("not found"),
"error should mention released savepoint, got: {err}"
);
session.commit().unwrap();
}
#[test]
fn test_begin_transaction_with_serializable_isolation() {
let db = setup();
let mut session = db.session();
session.begin_transaction().unwrap();
session
.execute("MATCH (p:Person) RETURN count(p) AS cnt")
.unwrap();
session.commit().unwrap();
}
#[test]
fn test_execute_with_params_direct() {
let db = setup();
let session = db.session();
let params = std::collections::HashMap::from([("min_age".to_string(), Value::Int64(28))]);
let r = session
.execute_with_params(
"MATCH (p:Person) WHERE p.age > $min_age RETURN p.name AS name ORDER BY name",
params,
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn test_use_graph_via_gql() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("CREATE GRAPH test_graph").unwrap();
session.execute("USE GRAPH test_graph").unwrap();
}
#[test]
fn test_optional_match_no_match() {
let db = setup();
let session = db.session();
let r = session
.execute(
"MATCH (p:Person {name: 'Alix'}) \
OPTIONAL MATCH (p)-[:MANAGES]->(e:Employee) \
RETURN p.name AS name, e.name AS emp",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
assert_eq!(r.rows()[0][1], Value::Null);
}
#[test]
fn test_optional_match_with_where() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let a = session
.create_node_with_props(&["Person"], [("name", Value::String("Alix".into()))])
.unwrap();
let b = session
.create_node_with_props(&["Person"], [("name", Value::String("Gus".into()))])
.unwrap();
session.create_edge(a, b, "KNOWS");
let r = session
.execute(
"MATCH (p:Person {name: 'Alix'}) \
OPTIONAL MATCH (p)-[:KNOWS]->(f:Person) WHERE f.name = 'Nonexistent' \
RETURN p.name AS name, f.name AS friend",
)
.unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][1], Value::Null);
}
#[test]
fn test_standalone_return_arithmetic() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s.execute("RETURN 1 + 2 AS result").unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(3));
}
#[test]
fn test_standalone_return_string() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s.execute("RETURN 'hello' AS greeting").unwrap();
assert_eq!(r.rows().len(), 1);
assert_eq!(r.rows()[0][0], Value::String("hello".into()));
}
#[test]
fn test_standalone_return_list() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s.execute("RETURN [1, 2, 3] AS nums").unwrap();
assert_eq!(r.rows().len(), 1);
if let Value::List(items) = &r.rows()[0][0] {
assert_eq!(items.len(), 3);
} else {
panic!("expected list, got {:?}", r.rows()[0][0]);
}
}
#[test]
fn test_unwind_list() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let r = session.execute("UNWIND [1, 2, 3] AS x RETURN x").unwrap();
assert_eq!(r.rows().len(), 3);
}
#[test]
fn test_call_subquery() {
let db = setup();
let session = db.session();
let r = session
.execute(
"MATCH (p:Person) CALL { WITH p RETURN p.age * 2 AS doubled } RETURN p.name, doubled ORDER BY p.name",
)
.unwrap();
assert_eq!(r.rows().len(), 2);
}
#[test]
fn test_session_recovers_after_parse_error() {
let db = setup();
let session = db.session();
let err = session.execute("MATCH (n:Person RETURN n.name");
assert!(err.is_err());
let result = session
.execute("MATCH (n:Person) RETURN n.name ORDER BY n.name")
.unwrap();
assert!(!result.rows().is_empty());
}
#[test]
fn test_session_recovers_after_runtime_error() {
let db = setup();
let session = db.session();
let err = session.execute("MATCH (n:NonExistent) SET n.x = 1/0 RETURN n");
let _ = err;
let result = session.execute("MATCH (n:Person) RETURN count(n)").unwrap();
assert_eq!(result.rows().len(), 1);
}
#[test]
fn test_session_recovers_after_rollback() {
let db = setup();
let mut session = db.session();
session.begin_transaction().unwrap();
session
.execute("INSERT (:Temp {value: 'should be rolled back'})")
.unwrap();
session.rollback().unwrap();
let result = session.execute("MATCH (t:Temp) RETURN count(t)").unwrap();
assert_eq!(result.rows()[0][0], Value::Int64(0));
session.begin_transaction().unwrap();
session.execute("INSERT (:Valid {ok: true})").unwrap();
session.commit().unwrap();
let result = session.execute("MATCH (v:Valid) RETURN count(v)").unwrap();
assert_eq!(result.rows()[0][0], Value::Int64(1));
}
#[test]
fn test_prepare_commit_lifecycle() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
session.begin_transaction().unwrap();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let mut prepared = session.prepare_commit().unwrap();
let info = prepared.info();
assert_eq!(info.nodes_written, 0);
prepared.set_metadata("audit_user", "admin");
let metadata = prepared.metadata();
assert_eq!(
metadata.get("audit_user").map(|s| s.as_str()),
Some("admin")
);
let epoch = prepared.commit().unwrap();
assert!(epoch.as_u64() > 0, "commit should return a valid epoch");
let reader = db.session();
let result = reader.execute("MATCH (n:Person) RETURN n").unwrap();
assert_eq!(result.row_count(), 1, "committed node should be visible");
}
#[test]
fn test_prepare_commit_abort() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
session.begin_transaction().unwrap();
session.execute("INSERT (:Temp {val: 1})").unwrap();
let prepared = session.prepare_commit().unwrap();
prepared.abort().unwrap();
let r = session.execute("MATCH (t:Temp) RETURN count(t)").unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(0));
}
#[test]
fn test_prepare_commit_without_transaction_fails() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
let result = session.prepare_commit();
match result {
Ok(_) => panic!("expected error when no transaction is active"),
Err(err) => {
let msg = err.to_string();
assert!(
msg.contains("transaction") || msg.contains("active"),
"error should mention no active transaction, got: {msg}"
);
}
}
}
#[test]
fn test_begin_transaction_with_read_committed() {
let db = setup();
let mut session = db.session();
session
.begin_transaction_with_isolation(grafeo_engine::transaction::IsolationLevel::ReadCommitted)
.unwrap();
let r = session.execute("MATCH (p:Person) RETURN count(p)").unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(2));
session.commit().unwrap();
}
#[test]
fn test_begin_transaction_with_serializable() {
let db = setup();
let mut session = db.session();
session
.begin_transaction_with_isolation(grafeo_engine::transaction::IsolationLevel::Serializable)
.unwrap();
let r = session.execute("MATCH (p:Person) RETURN count(p)").unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(2));
session.commit().unwrap();
}
#[test]
fn test_begin_transaction_with_isolation_nested_creates_savepoint() {
let db = setup();
let mut session = db.session();
session
.begin_transaction_with_isolation(
grafeo_engine::transaction::IsolationLevel::SnapshotIsolation,
)
.unwrap();
session
.begin_transaction_with_isolation(grafeo_engine::transaction::IsolationLevel::ReadCommitted)
.unwrap();
let r = session.execute("MATCH (p:Person) RETURN count(p)").unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(2));
session.rollback().unwrap();
}
#[test]
fn test_query_scalar_int64() {
let db = setup();
let count: i64 = db.query_scalar("MATCH (p:Person) RETURN count(p)").unwrap();
assert_eq!(count, 2);
}
#[test]
fn test_query_scalar_string() {
let db = setup();
let name: String = db
.query_scalar("MATCH (p:Person {name: 'Alix'}) RETURN p.name")
.unwrap();
assert_eq!(name, "Alix");
}
#[test]
fn test_clear_plan_cache() {
let db = setup();
let session = db.session();
session.execute("MATCH (p:Person) RETURN p.name").unwrap();
db.clear_plan_cache();
let r = session.execute("MATCH (p:Person) RETURN count(p)").unwrap();
assert_eq!(r.rows()[0][0], Value::Int64(2));
}
#[test]
fn test_buffer_manager_accessible() {
let db = GrafeoDB::new_in_memory();
let bm = db.buffer_manager();
assert!(
bm.budget() > 0,
"buffer manager should have a positive budget"
);
}
#[test]
fn test_query_cache_accessible() {
let db = GrafeoDB::new_in_memory();
let cache = db.query_cache();
let stats = cache.stats();
assert_eq!(stats.parsed_hits, 0);
assert_eq!(stats.parsed_misses, 0);
}
#[cfg(all(feature = "sparql", feature = "triple-store"))]
#[test]
fn test_execute_sparql_with_params() {
use grafeo_engine::config::{Config, GraphModel};
let db = GrafeoDB::with_config(Config::in_memory().with_graph_model(GraphModel::Rdf)).unwrap();
let session = db.session();
session
.execute_sparql(
r#"INSERT DATA {
<http://ex.org/alix> <http://ex.org/age> "30" .
<http://ex.org/gus> <http://ex.org/age> "25" .
}"#,
)
.unwrap();
let params = std::collections::HashMap::from([("unused".to_string(), Value::Int64(1))]);
let r = session
.execute_sparql_with_params(
r#"SELECT ?s ?age WHERE {
?s <http://ex.org/age> ?age .
}"#,
params,
)
.unwrap();
assert_eq!(
r.rows().len(),
2,
"should return two rows, got {}",
r.rows().len()
);
}
#[cfg(feature = "cdc")]
mod cdc_tests {
use grafeo_common::types::{EpochId, Value};
use grafeo_engine::{Config, GrafeoDB};
fn cdc_db() -> GrafeoDB {
GrafeoDB::with_config(Config::in_memory().with_cdc()).unwrap()
}
#[test]
fn test_cdc_history_records_create() {
let db = cdc_db();
let node_id = db.create_node(&["Person"]);
db.set_node_property(node_id, "name", Value::String("Alix".into()));
let session = db.session();
let history = session.history(node_id).unwrap();
assert!(
!history.is_empty(),
"CDC history should contain at least the create event"
);
assert!(
history
.iter()
.any(|e| e.kind == grafeo_engine::cdc::ChangeKind::Create),
"Should contain a Create event"
);
}
#[test]
fn test_cdc_history_records_update() {
let db = cdc_db();
let node_id = db.create_node(&["Person"]);
db.set_node_property(node_id, "name", Value::String("Alix".into()));
db.set_node_property(node_id, "name", Value::String("Gus".into()));
let session = db.session();
let history = session.history(node_id).unwrap();
let update_count = history
.iter()
.filter(|e| e.kind == grafeo_engine::cdc::ChangeKind::Update)
.count();
assert!(
update_count >= 2,
"Should have at least 2 update events for 2 set_node_property calls, got {update_count}"
);
}
#[test]
fn test_cdc_history_since_filters_by_epoch() {
let db = cdc_db();
let node_id = db.create_node(&["Person"]);
db.set_node_property(node_id, "name", Value::String("Alix".into()));
let session = db.session();
let history = session
.history_since(node_id, EpochId::new(u64::MAX))
.unwrap();
assert!(
history.is_empty(),
"history_since with future epoch should return empty"
);
let history = session.history_since(node_id, EpochId::new(0)).unwrap();
assert!(
!history.is_empty(),
"history_since epoch 0 should return all events"
);
}
#[test]
fn test_cdc_changes_between_epoch_range() {
let db = cdc_db();
db.create_node(&["Person"]);
db.create_node(&["Person"]);
let session = db.session();
let changes = session
.changes_between(EpochId::new(0), EpochId::new(u64::MAX))
.unwrap();
assert!(
changes.len() >= 2,
"Should have at least 2 change events for 2 node creations, got {}",
changes.len()
);
}
}
fn setup_questioned_edge() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let alix = session
.create_node_with_props(&["Person"], [("name", Value::String("Alix".into()))])
.unwrap();
let gus = session
.create_node_with_props(&["Person"], [("name", Value::String("Gus".into()))])
.unwrap();
let vincent = session
.create_node_with_props(&["Person"], [("name", Value::String("Vincent".into()))])
.unwrap();
session.create_edge(alix, gus, "KNOWS");
let _ = vincent;
db
}
#[test]
fn test_questioned_edge_preserves_source_rows() {
let db = setup_questioned_edge();
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,
"Questioned edge should preserve all source rows"
);
let names: Vec<&str> = result
.rows()
.iter()
.map(|r| r[0].as_str().unwrap_or("NULL"))
.collect();
assert!(names.contains(&"Alix"));
assert!(names.contains(&"Gus"));
assert!(names.contains(&"Vincent"));
}
#[test]
fn test_questioned_edge_null_when_no_match() {
let db = setup_questioned_edge();
let session = db.session();
let result = session
.execute("MATCH (a:Person)-[:KNOWS]->?(b:Person) RETURN a.name, b.name")
.unwrap();
let vincent_row = result
.rows()
.iter()
.find(|r| r[0].as_str() == Some("Vincent"))
.unwrap();
assert!(
vincent_row[1].is_null(),
"Vincent's target should be NULL (no KNOWS edge)"
);
}
#[test]
fn test_questioned_edge_with_target_label_filter() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let alix = session
.create_node_with_props(&["Person"], [("name", Value::String("Alix".into()))])
.unwrap();
let amsterdam = session
.create_node_with_props(&["City"], [("name", Value::String("Amsterdam".into()))])
.unwrap();
let gus = session
.create_node_with_props(&["Person"], [("name", Value::String("Gus".into()))])
.unwrap();
session.create_edge(alix, amsterdam, "LIVES_IN");
session.create_edge(alix, gus, "KNOWS");
let result = session
.execute("MATCH (a:Person)-[:KNOWS]->?(b:Person) RETURN a.name, b.name")
.unwrap();
assert_eq!(result.row_count(), 2, "Two Person nodes should appear");
let alix_row = result
.rows()
.iter()
.find(|r| r[0].as_str() == Some("Alix"))
.unwrap();
assert_eq!(
alix_row[1].as_str(),
Some("Gus"),
"Alix's target should be Gus (KNOWS edge to Person)"
);
let gus_row = result
.rows()
.iter()
.find(|r| r[0].as_str() == Some("Gus"))
.unwrap();
assert!(
gus_row[1].is_null(),
"Gus's target should be NULL (no outgoing KNOWS)"
);
}
#[test]
fn test_questioned_edge_combined_with_optional_match() {
let db = setup_questioned_edge();
let session = db.session();
session
.execute(
"MATCH (a:Person {name: 'Alix'}) INSERT (a)-[:LIVES_IN]->(:City {name: 'Amsterdam'})",
)
.unwrap();
let result = session
.execute(
"MATCH (a:Person)-[:KNOWS]->?(b:Person) \
OPTIONAL MATCH (a)-[:LIVES_IN]->(c:City) \
RETURN a.name, b.name, c.name",
)
.unwrap();
assert_eq!(
result.row_count(),
3,
"All persons should appear with both questioned edge and optional match"
);
let alix_row = result
.rows()
.iter()
.find(|r| r[0].as_str() == Some("Alix"))
.unwrap();
assert_eq!(alix_row[1].as_str(), Some("Gus"));
assert_eq!(alix_row[2].as_str(), Some("Amsterdam"));
}