1use crate::db;
6use crate::db::{bytes_to_embedding, parse_optional_json, with_transaction};
7#[cfg(feature = "hnsw")]
8use crate::db::{enqueue_pending_index_op, PendingIndexOpKind};
9#[cfg(feature = "hnsw")]
10use crate::episodes;
11use crate::error::MemoryError;
12use crate::quantize::{self, Quantizer};
13use crate::types::{Fact, NamespaceDeleteReport};
14use crate::{merge_trace_ctx, MemoryStore};
15use rusqlite::{params, Connection};
16use stack_ids::TraceCtx;
17
18#[allow(dead_code)]
20pub fn insert_fact_with_fts(
21 conn: &Connection,
22 fact_id: &str,
23 namespace: &str,
24 content: &str,
25 embedding_bytes: &[u8],
26 source: Option<&str>,
27 metadata: Option<&serde_json::Value>,
28) -> Result<(), MemoryError> {
29 insert_fact_with_fts_q8(
30 conn,
31 fact_id,
32 namespace,
33 content,
34 embedding_bytes,
35 None,
36 source,
37 metadata,
38 None,
39 )
40}
41
42#[allow(clippy::too_many_arguments)]
44pub fn insert_fact_with_fts_q8(
45 conn: &Connection,
46 fact_id: &str,
47 namespace: &str,
48 content: &str,
49 embedding_bytes: &[u8],
50 q8_bytes: Option<&[u8]>,
51 source: Option<&str>,
52 metadata: Option<&serde_json::Value>,
53 sparse: Option<(&crate::SparseWeights, &str)>,
54) -> Result<(), MemoryError> {
55 let metadata_str = metadata.map(|m| m.to_string());
56 with_transaction(conn, |tx| {
57 tx.execute(
58 "INSERT INTO facts (id, namespace, content, source, embedding, embedding_q8, metadata)
59 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
60 params![
61 fact_id,
62 namespace,
63 content,
64 source,
65 embedding_bytes,
66 q8_bytes,
67 metadata_str
68 ],
69 )?;
70
71 tx.execute(
72 "INSERT INTO facts_rowid_map (fact_id) VALUES (?1)",
73 params![fact_id],
74 )?;
75 let fts_rowid = tx.last_insert_rowid();
76
77 tx.execute(
78 "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
79 params![fts_rowid, content],
80 )?;
81
82 #[cfg(feature = "hnsw")]
83 enqueue_pending_index_op(
84 tx,
85 &format!("fact:{}", fact_id),
86 "fact",
87 PendingIndexOpKind::Upsert,
88 )?;
89 db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
90 if let Some((weights, representation)) = sparse {
91 db::store_sparse_vector(tx, &format!("fact:{fact_id}"), weights, representation)?;
92 }
93
94 let changed = tx.execute(
95 "UPDATE authority_state SET retrieval_epoch = retrieval_epoch + 1 WHERE id = 1",
96 [],
97 )?;
98 if changed != 1 {
99 return Err(MemoryError::Other(
100 "failed to advance corpus authority epoch after fact insert".to_string(),
101 ));
102 }
103
104 Ok(())
105 })
106}
107
108#[allow(clippy::too_many_arguments)]
112pub fn insert_fact_in_tx(
113 tx: &rusqlite::Transaction<'_>,
114 fact_id: &str,
115 namespace: &str,
116 content: &str,
117 embedding_bytes: &[u8],
118 q8_bytes: Option<&[u8]>,
119 source: Option<&str>,
120 metadata: Option<&serde_json::Value>,
121) -> Result<(), MemoryError> {
122 let metadata_str = metadata.map(|m| m.to_string());
123 tx.execute(
124 "INSERT INTO facts (id, namespace, content, source, embedding, embedding_q8, metadata)
125 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
126 params![
127 fact_id,
128 namespace,
129 content,
130 source,
131 embedding_bytes,
132 q8_bytes,
133 metadata_str
134 ],
135 )?;
136
137 tx.execute(
138 "INSERT INTO facts_rowid_map (fact_id) VALUES (?1)",
139 params![fact_id],
140 )?;
141 let fts_rowid = tx.last_insert_rowid();
142
143 tx.execute(
144 "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
145 params![fts_rowid, content],
146 )?;
147
148 #[cfg(feature = "hnsw")]
149 enqueue_pending_index_op(
150 tx,
151 &format!("fact:{}", fact_id),
152 "fact",
153 PendingIndexOpKind::Upsert,
154 )?;
155 db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
156
157 Ok(())
158}
159
160#[allow(dead_code)] pub fn delete_fact_with_fts(conn: &Connection, fact_id: &str) -> Result<(), MemoryError> {
163 with_transaction(conn, |tx| {
164 let fts_rowid: i64 = tx
165 .query_row(
166 "SELECT rowid FROM facts_rowid_map WHERE fact_id = ?1",
167 params![fact_id],
168 |row| row.get(0),
169 )
170 .map_err(|e| MemoryError::FactNotFound(format!("{}: {e}", fact_id)))?;
171
172 let content: String = tx
173 .query_row(
174 "SELECT content FROM facts WHERE id = ?1",
175 params![fact_id],
176 |row| row.get(0),
177 )
178 .map_err(|e| MemoryError::FactNotFound(format!("{}: {e}", fact_id)))?;
179
180 tx.execute(
181 "INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', ?1, ?2)",
182 params![fts_rowid, content],
183 )?;
184 tx.execute(
185 "DELETE FROM facts_rowid_map WHERE fact_id = ?1",
186 params![fact_id],
187 )?;
188 tx.execute(
189 "DELETE FROM episode_causes WHERE cause_node_id IN (?1, ?2)",
190 params![fact_id, format!("fact:{fact_id}")],
191 )?;
192 tx.execute(
193 "DELETE FROM derivation_edges
194 WHERE (source_kind = 'fact' AND source_id = ?1)
195 OR (target_kind = 'fact' AND target_id = ?1)",
196 params![fact_id],
197 )?;
198 tx.execute("DELETE FROM facts WHERE id = ?1", params![fact_id])?;
199
200 #[cfg(feature = "hnsw")]
201 enqueue_pending_index_op(
202 tx,
203 &format!("fact:{}", fact_id),
204 "fact",
205 PendingIndexOpKind::Delete,
206 )?;
207 db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
208
209 Ok(())
210 })
211}
212
213#[allow(dead_code)] pub fn update_fact_with_fts(
216 conn: &Connection,
217 fact_id: &str,
218 new_content: &str,
219 new_embedding_bytes: &[u8],
220 new_q8_bytes: Option<&[u8]>,
221) -> Result<(), MemoryError> {
222 with_transaction(conn, |tx| {
223 let (fts_rowid, old_content): (i64, String) = tx
224 .query_row(
225 "SELECT fm.rowid, f.content
226 FROM facts f
227 JOIN facts_rowid_map fm ON fm.fact_id = f.id
228 WHERE f.id = ?1",
229 params![fact_id],
230 |row| Ok((row.get(0)?, row.get(1)?)),
231 )
232 .map_err(|e| MemoryError::FactNotFound(format!("{}: {e}", fact_id)))?;
233
234 tx.execute(
235 "INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', ?1, ?2)",
236 params![fts_rowid, old_content],
237 )?;
238
239 tx.execute(
240 "UPDATE facts
241 SET content = ?1,
242 embedding = ?2,
243 embedding_q8 = ?3,
244 updated_at = datetime('now')
245 WHERE id = ?4",
246 params![new_content, new_embedding_bytes, new_q8_bytes, fact_id],
247 )?;
248
249 tx.execute(
250 "INSERT INTO facts_fts(rowid, content) VALUES (?1, ?2)",
251 params![fts_rowid, new_content],
252 )?;
253 tx.execute(
254 "DELETE FROM derivation_edges
255 WHERE (source_kind = 'fact' AND source_id = ?1)
256 OR (target_kind = 'fact' AND target_id = ?1)",
257 params![fact_id],
258 )?;
259
260 #[cfg(feature = "hnsw")]
261 enqueue_pending_index_op(
262 tx,
263 &format!("fact:{}", fact_id),
264 "fact",
265 PendingIndexOpKind::Upsert,
266 )?;
267 db::invalidate_derived_vector_artifact(tx, &format!("fact:{fact_id}"))?;
268
269 Ok(())
270 })
271}
272
273#[cfg(feature = "admin-ops")]
275pub fn delete_namespace(
276 conn: &Connection,
277 namespace: &str,
278) -> Result<NamespaceDeleteReport, MemoryError> {
279 with_transaction(conn, |tx| {
280 let mut report = NamespaceDeleteReport::default();
281 let delete_session = |session_id: &str| -> Result<(usize, usize), MemoryError> {
282 let message_data: Vec<(i64, String, i64, bool)> = {
283 let mut stmt = tx.prepare(
284 "SELECT m.id, m.content, mm.rowid, m.embedding IS NOT NULL
285 FROM messages m
286 JOIN messages_rowid_map mm ON mm.message_id = m.id
287 WHERE m.session_id = ?1",
288 )?;
289 let rows = stmt.query_map(params![session_id], |row| {
290 Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
291 })?;
292 rows.collect::<Result<Vec<_>, _>>()?
293 };
294
295 for (message_id, content, fts_rowid, has_embedding) in &message_data {
296 #[cfg(not(feature = "hnsw"))]
297 let _ = (message_id, has_embedding);
298 tx.execute(
299 "INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', ?1, ?2)",
300 params![fts_rowid, content],
301 )?;
302 #[cfg(feature = "hnsw")]
303 if *has_embedding {
304 enqueue_pending_index_op(
305 tx,
306 &format!("msg:{}", message_id),
307 "message",
308 PendingIndexOpKind::Delete,
309 )?;
310 }
311 }
312
313 let affected = tx.execute("DELETE FROM sessions WHERE id = ?1", params![session_id])?;
314 if affected == 0 {
315 return Err(MemoryError::SessionNotFound(session_id.to_string()));
316 }
317 let hnsw_ops = message_data
318 .iter()
319 .filter(|(_, _, _, has_embedding)| *has_embedding)
320 .count();
321 Ok((message_data.len(), hnsw_ops))
322 };
323
324 let document_ids: Vec<String> = {
325 let mut stmt = tx.prepare("SELECT id FROM documents WHERE namespace = ?1")?;
326 let ids = stmt
327 .query_map(params![namespace], |row| row.get(0))?
328 .collect::<Result<Vec<_>, _>>()?;
329 ids
330 };
331
332 let session_ids: Vec<String> = {
333 let mut stmt = tx.prepare("SELECT id, metadata FROM sessions")?;
334 let rows = stmt.query_map([], |row| {
335 Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?))
336 })?;
337 let mut ids = Vec::new();
338 for row in rows {
339 let (session_id, metadata_raw) = row?;
340 let metadata = parse_optional_json(
341 "sessions",
342 &session_id,
343 "metadata",
344 metadata_raw.as_deref(),
345 )?;
346 let namespace_matches = metadata
347 .as_ref()
348 .and_then(|value| {
349 value
350 .get("namespace")
351 .or_else(|| value.get("scope_namespace"))
352 })
353 .and_then(|value| value.as_str())
354 == Some(namespace);
355 if namespace_matches {
356 ids.push(session_id);
357 }
358 }
359 ids
360 };
361
362 for session_id in &session_ids {
363 let (messages, hnsw_ops) = delete_session(session_id)?;
364 report.messages += messages;
365 report.hnsw_ops += hnsw_ops;
366 }
367 report.sessions = session_ids.len();
368
369 let delete_derivation_edges_for_id = |kind: &str, id: &str| -> Result<(), MemoryError> {
370 tx.execute(
371 "DELETE FROM derivation_edges
372 WHERE (source_kind = ?1 AND source_id = ?2)
373 OR (target_kind = ?1 AND target_id = ?2)",
374 params![kind, id],
375 )?;
376 Ok(())
377 };
378
379 let delete_derivation_edges_for_ids =
380 |kind: &str, ids: &[String]| -> Result<(), MemoryError> {
381 for id in ids {
382 delete_derivation_edges_for_id(kind, id)?;
383 }
384 Ok(())
385 };
386
387 let facts: Vec<(String, i64, String)> = {
388 let mut stmt = tx.prepare(
389 "SELECT f.id, fm.rowid, f.content
390 FROM facts f
391 JOIN facts_rowid_map fm ON fm.fact_id = f.id
392 WHERE f.namespace = ?1",
393 )?;
394 let facts = stmt
395 .query_map(params![namespace], |row| {
396 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
397 })?
398 .collect::<Result<Vec<_>, _>>()?;
399 facts
400 };
401
402 for (fact_id, fts_rowid, content) in &facts {
403 tx.execute(
404 "INSERT INTO facts_fts(facts_fts, rowid, content) VALUES('delete', ?1, ?2)",
405 params![fts_rowid, content],
406 )?;
407 tx.execute(
408 "DELETE FROM facts_rowid_map WHERE fact_id = ?1",
409 params![fact_id],
410 )?;
411
412 #[cfg(feature = "hnsw")]
413 enqueue_pending_index_op(
414 tx,
415 &format!("fact:{}", fact_id),
416 "fact",
417 PendingIndexOpKind::Delete,
418 )?;
419 #[cfg(feature = "hnsw")]
420 {
421 report.hnsw_ops += 1;
422 }
423 }
424 tx.execute("DELETE FROM facts WHERE namespace = ?1", params![namespace])?;
425 report.facts = facts.len();
426
427 for doc_id in &document_ids {
428 let mut stmt = tx.prepare(
429 "SELECT c.id, c.content, cm.rowid
430 FROM chunks c
431 JOIN chunks_rowid_map cm ON cm.chunk_id = c.id
432 WHERE c.document_id = ?1",
433 )?;
434 let chunk_rows: Vec<(String, String, i64)> = stmt
435 .query_map(params![doc_id], |row| {
436 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
437 })?
438 .collect::<Result<Vec<_>, _>>()?;
439 report.chunks += chunk_rows.len();
440
441 for (chunk_id, content, fts_rowid) in &chunk_rows {
442 tx.execute(
443 "INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', ?1, ?2)",
444 params![fts_rowid, content],
445 )?;
446 tx.execute(
447 "DELETE FROM chunks_rowid_map WHERE chunk_id = ?1",
448 params![chunk_id],
449 )?;
450 #[cfg(feature = "hnsw")]
451 enqueue_pending_index_op(
452 tx,
453 &format!("chunk:{}", chunk_id),
454 "chunk",
455 PendingIndexOpKind::Delete,
456 )?;
457 #[cfg(feature = "hnsw")]
458 {
459 report.hnsw_ops += 1;
460 }
461 }
462
463 tx.execute("DELETE FROM chunks WHERE document_id = ?1", params![doc_id])?;
464 }
465
466 for doc_id in &document_ids {
467 let mut stmt = tx.prepare(
468 "SELECT e.episode_id, e.search_text, erm.rowid
469 FROM episodes e
470 JOIN episodes_rowid_map erm ON erm.episode_id = e.episode_id
471 WHERE e.document_id = ?1",
472 )?;
473 let episode_rows: Vec<(String, String, i64)> = stmt
474 .query_map(params![doc_id], |row| {
475 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
476 })?
477 .collect::<Result<Vec<_>, _>>()?;
478 report.episodes += episode_rows.len();
479
480 for (episode_id, search_text, fts_rowid) in &episode_rows {
481 tx.execute(
482 "INSERT INTO episodes_fts(episodes_fts, rowid, content) VALUES ('delete', ?1, ?2)",
483 params![fts_rowid, search_text],
484 )?;
485 tx.execute(
486 "DELETE FROM episodes_rowid_map WHERE episode_id = ?1",
487 params![episode_id],
488 )?;
489 tx.execute(
490 "DELETE FROM episode_causes WHERE episode_id = ?1",
491 params![episode_id],
492 )?;
493 #[cfg(feature = "hnsw")]
494 enqueue_pending_index_op(
495 tx,
496 &episodes::episode_item_key(episode_id),
497 "episode",
498 PendingIndexOpKind::Delete,
499 )?;
500 #[cfg(feature = "hnsw")]
501 {
502 report.hnsw_ops += 1;
503 }
504 }
505
506 tx.execute(
507 "DELETE FROM episodes WHERE document_id = ?1",
508 params![doc_id],
509 )?;
510 tx.execute("DELETE FROM documents WHERE id = ?1", params![doc_id])?;
511 }
512 report.documents = document_ids.len();
513
514 let claim_ids: Vec<String> = {
515 let mut stmt =
516 tx.prepare("SELECT claim_id FROM claim_versions WHERE scope_namespace = ?1")?;
517 let ids = stmt
518 .query_map(params![namespace], |row| row.get(0))?
519 .collect::<Result<Vec<_>, _>>()?;
520 ids
521 };
522
523 let claim_version_ids: Vec<String> = {
524 let mut stmt = tx.prepare(
525 "SELECT claim_version_id FROM claim_versions WHERE scope_namespace = ?1",
526 )?;
527 let ids = stmt
528 .query_map(params![namespace], |row| row.get(0))?
529 .collect::<Result<Vec<_>, _>>()?;
530 ids
531 };
532
533 let relation_version_ids: Vec<String> = {
534 let mut stmt = tx.prepare(
535 "SELECT relation_version_id FROM relation_versions WHERE scope_namespace = ?1",
536 )?;
537 let ids = stmt
538 .query_map(params![namespace], |row| row.get(0))?
539 .collect::<Result<Vec<_>, _>>()?;
540 ids
541 };
542
543 let alias_entity_ids: Vec<String> = {
544 let mut stmt = tx.prepare(
545 "SELECT canonical_entity_id FROM entity_aliases WHERE scope_namespace = ?1",
546 )?;
547 let ids = stmt
548 .query_map(params![namespace], |row| row.get(0))?
549 .collect::<Result<Vec<_>, _>>()?;
550 ids
551 };
552
553 let evidence_handles: Vec<String> = {
554 let mut stmt = tx.prepare(
555 "SELECT er.fetch_handle FROM evidence_refs er
556 JOIN projection_import_log pil ON er.source_envelope_id = pil.source_envelope_id
557 WHERE pil.scope_namespace = ?1",
558 )?;
559 let handles = stmt
560 .query_map(params![namespace], |row| row.get(0))?
561 .collect::<Result<Vec<_>, _>>()?;
562 handles
563 };
564
565 let episode_ids: Vec<String> = {
566 let mut stmt = tx.prepare(
567 "SELECT episode_id FROM episode_links
568 WHERE source_envelope_id IN (SELECT source_envelope_id FROM projection_import_log WHERE scope_namespace = ?1)",
569 )?;
570 let ids = stmt
571 .query_map(params![namespace], |row| row.get(0))?
572 .collect::<Result<Vec<_>, _>>()?;
573 ids
574 };
575
576 delete_derivation_edges_for_ids("claim", &claim_ids)?;
577 delete_derivation_edges_for_ids("claim_version", &claim_version_ids)?;
578 delete_derivation_edges_for_ids("relation_version", &relation_version_ids)?;
579 delete_derivation_edges_for_ids("entity", &alias_entity_ids)?;
580 delete_derivation_edges_for_ids("evidence_ref", &evidence_handles)?;
581 delete_derivation_edges_for_ids("episode", &episode_ids)?;
582
583 report.projection_rows += tx.execute(
584 "DELETE FROM claim_versions WHERE scope_namespace = ?1",
585 params![namespace],
586 )?;
587 report.projection_rows += tx.execute(
588 "DELETE FROM relation_versions WHERE scope_namespace = ?1",
589 params![namespace],
590 )?;
591 report.projection_rows += tx.execute(
592 "DELETE FROM entity_aliases WHERE scope_namespace = ?1",
593 params![namespace],
594 )?;
595 report.projection_rows += tx.execute(
596 "DELETE FROM evidence_refs
597 WHERE source_envelope_id IN (SELECT source_envelope_id FROM projection_import_log WHERE scope_namespace = ?1)",
598 params![namespace],
599 )?;
600 report.projection_rows += tx.execute(
601 "DELETE FROM episode_links
602 WHERE source_envelope_id IN (SELECT source_envelope_id FROM projection_import_log WHERE scope_namespace = ?1)",
603 params![namespace],
604 )?;
605 report.projection_rows += tx.execute(
606 "DELETE FROM projection_import_failures WHERE scope_namespace = ?1",
607 params![namespace],
608 )?;
609 report.projection_rows += tx.execute(
610 "DELETE FROM projection_import_log WHERE scope_namespace = ?1",
611 params![namespace],
612 )?;
613
614 Ok(report)
615 })
616}
617
618pub fn get_fact(conn: &Connection, fact_id: &str) -> Result<Option<Fact>, MemoryError> {
620 let result = conn.query_row(
621 "SELECT id, namespace, content, source, created_at, updated_at, metadata
622 FROM facts WHERE id = ?1",
623 params![fact_id],
624 |row| {
625 Ok((
626 row.get::<_, String>(0)?,
627 row.get::<_, String>(1)?,
628 row.get::<_, String>(2)?,
629 row.get::<_, Option<String>>(3)?,
630 row.get::<_, String>(4)?,
631 row.get::<_, String>(5)?,
632 row.get::<_, Option<String>>(6)?,
633 ))
634 },
635 );
636
637 match result {
638 Ok((id, namespace, content, source, created_at, updated_at, metadata_raw)) => {
639 Ok(Some(Fact {
640 metadata: parse_optional_json("facts", &id, "metadata", metadata_raw.as_deref())?,
641 id,
642 namespace,
643 content,
644 source,
645 created_at,
646 updated_at,
647 }))
648 }
649 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
650 Err(err) => Err(MemoryError::Database(err)),
651 }
652}
653
654pub fn get_fact_embedding(
656 conn: &Connection,
657 fact_id: &str,
658) -> Result<Option<Vec<f32>>, MemoryError> {
659 let result: Result<Option<Vec<u8>>, _> = conn.query_row(
660 "SELECT embedding FROM facts WHERE id = ?1",
661 params![fact_id],
662 |row| row.get(0),
663 );
664
665 match result {
666 Ok(Some(bytes)) => Ok(Some(bytes_to_embedding(&bytes)?)),
667 Ok(None) => Ok(None),
668 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
669 Err(err) => Err(MemoryError::Database(err)),
670 }
671}
672
673pub fn list_fact_namespaces(conn: &Connection) -> Result<Vec<String>, MemoryError> {
675 let mut stmt = conn.prepare("SELECT DISTINCT namespace FROM facts ORDER BY namespace")?;
676 let rows = stmt
677 .query_map([], |row| row.get::<_, String>(0))?
678 .collect::<Result<Vec<_>, _>>()?;
679 Ok(rows)
680}
681
682#[allow(dead_code)] pub fn list_facts(
685 conn: &Connection,
686 namespace: &str,
687 limit: usize,
688 offset: usize,
689) -> Result<Vec<Fact>, MemoryError> {
690 list_facts_with_view(conn, namespace, limit, offset, &StateView::Current)
691}
692
693#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
695pub enum StateView {
696 Current,
697 HistoricalAt(String),
698 RecordedAsOf(String),
699 IncludeSuperseded,
700}
701
702pub(crate) fn fact_is_visible_with_view(
703 conn: &Connection,
704 fact_id: &str,
705 view: &StateView,
706) -> Result<bool, MemoryError> {
707 let forgotten: bool = conn.query_row(
708 "SELECT EXISTS(SELECT 1 FROM forgotten_facts WHERE fact_id = ?1)",
709 params![fact_id],
710 |row| row.get(0),
711 )?;
712 if forgotten {
713 return Ok(false);
714 }
715 let cutoff = match view {
716 StateView::HistoricalAt(value) | StateView::RecordedAsOf(value) => {
717 let parsed = chrono::DateTime::parse_from_rfc3339(value).map_err(|e| {
718 MemoryError::Other(format!("invalid StateView timestamp '{value}': {e}"))
719 })?;
720 Some(
721 parsed
722 .with_timezone(&chrono::Utc)
723 .format("%Y-%m-%d %H:%M:%S%.6f")
724 .to_string(),
725 )
726 }
727 _ => None,
728 };
729 let include_superseded = matches!(view, StateView::IncludeSuperseded);
730 let visible: i64 = conn.query_row(
731 "SELECT EXISTS(
732 SELECT 1 FROM facts f
733 WHERE f.id = ?1
734 AND (?2 IS NULL OR f.created_at <= ?2)
735 AND (?3 = 1 OR NOT EXISTS (
736 SELECT 1 FROM graph_edges ge
737 WHERE ge.target = 'fact:' || f.id
738 AND ge.is_invalidated = 0
739 AND COALESCE(
740 json_extract(ge.edge_type, '$.relation'),
741 json_extract(ge.edge_type, '$.entity.relation')
742 ) IN ('supersedes', 'redacts')
743 AND (?2 IS NULL OR COALESCE(ge.recorded_time, ge.recorded_at) <= ?2)
744 ))
745 )",
746 params![fact_id, cutoff, include_superseded],
747 |row| row.get(0),
748 )?;
749 Ok(visible != 0)
750}
751
752pub fn list_facts_with_view(
754 conn: &Connection,
755 namespace: &str,
756 limit: usize,
757 offset: usize,
758 view: &StateView,
759) -> Result<Vec<Fact>, MemoryError> {
760 let cutoff = match view {
761 StateView::HistoricalAt(value) | StateView::RecordedAsOf(value) => {
762 let parsed = chrono::DateTime::parse_from_rfc3339(value).map_err(|e| {
763 MemoryError::Other(format!("invalid StateView timestamp '{value}': {e}"))
764 })?;
765 Some(
766 parsed
767 .with_timezone(&chrono::Utc)
768 .format("%Y-%m-%d %H:%M:%S%.6f")
769 .to_string(),
770 )
771 }
772 _ => None,
773 };
774 let inconsistent: i64 = conn.query_row(
775 "SELECT COUNT(*) FROM (
776 SELECT target FROM graph_edges
777 WHERE is_invalidated = 0
778 AND COALESCE(
779 json_extract(edge_type, '$.relation'),
780 json_extract(edge_type, '$.entity.relation')
781 ) IN ('supersedes', 'redacts')
782 AND (?1 IS NULL OR COALESCE(recorded_time, recorded_at) <= ?1)
783 GROUP BY target HAVING COUNT(DISTINCT source) > 1
784 )",
785 params![cutoff.as_deref()],
786 |row| row.get(0),
787 )?;
788 if inconsistent != 0 {
789 return Err(MemoryError::Other(
790 "inconsistent fact lineage: multiple active heads".into(),
791 ));
792 }
793 let include_superseded = matches!(view, StateView::IncludeSuperseded);
794 let mut stmt = conn.prepare(
795 "SELECT id, namespace, content, source, created_at, updated_at, metadata
796 FROM facts
797 WHERE namespace = ?1
798 AND NOT EXISTS (
799 SELECT 1 FROM forgotten_facts ff WHERE ff.fact_id = facts.id
800 )
801 AND (?4 IS NULL OR created_at <= ?4)
802 AND (?5 = 1 OR NOT EXISTS (
803 SELECT 1 FROM graph_edges ge
804 WHERE ge.target = 'fact:' || facts.id
805 AND ge.is_invalidated = 0
806 AND COALESCE(
807 json_extract(ge.edge_type, '$.relation'),
808 json_extract(ge.edge_type, '$.entity.relation')
809 ) IN ('supersedes', 'redacts')
810 AND (?4 IS NULL OR COALESCE(ge.recorded_time, ge.recorded_at) <= ?4)
811 ))
812 ORDER BY updated_at DESC
813 LIMIT ?2 OFFSET ?3",
814 )?;
815
816 let facts = stmt
817 .query_map(
818 params![
819 namespace,
820 limit as i64,
821 offset as i64,
822 cutoff,
823 include_superseded
824 ],
825 |row| {
826 Ok((
827 row.get::<_, String>(0)?,
828 row.get::<_, String>(1)?,
829 row.get::<_, String>(2)?,
830 row.get::<_, Option<String>>(3)?,
831 row.get::<_, String>(4)?,
832 row.get::<_, String>(5)?,
833 row.get::<_, Option<String>>(6)?,
834 ))
835 },
836 )?
837 .collect::<Result<Vec<_>, _>>()?
838 .into_iter()
839 .map(
840 |(id, namespace, content, source, created_at, updated_at, metadata_raw)| {
841 Ok(Fact {
842 metadata: parse_optional_json(
843 "facts",
844 &id,
845 "metadata",
846 metadata_raw.as_deref(),
847 )?,
848 id,
849 namespace,
850 content,
851 source,
852 created_at,
853 updated_at,
854 })
855 },
856 )
857 .collect::<Result<Vec<_>, MemoryError>>()?;
858
859 Ok(facts)
860}
861
862impl MemoryStore {
863 pub async fn add_fact_raw_compat(
868 &self,
869 namespace: &str,
870 content: &str,
871 source: Option<&str>,
872 metadata: Option<serde_json::Value>,
873 trace_ctx: Option<&TraceCtx>,
874 ) -> Result<Fact, MemoryError> {
875 let id = self
876 .add_fact_with_trace(namespace, content, source, metadata, trace_ctx)
877 .await?;
878 self.get_fact(&id)
879 .await?
880 .ok_or(MemoryError::FactNotFound(id))
881 }
882
883 pub async fn add_fact(
888 &self,
889 namespace: &str,
890 content: &str,
891 source: Option<&str>,
892 metadata: Option<serde_json::Value>,
893 ) -> Result<String, MemoryError> {
894 self.add_fact_with_trace(namespace, content, source, metadata, None)
895 .await
896 }
897
898 pub async fn add_fact_with_trace(
900 &self,
901 namespace: &str,
902 content: &str,
903 source: Option<&str>,
904 metadata: Option<serde_json::Value>,
905 trace_ctx: Option<&TraceCtx>,
906 ) -> Result<String, MemoryError> {
907 self.validate_content("fact.content", content)?;
908
909 let ns_check = namespace.to_string();
912 let ct_check = content.to_string();
913 let existing_id = self
914 .with_read_conn(move |conn| {
915 let result: Option<String> = conn
916 .query_row(
917 "SELECT id FROM facts WHERE content = ?1 AND namespace = ?2 LIMIT 1",
918 rusqlite::params![&ct_check, &ns_check],
919 |row| row.get::<_, String>(0),
920 )
921 .ok();
922 Ok(result)
923 })
924 .await?;
925
926 if let Some(id) = existing_id {
927 return Ok(id);
928 }
929
930 let (embedding, sparse, sparse_representation) = self
931 .embed_text_with_sparse_internal(content, crate::EmbeddingPurpose::Document)
932 .await?;
933 self.validate_embedding_dimensions(&embedding)?;
934 let embedding_bytes = db::embedding_to_bytes(&embedding);
935 let fact_id = uuid::Uuid::new_v4().to_string();
936 let max_facts_per_namespace = self.inner.config.limits.max_facts_per_namespace;
937
938 let quantizer = Quantizer::new(self.inner.config.embedding.dimensions);
939 let q8_bytes = quantizer
941 .quantize(&embedding)
942 .map(|qv| quantize::pack_quantized(&qv))
943 .ok();
944
945 let ns = namespace.to_string();
946 let ct = content.to_string();
947 let fid = fact_id.clone();
948 let src = source.map(|s| s.to_string());
949 let meta = merge_trace_ctx(metadata, trace_ctx);
950 self.with_write_conn(move |conn| {
951 let current_count: usize = conn.query_row(
952 "SELECT COUNT(*) FROM facts WHERE namespace = ?1",
953 rusqlite::params![&ns],
954 |row| row.get(0),
955 )?;
956 if current_count >= max_facts_per_namespace {
957 return Err(MemoryError::NamespaceFull {
958 namespace: ns.clone(),
959 count: current_count,
960 limit: max_facts_per_namespace,
961 });
962 }
963 insert_fact_with_fts_q8(
964 conn,
965 &fid,
966 &ns,
967 &ct,
968 &embedding_bytes,
969 q8_bytes.as_deref(),
970 src.as_deref(),
971 meta.as_ref(),
972 sparse.as_ref().zip(sparse_representation.as_deref()),
973 )
974 })
975 .await?;
976
977 self.clear_search_cache();
978
979 #[cfg(feature = "hnsw")]
980 self.sync_pending_hnsw_ops_best_effort("add_fact").await;
981
982 Ok(fact_id)
983 }
984
985 pub async fn add_fact_with_embedding(
987 &self,
988 namespace: &str,
989 content: &str,
990 embedding: &[f32],
991 source: Option<&str>,
992 metadata: Option<serde_json::Value>,
993 ) -> Result<String, MemoryError> {
994 self.add_fact_with_embedding_and_trace(
995 namespace, content, embedding, source, metadata, None,
996 )
997 .await
998 }
999
1000 pub async fn add_fact_with_embedding_and_trace(
1002 &self,
1003 namespace: &str,
1004 content: &str,
1005 embedding: &[f32],
1006 source: Option<&str>,
1007 metadata: Option<serde_json::Value>,
1008 trace_ctx: Option<&TraceCtx>,
1009 ) -> Result<String, MemoryError> {
1010 self.validate_content("fact.content", content)?;
1011 self.validate_embedding_dimensions(embedding)?;
1012 let embedding_bytes = db::embedding_to_bytes(embedding);
1013 let sparse = self.inner.config.search.derive_sparse_from_dense.then(|| {
1014 crate::SparseWeights::from_dense(
1015 embedding,
1016 self.inner.config.search.sparse_derive_top_k,
1017 self.inner.config.search.sparse_derive_min_weight,
1018 )
1019 });
1020 let fact_id = uuid::Uuid::new_v4().to_string();
1021 let max_facts_per_namespace = self.inner.config.limits.max_facts_per_namespace;
1022
1023 let quantizer = Quantizer::new(self.inner.config.embedding.dimensions);
1024 let q8_bytes = quantizer
1026 .quantize(embedding)
1027 .map(|qv| quantize::pack_quantized(&qv))
1028 .ok();
1029
1030 let ns = namespace.to_string();
1031 let ct = content.to_string();
1032 let fid = fact_id.clone();
1033 let src = source.map(|s| s.to_string());
1034 let meta = merge_trace_ctx(metadata, trace_ctx);
1035 self.with_write_conn(move |conn| {
1036 let current_count: usize = conn.query_row(
1037 "SELECT COUNT(*) FROM facts WHERE namespace = ?1",
1038 rusqlite::params![&ns],
1039 |row| row.get(0),
1040 )?;
1041 if current_count >= max_facts_per_namespace {
1042 return Err(MemoryError::NamespaceFull {
1043 namespace: ns.clone(),
1044 count: current_count,
1045 limit: max_facts_per_namespace,
1046 });
1047 }
1048 insert_fact_with_fts_q8(
1049 conn,
1050 &fid,
1051 &ns,
1052 &ct,
1053 &embedding_bytes,
1054 q8_bytes.as_deref(),
1055 src.as_deref(),
1056 meta.as_ref(),
1057 sparse
1058 .as_ref()
1059 .map(|weights| (weights, "generic_dense_derived_sparse")),
1060 )
1061 })
1062 .await?;
1063
1064 self.clear_search_cache();
1065
1066 #[cfg(feature = "hnsw")]
1067 self.sync_pending_hnsw_ops_best_effort("add_fact_with_embedding")
1068 .await;
1069
1070 Ok(fact_id)
1071 }
1072
1073 #[cfg(feature = "admin-ops")]
1078 pub async fn update_fact(&self, fact_id: &str, content: &str) -> Result<(), MemoryError> {
1079 self.validate_content("fact.content", content)?;
1080 let (embedding, sparse, sparse_representation) = self
1081 .embed_text_with_sparse_internal(content, crate::EmbeddingPurpose::Document)
1082 .await?;
1083 self.validate_embedding_dimensions(&embedding)?;
1084 let embedding_bytes = db::embedding_to_bytes(&embedding);
1085 let q8_bytes = Quantizer::new(self.inner.config.embedding.dimensions)
1087 .quantize(&embedding)
1088 .map(|qv| quantize::pack_quantized(&qv))
1089 .ok();
1090
1091 let fid = fact_id.to_string();
1092 let ct = content.to_string();
1093 self.with_write_conn(move |conn| {
1094 update_fact_with_fts(conn, &fid, &ct, &embedding_bytes, q8_bytes.as_deref())?;
1095 let item_key = format!("fact:{fid}");
1096 if let Some((weights, representation)) =
1097 sparse.as_ref().zip(sparse_representation.as_deref())
1098 {
1099 db::store_sparse_vector(conn, &item_key, weights, representation)?;
1100 } else {
1101 db::delete_sparse_vector(conn, &item_key)?;
1102 }
1103 Ok(())
1104 })
1105 .await?;
1106
1107 #[cfg(feature = "hnsw")]
1108 self.sync_pending_hnsw_ops_best_effort("update_fact").await;
1109
1110 self.clear_search_cache();
1111
1112 Ok(())
1113 }
1114
1115 #[cfg(feature = "admin-ops")]
1120 pub async fn delete_fact(&self, fact_id: &str) -> Result<(), MemoryError> {
1121 let fid = fact_id.to_string();
1122 self.with_write_conn(move |conn| delete_fact_with_fts(conn, &fid))
1123 .await?;
1124
1125 #[cfg(feature = "hnsw")]
1126 self.sync_pending_hnsw_ops_best_effort("delete_fact").await;
1127
1128 self.clear_search_cache();
1129
1130 Ok(())
1131 }
1132
1133 #[cfg(not(feature = "admin-ops"))]
1134 #[doc = r#"
1135 ```compile_fail
1136 use semantic_memory::MemoryStore;
1137
1138 fn main() {
1139 let _ = MemoryStore::update_fact;
1140 let _ = MemoryStore::delete_fact;
1141 let _ = MemoryStore::delete_namespace;
1142 let _ = MemoryStore::reembed_all;
1143 }
1144 ```"#]
1145 pub(crate) const _MEM014_ADMIN_OPS_GUARD: () = ();
1146
1147 #[cfg(feature = "admin-ops")]
1151 pub async fn delete_namespace(
1152 &self,
1153 namespace: &str,
1154 ) -> Result<NamespaceDeleteReport, MemoryError> {
1155 let ns = namespace.to_string();
1156 let count = self
1157 .with_write_conn(move |conn| delete_namespace(conn, &ns))
1158 .await?;
1159
1160 #[cfg(feature = "hnsw")]
1161 self.sync_pending_hnsw_ops_best_effort("delete_namespace")
1162 .await;
1163
1164 self.clear_search_cache();
1165
1166 Ok(count)
1167 }
1168
1169 pub async fn get_fact(&self, fact_id: &str) -> Result<Option<Fact>, MemoryError> {
1171 let fid = fact_id.to_string();
1172 self.with_read_conn(move |conn| get_fact(conn, &fid)).await
1173 }
1174
1175 pub async fn get_fact_raw_compat(&self, fact_id: &str) -> Result<Option<Fact>, MemoryError> {
1177 self.get_fact(fact_id).await
1178 }
1179
1180 pub async fn get_fact_embedding(&self, fact_id: &str) -> Result<Option<Vec<f32>>, MemoryError> {
1182 let fid = fact_id.to_string();
1183 self.with_read_conn(move |conn| get_fact_embedding(conn, &fid))
1184 .await
1185 }
1186
1187 pub async fn list_facts(
1189 &self,
1190 namespace: &str,
1191 limit: usize,
1192 offset: usize,
1193 ) -> Result<Vec<Fact>, MemoryError> {
1194 self.list_facts_with_view(namespace, limit, offset, StateView::Current)
1195 .await
1196 }
1197
1198 pub async fn list_facts_with_view(
1200 &self,
1201 namespace: &str,
1202 limit: usize,
1203 offset: usize,
1204 view: StateView,
1205 ) -> Result<Vec<Fact>, MemoryError> {
1206 let ns = namespace.to_string();
1207 self.with_read_conn(move |conn| list_facts_with_view(conn, &ns, limit, offset, &view))
1208 .await
1209 }
1210
1211 pub async fn list_fact_namespaces(&self) -> Result<Vec<String>, MemoryError> {
1213 self.with_read_conn(move |conn| list_fact_namespaces(conn))
1214 .await
1215 }
1216}
1217
1218#[cfg(test)]
1219mod state_view_regression_tests {
1220 use super::*;
1221 use crate::db::run_migrations;
1222 use rusqlite::Connection;
1223
1224 fn seeded() -> Connection {
1225 let conn = Connection::open_in_memory().unwrap();
1226 run_migrations(&conn).unwrap();
1227 for (id, content, created) in [
1228 ("old", "same topic old", "2026-07-10 21:00:00"),
1229 ("new", "same topic new", "2026-07-10 21:12:01"),
1230 ] {
1231 conn.execute(
1232 "INSERT INTO facts(id, namespace, content, created_at, updated_at) VALUES (?1, 'n', ?2, ?3, ?3)",
1233 params![id, content, created],
1234 ).unwrap();
1235 }
1236 conn
1237 }
1238
1239 fn supersedes(conn: &Connection, source: &str, target: &str, recorded: &str) {
1240 conn.execute(
1241 "INSERT INTO graph_edges(id, source, target, edge_type, weight, content_digest, recorded_at, valid_time, recorded_time)
1242 VALUES (lower(hex(randomblob(16))), ?1, ?2, '{\"type\":\"entity\",\"relation\":\"supersedes\"}', 1, lower(hex(randomblob(16))), ?3, ?3, ?3)",
1243 params![format!("fact:{source}"), format!("fact:{target}"), recorded],
1244 ).unwrap();
1245 }
1246
1247 fn supersedes_canonical(conn: &Connection, source: &str, target: &str, recorded: &str) {
1248 conn.execute(
1249 "INSERT INTO graph_edges(id, source, target, edge_type, weight, content_digest, recorded_at, valid_time, recorded_time)
1250 VALUES (lower(hex(randomblob(16))), ?1, ?2, '{\"entity\":{\"relation\":\"supersedes\"}}', 1, lower(hex(randomblob(16))), ?3, ?3, ?3)",
1251 params![format!("fact:{source}"), format!("fact:{target}"), recorded],
1252 ).unwrap();
1253 }
1254
1255 #[test]
1256 fn historical_view_excludes_future_fact_and_reconstructs_pre_supersession_head() {
1257 let conn = seeded();
1258 supersedes(&conn, "new", "old", "2026-07-10 21:12:01");
1259 let rows = list_facts_with_view(
1260 &conn,
1261 "n",
1262 10,
1263 0,
1264 &StateView::HistoricalAt("2026-07-10T21:11:50Z".into()),
1265 )
1266 .unwrap();
1267 assert_eq!(
1268 rows.iter().map(|f| f.id.as_str()).collect::<Vec<_>>(),
1269 ["old"]
1270 );
1271 }
1272
1273 #[test]
1274 fn historical_view_preserves_pre_adjudication_conflict() {
1275 let conn = seeded();
1276 conn.execute(
1277 "UPDATE facts SET created_at = '2026-07-10 21:10:00', updated_at = '2026-07-10 21:10:00' WHERE id = 'new'",
1278 [],
1279 )
1280 .unwrap();
1281 supersedes(&conn, "new", "old", "2026-07-10 21:12:01");
1282
1283 let rows = list_facts_with_view(
1284 &conn,
1285 "n",
1286 10,
1287 0,
1288 &StateView::HistoricalAt("2026-07-10T21:11:50Z".into()),
1289 )
1290 .unwrap();
1291 let ids = rows.iter().map(|fact| fact.id.as_str()).collect::<Vec<_>>();
1292 assert!(
1293 ids.contains(&"old"),
1294 "prior observation must remain visible"
1295 );
1296 assert!(ids.contains(&"new"), "conflicting observation created before the cutoff must remain visible until adjudication");
1297 }
1298
1299 #[test]
1300 fn current_view_excludes_superseded_fact() {
1301 let conn = seeded();
1302 supersedes(&conn, "new", "old", "2026-07-10 21:12:01");
1303 let rows = list_facts_with_view(&conn, "n", 10, 0, &StateView::Current).unwrap();
1304 assert_eq!(
1305 rows.iter().map(|f| f.id.as_str()).collect::<Vec<_>>(),
1306 ["new"]
1307 );
1308 }
1309
1310 #[test]
1311 fn current_view_accepts_canonical_entity_edge_serialization() {
1312 let conn = seeded();
1313 supersedes_canonical(&conn, "new", "old", "2026-07-10 21:12:01");
1314 let rows = list_facts_with_view(&conn, "n", 10, 0, &StateView::Current).unwrap();
1315 assert_eq!(
1316 rows.iter().map(|fact| fact.id.as_str()).collect::<Vec<_>>(),
1317 vec!["new"]
1318 );
1319 }
1320
1321 #[test]
1322 fn multiple_active_heads_fail_closed() {
1323 let conn = seeded();
1324 conn.execute("INSERT INTO facts(id, namespace, content, created_at, updated_at) VALUES ('other', 'n', 'same topic conflicting', '2026-07-10 21:13:00', '2026-07-10 21:13:00')", []).unwrap();
1325 supersedes(&conn, "new", "old", "2026-07-10 21:12:01");
1326 supersedes(&conn, "other", "old", "2026-07-10 21:13:00");
1327 assert!(list_facts_with_view(&conn, "n", 10, 0, &StateView::Current).is_err());
1328 }
1329}