use dr_strange_core::{Database, Dir, PlaneHandle, PropValue};
use dr_strange_parser::{
Params, ParseError, Statement, WriteSummary, parse, parse_statement, parse_statement_full,
};
fn write(db: &Database, q: &str) -> WriteSummary {
match parse_statement(q).unwrap_or_else(|e| panic!("parse `{q}`: {e}")) {
Statement::Write(w) => w
.apply(&db.plane("startup").unwrap())
.unwrap_or_else(|e| panic!("apply `{q}`: {e}")),
Statement::Read(_) => panic!("expected a write for `{q}`"),
}
}
fn plane(db: &Database) -> PlaneHandle<'_> {
db.plane("startup").unwrap()
}
#[test]
fn create_nodes_edge_and_props() {
let db = Database::in_memory().unwrap();
let s = write(
&db,
r#"CREATE (a:Person {key:"alice", age:30, name:"Alice", active:true}),
(b:Person {key:"bob"}),
(a)-[:KNOWS {since:2020}]->(b)"#,
);
assert_eq!(s.nodes_created, 2);
assert_eq!(s.edges_created, 1);
let p = plane(&db);
let alice = p.node_by_key("alice").unwrap().unwrap();
assert!(alice.labels.iter().any(|l| l == "Person"));
assert_eq!(alice.properties["age"].value, PropValue::Int(30));
assert_eq!(
alice.properties["name"].value,
PropValue::Str("Alice".into())
);
assert_eq!(alice.properties["active"].value, PropValue::Bool(true));
assert!(!alice.properties.contains_key("key"));
let bob = p.node_by_key("bob").unwrap().unwrap();
let out = p.neighbors(alice.id, Dir::Out, None).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].node, bob.id);
}
#[test]
fn incoming_edge_direction() {
let db = Database::in_memory().unwrap();
write(
&db,
r#"CREATE (a:N {key:"a"}), (b:N {key:"b"}), (a)<-[:REF]-(b)"#,
);
let p = plane(&db);
let a = p.node_by_key("a").unwrap().unwrap();
let b = p.node_by_key("b").unwrap().unwrap();
let out = p.neighbors(b.id, Dir::Out, None).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].node, a.id);
}
#[test]
fn create_with_negative_and_vector_props() {
let db = Database::in_memory().unwrap();
write(
&db,
r#"CREATE (n:Doc {key:"d", score:-1.5, emb:[0.0, 1.0]})"#,
);
let p = plane(&db);
let d = p.node_by_key("d").unwrap().unwrap();
assert_eq!(d.properties["score"].value, PropValue::Float(-1.5));
assert_eq!(d.properties["emb"].value, PropValue::Vector(vec![0.0, 1.0]));
}
#[test]
fn anonymous_and_unlabeled_nodes() {
let db = Database::in_memory().unwrap();
let s = write(&db, "CREATE (), (:Tag), (n)");
assert_eq!(s.nodes_created, 3);
assert_eq!(s.edges_created, 0);
}
#[test]
fn rejects_undirected_create_edge() {
assert!(matches!(
parse_statement(r#"CREATE (a {key:"a"})-[:R]-(b {key:"b"})"#),
Err(ParseError::Syntax(_))
));
}
#[test]
fn set_prop_label_and_merge() {
let db = Database::in_memory().unwrap();
write(&db, r#"CREATE (a:Person {key:"alice", age:30})"#);
let s = write(
&db,
r#"MATCH (n:Person) WHERE n.age = 30 SET n.age = 31, n:VIP, n += {city:"NYC", zip:10001}"#,
);
assert_eq!(s.props_set, 3); assert_eq!(s.labels_set, 1);
let a = plane(&db).node_by_key("alice").unwrap().unwrap();
assert_eq!(a.properties["age"].value, PropValue::Int(31));
assert_eq!(a.properties["city"].value, PropValue::Str("NYC".into()));
assert!(a.labels.iter().any(|l| l == "VIP"));
assert!(a.labels.iter().any(|l| l == "Person")); }
#[test]
fn remove_prop_and_label() {
let db = Database::in_memory().unwrap();
write(&db, r#"CREATE (a:Person {key:"a", tmp:1})"#);
write(&db, "MATCH (n:Person) SET n:Archived");
let s = write(&db, "MATCH (n:Person) REMOVE n.tmp, n:Archived");
assert_eq!(s.props_set, 1);
assert_eq!(s.labels_set, 1);
let a = plane(&db).node_by_key("a").unwrap().unwrap();
assert!(!a.properties.contains_key("tmp"));
assert!(!a.labels.iter().any(|l| l == "Archived"));
assert!(a.labels.iter().any(|l| l == "Person"));
}
#[test]
fn delete_unconnected_node() {
let db = Database::in_memory().unwrap();
write(&db, r#"CREATE (a:N {key:"a", tag:"x"})"#);
let s = write(&db, r#"MATCH (n:N) WHERE n.tag = "x" DELETE n"#);
assert_eq!(s.nodes_deleted, 1);
assert!(plane(&db).node_by_key("a").unwrap().is_none());
}
#[test]
fn plain_delete_refuses_connected_node_detach_cascades() {
let db = Database::in_memory().unwrap();
write(
&db,
r#"CREATE (a:N {key:"a", tag:"x"})-[:R]->(b:N {key:"b", tag:"y"})"#,
);
let stmt = parse_statement(r#"MATCH (n:N) WHERE n.tag = "x" DELETE n"#).unwrap();
let e = match stmt {
Statement::Write(w) => w.apply(&plane(&db)).unwrap_err(),
Statement::Read(_) => panic!("expected write"),
};
assert!(e.contains("DETACH"), "{e}");
assert!(plane(&db).node_by_key("a").unwrap().is_some());
let s = write(&db, r#"MATCH (n:N) WHERE n.tag = "x" DETACH DELETE n"#);
assert_eq!(s.nodes_deleted, 1);
let p = plane(&db);
assert!(p.node_by_key("a").unwrap().is_none());
assert!(p.node_by_key("b").unwrap().is_some());
}
#[test]
fn a_node_the_pattern_reaches_twice_is_mutated_once() {
let db = Database::in_memory().unwrap();
write(
&db,
r#"CREATE (a:A {key:"a1"}), (b:A {key:"a2"}), (n:N {key:"n"}),
(a)-[:R]->(n), (b)-[:R]->(n)"#,
);
let s = write(&db, "MATCH (a:A)-[:R]->(n:N) DETACH DELETE n");
assert_eq!(s.nodes_deleted, 1);
assert!(plane(&db).node_by_key("n").unwrap().is_none());
}
#[test]
fn merge_sees_the_edges_this_statement_already_created() {
let db = Database::in_memory().unwrap();
let s = write(
&db,
r#"MERGE (a:N {key:"a"})-[:R]->(b:N {key:"b"})<-[:R]-(a)"#,
);
assert_eq!(s.nodes_created, 2);
assert_eq!(s.edges_created, 1);
let s = write(
&db,
r#"MATCH (n:N) WHERE key(n) = "a"
MERGE (n)-[:R]->(t:N {key:"t"})
MERGE (n)-[:R]->(t2:N {key:"t"})"#,
);
assert_eq!(s.nodes_created, 1);
assert_eq!(s.edges_created, 1);
let s = write(
&db,
r#"MATCH (n:N) WHERE key(n) = "a"
CREATE (n)-[:S]->(u:N {key:"u"})
MERGE (n)-[:S]->(u2:N {key:"u"})"#,
);
assert_eq!(s.edges_created, 1);
let p = plane(&db);
let a = p.node_by_key("a").unwrap().unwrap();
let out = p.neighbors(a.id, Dir::Out, None).unwrap();
assert_eq!(out.len(), 3);
}
#[test]
fn match_create_anchors_new_nodes_to_matched_rows() {
let db = Database::in_memory().unwrap();
write(
&db,
r#"CREATE (a:Person {key:"alice", active:true}),
(b:Person {key:"bob", active:true}),
(c:Person {key:"carol", active:false})"#,
);
let s = write(
&db,
r#"MATCH (p:Person) WHERE p.active = true CREATE (p)-[:HAS]->(badge:Badge {name:"gold"})"#,
);
assert_eq!(s.nodes_created, 2); assert_eq!(s.edges_created, 2);
let p = plane(&db);
let alice = p.node_by_key("alice").unwrap().unwrap();
assert_eq!(
p.neighbors(alice.id, Dir::Out, Some("HAS")).unwrap().len(),
1
);
let carol = p.node_by_key("carol").unwrap().unwrap(); assert!(
p.neighbors(carol.id, Dir::Out, Some("HAS"))
.unwrap()
.is_empty()
);
assert_eq!(p.catalog().unwrap().node_count, 5); }
#[test]
fn rejects_mutation_of_non_terminal_variable() {
let e = parse_statement(r#"MATCH (a:N)-[:R]->(n:N) SET a.x = 1"#).unwrap_err();
assert!(matches!(e, ParseError::Compile(_)));
}
#[test]
fn rejects_match_write_with_anonymous_terminal() {
let e = parse_statement(r#"MATCH (a:N)-[:R]->() SET x.p = 1"#).unwrap_err();
assert!(matches!(e, ParseError::Compile(_)));
}
#[test]
fn merge_creates_when_absent() {
let db = Database::in_memory().unwrap();
let s = write(
&db,
r#"MERGE (a:Person {key:"alice", age:30})
ON CREATE SET a.created = true
ON MATCH SET a.seen = true"#,
);
assert_eq!(s.nodes_created, 1);
let a = plane(&db).node_by_key("alice").unwrap().unwrap();
assert!(a.labels.iter().any(|l| l == "Person"));
assert_eq!(a.properties["age"].value, PropValue::Int(30)); assert_eq!(a.properties["created"].value, PropValue::Bool(true)); assert!(!a.properties.contains_key("seen")); }
#[test]
fn merge_binds_when_present() {
let db = Database::in_memory().unwrap();
write(&db, r#"CREATE (a:Person {key:"bob", age:40})"#);
let s = write(
&db,
r#"MERGE (a:Person {key:"bob", age:99})
ON CREATE SET a.created = true
ON MATCH SET a.seen = true"#,
);
assert_eq!(s.nodes_created, 0); let b = plane(&db).node_by_key("bob").unwrap().unwrap();
assert_eq!(b.properties["age"].value, PropValue::Int(40)); assert!(b.properties.contains_key("seen")); assert!(!b.properties.contains_key("created")); }
#[test]
fn merge_is_idempotent() {
let db = Database::in_memory().unwrap();
assert_eq!(write(&db, r#"MERGE (a:Tag {key:"t"})"#).nodes_created, 1);
assert_eq!(write(&db, r#"MERGE (a:Tag {key:"t"})"#).nodes_created, 0);
assert_eq!(plane(&db).catalog().unwrap().node_count, 1);
}
#[test]
fn merge_requires_a_key() {
let e = parse_statement(r#"MERGE (a:Person {name:"x"})"#).unwrap_err();
assert!(matches!(e, ParseError::Compile(_)));
assert!(e.to_string().contains("key"), "{e}");
}
#[test]
fn merge_on_set_must_reference_the_merge_variable() {
let e = parse_statement(r#"MERGE (a:Person {key:"k"}) ON CREATE SET b.x = 1"#).unwrap_err();
assert!(matches!(e, ParseError::Compile(_)));
}
#[test]
fn merge_path_upserts_nodes_and_edge_idempotently() {
let db = Database::in_memory().unwrap();
let q = r#"MERGE (a:Person {key:"alice"})-[:KNOWS]->(b:Person {key:"bob"})"#;
let s = write(&db, q);
assert_eq!(s.nodes_created, 2);
assert_eq!(s.edges_created, 1);
let p = plane(&db);
let a = p.node_by_key("alice").unwrap().unwrap();
let b = p.node_by_key("bob").unwrap().unwrap();
let out = p.neighbors(a.id, Dir::Out, Some("KNOWS")).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].node, b.id);
let s2 = write(&db, q);
assert_eq!((s2.nodes_created, s2.edges_created), (0, 0));
assert_eq!(p.neighbors(a.id, Dir::Out, Some("KNOWS")).unwrap().len(), 1);
}
#[test]
fn merge_path_binds_existing_node_creates_the_rest() {
let db = Database::in_memory().unwrap();
write(&db, r#"CREATE (a:Person {key:"alice", age:30})"#);
let s = write(
&db,
r#"MERGE (a:Person {key:"alice"})-[:KNOWS]->(b:Person {key:"bob"})"#,
);
assert_eq!(s.nodes_created, 1); assert_eq!(s.edges_created, 1);
let a = plane(&db).node_by_key("alice").unwrap().unwrap();
assert_eq!(a.properties["age"].value, PropValue::Int(30));
}
#[test]
fn merge_after_match_shares_target_across_rows() {
let db = Database::in_memory().unwrap();
write(
&db,
r#"CREATE (a:Person {key:"alice"}), (b:Person {key:"bob"})"#,
);
let q = r#"MATCH (p:Person) MERGE (p)-[:MEMBER_OF]->(t:Team {key:"eng"})"#;
let s = write(&db, q);
assert_eq!(s.nodes_created, 1); assert_eq!(s.edges_created, 2);
let p = plane(&db);
let team = p.node_by_key("eng").unwrap().unwrap();
let alice = p.node_by_key("alice").unwrap().unwrap();
assert_eq!(
p.neighbors(alice.id, Dir::Out, Some("MEMBER_OF")).unwrap()[0].node,
team.id
);
let s2 = write(&db, q);
assert_eq!((s2.nodes_created, s2.edges_created), (0, 0));
}
#[test]
fn merge_after_match_validation() {
assert!(matches!(
parse_statement(r#"MATCH (p:Person) MERGE (x)-[:R]->(t {key:"t"})"#),
Err(ParseError::Compile(_))
));
assert!(matches!(
parse_statement("MATCH (p:Person) MERGE (p)"),
Err(ParseError::Compile(_))
));
assert!(matches!(
parse_statement(r#"MATCH (p:Person) MERGE (p)-[:R]->(t {key:"t"}) ON CREATE SET p.x = 1"#),
Err(ParseError::Compile(_))
));
}
#[test]
fn path_merge_rejects_on_clauses_and_keyless_nodes() {
assert!(matches!(
parse_statement(r#"MERGE (a {key:"a"})-[:R]->(b {key:"b"}) ON CREATE SET a.x = 1"#),
Err(ParseError::Compile(_))
));
assert!(matches!(
parse_statement(r#"MERGE (a {key:"a"})-[:R]->(b:Person)"#),
Err(ParseError::Compile(_))
));
}
#[test]
fn create_duplicate_key_errors_and_rolls_back() {
let db = Database::in_memory().unwrap();
let stmt = parse_statement(r#"CREATE (a {key:"x"}), (b {key:"x"})"#).unwrap();
let err = match stmt {
Statement::Write(w) => w.apply(&plane(&db)).unwrap_err(),
Statement::Read(_) => panic!("expected write"),
};
assert!(!err.is_empty(), "duplicate key should error");
assert!(plane(&db).node_by_key("x").unwrap().is_none()); }
fn write_p(db: &Database, q: &str, params: &Params) -> WriteSummary {
match parse_statement_full(q, None, params).unwrap() {
Statement::Write(w) => w.apply(&db.plane("startup").unwrap()).unwrap(),
Statement::Read(_) => panic!("expected write"),
}
}
#[test]
fn create_and_set_with_params() {
let db = Database::in_memory().unwrap();
let mut params = Params::new();
params.insert("age".into(), PropValue::Int(42));
params.insert("name".into(), PropValue::Str("Zoe".into()));
let s = write_p(
&db,
r#"CREATE (n:Person {key:"zoe", age:$age, name:$name})"#,
¶ms,
);
assert_eq!(s.nodes_created, 1);
let n = plane(&db).node_by_key("zoe").unwrap().unwrap();
assert_eq!(n.properties["age"].value, PropValue::Int(42));
assert_eq!(n.properties["name"].value, PropValue::Str("Zoe".into()));
let mut p2 = Params::new();
p2.insert("age".into(), PropValue::Int(99));
write_p(&db, "MATCH (n:Person) SET n.age = $age", &p2);
assert_eq!(
plane(&db).node_by_key("zoe").unwrap().unwrap().properties["age"].value,
PropValue::Int(99)
);
}
#[test]
fn unbound_param_in_a_write_errors_at_apply() {
let db = Database::in_memory().unwrap();
let stmt = parse_statement(r#"CREATE (n {key:"k", x:$missing})"#).unwrap();
let e = match stmt {
Statement::Write(w) => w.apply(&plane(&db)).unwrap_err(),
Statement::Read(_) => panic!("expected write"),
};
assert!(e.contains("unbound parameter"), "{e}");
assert!(plane(&db).node_by_key("k").unwrap().is_none()); }
#[test]
fn read_parse_rejects_a_write() {
let e = parse("CREATE (n:Person)").unwrap_err();
assert!(matches!(e, ParseError::Compile(_)));
assert!(e.to_string().contains("write statement"));
}
#[test]
fn merge_and_create_take_a_parameterised_key() {
let db = Database::in_memory().unwrap();
let mut params = Params::new();
params.insert("k".into(), PropValue::Str("alice".into()));
params.insert("age".into(), PropValue::Int(30));
let s = write_p(&db, "MERGE (a:Person {key: $k, age: $age})", ¶ms);
assert_eq!(s.nodes_created, 1);
let a = plane(&db).node_by_key("alice").unwrap().unwrap();
assert!(!a.properties.contains_key("key"));
assert_eq!(
write_p(&db, "MERGE (a:Person {key: $k})", ¶ms).nodes_created,
0
);
let mut p2 = Params::new();
p2.insert("k".into(), PropValue::Str("bob".into()));
write_p(&db, "CREATE (b:Person {key: $k})", &p2);
assert!(plane(&db).node_by_key("bob").unwrap().is_some());
let mut p3 = Params::new();
p3.insert("team".into(), PropValue::Str("eng".into()));
let s = write_p(
&db,
"MATCH (p:Person) MERGE (p)-[:MEMBER_OF]->(t:Team {key: $team})",
&p3,
);
assert_eq!((s.nodes_created, s.edges_created), (1, 2));
}
#[test]
fn a_parameterised_key_must_resolve_to_a_string() {
let mut params = Params::new();
params.insert("k".into(), PropValue::Int(7));
let e = parse_statement_full("MERGE (a:Person {key: $k})", None, ¶ms).unwrap_err();
assert!(matches!(e, ParseError::Compile(_)), "{e}");
assert!(e.to_string().contains("must be a string"), "{e}");
let e = parse_statement("MERGE (a:Person {key: $k})").unwrap_err();
assert!(e.to_string().contains("unbound parameter"), "{e}");
}
#[test]
fn string_escapes_reach_the_stored_value() {
let db = Database::in_memory().unwrap();
write(
&db,
r#"CREATE (a:Person {key:"o\"k", quote:'it\'s', path:"a\\b", nl:"x\ny"})"#,
);
let a = plane(&db).node_by_key("o\"k").unwrap().unwrap();
assert_eq!(a.properties["quote"].value, PropValue::Str("it's".into()));
assert_eq!(a.properties["path"].value, PropValue::Str("a\\b".into()));
assert_eq!(a.properties["nl"].value, PropValue::Str("x\ny".into()));
}