use super::*;
use crate::graph::property_types::DeclaredType;
fn doc_entity_graph() -> DirGraph {
let mut g = DirGraph::new();
let docs = DataFrame::from_cypher_rows(
vec!["id".to_string()],
vec![vec![Value::Int64(1)], vec![Value::Int64(2)]],
)
.unwrap();
add_nodes(
&mut g,
docs,
"Doc".to_string(),
"id".to_string(),
Some("id".to_string()),
None,
)
.unwrap();
let ents = DataFrame::from_cypher_rows(
vec!["id".to_string()],
vec![
vec![Value::String("A".into())],
vec![Value::String("B".into())],
vec![Value::String("C".into())],
],
)
.unwrap();
add_nodes(
&mut g,
ents,
"Entity".to_string(),
"id".to_string(),
Some("id".to_string()),
None,
)
.unwrap();
g
}
fn edges_df(pairs: &[(i64, &str)]) -> DataFrame {
let rows: Vec<Vec<Value>> = pairs
.iter()
.map(|(s, t)| vec![Value::Int64(*s), Value::String((*t).into())])
.collect();
DataFrame::from_cypher_rows(vec!["s".to_string(), "t".to_string()], rows).unwrap()
}
fn count_edges_of_type(g: &DirGraph, node_type: &str, id: i64, conn: &str) -> usize {
let idx = g
.lookup_by_id_readonly(node_type, &Value::Int64(id))
.unwrap();
let key = InternedKey::from_str(conn);
g.graph
.edges_directed_filtered(idx, petgraph::Direction::Outgoing, Some(key))
.filter(|e| e.connection_type() == key)
.count()
}
fn add_mentions(g: &mut DirGraph, pairs: &[(i64, &str)]) {
add_connections(
g,
edges_df(pairs),
"MENTIONS".to_string(),
"Doc".to_string(),
"s".to_string(),
"Entity".to_string(),
"t".to_string(),
None,
None,
None,
)
.unwrap();
}
fn replace_mentions(g: &mut DirGraph, pairs: &[(i64, &str)]) {
replace_connections(
g,
edges_df(pairs),
"MENTIONS".to_string(),
"Doc".to_string(),
"s".to_string(),
"Entity".to_string(),
"t".to_string(),
None,
None,
None,
)
.unwrap();
}
#[test]
fn replace_sets_exact_edge_set() {
let mut g = doc_entity_graph();
add_mentions(&mut g, &[(1, "A"), (1, "B")]);
assert_eq!(count_edges_of_type(&g, "Doc", 1, "MENTIONS"), 2);
replace_mentions(&mut g, &[(1, "B"), (1, "C")]);
assert_eq!(count_edges_of_type(&g, "Doc", 1, "MENTIONS"), 2);
}
#[test]
fn replace_is_scoped_to_input_sources_and_type() {
let mut g = doc_entity_graph();
add_mentions(&mut g, &[(1, "A"), (2, "A")]);
add_connections(
&mut g,
edges_df(&[(1, "B")]),
"CITES".to_string(),
"Doc".to_string(),
"s".to_string(),
"Entity".to_string(),
"t".to_string(),
None,
None,
None,
)
.unwrap();
replace_mentions(&mut g, &[(1, "C")]);
assert_eq!(count_edges_of_type(&g, "Doc", 1, "MENTIONS"), 1);
assert_eq!(count_edges_of_type(&g, "Doc", 2, "MENTIONS"), 1);
assert_eq!(count_edges_of_type(&g, "Doc", 1, "CITES"), 1);
}
#[test]
fn replace_validates_before_pruning() {
let mut g = doc_entity_graph();
add_mentions(&mut g, &[(1, "A")]);
let bad = DataFrame::from_cypher_rows(
vec!["s".to_string(), "wrong".to_string()],
vec![vec![Value::Int64(1), Value::String("B".into())]],
)
.unwrap();
let err = replace_connections(
&mut g,
bad,
"MENTIONS".to_string(),
"Doc".to_string(),
"s".to_string(),
"Entity".to_string(),
"t".to_string(),
None,
None,
None,
);
assert!(err.is_err());
assert_eq!(count_edges_of_type(&g, "Doc", 1, "MENTIONS"), 1);
}
fn replace_expecting_failure(
graph: &mut DirGraph,
pairs: &[(i64, &str)],
conflict_handling: Option<String>,
) -> String {
replace_connections(
graph,
edges_df(pairs),
"MENTIONS".to_string(),
"Doc".to_string(),
"s".to_string(),
"Entity".to_string(),
"t".to_string(),
None,
None,
conflict_handling,
)
.expect_err("the replace was expected to fail")
}
#[test]
fn an_invalid_conflict_mode_does_not_destroy_the_existing_edges() {
let mut graph = doc_entity_graph();
add_mentions(&mut graph, &[(1, "A"), (1, "B")]);
assert_eq!(count_edges_of_type(&graph, "Doc", 1, "MENTIONS"), 2);
let error = replace_expecting_failure(&mut graph, &[(1, "C")], Some("bogus-mode".to_string()));
assert!(error.contains("conflict handling"), "got: {error}");
assert_eq!(
count_edges_of_type(&graph, "Doc", 1, "MENTIONS"),
2,
"a rejected replace must leave the edges it was going to replace"
);
}
#[test]
fn a_constraint_refused_stub_does_not_destroy_the_existing_edges() {
let mut graph = doc_entity_graph();
add_mentions(&mut graph, &[(1, "A"), (1, "B")]);
graph
.create_property_type_constraint("Entity", "id", DeclaredType::Integer)
.expect_err("existing string ids already violate it");
graph
.create_property_type_constraint("Doc", "id", DeclaredType::Integer)
.expect("Doc ids are integers");
let error = replace_connections(
&mut graph,
DataFrame::from_cypher_rows(
vec!["s".to_string(), "t".to_string()],
vec![vec![Value::Int64(1), Value::String("missing-doc".into())]],
)
.unwrap(),
"MENTIONS".to_string(),
"Doc".to_string(),
"s".to_string(),
"Doc".to_string(),
"t".to_string(),
None,
None,
None,
)
.expect_err("a stub violating the declared id type must refuse the load");
assert!(error.contains("INTEGER"), "got: {error}");
assert_eq!(
count_edges_of_type(&graph, "Doc", 1, "MENTIONS"),
2,
"a rejected replace must leave the edges it was going to replace"
);
}
#[test]
fn a_constraint_refused_stub_leaves_no_edges_from_a_plain_add() {
let mut graph = doc_entity_graph();
graph
.create_property_type_constraint("Doc", "id", DeclaredType::Integer)
.expect("Doc ids are integers");
let before = count_edges_of_type(&graph, "Doc", 1, "MENTIONS");
assert_eq!(before, 0);
let error = add_connections(
&mut graph,
DataFrame::from_cypher_rows(
vec!["s".to_string(), "t".to_string()],
vec![
vec![Value::Int64(1), Value::Int64(2)],
vec![Value::Int64(1), Value::String("missing".into())],
],
)
.unwrap(),
"MENTIONS".to_string(),
"Doc".to_string(),
"s".to_string(),
"Doc".to_string(),
"t".to_string(),
None,
None,
None,
)
.expect_err("a stub violating the declared id type must refuse the load");
assert!(error.contains("INTEGER"), "got: {error}");
assert_eq!(
count_edges_of_type(&graph, "Doc", 1, "MENTIONS"),
0,
"a refused add must commit none of its edges, not even the valid rows"
);
assert!(
graph
.lookup_by_id_readonly("Doc", &Value::String("missing".into()))
.is_none(),
"the refused stub must not exist"
);
}