use std::collections::HashMap;
use grafeo_common::types::Value;
use grafeo_engine::GrafeoDB;
fn seed_people() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
s.execute("INSERT (:Person {name: 'Gus', age: 25})")
.unwrap();
s.execute("INSERT (:Person {name: 'Vincent', age: 40})")
.unwrap();
db
}
#[test]
fn union_all_preserves_duplicates() {
let db = seed_people();
let s = db.session();
let r = s
.execute(
"MATCH (n:Person) WHERE n.age >= 30 RETURN n.name \
UNION ALL \
MATCH (n:Person) WHERE n.age <= 30 RETURN n.name",
)
.unwrap();
assert_eq!(r.row_count(), 4, "UNION ALL should not deduplicate");
}
#[test]
fn union_distinct_deduplicates() {
let db = seed_people();
let s = db.session();
let r = s
.execute(
"MATCH (n:Person) WHERE n.age >= 30 RETURN n.name \
UNION \
MATCH (n:Person) WHERE n.age <= 30 RETURN n.name",
)
.unwrap();
assert_eq!(r.row_count(), 3, "UNION should deduplicate");
}
#[test]
fn next_composition_two_matches() {
let db = seed_people();
let s = db.session();
let r = s
.execute(
"MATCH (n:Person) WHERE n.age = 30 RETURN n.name \
NEXT \
MATCH (m:Person) WHERE m.age = 25 RETURN m.name",
)
.unwrap();
assert_eq!(r.row_count(), 1);
}
#[test]
fn next_triple_chain() {
let db = seed_people();
let s = db.session();
let r = s
.execute(
"MATCH (a:Person) WHERE a.age = 25 RETURN a.name \
NEXT \
MATCH (b:Person) WHERE b.age = 30 RETURN b.name \
NEXT \
MATCH (c:Person) WHERE c.age = 40 RETURN c.name",
)
.unwrap();
assert_eq!(r.row_count(), 1);
}
#[test]
fn next_fan_out() {
let db = seed_people();
let s = db.session();
let r = s
.execute(
"MATCH (n:Person) WHERE n.age >= 30 RETURN n.name \
NEXT \
MATCH (m:Person) WHERE m.age = 25 RETURN m.name",
)
.unwrap();
assert_eq!(r.row_count(), 2);
}
#[test]
fn param_missing_returns_error() {
let db = seed_people();
let s = db.session();
let params = HashMap::new(); let r = s.execute_with_params("MATCH (n:Person) WHERE n.age > $min_age RETURN n", params);
assert!(r.is_err());
let err_msg = r.unwrap_err().to_string();
assert!(
err_msg.contains("Missing parameter") || err_msg.contains("min_age"),
"Error should mention missing parameter, got: {err_msg}"
);
}
#[test]
fn param_string_filter() {
let db = seed_people();
let s = db.session();
let mut params = HashMap::new();
params.insert("target".to_string(), Value::String("Gus".into()));
let r = s
.execute_with_params(
"MATCH (n:Person) WHERE n.name = $target RETURN n.age",
params,
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(25));
}
#[test]
fn param_multiple_in_and_condition() {
let db = seed_people();
let s = db.session();
let mut params = HashMap::new();
params.insert("min_age".to_string(), Value::Int64(26));
params.insert("max_age".to_string(), Value::Int64(35));
let r = s
.execute_with_params(
"MATCH (n:Person) WHERE n.age >= $min_age AND n.age <= $max_age RETURN n.name",
params,
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn param_in_insert_property() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let mut params = HashMap::new();
params.insert("city".to_string(), Value::String("Amsterdam".into()));
s.execute_with_params("INSERT (:City {name: $city})", params)
.unwrap();
let r = s.execute("MATCH (c:City) RETURN c.name").unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Amsterdam".into()));
}
#[test]
fn param_boolean_value() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
s.execute("INSERT (:Flag {name: 'Alix', active: true})")
.unwrap();
s.execute("INSERT (:Flag {name: 'Gus', active: false})")
.unwrap();
let mut params = HashMap::new();
params.insert("wanted".to_string(), Value::Bool(true));
let r = s
.execute_with_params(
"MATCH (f:Flag) WHERE f.active = $wanted RETURN f.name",
params,
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn param_float_comparison() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
s.execute("INSERT (:Metric {name: 'alpha', score: 3.14})")
.unwrap();
s.execute("INSERT (:Metric {name: 'beta', score: 2.71})")
.unwrap();
let mut params = HashMap::new();
params.insert("threshold".to_string(), Value::Float64(3.0));
let r = s
.execute_with_params(
"MATCH (m:Metric) WHERE m.score > $threshold RETURN m.name",
params,
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("alpha".into()));
}
#[test]
fn param_extra_unused_params_ok() {
let db = seed_people();
let s = db.session();
let mut params = HashMap::new();
params.insert("unused".to_string(), Value::Int64(999));
let r = s
.execute_with_params("MATCH (n:Person) RETURN n.name ORDER BY n.name", params)
.unwrap();
assert_eq!(r.row_count(), 3);
}
#[test]
fn language_dispatch_gql() {
let db = seed_people();
let s = db.session();
let r = s
.execute_language(
"MATCH (n:Person) RETURN n.name ORDER BY n.name",
"gql",
None,
)
.unwrap();
assert_eq!(r.row_count(), 3);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn language_dispatch_unknown_returns_error() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s.execute_language("MATCH (n) RETURN n", "klingon", None);
assert!(r.is_err());
let err_msg = r.unwrap_err().to_string();
assert!(
err_msg.contains("Unknown query language") || err_msg.contains("klingon"),
"Error should mention unknown language, got: {err_msg}"
);
}
#[test]
fn language_dispatch_gql_with_params() {
let db = seed_people();
let s = db.session();
let mut params = HashMap::new();
params.insert("name".to_string(), Value::String("Vincent".into()));
let r = s
.execute_language(
"MATCH (n:Person) WHERE n.name = $name RETURN n.age",
"gql",
Some(params),
)
.unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::Int64(40));
}
#[cfg(feature = "cypher")]
#[test]
fn language_dispatch_cypher() {
let db = seed_people();
let s = db.session();
let r = s
.execute_language(
"MATCH (n:Person) WHERE n.age > 25 RETURN n.name ORDER BY n.name",
"cypher",
None,
)
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(r.rows()[0][0], Value::String("Alix".into()));
assert_eq!(r.rows()[1][0], Value::String("Vincent".into()));
}
#[cfg(feature = "cypher")]
#[test]
fn language_dispatch_cypher_with_params() {
let db = seed_people();
let s = db.session();
let mut params = HashMap::new();
params.insert("min".to_string(), Value::Int64(30));
let r = s
.execute_language(
"MATCH (n:Person) WHERE n.age >= $min RETURN n.name ORDER BY n.name",
"cypher",
Some(params),
)
.unwrap();
assert_eq!(r.row_count(), 2);
}
#[test]
fn error_empty_query() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s.execute("");
assert!(r.is_err(), "Empty query should produce an error");
}
#[test]
fn error_syntax_garbage() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s.execute("ZORK BLARG FLIM");
assert!(r.is_err(), "Garbage query should produce a parse error");
}
#[test]
fn error_unclosed_paren() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s.execute("MATCH (n:Person RETURN n");
assert!(r.is_err());
}
#[test]
fn error_whitespace_only_query() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s.execute(" \t\n ");
assert!(r.is_err(), "Whitespace-only query should produce an error");
}
#[test]
fn explain_returns_plan_tree() {
let db = seed_people();
let s = db.session();
let r = s
.execute("EXPLAIN MATCH (n:Person) WHERE n.age > 25 RETURN n.name")
.unwrap();
assert_eq!(r.columns, vec!["plan"]);
assert_eq!(r.row_count(), 1);
if let Value::String(plan_text) = &r.rows()[0][0] {
assert!(
!plan_text.is_empty(),
"EXPLAIN plan text should not be empty"
);
} else {
panic!("EXPLAIN should return a String value");
}
}
#[test]
fn transaction_rollback_undoes_inserts() {
let db = GrafeoDB::new_in_memory();
let mut s = db.session();
s.begin_transaction().unwrap();
s.execute("INSERT (:Ghost {name: 'Mia'})").unwrap();
let r = s.execute("MATCH (g:Ghost) RETURN g.name").unwrap();
assert_eq!(
r.row_count(),
1,
"Should see node within active transaction"
);
s.rollback().unwrap();
let r2 = s.execute("MATCH (g:Ghost) RETURN g.name").unwrap();
assert_eq!(r2.row_count(), 0, "Rollback should undo the insert");
}
#[test]
fn transaction_commit_persists() {
let db = GrafeoDB::new_in_memory();
let mut s = db.session();
s.begin_transaction().unwrap();
s.execute("INSERT (:Keeper {name: 'Butch'})").unwrap();
s.commit().unwrap();
let s2 = db.session();
let r = s2.execute("MATCH (k:Keeper) RETURN k.name").unwrap();
assert_eq!(r.row_count(), 1);
assert_eq!(r.rows()[0][0], Value::String("Butch".into()));
}
#[test]
fn transaction_multiple_inserts_rollback() {
let db = GrafeoDB::new_in_memory();
let mut s = db.session();
s.begin_transaction().unwrap();
s.execute("INSERT (:Item {name: 'Berlin'})").unwrap();
s.execute("INSERT (:Item {name: 'Paris'})").unwrap();
s.execute("INSERT (:Item {name: 'Prague'})").unwrap();
s.rollback().unwrap();
let r = s.execute("MATCH (i:Item) RETURN i.name").unwrap();
assert_eq!(r.row_count(), 0, "All 3 inserts should be rolled back");
}
#[test]
fn session_isolation() {
let db = GrafeoDB::new_in_memory();
let mut s1 = db.session();
let s2 = db.session();
s1.begin_transaction().unwrap();
s1.execute("INSERT (:Secret {name: 'Django'})").unwrap();
let r = s2.execute("MATCH (s:Secret) RETURN s").unwrap();
assert_eq!(
r.row_count(),
0,
"Uncommitted data should not be visible to other sessions"
);
s1.commit().unwrap();
}
#[test]
fn session_state_persists_across_queries() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
s.execute("INSERT (:Counter {val: 1})").unwrap();
s.execute("INSERT (:Counter {val: 2})").unwrap();
let r = s
.execute("MATCH (c:Counter) RETURN c.val ORDER BY c.val")
.unwrap();
assert_eq!(r.row_count(), 2);
assert_eq!(r.rows()[0][0], Value::Int64(1));
assert_eq!(r.rows()[1][0], Value::Int64(2));
}
#[test]
fn empty_result_preserves_columns() {
let db = GrafeoDB::new_in_memory();
let s = db.session();
let r = s
.execute("MATCH (n:Nothing) RETURN n.x AS x, n.y AS y")
.unwrap();
assert_eq!(r.row_count(), 0);
assert_eq!(r.columns, vec!["x", "y"]);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn result_has_execution_time() {
let db = seed_people();
let s = db.session();
let r = s.execute("MATCH (n:Person) RETURN n.name").unwrap();
assert!(
r.execution_time_ms.is_some(),
"execution_time_ms should be populated"
);
}