use super::*;
impl GraphStore {
#[doc(hidden)]
pub fn collect_prepared_adjacency_dependencies(
&self,
captured: &mut GraphReadDependencies,
node_id: NodeId,
rel_type: Option<RelTypeId>,
direction: AdjacencyDirection,
remaining_work: &mut usize,
) -> Result<()> {
let result = self.collect_adjacency_dependencies_inner(
captured,
node_id,
rel_type,
direction,
remaining_work,
);
captured.refused |= result.is_err();
self.poison_on_storage_error(&result);
result
}
fn collect_adjacency_dependencies_inner(
&self,
captured: &mut GraphReadDependencies,
node_id: NodeId,
rel_type: Option<RelTypeId>,
direction: AdjacencyDirection,
remaining_work: &mut usize,
) -> Result<()> {
self.ensure_usable()?;
if !Arc::ptr_eq(&self.read_dependency_readers, &captured.readers)
|| self.commit_epoch != captured.source_epoch
{
return Err(HawDBError::Execution(
"adjacency dependency scan requires the captured source snapshot".into(),
));
}
if self.canonical_base.is_some() && self.canonical_adjacency.is_none() {
return Err(HawDBError::Execution(
"prepared adjacency requires a qualified canonical adjacency descriptor".into(),
));
}
record(captured, VersionKey::GraphNode(node_id))?;
record(captured, VersionKey::GraphAdjacency { node_id, direction })?;
let adjacency = match direction {
AdjacencyDirection::Outgoing => &self.outgoing,
AdjacencyDirection::Incoming => &self.incoming,
};
for ((_, kind), entries) in
adjacency.range(&(node_id, RelTypeId(0)), &(node_id, RelTypeId(u32::MAX)))
{
charge(remaining_work)?;
if rel_type.is_some_and(|expected| expected != *kind) {
continue;
}
for entry in entries.iter_copied() {
charge(remaining_work)?;
identity(captured, entry.relationship_id, entry.neighbor_id)?;
}
}
if let Some(adjacency) = &self.canonical_adjacency {
let mut callback_error = None;
let result = adjacency
.scan_endpoint_inputs_control(node_id, direction, rel_type, |entry| {
let result = charge(remaining_work).and_then(|()| {
if self
.relationship_tombstones
.contains(&entry.relationship_id)
|| self.relationships.contains_key(&entry.relationship_id)
{
return Ok(());
}
identity(captured, entry.relationship_id, entry.neighbor_id)
});
if let Err(error) = result {
callback_error = Some(error);
Ok(CanonicalScanControl::Stop)
} else {
Ok(CanonicalScanControl::Continue)
}
})
.map_err(|error| HawDBError::StorageIntegrity(error.to_string()));
self.poison_on_storage_error(&result);
result?;
if let Some(error) = callback_error {
return Err(error);
}
}
Ok(())
}
}
fn charge(remaining: &mut usize) -> Result<()> {
*remaining = remaining.checked_sub(1).ok_or_else(|| {
HawDBError::Execution("prepared adjacency identity work budget exceeded".into())
})?;
Ok(())
}
fn record(captured: &mut GraphReadDependencies, key: VersionKey) -> Result<()> {
captured
.record(key)
.map_err(|error| HawDBError::Execution(error.to_string()))
}
fn identity(
captured: &mut GraphReadDependencies,
relationship: RelId,
neighbor: NodeId,
) -> Result<()> {
record(captured, VersionKey::GraphRelationship(relationship))?;
record(captured, VersionKey::GraphNode(neighbor))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ignored_metadata_work_refusal_cannot_validate_a_partial_footprint() {
let mut catalog = Catalog::default();
let mut store = GraphStore::default();
let root = store
.create_node(&mut catalog, "Memory", BTreeMap::new())
.unwrap();
let neighbor = store
.create_node(&mut catalog, "Label", BTreeMap::new())
.unwrap();
store
.create_relationship(&mut catalog, root, neighbor, "HAS_LABEL", BTreeMap::new())
.unwrap();
let kind = catalog.rel_type_id("HAS_LABEL");
let mut captured = store.capture_read_dependencies(32, 8192).unwrap();
assert!(store
.collect_prepared_adjacency_dependencies(
&mut captured,
root,
kind,
AdjacencyDirection::Outgoing,
&mut 0
)
.is_err());
assert!(matches!(
store.validate_read_dependencies(&captured),
Err(GraphReadDependencyError::CaptureRefused)
));
store.ensure_usable().unwrap();
}
#[test]
fn metadata_enumeration_collects_identities_with_large_relationship_properties() {
let mut catalog = Catalog::default();
let mut store = GraphStore::default();
let root = store
.create_node(&mut catalog, "Memory", BTreeMap::new())
.unwrap();
let neighbor = store
.create_node(&mut catalog, "Label", BTreeMap::new())
.unwrap();
let relationship = store
.create_relationship(
&mut catalog,
root,
neighbor,
"HAS_LABEL",
BTreeMap::from([("body".into(), Value::String("x".repeat(1024 * 1024)))]),
)
.unwrap();
let mut captured = store.capture_read_dependencies(32, 8192).unwrap();
store
.collect_prepared_adjacency_dependencies(
&mut captured,
root,
catalog.rel_type_id("HAS_LABEL"),
AdjacencyDirection::Outgoing,
&mut 2,
)
.unwrap();
assert!(captured
.keys
.contains_key(&VersionKey::GraphRelationship(relationship)));
assert!(captured.keys.contains_key(&VersionKey::GraphNode(neighbor)));
store.validate_read_dependencies(&captured).unwrap();
}
}