engram/graph.rs
1//! `GraphStore` trait — the storage interface for the entity-relationship graph.
2//!
3//! Implementations (SQLite, Postgres, in-memory) must implement this trait.
4//! The trait is `async_trait`-annotated and requires `Send + Sync` so it can
5//! be used behind an `Arc<dyn GraphStore>` across async task boundaries.
6
7use crate::fact::{Entity, EntityId, Relationship, RelationshipId, SubGraph};
8use crate::scope::Scope;
9use crate::store::MemoryError;
10use async_trait::async_trait;
11use chrono::{DateTime, Utc};
12
13/// Storage interface for the entity-relationship knowledge graph.
14///
15/// All mutation methods are fallible and return `Result<_, MemoryError>`.
16/// Temporal queries use RFC 3339 `DateTime<Utc>` values for point-in-time
17/// filtering consistent with `FactStore` semantics.
18#[async_trait]
19pub trait GraphStore: Send + Sync {
20 /// Insert or update an entity.
21 ///
22 /// If an entity with `entity.id` already exists, its `name`, `entity_type`,
23 /// `attributes`, and `updated_at` fields are overwritten. Scope and
24 /// `created_at` are preserved.
25 async fn upsert_entity(&self, entity: &Entity) -> Result<(), MemoryError>;
26
27 /// Insert or update a relationship.
28 ///
29 /// If a relationship with `rel.id` already exists, its `relation` and
30 /// `invalid_at` fields are overwritten.
31 async fn upsert_relationship(&self, rel: &Relationship) -> Result<(), MemoryError>;
32
33 /// Mark a relationship as invalid as of `invalid_at`.
34 ///
35 /// Does not delete the record; historical graph queries still traverse it.
36 async fn invalidate_relationship(
37 &self,
38 id: RelationshipId,
39 invalid_at: DateTime<Utc>,
40 ) -> Result<(), MemoryError>;
41
42 /// Retrieve a single entity by id. Returns `None` if not found.
43 async fn get_entity(&self, id: EntityId) -> Result<Option<Entity>, MemoryError>;
44
45 /// BFS neighbourhood query starting from `id`.
46 ///
47 /// Expands up to `depth` hops along currently-valid relationships. When
48 /// `as_of` is `Some(t)`, a relationship is considered valid if
49 /// `valid_from <= t AND (invalid_at IS NULL OR invalid_at > t)`. When
50 /// `as_of` is `None`, only relationships with `invalid_at IS NULL` are
51 /// traversed.
52 ///
53 /// Returns a `SubGraph` containing all discovered entities (excluding the
54 /// start node) and the relationships that connect them.
55 async fn neighbors(
56 &self,
57 id: EntityId,
58 depth: u8,
59 as_of: Option<DateTime<Utc>>,
60 ) -> Result<SubGraph, MemoryError>;
61
62 /// Full-text search over entity names.
63 ///
64 /// Returns up to `top_k` entities whose `name` contains `query`
65 /// (case-insensitive substring match).
66 async fn search_entities(&self, query: &str, top_k: usize) -> Result<Vec<Entity>, MemoryError>;
67
68 /// Hard-delete all entities and relationships belonging to `scope`.
69 ///
70 /// Relationships are deleted first to avoid foreign-key-style dangling
71 /// references. Returns the total number of rows deleted (entities +
72 /// relationships combined).
73 async fn delete_by_scope(&self, scope: &Scope) -> Result<u64, MemoryError>;
74}