Skip to main content

khive_runtime/
backend_handle.rs

1//! Required core capabilities and optional, per-binding retrieval factories (ADR-071).
2
3use std::fmt;
4use std::sync::Arc;
5
6use khive_db::StorageBackend;
7use khive_storage::{
8    EntityStore, EventStore, GraphStore, NoteStore, SparseStore, SparseStoreFactory, SqlAccess,
9    StorageError, TextSearch, TextSearchFactory, VectorStore, VectorStoreFactory,
10};
11use thiserror::Error;
12
13/// An optional retrieval capability required by an operation.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum RetrievalTier {
16    Vector,
17    Sparse,
18    Text,
19}
20
21impl fmt::Display for RetrievalTier {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        f.write_str(match self {
24            Self::Vector => "vector",
25            Self::Sparse => "sparse",
26            Self::Text => "text",
27        })
28    }
29}
30
31/// Failure to acquire one bound retrieval capability.
32#[derive(Debug, Error)]
33pub enum BackendHandleError {
34    #[error("backend does not provide the {tier} capability")]
35    MissingCapability { tier: RetrievalTier },
36    #[error(transparent)]
37    Storage(#[from] StorageError),
38}
39
40/// Backend-neutral handles with lazy SQLite readiness and per-operation retrieval bindings.
41#[derive(Clone)]
42pub struct BackendHandle {
43    entity: Arc<dyn EntityStore>,
44    note: Arc<dyn NoteStore>,
45    graph: Arc<dyn GraphStore>,
46    event: Arc<dyn EventStore>,
47    sql: Arc<dyn SqlAccess>,
48    vector: Option<Arc<dyn VectorStoreFactory>>,
49    sparse: Option<Arc<dyn SparseStoreFactory>>,
50    text: Option<Arc<dyn TextSearchFactory>>,
51}
52
53impl BackendHandle {
54    /// Construction performs no storage operation; readiness errors occur at first use.
55    pub fn from_sqlite(backend: Arc<StorageBackend>) -> Self {
56        Self {
57            entity: backend.clone(),
58            note: backend.clone(),
59            graph: backend.clone(),
60            event: backend.clone(),
61            sql: backend.clone(),
62            vector: Some(backend.clone()),
63            sparse: Some(backend.clone()),
64            text: Some(backend),
65        }
66    }
67
68    /// Retain the supplied core handles and independently optional retrieval factories.
69    #[allow(clippy::too_many_arguments)]
70    pub fn from_parts(
71        entity: Arc<dyn EntityStore>,
72        note: Arc<dyn NoteStore>,
73        graph: Arc<dyn GraphStore>,
74        event: Arc<dyn EventStore>,
75        sql: Arc<dyn SqlAccess>,
76        vector: Option<Arc<dyn VectorStoreFactory>>,
77        sparse: Option<Arc<dyn SparseStoreFactory>>,
78        text: Option<Arc<dyn TextSearchFactory>>,
79    ) -> Self {
80        Self {
81            entity,
82            note,
83            graph,
84            event,
85            sql,
86            vector,
87            sparse,
88            text,
89        }
90    }
91
92    pub fn entity(&self) -> &Arc<dyn EntityStore> {
93        &self.entity
94    }
95
96    pub fn note(&self) -> &Arc<dyn NoteStore> {
97        &self.note
98    }
99
100    pub fn graph(&self) -> &Arc<dyn GraphStore> {
101        &self.graph
102    }
103
104    pub fn event(&self) -> &Arc<dyn EventStore> {
105        &self.event
106    }
107
108    pub fn sql(&self) -> &Arc<dyn SqlAccess> {
109        &self.sql
110    }
111
112    pub fn vector(
113        &self,
114        model_key: &str,
115        embedding_model: &str,
116        dimensions: usize,
117        namespace: &str,
118    ) -> Result<Arc<dyn VectorStore>, BackendHandleError> {
119        self.vector
120            .as_ref()
121            .ok_or(BackendHandleError::MissingCapability {
122                tier: RetrievalTier::Vector,
123            })?
124            .open(model_key, embedding_model, dimensions, namespace)
125            .map_err(BackendHandleError::Storage)
126    }
127
128    pub fn sparse(
129        &self,
130        model_key: &str,
131        namespace: &str,
132    ) -> Result<Arc<dyn SparseStore>, BackendHandleError> {
133        self.sparse
134            .as_ref()
135            .ok_or(BackendHandleError::MissingCapability {
136                tier: RetrievalTier::Sparse,
137            })?
138            .open(model_key, namespace)
139            .map_err(BackendHandleError::Storage)
140    }
141
142    pub fn text(
143        &self,
144        table_key: &str,
145        tokenizer: &str,
146    ) -> Result<Arc<dyn TextSearch>, BackendHandleError> {
147        self.text
148            .as_ref()
149            .ok_or(BackendHandleError::MissingCapability {
150                tier: RetrievalTier::Text,
151            })?
152            .open(table_key, tokenizer)
153            .map_err(BackendHandleError::Storage)
154    }
155}