capnp_store: remove dead has_node trait method, fix fix_categories bulk write

- has_node: defined on StoreView trait but never called externally
- fix_categories: was appending ALL nodes when only changed ones needed
  persisting; now collects changed nodes and appends only those
- save_snapshot: pass log sizes from caller instead of re-statting files
- params: use Copy instead of .clone() in snapshot construction
This commit is contained in:
ProofOfConcept 2026-03-03 12:42:16 -05:00
parent 70a5f05ce0
commit a2ec8657d2

View file

@ -440,9 +440,6 @@ pub trait StoreView {
/// Node content by key. /// Node content by key.
fn node_content(&self, key: &str) -> Option<&str>; fn node_content(&self, key: &str) -> Option<&str>;
/// Check if a node exists.
fn has_node(&self, key: &str) -> bool;
/// Search/graph parameters. /// Search/graph parameters.
fn params(&self) -> Params; fn params(&self) -> Params;
} }
@ -469,10 +466,6 @@ impl StoreView for Store {
self.nodes.get(key).map(|n| n.content.as_str()) self.nodes.get(key).map(|n| n.content.as_str())
} }
fn has_node(&self, key: &str) -> bool {
self.nodes.contains_key(key)
}
fn params(&self) -> Params { fn params(&self) -> Params {
self.params self.params
} }
@ -554,10 +547,6 @@ impl StoreView for MmapView {
snap.nodes.get(key).map(|n| &*n.content) snap.nodes.get(key).map(|n| &*n.content)
} }
fn has_node(&self, key: &str) -> bool {
self.snapshot().nodes.get(key).is_some()
}
fn params(&self) -> Params { fn params(&self) -> Params {
let p = &self.snapshot().params; let p = &self.snapshot().params;
Params { Params {
@ -608,9 +597,6 @@ impl StoreView for AnyView {
fn node_content(&self, key: &str) -> Option<&str> { fn node_content(&self, key: &str) -> Option<&str> {
match self { AnyView::Mmap(v) => v.node_content(key), AnyView::Owned(s) => s.node_content(key) } match self { AnyView::Mmap(v) => v.node_content(key), AnyView::Owned(s) => s.node_content(key) }
} }
fn has_node(&self, key: &str) -> bool {
match self { AnyView::Mmap(v) => v.has_node(key), AnyView::Owned(s) => s.has_node(key) }
}
fn params(&self) -> Params { fn params(&self) -> Params {
match self { AnyView::Mmap(v) => v.params(), AnyView::Owned(s) => s.params() } match self { AnyView::Mmap(v) => v.params(), AnyView::Owned(s) => s.params() }
} }
@ -652,7 +638,7 @@ impl Store {
} }
// Bootstrap: write rkyv snapshot if missing // Bootstrap: write rkyv snapshot if missing
if !snapshot_path().exists() { if !snapshot_path().exists() {
if let Err(e) = store.save_snapshot_inner() { if let Err(e) = store.save_snapshot(cached_nodes, cached_rels) {
eprintln!("rkyv bootstrap: {}", e); eprintln!("rkyv bootstrap: {}", e);
} }
} }
@ -819,7 +805,7 @@ impl Store {
.map_err(|e| format!("rename {}{}: {}", tmp_path.display(), path.display(), e))?; .map_err(|e| format!("rename {}{}: {}", tmp_path.display(), path.display(), e))?;
// Also write rkyv snapshot (mmap-friendly) // Also write rkyv snapshot (mmap-friendly)
if let Err(e) = self.save_snapshot_inner() { if let Err(e) = self.save_snapshot(nodes_size, rels_size) {
eprintln!("rkyv snapshot save: {}", e); eprintln!("rkyv snapshot save: {}", e);
} }
@ -828,20 +814,17 @@ impl Store {
/// Serialize store as rkyv snapshot with staleness header. /// Serialize store as rkyv snapshot with staleness header.
/// Assumes StoreLock is already held by caller. /// Assumes StoreLock is already held by caller.
fn save_snapshot_inner(&self) -> Result<(), String> { fn save_snapshot(&self, nodes_size: u64, rels_size: u64) -> Result<(), String> {
let snap = Snapshot { let snap = Snapshot {
nodes: self.nodes.clone(), nodes: self.nodes.clone(),
relations: self.relations.iter().filter(|r| !r.deleted).cloned().collect(), relations: self.relations.iter().filter(|r| !r.deleted).cloned().collect(),
gaps: self.gaps.clone(), gaps: self.gaps.clone(),
params: self.params.clone(), params: self.params,
}; };
let rkyv_data = rkyv::to_bytes::<_, 256>(&snap) let rkyv_data = rkyv::to_bytes::<_, 256>(&snap)
.map_err(|e| format!("rkyv serialize: {}", e))?; .map_err(|e| format!("rkyv serialize: {}", e))?;
let nodes_size = fs::metadata(nodes_path()).map(|m| m.len()).unwrap_or(0);
let rels_size = fs::metadata(relations_path()).map(|m| m.len()).unwrap_or(0);
let mut data = Vec::with_capacity(RKYV_HEADER_LEN + rkyv_data.len()); let mut data = Vec::with_capacity(RKYV_HEADER_LEN + rkyv_data.len());
data.extend_from_slice(&RKYV_MAGIC); data.extend_from_slice(&RKYV_MAGIC);
data.extend_from_slice(&1u32.to_le_bytes()); // format version data.extend_from_slice(&1u32.to_le_bytes()); // format version
@ -1288,7 +1271,7 @@ impl Store {
]; ];
let obs_prefixes = ["skill-", "worked-example-"]; let obs_prefixes = ["skill-", "worked-example-"];
let mut changed = 0; let mut changed_nodes = Vec::new();
let mut unchanged = 0; let mut unchanged = 0;
let keys: Vec<String> = self.nodes.keys().cloned().collect(); let keys: Vec<String> = self.nodes.keys().cloned().collect();
@ -1321,18 +1304,17 @@ impl Store {
let node = self.nodes.get_mut(key).unwrap(); let node = self.nodes.get_mut(key).unwrap();
node.category = cat; node.category = cat;
node.version += 1; node.version += 1;
changed += 1; changed_nodes.push(node.clone());
} else { } else {
unchanged += 1; unchanged += 1;
} }
} }
if changed > 0 { if !changed_nodes.is_empty() {
let updated: Vec<Node> = self.nodes.values().cloned().collect(); self.append_nodes(&changed_nodes)?;
self.append_nodes(&updated)?;
} }
Ok((changed, unchanged)) Ok((changed_nodes.len(), unchanged))
} }
/// Cap node degree by soft-deleting edges from mega-hubs. /// Cap node degree by soft-deleting edges from mega-hubs.