Skip to main content

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}