use std::sync::Arc;
use petgraph::graph::NodeIndex;
use crate::datatypes::values::{raw_string, Value};
use crate::graph::dir_graph::DirGraph;
use crate::graph::embedder::Embedder;
use crate::graph::schema;
use crate::graph::storage::GraphRead;
use crate::graph::{SourceLocation, SourceLookup};
pub const CODE_TYPES: &[&str] = &[
"Function",
"Struct",
"Class",
"Mixin",
"Enum",
"Trait",
"Protocol",
"Interface",
"Module",
"Constant",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodeEntityMatch {
Exact,
Contains,
StartsWith,
}
pub fn find_code_entities(
dir: &Arc<DirGraph>,
name: &str,
node_type: Option<&str>,
match_type: CodeEntityMatch,
) -> Vec<schema::NodeInfo> {
let _arena_guard = dir.graph.begin_query();
let name_lower = name.to_lowercase();
let name_value = Value::String(name.to_string());
let types_to_search: Vec<&str> = match node_type {
Some(nt) => vec![nt],
None => CODE_TYPES.to_vec(),
};
let mut results = Vec::new();
for node_type in types_to_search {
let Some(indices) = dir.type_indices.get(node_type) else {
continue;
};
for index in indices.iter() {
let Some(node) = dir.node_view(index) else {
continue;
};
let title = node.title();
let title_string = match &*title {
Value::String(value) => Some(value.as_str()),
_ => None,
};
let matches = match match_type {
CodeEntityMatch::Contains => {
node.field_contains_ci("name", &name_lower)
|| title_string
.is_some_and(|value| value.to_lowercase().contains(&name_lower))
}
CodeEntityMatch::StartsWith => {
node.field_starts_with_ci("name", &name_lower)
|| title_string
.is_some_and(|value| value.to_lowercase().starts_with(&name_lower))
}
CodeEntityMatch::Exact => {
node.get_field_ref("name")
.is_some_and(|value| *value == name_value)
|| *title == name_value
}
};
if matches {
results.push(node.to_node_info(&dir.interner));
}
}
}
results
}
#[derive(Debug)]
pub struct CodeEntityContext {
pub node: schema::NodeInfo,
pub defined_in: Option<String>,
pub outgoing: std::collections::HashMap<String, Vec<schema::NodeInfo>>,
pub incoming: std::collections::HashMap<String, Vec<schema::NodeInfo>>,
}
#[derive(Debug)]
pub enum CodeContextLookup {
Found(Box<CodeEntityContext>),
Ambiguous(Vec<schema::NodeInfo>),
NotFound,
}
pub fn code_entity_context(
dir: &Arc<DirGraph>,
name: &str,
node_type: Option<&str>,
hops: usize,
) -> CodeContextLookup {
let _arena_guard = dir.graph.begin_query();
let (resolved, matches) = resolve_code_entity(dir, name, node_type);
let Some(target_idx) = resolved else {
return if matches.is_empty() {
CodeContextLookup::NotFound
} else {
CodeContextLookup::Ambiguous(matches.into_iter().map(|(_, info)| info).collect())
};
};
let Some(target_node) = dir.node_view(target_idx) else {
return CodeContextLookup::NotFound;
};
let neighbor_indices = if hops <= 1 {
let mut neighbors = std::collections::HashSet::new();
for edge in dir
.graph
.edges_directed(target_idx, petgraph::Direction::Outgoing)
{
neighbors.insert(edge.target());
}
for edge in dir
.graph
.edges_directed(target_idx, petgraph::Direction::Incoming)
{
neighbors.insert(edge.source());
}
neighbors
} else {
let mut visited = std::collections::HashSet::from([target_idx]);
let mut frontier = std::collections::HashSet::from([target_idx]);
for _ in 0..hops {
let mut next_frontier = std::collections::HashSet::new();
for &node in &frontier {
for neighbor in dir.graph.neighbors_undirected(node) {
if visited.insert(neighbor) {
next_frontier.insert(neighbor);
}
}
}
if next_frontier.is_empty() {
break;
}
frontier = next_frontier;
}
visited.remove(&target_idx);
visited
};
let mut outgoing_indices: std::collections::HashMap<String, Vec<NodeIndex>> =
std::collections::HashMap::new();
let mut incoming_indices: std::collections::HashMap<String, Vec<NodeIndex>> =
std::collections::HashMap::new();
for edge in dir
.graph
.edges_directed(target_idx, petgraph::Direction::Outgoing)
{
let target = edge.target();
if hops <= 1 || neighbor_indices.contains(&target) {
outgoing_indices
.entry(edge.weight().connection_type_str(&dir.interner).to_string())
.or_default()
.push(target);
}
}
for edge in dir
.graph
.edges_directed(target_idx, petgraph::Direction::Incoming)
{
let source = edge.source();
if hops <= 1 || neighbor_indices.contains(&source) {
incoming_indices
.entry(edge.weight().connection_type_str(&dir.interner).to_string())
.or_default()
.push(source);
}
}
if hops > 1 {
for &node_idx in &neighbor_indices {
for edge in dir
.graph
.edges_directed(node_idx, petgraph::Direction::Outgoing)
{
let target = edge.target();
if target != target_idx && neighbor_indices.contains(&target) {
outgoing_indices
.entry(edge.weight().connection_type_str(&dir.interner).to_string())
.or_default()
.push(target);
}
}
}
}
let materialise_groups = |groups: std::collections::HashMap<String, Vec<NodeIndex>>| {
groups
.into_iter()
.map(|(edge_type, indices)| {
let mut seen = std::collections::HashSet::new();
let nodes = indices
.into_iter()
.filter(|index| seen.insert(*index))
.filter_map(|index| dir.node_view(index))
.map(|node| node.to_node_info(&dir.interner))
.collect();
(edge_type, nodes)
})
.collect()
};
CodeContextLookup::Found(Box::new(CodeEntityContext {
node: target_node.to_node_info(&dir.interner),
defined_in: match target_node.get_field_ref("file_path").as_deref() {
Some(Value::String(path)) => Some(path.clone()),
_ => None,
},
outgoing: materialise_groups(outgoing_indices),
incoming: materialise_groups(incoming_indices),
}))
}
pub fn resolve_code_entity(
dir: &Arc<DirGraph>,
name: &str,
node_type: Option<&str>,
) -> (Option<NodeIndex>, Vec<(NodeIndex, schema::NodeInfo)>) {
let _arena_guard = dir.graph.begin_query();
let name_val = Value::String(name.to_string());
let types_to_search: Vec<&str> = match node_type {
Some(nt) => vec![nt],
None => CODE_TYPES.to_vec(),
};
for nt in &types_to_search {
if let Some(indices) = dir.type_indices.get(nt) {
for idx in indices.iter() {
if let Some(node) = dir.node_view(idx) {
if *node.id() == name_val {
return (Some(idx), Vec::new());
}
}
}
}
}
if name.contains("::") {
let suffix = format!("::{}", name);
let mut matches: Vec<(NodeIndex, schema::NodeInfo)> = Vec::new();
for nt in &types_to_search {
if let Some(indices) = dir.type_indices.get(nt) {
for idx in indices.iter() {
if let Some(node) = dir.node_view(idx) {
if let Value::String(qn) = &*node.id() {
if qn.ends_with(&suffix) {
matches.push((idx, node.to_node_info(&dir.interner)));
}
}
}
}
}
}
if matches.len() == 1 {
return (Some(matches[0].0), matches);
} else if !matches.is_empty() {
return (None, matches);
}
}
let mut matches: Vec<(NodeIndex, schema::NodeInfo)> = Vec::new();
for nt in &types_to_search {
if let Some(indices) = dir.type_indices.get(nt) {
for idx in indices.iter() {
if let Some(node) = dir.node_view(idx) {
let name_match = node
.get_field_ref("name")
.map(|v| *v == name_val)
.unwrap_or(false)
|| node
.get_field_ref("title")
.map(|v| *v == name_val)
.unwrap_or(false);
if name_match {
matches.push((idx, node.to_node_info(&dir.interner)));
}
}
}
}
}
if matches.len() == 1 {
(Some(matches[0].0), matches)
} else {
(None, matches)
}
}
pub fn infer_selection_node_type(
selection: &crate::graph::schema::CowSelection,
dir: &Arc<DirGraph>,
) -> Option<String> {
let level_idx = selection.get_level_count().saturating_sub(1);
let level = selection.get_level(level_idx)?;
let first_idx = level.iter_node_indices().next()?;
let _arena_guard = dir.graph.begin_query();
dir.graph
.node_view(first_idx)
.map(|n| n.node_type_str(&dir.interner).to_string())
}
pub const CANONICAL_NODE_COLUMNS: [&str; 3] = ["id", "title", "type"];
pub fn is_canonical_node_column(key: &str) -> bool {
CANONICAL_NODE_COLUMNS.contains(&key)
}
pub fn discover_property_keys_from_data(
nodes: &[(&str, crate::graph::storage::NodeView<'_>)],
interner: &crate::graph::schema::StringInterner,
) -> Vec<String> {
discover_property_keys_excluding(nodes, interner, &CANONICAL_NODE_COLUMNS)
}
pub fn discover_property_keys_excluding(
nodes: &[(&str, crate::graph::storage::NodeView<'_>)],
interner: &crate::graph::schema::StringInterner,
excluded: &[&str],
) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
let mut keys = Vec::new();
for (_, node) in nodes {
for key in node.property_keys(interner) {
if excluded.contains(&key) {
continue;
}
if seen.insert(key.to_string()) {
keys.push(key.to_string());
}
}
}
keys.sort();
keys
}
pub fn source_location(dir: &Arc<DirGraph>, name: &str, node_type: Option<&str>) -> SourceLookup {
let _arena_guard = dir.graph.begin_query();
let (resolved, matches) = resolve_code_entity(dir, name, node_type);
if let Some(target_idx) = resolved {
let node = match dir.node_view(target_idx) {
Some(n) => n,
None => return SourceLookup::NotFound,
};
let type_name = node.get_node_type_ref(&dir.interner).to_string();
let entity_name = raw_string(&node.title());
let qname = raw_string(&node.id());
let file_path = node.get_field_ref("file_path").as_deref().map(raw_string);
let line_number = node
.get_field_ref("line_number")
.as_deref()
.and_then(|v| match v {
Value::Int64(n) => Some(*n),
_ => None,
});
let end_line = node
.get_field_ref("end_line")
.as_deref()
.and_then(|v| match v {
Value::Int64(n) => Some(*n),
_ => None,
});
let signature = node.get_field_ref("signature").as_deref().map(raw_string);
SourceLookup::Found(SourceLocation {
type_name,
name: entity_name,
qualified_name: qname,
file_path,
line_number,
end_line,
signature,
})
} else if matches.is_empty() {
SourceLookup::NotFound
} else {
let qnames: Vec<String> = matches
.iter()
.map(|(_, info)| raw_string(&info.id))
.collect();
SourceLookup::Ambiguous(qnames)
}
}
pub struct KnowledgeGraph {
inner: Arc<DirGraph>,
embedder: Option<Arc<dyn Embedder>>,
}
impl KnowledgeGraph {
pub fn from_arc(inner: Arc<DirGraph>) -> Self {
KnowledgeGraph {
inner,
embedder: None,
}
}
pub fn dir(&self) -> &Arc<DirGraph> {
&self.inner
}
pub fn dir_mut(&mut self) -> &mut Arc<DirGraph> {
&mut self.inner
}
pub fn set_embedder_native(&mut self, embedder: Arc<dyn Embedder>) {
self.embedder = Some(embedder);
}
pub fn embedder(&self) -> Option<&Arc<dyn Embedder>> {
self.embedder.as_ref()
}
pub fn source_location(&self, name: &str, node_type: Option<&str>) -> SourceLookup {
source_location(&self.inner, name, node_type)
}
}
pub(crate) fn make_dir_graph_mut_preserving_lineage(arc: &mut Arc<DirGraph>) -> &mut DirGraph {
let parent = if Arc::get_mut(arc).is_none() {
Some(Arc::clone(arc))
} else {
None
};
let graph = Arc::make_mut(arc);
if let Some(parent) = parent {
graph.graph.adopt_shared_writer_lineage(&parent.graph);
}
graph.graph.try_compact();
graph.graph.ensure_writable();
graph.id_indices.try_compact();
graph.type_indices.try_compact();
for index in graph.property_indices.values_mut() {
index.try_compact();
}
for index in graph.composite_indices.values_mut() {
index.try_compact();
}
graph
}
pub fn make_dir_graph_mut(arc: &mut Arc<DirGraph>) -> &mut DirGraph {
let graph = make_dir_graph_mut_preserving_lineage(arc);
graph.bump_version();
graph
}
#[cfg(test)]
mod boundary_lift_tests {
use super::*;
use crate::graph::session::{execute_mut, ExecuteOptions};
use std::collections::HashMap;
fn code_graph() -> Arc<DirGraph> {
let mut graph = DirGraph::new();
let params = HashMap::new();
execute_mut(
&mut graph,
"CREATE (a:Function {id:'mod::alpha', title:'alpha', name:'alpha', file_path:'src/a.rs'}), \
(b:Function {id:'mod::beta', title:'BetaWorker', name:'beta', file_path:'src/b.rs'}), \
(c:Function {id:'mod::gamma', title:'gamma', name:'gamma', file_path:'src/c.rs'}), \
(f:File {id:'src/a.rs', title:'src/a.rs'})",
&ExecuteOptions::eager(¶ms),
)
.expect("fixture nodes");
execute_mut(
&mut graph,
"MATCH (a:Function {id:'mod::alpha'}), (b:Function {id:'mod::beta'}), \
(c:Function {id:'mod::gamma'}), (f:File {id:'src/a.rs'}) \
CREATE (a)-[:CALLS]->(b), (b)-[:CALLS]->(c), (f)-[:DEFINES]->(a)",
&ExecuteOptions::eager(¶ms),
)
.expect("fixture edges");
Arc::new(graph)
}
#[test]
fn find_code_entities_supports_match_modes_and_type_filter() {
let graph = code_graph();
let exact = find_code_entities(&graph, "alpha", Some("Function"), CodeEntityMatch::Exact);
assert_eq!(exact.len(), 1);
assert_eq!(exact[0].id, Value::String("mod::alpha".into()));
let contains = find_code_entities(&graph, "et", None, CodeEntityMatch::Contains);
assert_eq!(contains.len(), 1);
assert_eq!(contains[0].id, Value::String("mod::beta".into()));
let starts_with = find_code_entities(&graph, "bet", None, CodeEntityMatch::StartsWith);
assert_eq!(starts_with.len(), 1);
assert_eq!(starts_with[0].id, Value::String("mod::beta".into()));
}
#[test]
fn code_entity_context_groups_directional_multi_hop_neighbors() {
let graph = code_graph();
let CodeContextLookup::Found(context) =
code_entity_context(&graph, "alpha", Some("Function"), 2)
else {
panic!("expected resolved context");
};
assert_eq!(context.defined_in.as_deref(), Some("src/a.rs"));
let calls = &context.outgoing["CALLS"];
assert_eq!(calls.len(), 2);
assert!(calls
.iter()
.any(|node| node.id == Value::String("mod::beta".into())));
assert!(calls
.iter()
.any(|node| node.id == Value::String("mod::gamma".into())));
assert_eq!(context.incoming["DEFINES"].len(), 1);
}
#[test]
fn code_entity_context_distinguishes_miss_from_ambiguity() {
let mut graph = match Arc::try_unwrap(code_graph()) {
Ok(graph) => graph,
Err(_) => panic!("expected sole graph owner"),
};
let params = HashMap::new();
execute_mut(
&mut graph,
"CREATE (:Function {id:'other::alpha', title:'alpha', name:'alpha', file_path:'other.rs'})",
&ExecuteOptions::eager(¶ms),
)
.expect("add ambiguous entity");
let graph = Arc::new(graph);
assert!(matches!(
code_entity_context(&graph, "alpha", Some("Function"), 1),
CodeContextLookup::Ambiguous(matches) if matches.len() == 2
));
assert!(matches!(
code_entity_context(&graph, "missing", None, 1),
CodeContextLookup::NotFound
));
}
fn collision_graph() -> Arc<DirGraph> {
let mut graph = DirGraph::new();
let params = HashMap::new();
execute_mut(
&mut graph,
"CREATE (:T {id:1, title:'a', v:2}), (:T {id:2, title:'b', type:'USER', w:3})",
&ExecuteOptions::eager(¶ms),
)
.expect("fixture nodes");
Arc::new(graph)
}
fn nodes_of(graph: &DirGraph) -> Vec<(&str, crate::graph::storage::NodeView<'_>)> {
graph
.graph
.node_indices()
.filter_map(|idx| {
graph
.node_view(idx)
.map(|n| (n.node_type_str(&graph.interner), n))
})
.collect()
}
#[test]
fn discovered_property_keys_exclude_canonical_columns() {
let graph = collision_graph();
let keys = discover_property_keys_from_data(&nodes_of(&graph), &graph.interner);
assert_eq!(keys, vec!["v".to_string(), "w".to_string()]);
for canonical in CANONICAL_NODE_COLUMNS {
assert!(
!keys.contains(&canonical.to_string()),
"canonical column {canonical} leaked into the property key set"
);
}
}
#[test]
fn an_unemitted_canonical_column_keeps_its_stored_property() {
let graph = collision_graph();
let keys =
discover_property_keys_excluding(&nodes_of(&graph), &graph.interner, &["id", "title"]);
assert_eq!(
keys,
vec!["type".to_string(), "v".to_string(), "w".to_string()]
);
}
#[test]
fn is_canonical_node_column_covers_exactly_the_identity_names() {
assert!(is_canonical_node_column("id"));
assert!(is_canonical_node_column("title"));
assert!(is_canonical_node_column("type"));
assert!(!is_canonical_node_column("titles"));
assert!(!is_canonical_node_column("node_type"));
assert!(!is_canonical_node_column("Title"));
}
}
#[cfg(test)]
mod held_reference_clone_tests {
use super::*;
use crate::graph::session::{execute_mut, ExecuteOptions};
use crate::graph::storage::backend::{backend_clone_nodes, reset_backend_clone_count};
use crate::graph::storage::GraphRead;
use std::collections::HashMap;
const FIXTURE_NODES: usize = 10;
fn snapshot_ids_and_titles(graph: &DirGraph) -> Vec<(usize, String, String)> {
graph
.graph
.node_indices()
.map(|idx| {
let view = graph.graph.node_view(idx).expect("live node");
(
idx.index(),
format!("{:?}", view.id()),
format!("{:?}", view.title()),
)
})
.collect()
}
fn seeded_arc() -> Arc<DirGraph> {
let mut graph = DirGraph::new();
let params = HashMap::new();
for i in 0..FIXTURE_NODES {
execute_mut(
&mut graph,
&format!("CREATE (:Item {{id: {i}, name: 'item-{i}'}})"),
&ExecuteOptions::eager(¶ms),
)
.expect("fixture node");
}
Arc::new(graph)
}
#[test]
fn unique_handle_copies_nothing() {
let mut arc = seeded_arc();
reset_backend_clone_count();
let _ = make_dir_graph_mut(&mut arc);
assert_eq!(
backend_clone_nodes(),
0,
"a uniquely-owned Arc<DirGraph> must mutate in place"
);
}
#[test]
fn held_reader_copies_no_nodes() {
let mut arc = seeded_arc();
let reader = Arc::clone(&arc);
reset_backend_clone_count();
let _ = make_dir_graph_mut(&mut arc);
let copied = backend_clone_nodes();
assert_eq!(reader.graph.node_count(), FIXTURE_NODES);
assert_eq!(
copied, 0,
"a live second Arc<DirGraph> must fork to a copy-on-write overlay, \
not copy the graph; getting {FIXTURE_NODES} here means the fork \
regressed to a deep clone (storage/forked.rs)"
);
assert!(
arc.graph.is_forked(),
"the writer's backend must be the overlay variant while the reader lives"
);
}
#[test]
fn a_held_reader_never_observes_the_writers_edits() {
let mut arc = seeded_arc();
let reader = Arc::clone(&arc);
let before = snapshot_ids_and_titles(&reader);
{
let graph = make_dir_graph_mut(&mut arc);
let params = HashMap::new();
execute_mut(
graph,
"MATCH (n:Item {id: 3}) SET n.name = 'rewritten'",
&ExecuteOptions::eager(¶ms),
)
.expect("write");
execute_mut(
graph,
"CREATE (:Item {id: 999, name: 'appended'})",
&ExecuteOptions::eager(¶ms),
)
.expect("append");
}
assert_eq!(
snapshot_ids_and_titles(&reader),
before,
"the reader's graph must be byte-for-byte what it was before the \
write; a difference here means the writer mutated the shared base, \
which silently corrupts every holder of that snapshot"
);
assert_eq!(reader.graph.node_count(), FIXTURE_NODES);
assert_eq!(arc.graph.node_count(), FIXTURE_NODES + 1);
}
#[test]
fn a_held_reader_resolves_ids_against_its_own_snapshot() {
let mut arc = seeded_arc();
let params = HashMap::new();
{
let graph = make_dir_graph_mut(&mut arc);
execute_mut(
graph,
"MATCH (n:Item {id: 0}) RETURN n.id",
&ExecuteOptions::eager(¶ms),
)
.expect("warm the id index");
}
let reader = Arc::clone(&arc);
assert!(
reader
.id_indices
.lookup("Item", &crate::datatypes::Value::Int64(0))
.is_some(),
"fixture must be id-indexed, or this test proves nothing"
);
{
let graph = make_dir_graph_mut(&mut arc);
execute_mut(
graph,
"CREATE (:Item {id: 4242, name: 'appended'})",
&ExecuteOptions::eager(¶ms),
)
.expect("append");
}
let new_id = crate::datatypes::Value::Int64(4242);
assert!(
arc.id_indices.lookup("Item", &new_id).is_some(),
"the writer must resolve the id it just created"
);
assert!(
reader.id_indices.lookup("Item", &new_id).is_none(),
"the reader's snapshot never saw this id; resolving it means the \
writer's delta leaked into the shared base"
);
for id in 0..FIXTURE_NODES {
let value = crate::datatypes::Value::Int64(id as i64);
assert_eq!(
reader.id_indices.lookup("Item", &value),
arc.id_indices.lookup("Item", &value),
"id {id} must resolve the same in both graphs"
);
}
}
#[test]
fn a_held_reader_scans_types_against_its_own_snapshot() {
let mut arc = seeded_arc();
let reader = Arc::clone(&arc);
let before: Vec<usize> = reader
.type_indices
.get("Item")
.expect("fixture is type-indexed")
.iter()
.map(|idx| idx.index())
.collect();
assert_eq!(before.len(), FIXTURE_NODES);
{
let graph = make_dir_graph_mut(&mut arc);
let params = HashMap::new();
execute_mut(
graph,
"CREATE (:Item {id: 7007, name: 'appended'})",
&ExecuteOptions::eager(¶ms),
)
.expect("append");
}
let after: Vec<usize> = reader
.type_indices
.get("Item")
.expect("the reader keeps its bucket")
.iter()
.map(|idx| idx.index())
.collect();
assert_eq!(
after, before,
"the reader's type bucket must be what it was before a write it \
never asked for; a difference means the writer appended into a \
level the reader shares"
);
assert_eq!(
arc.type_indices.get("Item").map(|members| members.len()),
Some(FIXTURE_NODES + 1),
"the writer must see its own append"
);
for idx in &after {
assert!(
reader
.graph
.node_weight(petgraph::graph::NodeIndex::new(*idx))
.is_some(),
"the reader's bucket points at a node its backend does not have"
);
}
}
#[test]
fn a_held_reader_resolves_user_indexes_against_its_own_snapshot() {
let mut arc = seeded_arc();
let params = HashMap::new();
{
let graph = make_dir_graph_mut(&mut arc);
execute_mut(
graph,
"MATCH (n:Item) SET n.qty = n.id",
&ExecuteOptions::eager(¶ms),
)
.expect("seed qty");
graph.create_index("Item", "name");
graph.create_composite_index("Item", &["name", "qty"]);
}
let reader = Arc::clone(&arc);
let existing = crate::datatypes::Value::String("item-3".to_string());
let created = crate::datatypes::Value::String("appended".to_string());
let composite_of = |value: &crate::datatypes::Value, qty: i64| {
vec![value.clone(), crate::datatypes::Value::Int64(qty)]
};
assert!(
reader
.lookup_by_index("Item", "name", &existing)
.is_some_and(|members| !members.is_empty()),
"the fixture must be indexed, or this test proves nothing"
);
let before_existing = reader.lookup_by_index("Item", "name", &existing);
{
let graph = make_dir_graph_mut(&mut arc);
execute_mut(
graph,
"CREATE (:Item {id: 4242, name: 'appended', qty: 7})",
&ExecuteOptions::eager(¶ms),
)
.expect("append");
}
assert!(
arc.lookup_by_index("Item", "name", &created).is_some(),
"the writer must find the value it just indexed"
);
assert!(
arc.lookup_by_composite_index(
"Item",
&["name".to_string(), "qty".to_string()],
&composite_of(&created, 7)
)
.is_some(),
"the writer's composite index must carry its own write"
);
assert_eq!(
reader.lookup_by_index("Item", "name", &created),
None,
"the reader's snapshot never saw this value; finding it means the \
writer's delta leaked into a shared level"
);
assert_eq!(
reader.lookup_by_composite_index(
"Item",
&["name".to_string(), "qty".to_string()],
&composite_of(&created, 7)
),
None,
"same for the composite index"
);
assert_eq!(
reader.lookup_by_index("Item", "name", &existing),
before_existing,
"a value the reader already had must be unchanged, in bucket order"
);
assert_eq!(
arc.lookup_by_index("Item", "name", &existing),
before_existing,
"the writer must still resolve the values it inherited"
);
assert!(
arc.lookup_by_composite_index(
"Item",
&["name".to_string(), "qty".to_string()],
&composite_of(&existing, 3)
)
.is_some_and(|members| !members.is_empty()),
"the writer's composite index must still carry the inherited tuples"
);
for idx in reader
.lookup_by_index("Item", "name", &existing)
.unwrap_or_default()
{
assert!(
reader.graph.node_weight(idx).is_some(),
"the reader's property index points at a node it does not have"
);
}
}
#[test]
fn dropping_the_reader_compacts_the_overlay_back_into_the_base() {
let mut arc = seeded_arc();
let reader = Arc::clone(&arc);
let params = HashMap::new();
{
let graph = make_dir_graph_mut(&mut arc);
execute_mut(
graph,
"CREATE (:Item {id: 999, name: 'appended'})",
&ExecuteOptions::eager(¶ms),
)
.expect("append");
}
assert!(arc.graph.is_forked(), "precondition: the write forked");
let forked_view = snapshot_ids_and_titles(&arc);
drop(reader);
reset_backend_clone_count();
let _ = make_dir_graph_mut(&mut arc);
assert!(
!arc.graph.is_forked(),
"the next write after the reader drops must collapse the overlay"
);
assert_eq!(
backend_clone_nodes(),
0,
"compaction folds in place; it must not copy the graph"
);
assert_eq!(
snapshot_ids_and_titles(&arc),
forked_view,
"compaction must preserve content AND petgraph slot identity — the \
snapshot is keyed by slot, so a re-indexed node shows up here"
);
}
#[test]
fn dropping_the_reader_restores_in_place_mutation() {
let mut arc = seeded_arc();
let reader = Arc::clone(&arc);
drop(reader);
reset_backend_clone_count();
let _ = make_dir_graph_mut(&mut arc);
assert_eq!(
backend_clone_nodes(),
0,
"once the extra Arc is gone the write must mutate in place again"
);
}
}