1use crate::digest::sanitize;
14use crate::memory::identity::{
15 identity_props_after_forgetting_name, ALIASES_FIELD, ALIAS_KEYS_FIELD,
16};
17use crate::memory_schema::NAME_FIELD;
18use crate::{GraphDb, PredicateSummary, RuleDef};
19use core_rules::engine::DEFAULT_MAX_EDGES;
20use core_rules::{evaluate, NodeView};
21use core_storage::fs::Fs;
22use core_storage::{namespace_of_value, Direction, GraphError, Result, NS_PROP};
23use std::collections::BTreeSet;
24
25pub const FORGET_NOTE_LIST: usize = 10;
28
29pub const FORGET_SHAPE: &str = "pass exactly one of: key (forget a node), key and prop \
31 (forget one property), or fact {subject, predicate, object} (retract one edge)";
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum ForgetTarget {
36 Node { key: String },
38 Prop { key: String, prop: String },
40 Fact {
42 subject: String,
43 predicate: String,
44 object: String,
45 },
46}
47
48impl ForgetTarget {
49 #[must_use]
52 pub fn from_parts(
53 key: Option<String>,
54 prop: Option<String>,
55 fact: Option<(String, String, String)>,
56 ) -> Option<Self> {
57 match (key, prop, fact) {
58 (Some(key), None, None) => Some(Self::Node { key }),
59 (Some(key), Some(prop), None) => Some(Self::Prop { key, prop }),
60 (None, None, Some((subject, predicate, object))) => Some(Self::Fact {
61 subject,
62 predicate,
63 object,
64 }),
65 _ => None,
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, serde::Serialize)]
72pub struct ForgetReport {
73 pub mode: &'static str,
75 pub target: String,
77 pub changed: bool,
79 pub manual_edges: u64,
83 pub derived_edges: u64,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub prop: Option<String>,
87 pub notes: Vec<String>,
90 pub notes_total: usize,
91 pub history_floor: u64,
93 pub aliases_rewritten: bool,
96 pub alias_keys_remain: bool,
99}
100
101#[must_use]
103pub fn predicate_fields(p: &PredicateSummary) -> Vec<String> {
104 let mut out: BTreeSet<String> = p.fields.iter().cloned().collect();
105 for part in p.parts.iter().flatten() {
106 out.extend(predicate_fields(part));
107 }
108 out.into_iter().collect()
109}
110
111fn notes_about<F: Fs>(db: &GraphDb<F>, keys: &[&str]) -> Vec<String> {
113 let mut common: Option<BTreeSet<String>> = None;
114 for key in keys {
115 let into: BTreeSet<String> = db
116 .neighbors(key, "ABOUT", Direction::In)
117 .unwrap_or_default()
118 .into_iter()
119 .filter(|k| db.node_ref(k).is_some_and(|n| n.label() == "Note"))
120 .collect();
121 common = Some(match common {
122 None => into,
123 Some(c) => c.intersection(&into).cloned().collect(),
124 });
125 }
126 common.unwrap_or_default().into_iter().collect()
127}
128
129type EdgeTriple = (String, String, String);
131
132fn derived_edges_on<F: Fs>(db: &GraphDb<F>, key: &str) -> Result<BTreeSet<EdgeTriple>> {
134 Ok(db
135 .node_edges(key)?
136 .into_iter()
137 .filter(|e| e.derived)
138 .map(|e| (e.edge_type, e.src_key, e.dst_key))
139 .collect())
140}
141
142pub fn forget<F: Fs>(db: &mut GraphDb<F>, target: &ForgetTarget) -> Result<ForgetReport> {
157 match target {
158 ForgetTarget::Node { key } => {
159 if !db.has_node(key) {
160 return Err(GraphError::KeyNotFound { key: key.clone() });
161 }
162 let notes = notes_about(db, &[key.as_str()]);
163 let label = db
164 .node_ref(key)
165 .map(|n| n.label().to_string())
166 .unwrap_or_default();
167 let deleted = db.delete_node(key)?;
168 Ok(ForgetReport {
169 mode: "node",
170 target: format!("{key} ({label})"),
171 changed: true,
172 manual_edges: deleted.manual_edges,
173 derived_edges: deleted.derived_edges,
174 prop: None,
175 notes_total: notes.len(),
176 notes: notes.into_iter().take(FORGET_NOTE_LIST).collect(),
177 history_floor: db.stats().history_floor,
178 aliases_rewritten: false,
179 alias_keys_remain: false,
180 })
181 }
182 ForgetTarget::Prop { key, prop } => {
183 let before = derived_edges_on(db, key)?;
187 let rewrite = if prop == NAME_FIELD && db.get_prop(key, prop).is_some() {
192 identity_props_after_forgetting_name(db, key).unwrap_or_default()
193 } else {
194 Vec::new()
195 };
196 let changed = if rewrite.is_empty() {
197 db.remove_prop(key, prop)?
198 } else {
199 let mut batch = db.batch();
200 batch.remove_prop(key, prop);
201 for (field, value) in &rewrite {
202 batch.set_prop(key, field, value.clone());
203 }
204 batch.commit().map(|_| true)?
205 };
206 let aliases_rewritten = changed && !rewrite.is_empty();
207 let alias_keys_remain =
208 changed && prop == ALIASES_FIELD && db.get_prop(key, ALIAS_KEYS_FIELD).is_some();
209 let retracted = if changed {
210 let after = derived_edges_on(db, key).unwrap_or_default();
211 before.difference(&after).count() as u64
212 } else {
213 0
214 };
215 Ok(ForgetReport {
216 mode: "prop",
217 target: format!("{key}.{prop}"),
218 changed,
219 manual_edges: 0,
220 derived_edges: retracted,
221 prop: Some(prop.clone()),
222 notes: Vec::new(),
223 notes_total: 0,
224 history_floor: db.stats().history_floor,
225 aliases_rewritten,
226 alias_keys_remain,
227 })
228 }
229 ForgetTarget::Fact {
230 subject,
231 predicate,
232 object,
233 } => {
234 let changed = match db.delete_edge(predicate, subject, object) {
235 Ok(c) => c,
236 Err(GraphError::RuleOwned { .. }) if !has_edge(db, predicate, subject, object) => {
241 false
242 }
243 Err(GraphError::RuleOwned { detail }) => {
244 return Err(GraphError::RuleOwned {
245 detail: rule_owned_refusal(db, predicate, subject, object, &detail),
246 })
247 }
248 Err(e) => return Err(e),
249 };
250 let notes = if changed {
251 notes_about(db, &[subject.as_str(), object.as_str()])
252 } else {
253 Vec::new()
254 };
255 Ok(ForgetReport {
256 mode: "fact",
257 target: format!("{predicate} {subject} → {object}"),
258 changed,
259 manual_edges: 0,
260 derived_edges: 0,
261 prop: None,
262 notes_total: notes.len(),
263 notes: notes.into_iter().take(FORGET_NOTE_LIST).collect(),
264 history_floor: db.stats().history_floor,
265 aliases_rewritten: false,
266 alias_keys_remain: false,
267 })
268 }
269 }
270}
271
272fn has_edge<F: Fs>(db: &GraphDb<F>, predicate: &str, subject: &str, object: &str) -> bool {
274 db.neighbors(subject, predicate, Direction::Out)
275 .is_ok_and(|keys| keys.iter().any(|k| k == object))
276}
277
278pub fn rule_owned_refusal<F: Fs>(
300 db: &GraphDb<F>,
301 predicate: &str,
302 subject: &str,
303 object: &str,
304 detail: &str,
305) -> String {
306 let by_provenance: BTreeSet<String> = db
311 .explain(subject, object)
312 .unwrap_or_default()
313 .into_iter()
314 .filter(|e| e.edge_type == predicate && e.src_key == subject && e.dst_key == object)
315 .map(|e| e.rule)
316 .collect();
317 let rules = db.rules();
318 let derived_by: Vec<&RuleDef> = rules
319 .iter()
320 .filter(|r| by_provenance.contains(&r.name))
321 .collect();
322 if !derived_by.is_empty() {
323 let (names, fields) = names_and_fields(&derived_by);
324 return format!(
325 "refused: {} {} → {} is derived by rule {names}. It changes only when the fields \
326 that rule reads change ({fields}), or when the rule is deleted. Nothing was written.",
327 sanitize(predicate),
328 sanitize(subject),
329 sanitize(object),
330 );
331 }
332 let matching: Vec<&RuleDef> = rules
333 .iter()
334 .filter(|r| guard_matches(db, r, predicate, subject, object))
335 .collect();
336 if matching.is_empty() {
337 return sanitize(detail);
338 }
339 let (names, fields) = names_and_fields(&matching);
340 let claim = if matching
342 .iter()
343 .all(|r| would_derive(db, r, subject, object))
344 {
345 "would derive it again"
346 } else {
347 "matches these two nodes' properties and may derive it again"
348 };
349 format!(
350 "refused: {} {} → {} was written by hand and no rule derived it, but rule {names} \
351 {claim}, so the delete is refused. It can be deleted once the fields that rule reads \
352 ({fields}) no longer match, or once the rule is deleted. Nothing was written.",
353 sanitize(predicate),
354 sanitize(subject),
355 sanitize(object),
356 )
357}
358
359fn names_and_fields(rules: &[&RuleDef]) -> (String, String) {
362 let names: Vec<String> = rules.iter().map(|r| sanitize(&r.name)).collect();
363 let mut fields: BTreeSet<String> = BTreeSet::new();
364 for r in rules {
365 fields.extend(predicate_fields(&PredicateSummary::from(&r.predicate)));
366 }
367 let fields: Vec<String> = fields.iter().map(|f| sanitize(f)).collect();
368 (names.join(", "), fields.join(", "))
369}
370
371fn predicate_holds<F: Fs>(db: &GraphDb<F>, rule: &RuleDef, a: &str, b: &str) -> bool {
373 let a_props = |field: &str| db.get_prop(a, field);
374 let b_props = |field: &str| db.get_prop(b, field);
375 evaluate(
376 &rule.predicate,
377 &NodeView {
378 key: a,
379 props: &a_props,
380 },
381 &NodeView {
382 key: b,
383 props: &b_props,
384 },
385 )
386 .is_some()
387}
388
389fn guard_matches<F: Fs>(
399 db: &GraphDb<F>,
400 rule: &RuleDef,
401 predicate: &str,
402 subject: &str,
403 object: &str,
404) -> bool {
405 if subject == object || rule.edge_type != predicate {
406 return false;
407 }
408 let label_is = |key: &str, label: &str| db.node_ref(key).is_some_and(|n| n.label() == label);
409 label_is(subject, &rule.src_label)
410 && label_is(object, &rule.dst_label)
411 && predicate_holds(db, rule, subject, object)
412}
413
414fn would_derive<F: Fs>(db: &GraphDb<F>, rule: &RuleDef, subject: &str, object: &str) -> bool {
431 let sees = |key: &str| match rule.namespace.as_deref() {
432 None => true,
433 Some(ns) => namespace_of_value(db.get_prop(key, NS_PROP).as_ref()) == ns,
434 };
435 if !sees(subject) || !sees(object) {
436 return false;
437 }
438 let sources = db.nodes_with_label(&rule.src_label).len() as u64;
439 let targets = db.nodes_with_label(&rule.dst_label).len() as u64;
440 let cap_cannot_bind = match rule.max_edges {
441 Some(k) => targets <= k,
442 None => sources.saturating_mul(targets) <= DEFAULT_MAX_EDGES,
443 };
444 if !cap_cannot_bind {
445 return false;
446 }
447 let (Some(via_label), Some(via_edge)) = (rule.via_label.as_deref(), rule.via_edge.as_deref())
448 else {
449 return true;
451 };
452 db.neighbors(subject, via_edge, rule.via_dir.unwrap_or(Direction::Out))
453 .unwrap_or_default()
454 .iter()
455 .any(|via| {
456 db.node_ref(via).is_some_and(|n| n.label() == via_label)
457 && sees(via)
458 && predicate_holds(db, rule, via, object)
459 })
460}