khive_storage/graph.rs
1//! Graph storage capability — edge CRUD and traversal.
2
3use async_trait::async_trait;
4use uuid::Uuid;
5
6use crate::types::{
7 BatchWriteSummary, DeleteMode, Edge, EdgeFilter, EdgeSortField, GraphPath, LinkId, NeighborHit,
8 NeighborQuery, Page, PageRequest, SortOrder, StorageResult, TraversalRequest,
9};
10
11/// Directed edge CRUD and graph traversal over the knowledge graph.
12#[async_trait]
13pub trait GraphStore: Send + Sync + 'static {
14 /// Insert or update a single edge.
15 async fn upsert_edge(&self, edge: Edge) -> StorageResult<()>;
16 /// Insert or update a batch of edges.
17 async fn upsert_edges(&self, edges: Vec<Edge>) -> StorageResult<BatchWriteSummary>;
18 /// Fetch an edge by link ID, returning `None` if absent. Filters soft-deleted rows.
19 async fn get_edge(&self, id: LinkId) -> StorageResult<Option<Edge>>;
20 /// Fetch an edge by link ID including soft-deleted rows. Used by the runtime hard-delete path
21 /// to locate and namespace-check an already-soft-deleted edge before purging it.
22 async fn get_edge_including_deleted(&self, id: LinkId) -> StorageResult<Option<Edge>>;
23 /// Delete an edge by link ID using the specified delete mode.
24 async fn delete_edge(&self, id: LinkId, mode: DeleteMode) -> StorageResult<bool>;
25 /// Query edges with filter, sort, and pagination.
26 async fn query_edges(
27 &self,
28 filter: EdgeFilter,
29 sort: Vec<SortOrder<EdgeSortField>>,
30 page: PageRequest,
31 ) -> StorageResult<Page<Edge>>;
32 /// Count edges matching the given filter.
33 async fn count_edges(&self, filter: EdgeFilter) -> StorageResult<u64>;
34 /// Return immediate neighbors of a graph node.
35 async fn neighbors(
36 &self,
37 node_id: Uuid,
38 query: NeighborQuery,
39 ) -> StorageResult<Vec<NeighborHit>>;
40 /// Fetch multiple edges by their link IDs in a single round-trip.
41 ///
42 /// IDs that are not found (absent or soft-deleted) are silently skipped;
43 /// the returned `Vec` may be shorter than `ids`. Backends that support
44 /// batched `IN (...)` queries should override this; the default loops
45 /// `get_edge` so non-SQLite backends keep compiling unchanged.
46 ///
47 /// Callers must chunk large ID lists before calling if they need a strict
48 /// size bound; this method does not enforce a maximum.
49 async fn get_edges(&self, ids: &[LinkId]) -> StorageResult<Vec<Edge>> {
50 let mut out = Vec::with_capacity(ids.len());
51 for &id in ids {
52 if let Some(edge) = self.get_edge(id).await? {
53 out.push(edge);
54 }
55 }
56 Ok(out)
57 }
58 /// Return neighbors for multiple source nodes in a single round-trip,
59 /// yielding `(source_id, hit)` pairs.
60 ///
61 /// The `query` parameters (direction, relations, min_weight) are applied
62 /// uniformly to every source node. `query.limit` is applied **per source**:
63 /// each source returns at most `limit` hits. Backends that support batched
64 /// `source_id IN (...)` queries should override this; the default loops
65 /// `neighbors` so non-SQLite backends keep compiling unchanged.
66 async fn batch_neighbors(
67 &self,
68 sources: &[Uuid],
69 query: NeighborQuery,
70 ) -> StorageResult<Vec<(Uuid, NeighborHit)>> {
71 let mut out = Vec::new();
72 for &src in sources {
73 let hits = self.neighbors(src, query.clone()).await?;
74 for hit in hits {
75 out.push((src, hit));
76 }
77 }
78 Ok(out)
79 }
80 /// Multi-hop BFS traversal from the given roots.
81 async fn traverse(&self, request: TraversalRequest) -> StorageResult<Vec<GraphPath>>;
82 /// Hard-delete every incident edge (source or target) for `node_id`, regardless of soft-delete
83 /// state. Used during endpoint hard-delete to prevent dangling `graph_edges` rows (ADR-002
84 /// no-dangling-references contract).
85 async fn purge_incident_edges(&self, node_id: Uuid) -> StorageResult<u64>;
86}