1use crate::{storage::{self, KnowledgeBase}, text, types::*, Error, Result};
2use rusqlite::{params, Connection, OptionalExtension};
3use serde::{Deserialize, Serialize};
4use serde_json::json;
5use std::collections::{BTreeMap, BTreeSet, HashSet};
6
7fn concept() -> String { "concept".into() }
8fn confidence() -> f64 { 0.8 }
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct EntityInput {
12 #[serde(flatten)] pub record: RecordInput,
13 pub name: String,
14 #[serde(default = "concept")] pub entity_type: String,
15 #[serde(default)] pub aliases: Vec<String>,
16 #[serde(default)] pub attributes: BTreeMap<String, Vec<String>>,
17 #[serde(default)] pub summary: String,
18}
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Entity {
21 #[serde(flatten)] pub header: RecordHeader,
22 pub name: String, pub entity_type: String, pub aliases: Vec<String>,
23 pub attributes: BTreeMap<String, Vec<String>>, pub summary: String,
24}
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct RelationInput {
27 #[serde(flatten)] pub record: RecordInput,
28 pub subject_id: i64, pub predicate: String, pub object_id: i64,
29 #[serde(default = "confidence")] pub confidence: f64,
30 #[serde(default)] pub reason: String,
31}
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Relation {
34 #[serde(flatten)] pub header: RecordHeader,
35 pub subject_id: i64, pub predicate: String, pub object_id: i64,
36 pub confidence: f64, pub reason: String,
37}
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct EventInput {
40 #[serde(flatten)] pub record: RecordInput,
41 pub name: String,
42 #[serde(default)] pub summary: String,
43 #[serde(default)] pub participants: Vec<i64>,
44 #[serde(default = "confidence")] pub confidence: f64,
45 #[serde(default)] pub reason: String,
46}
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Event {
49 #[serde(flatten)] pub header: RecordHeader,
50 pub name: String, pub summary: String, pub participants: Vec<i64>,
51 pub confidence: f64, pub reason: String,
52}
53#[derive(Debug, Clone, Default, Serialize, Deserialize)]
54pub struct GraphBatch {
55 #[serde(default)] pub entities: Vec<EntityInput>,
56 #[serde(default)] pub relations: Vec<RelationInput>,
57 #[serde(default)] pub events: Vec<EventInput>,
58}
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct GraphBatchResult { pub entities: Vec<Entity>, pub relations: Vec<Relation>, pub events: Vec<Event> }
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Neighborhood { pub entities: Vec<Entity>, pub relations: Vec<Relation> }
63#[derive(Clone)]
64pub struct GraphStore(pub(crate) KnowledgeBase);
65
66fn check_confidence(score: f64) -> Result<()> {
67 if !score.is_finite() || !(0.0..=1.0).contains(&score) { return Err(Error::Validation("confidence must be in [0,1]".into())); }
68 Ok(())
69}
70
71const EXPAND_QUERY_LIMIT: usize = 64;
73
74fn namespace_term(conn: &Connection, namespace: &str) -> Result<Option<i64>> {
76 Ok(conn.query_row("SELECT id FROM strings WHERE text=?1", [text::normalized_tag(namespace)], |r| r.get(0)).optional()?)
77}
78
79pub(crate) fn expand_query_conn(conn: &Connection, namespace: &str, text_value: &str) -> Result<Vec<String>> {
82 let Some(namespace_id) = namespace_term(conn, namespace)? else { return Ok(Vec::new()); };
83 let normalized = text::normalized_tag(text_value);
84 let mut stmt = conn.prepare("SELECT s.text, pe.canonical_id FROM predicate_equivalents pe \
85 JOIN strings s ON s.id=pe.predicate_id WHERE pe.namespace_id=?1")?;
86 let mut canonical_ids: BTreeSet<i64> = BTreeSet::new();
87 for row in stmt.query_map([namespace_id], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?)))? {
88 let (predicate_text, canonical_id) = row?;
89 let probe = text::normalized_tag(&predicate_text);
90 if !probe.is_empty() && normalized.contains(&probe) { canonical_ids.insert(canonical_id); }
91 }
92 if canonical_ids.is_empty() { return Ok(Vec::new()); }
93 let mut member_stmt = conn.prepare("SELECT s.text FROM predicate_equivalents pe JOIN strings s ON s.id=pe.predicate_id \
94 WHERE pe.namespace_id=?1 AND pe.canonical_id=?2 ORDER BY s.text")?;
95 let mut seen: HashSet<String> = HashSet::new();
96 let mut expanded: Vec<String> = Vec::new();
97 for canonical_id in &canonical_ids {
98 for row in member_stmt.query_map(params![namespace_id, canonical_id], |r| r.get::<_, String>(0))? {
99 let term = row?;
100 if seen.insert(term.clone()) { expanded.push(term); }
101 if expanded.len() >= EXPAND_QUERY_LIMIT { return Ok(expanded); }
102 }
103 }
104 Ok(expanded)
105}
106
107pub(crate) fn match_predicate_synonyms(conn: &Connection, namespace: &str, query: &str) -> Result<String> {
111 let extra = expand_query_conn(conn, namespace, query)?;
112 if extra.is_empty() { return Ok(query.to_string()); }
113 Ok(format!("{query} {}", extra.join(" ")))
114}
115
116fn referenced_entity(conn: &Connection, record: &RecordInput, id: i64) -> Result<Entity> {
117 storage::get(conn, &RecordKey { id },
118 &ReadFilter { namespace: record.namespace.clone(), scopes: vec![record.scope.clone()], tags: vec![], note_ids: vec![] })
119}
120
121pub(crate) fn upsert_entity(conn: &Connection, input: &EntityInput) -> Result<(Entity, crate::index::IndexDocument)> {
122 storage::validate_identity("entity name", &input.name)?;
123 storage::validate_identity("entity_type", &input.entity_type)?;
124 let aliases: Vec<_> = input.aliases.iter().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect::<BTreeSet<_>>().into_iter().collect();
125 let attributes = input.attributes.clone();
126 let attr_text = attributes.iter().map(|(k, values)| format!("{k} {}", values.join(" "))).collect::<Vec<_>>().join(" ");
127 let body = format!("{} {} {}", aliases.join(" "), input.summary, attr_text);
128 let (header, document) = storage::put_record(conn, RecordKind::Entity, &input.record,
129 &json!({"name":input.name,"entity_type":input.entity_type,"aliases":aliases,"attributes":attributes,"summary":input.summary}), &body)?;
130 let entity_type_id = storage::term_id(conn, &input.entity_type)?;
131 conn.execute("INSERT INTO entities(record_id,name,entity_type_id) VALUES (?1,?2,?3)
132 ON CONFLICT(record_id) DO UPDATE SET name=excluded.name,entity_type_id=excluded.entity_type_id",
133 params![header.id, input.name, entity_type_id])?;
134 conn.execute("DELETE FROM entity_aliases WHERE entity_id=?1", [header.id])?;
135 for alias in std::iter::once(&input.name).chain(aliases.iter()) {
136 let alias_id = storage::term_id(conn, alias)?;
137 conn.execute("INSERT OR IGNORE INTO entity_aliases(entity_id,alias_id) VALUES (?1,?2)", params![header.id, alias_id])?;
138 }
139 conn.execute("DELETE FROM entity_attributes WHERE entity_id=?1", [header.id])?;
140 for (key, values) in &attributes {
141 let key_id = storage::term_id(conn, key)?;
142 for value in values {
143 conn.execute("INSERT OR IGNORE INTO entity_attributes(entity_id,attr_key_id,attr_value) VALUES (?1,?2,?3)", params![header.id, key_id, value])?;
144 }
145 }
146 Ok((Entity { header, name: input.name.clone(), entity_type: input.entity_type.clone(), aliases, attributes, summary: input.summary.clone() }, document))
147}
148
149pub(crate) fn upsert_relation(conn: &Connection, input: &RelationInput) -> Result<(Relation, crate::index::IndexDocument)> {
150 storage::validate_identity("predicate", &input.predicate)?;
151 check_confidence(input.confidence)?;
152 let subject = referenced_entity(conn, &input.record, input.subject_id)?;
153 let object = referenced_entity(conn, &input.record, input.object_id)?;
154 let body = format!("{} {} {} {}", subject.name, input.predicate, object.name, input.reason);
155 let (header, document) = storage::put_record(conn, RecordKind::Relation, &input.record,
156 &json!({"subject_id":input.subject_id,"predicate":input.predicate,"object_id":input.object_id,"confidence":input.confidence,"reason":input.reason,
157 "subject_name":subject.name,"object_name":object.name}), &body)?;
158 let predicate_id = storage::term_id(conn, &input.predicate)?;
159 conn.execute("INSERT INTO relations(record_id,subject_id,predicate_id,object_id) VALUES (?1,?2,?3,?4)
160 ON CONFLICT(record_id) DO UPDATE SET subject_id=excluded.subject_id,predicate_id=excluded.predicate_id,object_id=excluded.object_id",
161 params![header.id, input.subject_id, predicate_id, input.object_id])?;
162 Ok((Relation { header, subject_id: input.subject_id, predicate: input.predicate.clone(), object_id: input.object_id, confidence: input.confidence, reason: input.reason.clone() }, document))
163}
164
165pub(crate) fn upsert_event(conn: &Connection, input: &EventInput) -> Result<(Event, crate::index::IndexDocument)> {
166 storage::validate_identity("event name", &input.name)?;
167 check_confidence(input.confidence)?;
168 let participants: Vec<_> = input.participants.iter().copied().collect::<BTreeSet<_>>().into_iter().collect();
169 let names = participants.iter().map(|id| referenced_entity(conn, &input.record, *id).map(|e| e.name)).collect::<Result<Vec<_>>>()?;
170 let body = format!("{} {} {} {}", input.name, input.summary, names.join(" "), input.reason);
171 let (header, document) = storage::put_record(conn, RecordKind::Event, &input.record,
172 &json!({"name":input.name,"summary":input.summary,"participants":participants,"confidence":input.confidence,"reason":input.reason,
173 "participant_names":names}), &body)?;
174 conn.execute("DELETE FROM event_participants WHERE event_id=?1", [header.id])?;
175 for id in &participants {
176 conn.execute("INSERT INTO event_participants(event_id,entity_id) VALUES (?1,?2)", params![header.id, id])?;
177 }
178 Ok((Event { header, name: input.name.clone(), summary: input.summary.clone(), participants, confidence: input.confidence, reason: input.reason.clone() }, document))
179}
180
181pub(crate) fn apply_batch(conn: &Connection, batch: &GraphBatch) -> Result<(GraphBatchResult, Vec<crate::index::IndexDocument>)> {
182 let mut documents = Vec::new();
183 let mut entities = Vec::new();
185 for input in &batch.entities { let (entity, document) = upsert_entity(conn, input)?; entities.push(entity); documents.push(document); }
186 let mut relations = Vec::new();
187 for input in &batch.relations { let (relation, document) = upsert_relation(conn, input)?; relations.push(relation); documents.push(document); }
188 let mut events = Vec::new();
189 for input in &batch.events { let (event, document) = upsert_event(conn, input)?; events.push(event); documents.push(document); }
190 documents.extend(refresh_dependents(conn, &entities)?);
192 Ok((GraphBatchResult { entities, relations, events }, documents))
193}
194
195fn refresh_dependents(conn: &Connection, entities: &[Entity]) -> Result<Vec<crate::index::IndexDocument>> {
197 let mut keys = BTreeSet::new();
198 let mut documents = Vec::new();
199 for entity in entities {
200 let mut stmt = conn.prepare("SELECT record_id FROM relations WHERE subject_id=?1 OR object_id=?1
201 UNION SELECT event_id FROM event_participants WHERE entity_id=?1")?;
202 for row in stmt.query_map([entity.header.id], |r| r.get::<_, i64>(0))? { keys.insert(RecordKey { id: row? }); }
203 }
204 for key in keys {
205 let value = storage::record_value(conn, &key)?.ok_or_else(|| Error::NotFound(key.id.to_string()))?;
206 let kind_code: i64 = conn.query_row("SELECT kind FROM records WHERE id=?1", [key.id], |r| r.get(0))?;
207 let kind = RecordKind::from_code(kind_code).ok_or_else(|| Error::Validation("invalid stored record kind".into()))?;
208 let (body, patch) = if kind == RecordKind::Relation {
210 let r: Relation = serde_json::from_value(value.clone())?;
211 let input = r.header.as_input();
212 let subject = referenced_entity(conn, &input, r.subject_id)?.name;
213 let object = referenced_entity(conn, &input, r.object_id)?.name;
214 (format!("{} {} {} {}", subject, r.predicate, object, r.reason), json!({"subject_name":subject,"object_name":object}))
215 } else {
216 let e: Event = serde_json::from_value(value.clone())?;
217 let names = e.participants.iter().map(|id| referenced_entity(conn, &e.header.as_input(), *id).map(|v| v.name)).collect::<Result<Vec<_>>>()?;
218 (format!("{} {} {} {}", e.name, e.summary, names.join(" "), e.reason), json!({"participant_names":names}))
219 };
220 let old = storage::record_text(kind, &value);
221 if old != body {
222 let raw: String = conn.query_row("SELECT payload_json FROM records WHERE id=?1", [key.id], |r| r.get(0))?;
223 let mut payload: serde_json::Value = serde_json::from_str(&raw)?;
224 if let Some(object) = payload.as_object_mut() {
225 if let Some(extra) = patch.as_object() {
226 for (name, value) in extra { object.insert(name.clone(), value.clone()); }
227 }
228 }
229 let tags: Vec<String> = value.get("tags").and_then(|v| v.as_array())
230 .map(|list| list.iter().filter_map(|v| v.as_str()).map(str::to_string).collect()).unwrap_or_default();
231 let fingerprint = storage::record_fingerprint(&body, &tags);
232 let revision = storage::next_revision(conn, key.id)?;
233 conn.execute("UPDATE records SET payload_json=?2,fingerprint=?3,revision=?4,updated_at_us=MAX(updated_at_us,?5) WHERE id=?1",
234 params![key.id, serde_json::to_string(&payload)?, fingerprint, revision, storage::now_us()])?;
235 conn.execute("DELETE FROM embeddings WHERE record_id=?1", [key.id])?;
236 storage::touch_record_namespace(conn, key.id)?;
238 documents.push(storage::index_document(conn, key.id, kind, body.clone())?);
239 }
240 }
241 Ok(documents)
242}
243
244impl GraphStore {
245 pub fn apply_batch(&self, batch: &GraphBatch) -> Result<WriteReceipt<GraphBatchResult>> {
246 let mut documents: Vec<crate::index::IndexDocument> = Vec::new();
247 let receipt = self.0.mutate(|tx| {
248 let (result, staged) = apply_batch(tx, batch)?;
249 documents = staged;
250 Ok(result)
251 })?;
252 self.0.index_documents(&documents)?;
253 Ok(receipt)
254 }
255 pub fn get(&self, kind: RecordKind, id: i64, filter: &ReadFilter) -> Result<serde_json::Value> {
256 if !matches!(kind, RecordKind::Entity | RecordKind::Relation | RecordKind::Event) { return Err(Error::Validation("expected a graph record kind".into())); }
257 storage::get(self.0.read()?.conn(), &RecordKey { id }, filter)
258 }
259 pub fn list(&self, kind: RecordKind, page: &PageRequest) -> Result<Page<serde_json::Value>> {
260 if !matches!(kind, RecordKind::Entity | RecordKind::Relation | RecordKind::Event) { return Err(Error::Validation("expected a graph record kind".into())); }
261 storage::list(self.0.read()?.conn(), kind, page)
262 }
263 pub fn set_predicate_equivalents(&self, namespace: &str, groups: &[Vec<String>]) -> Result<WriteReceipt<usize>> {
267 storage::validate_identity("namespace", namespace)?;
268 for group in groups {
269 if group.is_empty() { return Err(Error::Validation("an equivalent group must not be empty".into())); }
270 for term in group { storage::validate_identity("predicate", term)?; }
271 }
272 self.0.mutate_meta(|tx| {
273 let namespace_id = storage::term_id(tx, namespace)?;
274 let mut count = 0usize;
275 for group in groups {
276 let canonical_id = storage::term_id(tx, &group[0])?;
277 for term in group {
278 let predicate_id = storage::term_id(tx, term)?;
279 tx.execute("INSERT INTO predicate_equivalents(namespace_id,predicate_id,canonical_id) VALUES (?1,?2,?3) \
280 ON CONFLICT(namespace_id,predicate_id) DO UPDATE SET canonical_id=excluded.canonical_id",
281 params![namespace_id, predicate_id, canonical_id])?;
282 count += 1;
283 }
284 }
285 Ok(count)
286 })
287 }
288
289 pub fn predicate_equivalents(&self, namespace: &str) -> Result<Vec<Vec<String>>> {
291 storage::validate_identity("namespace", namespace)?;
292 let state = self.0.read()?;
293 let conn = state.conn();
294 let Some(namespace_id) = namespace_term(conn, namespace)? else { return Ok(Vec::new()); };
295 let mut stmt = conn.prepare("SELECT pe.canonical_id, s.text FROM predicate_equivalents pe \
296 JOIN strings s ON s.id=pe.predicate_id WHERE pe.namespace_id=?1 ORDER BY pe.canonical_id, s.text")?;
297 let mut groups: BTreeMap<i64, Vec<String>> = BTreeMap::new();
298 for row in stmt.query_map([namespace_id], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, String>(1)?)))? {
299 let (canonical_id, term) = row?;
300 groups.entry(canonical_id).or_default().push(term);
301 }
302 Ok(groups.into_values().map(|mut group| { group.sort(); group.dedup(); group }).collect())
303 }
304
305 pub fn expand_query(&self, namespace: &str, text_value: &str) -> Result<Vec<String>> {
309 storage::validate_identity("namespace", namespace)?;
310 expand_query_conn(self.0.read()?.conn(), namespace, text_value)
311 }
312
313 pub fn resolve(&self, name: &str, filter: &ReadFilter, limit: usize) -> Result<Vec<Entity>> {
314 storage::validate_filter(filter)?; storage::validate_limit(limit)?;
315 let state = self.0.read()?;
316 let conn = state.conn();
317 let mut stmt = conn.prepare("SELECT DISTINCT entity_id FROM entity_aliases WHERE alias_id=(SELECT id FROM strings WHERE text=?1) ORDER BY entity_id")?;
318 let mut result = Vec::new();
319 for row in stmt.query_map([text::normalized_tag(name)], |r| r.get::<_, i64>(0))? {
320 let key = RecordKey { id: row? };
321 if storage::matches_filter(conn, &key, filter)? {
322 result.push(storage::get(conn, &key, filter)?);
323 if result.len() == limit { break; }
324 }
325 }
326 Ok(result)
327 }
328 pub fn neighbors(&self, id: i64, filter: &ReadFilter, limit: usize) -> Result<Neighborhood> {
329 storage::validate_limit(limit)?;
330 let state = self.0.read()?;
331 let conn = state.conn();
332 let root = RecordKey { id };
333 let _: Entity = storage::get(conn, &root, filter)?;
334 let mut stmt = conn.prepare("SELECT record_id FROM relations WHERE subject_id=?1 OR object_id=?1 ORDER BY record_id")?;
335 let mut entities = BTreeMap::new(); let mut relations = Vec::new();
336 for row in stmt.query_map([id], |r| r.get::<_, i64>(0))? {
337 let key = RecordKey { id: row? };
338 if !storage::matches_filter(conn, &key, filter)? { continue; }
339 let relation: Relation = storage::get(conn, &key, filter)?;
340 let endpoint = if relation.subject_id == id { relation.object_id } else { relation.subject_id };
341 let entity_key = RecordKey { id: endpoint };
342 let entity_filter = ReadFilter { tags: vec![], ..filter.clone() };
344 if !storage::matches_filter(conn, &entity_key, &entity_filter)? { continue; }
345 entities.insert(endpoint, storage::get(conn, &entity_key, &entity_filter)?);
346 relations.push(relation);
347 if relations.len() == limit { break; }
348 }
349 Ok(Neighborhood { entities: entities.into_values().collect(), relations })
350 }
351 pub fn events_for_entity(&self, id: i64, filter: &ReadFilter, limit: usize) -> Result<Vec<Event>> {
352 storage::validate_limit(limit)?;
353 let state = self.0.read()?;
354 let conn = state.conn();
355 let _: Entity = storage::get(conn, &RecordKey { id }, &ReadFilter { tags: vec![], ..filter.clone() })?;
356 let mut stmt = conn.prepare("SELECT event_id FROM event_participants WHERE entity_id=?1 ORDER BY event_id")?;
357 let mut events = Vec::new();
358 for row in stmt.query_map([id], |r| r.get::<_, i64>(0))? {
359 let key = RecordKey { id: row? };
360 if storage::matches_filter(conn, &key, filter)? { events.push(storage::get(conn, &key, filter)?); }
361 if events.len() == limit { break; }
362 }
363 Ok(events)
364 }
365 pub fn delete(&self, kind: RecordKind, id: i64, filter: &ReadFilter) -> Result<WriteReceipt<bool>> {
366 if !matches!(kind, RecordKind::Entity | RecordKind::Relation | RecordKind::Event) { return Err(Error::Validation("expected a graph record kind".into())); }
367 self.0.mutate(|tx| {
368 let key = RecordKey { id };
369 if !storage::matches_filter(tx, &key, filter)? { return Err(Error::NotFound(id.to_string())); }
370 storage::delete_record(tx, &key)
371 })
372 }
373 pub fn delete_by_filter(&self, filter: &ReadFilter) -> Result<WriteReceipt<usize>> {
380 self.0.mutate(|tx| {
381 let mut removed = 0;
382 for kind in [RecordKind::Relation, RecordKind::Event, RecordKind::Entity] {
383 for id in storage::select_ids(tx, filter, &[kind])? {
384 if storage::delete_record(tx, &RecordKey { id })? { removed += 1; }
385 }
386 }
387 Ok(removed)
388 })
389 }
390}