use super::*;
#[test]
fn add_nodes_builds_id_index() {
let mut g = DirGraph::new();
let rows: Vec<Vec<Value>> = (0..1000).map(|i| vec![Value::Int64(i)]).collect();
let df = DataFrame::from_cypher_rows(vec!["id".to_string()], rows).unwrap();
add_nodes(
&mut g,
df,
"Person".to_string(),
"id".to_string(),
Some("id".to_string()),
None,
)
.unwrap();
assert!(
g.id_indices.contains_key("Person"),
"id_index must be built after add_nodes so reads are O(1), not a linear scan"
);
assert!(g
.lookup_by_id_readonly("Person", &Value::Int64(999))
.is_some());
}
#[test]
fn add_nodes_collision_preflight_leaves_graph_unchanged() {
let mut g = DirGraph::new();
let incoming = "CollisionType";
g.interner
.try_register(
crate::graph::schema::InternedKey::from_str(incoming),
"conflicting-existing",
)
.unwrap();
let before_interner: Vec<_> = g
.interner
.iter()
.map(|(key, value)| (key, value.to_string()))
.collect();
let df =
DataFrame::from_cypher_rows(vec!["id".to_string()], vec![vec![Value::Int64(1)]]).unwrap();
let err = add_nodes(
&mut g,
df,
incoming.to_string(),
"id".to_string(),
None,
None,
)
.unwrap_err();
assert!(err.contains("hash collision"));
assert_eq!(g.graph.node_count(), 0);
assert!(g.node_type_metadata.is_empty());
assert!(g.type_indices.is_empty());
assert_eq!(
g.interner
.iter()
.map(|(key, value)| (key, value.to_string()))
.collect::<Vec<_>>(),
before_interner
);
}
#[test]
fn add_nodes_rejects_within_batch_pk_duplicate() {
use crate::graph::schema::{NodeSchemaDefinition, SchemaDefinition, SchemaInstall};
let mut g = DirGraph::new();
let mut schema = SchemaDefinition::new();
schema.add_node_schema(
"Person".to_string(),
NodeSchemaDefinition {
primary_key: Some("id".to_string()),
..Default::default()
},
);
g.set_schema(schema, SchemaInstall::Merge)
.expect("schema install");
let dup = DataFrame::from_cypher_rows(
vec!["id".to_string()],
vec![
vec![Value::Int64(1)],
vec![Value::Int64(2)],
vec![Value::Int64(2)],
],
)
.unwrap();
let err = add_nodes(
&mut g,
dup,
"Person".to_string(),
"id".to_string(),
Some("id".to_string()),
None,
)
.unwrap_err();
assert!(err.contains("duplicate primary key"), "got: {err}");
let clean = DataFrame::from_cypher_rows(
vec!["id".to_string()],
vec![vec![Value::Int64(10)], vec![Value::Int64(11)]],
)
.unwrap();
let report = add_nodes(
&mut g,
clean,
"Person".to_string(),
"id".to_string(),
Some("id".to_string()),
None,
);
assert!(report.is_ok(), "clean batch should load: {report:?}");
}
#[test]
fn add_nodes_update_is_partial() {
let mut g = DirGraph::new();
let seed = DataFrame::from_cypher_rows(
vec!["id".to_string(), "status".to_string(), "notes".to_string()],
vec![vec![
Value::Int64(1),
Value::String("in_progress".into()),
Value::String("agent work".into()),
]],
)
.unwrap();
add_nodes(
&mut g,
seed,
"Task".to_string(),
"id".to_string(),
Some("id".to_string()),
None,
)
.unwrap();
let reload = DataFrame::from_cypher_rows(
vec!["id".to_string(), "spec_link".to_string()],
vec![vec![Value::Int64(1), Value::String("AlgoSpec-7".into())]],
)
.unwrap();
add_nodes(
&mut g,
reload,
"Task".to_string(),
"id".to_string(),
Some("id".to_string()),
Some("update".to_string()),
)
.unwrap();
let idx = g.lookup_by_id("Task", &Value::Int64(1)).unwrap();
let node = g.graph.node_view(idx).unwrap();
assert_eq!(
node.get_field_ref("status").as_deref(),
Some(&Value::String("in_progress".into())),
"status must survive a partial update"
);
assert_eq!(
node.get_field_ref("notes").as_deref(),
Some(&Value::String("agent work".into())),
"notes must survive a partial update"
);
assert_eq!(
node.get_field_ref("spec_link").as_deref(),
Some(&Value::String("AlgoSpec-7".into())),
"the new field must be written"
);
}
#[test]
fn readonly_lookup_self_heals_when_index_absent() {
let mut g = DirGraph::new();
let rows: Vec<Vec<Value>> = (0..1000).map(|i| vec![Value::Int64(i)]).collect();
let df = DataFrame::from_cypher_rows(vec!["id".to_string()], rows).unwrap();
add_nodes(
&mut g,
df,
"Person".to_string(),
"id".to_string(),
Some("id".to_string()),
None,
)
.unwrap();
g.id_indices.remove("Person");
assert!(!g.id_indices.contains_key("Person"));
assert!(g
.lookup_by_id_readonly("Person", &Value::Int64(999))
.is_some());
assert!(
g.id_indices.contains_key("Person"),
"read path must build + cache the id_index on a miss (issue #20)"
);
assert!(g
.lookup_by_id_readonly("Person", &Value::Int64(424242))
.is_none());
}