Skip to main content

storage_kit/
database_handle.rs

1//! Private implementation of the root database owner.
2
3use std::{fmt, future::Future, sync::Arc};
4
5#[cfg(feature = "surrealdb")]
6use crate::databases::surrealdb::backend::SurrealBackend;
7use crate::{
8    CapabilitySet, CatalogRequest, CatalogResponse, DataRequest, DataResponse, IndexRequest,
9    IndexResponse, QueryRequest, QueryResponse, StorageError,
10    databases::{config::DatabaseConfig, lancedb::backend::LanceBackend},
11};
12
13/// Portable data, catalog, query, and index behavior.
14///
15/// Implement this trait directly on a caller-owned type; `Clone` is not
16/// required. Operations are authoritative: an optional capability bit does not
17/// guarantee that an arbitrary request is supported. Unsupported operations
18/// fail before mutation, and responses preserve real ids, counts and score
19/// semantics. Supporting DTOs live in [`crate::database`].
20///
21/// # Caller-owned catalog example
22///
23/// This small test double validates one request and returns an empty catalog.
24/// It demonstrates direct trait consumption, not a database engine. Other
25/// operations reject explicitly; no native storage or service is constructed.
26///
27/// ```
28/// use storage_kit::{DatabaseBackend, StorageError};
29/// use storage_kit::database::CapabilitySet;
30/// use storage_kit::database::catalog::{CatalogRequest, CatalogResponse, ListCollectionsRequest};
31/// # use storage_kit::database::data::{DataRequest, DataResponse};
32/// # use storage_kit::database::query::{QueryRequest, QueryResponse};
33/// # use storage_kit::database::index::{IndexRequest, IndexResponse};
34///
35/// struct EmptyCatalog;
36/// impl DatabaseBackend for EmptyCatalog {
37///     fn capabilities(&self) -> CapabilitySet { CapabilitySet::empty() }
38///     async fn catalog(&self, request: CatalogRequest) -> Result<CatalogResponse, StorageError> {
39///         assert_eq!(request, CatalogRequest::ListCollections(ListCollectionsRequest {
40///             namespace: Vec::new(), limit: Some(10), start_after: None,
41///         }));
42///         Ok(CatalogResponse::Collections(Vec::new()))
43///     }
44/// #     async fn data(&self, _: DataRequest) -> Result<DataResponse, StorageError> {
45/// #         Err(StorageError::unsupported_capability("data", "catalog-only example"))
46/// #     }
47/// #     async fn query(&self, _: QueryRequest) -> Result<QueryResponse, StorageError> {
48/// #         Err(StorageError::unsupported_capability("query", "catalog-only example"))
49/// #     }
50/// #     async fn index(&self, _: IndexRequest) -> Result<IndexResponse, StorageError> {
51/// #         Err(StorageError::unsupported_capability("index", "catalog-only example"))
52/// #     }
53/// }
54/// # tokio::runtime::Runtime::new()?.block_on(async {
55/// let database = EmptyCatalog;
56/// let response = database.catalog(CatalogRequest::ListCollections(ListCollectionsRequest {
57///     namespace: Vec::new(), limit: Some(10), start_after: None,
58/// })).await?;
59/// let CatalogResponse::Collections(collections) = response else {
60///     panic!("expected the catalog response");
61/// };
62/// assert!(collections.is_empty());
63/// assert!(database.capabilities().is_empty());
64/// # Ok::<(), StorageError>(())
65/// # })?;
66/// # Ok::<(), Box<dyn std::error::Error>>(())
67/// ```
68///
69/// An application may compose different implementations in its own exhaustive
70/// enum, forwarding requests and errors unchanged. See the
71/// [executable caller composition tests](https://github.com/jecsand838/ai-kit/blob/main/storage-kit/tests/mock_contracts.rs).
72/// The [local collection workflow](crate::database) uses real embedded storage.
73pub trait DatabaseBackend: Send + Sync + 'static {
74    /// Returns optional retained capabilities.
75    ///
76    /// Use this to discover optional operation families, not to identify the
77    /// implementation or assume an arbitrary request will succeed.
78    ///
79    /// See the [caller-owned catalog example](DatabaseBackend) for an executable example.
80    fn capabilities(&self) -> CapabilitySet;
81
82    /// Executes a retained catalog operation.
83    ///
84    /// Creation, opening, listing, deletion, and schema evolution remain distinct
85    /// from data writes and StateStore transactions.
86    ///
87    /// # Errors
88    ///
89    /// Returns [`StorageError`] for invalid scope or request data, unsupported
90    /// operations, and native connection, deadline, or execution failures. An
91    /// unsupported operation must fail before mutation.
92    ///
93    /// See the [caller-owned catalog example](DatabaseBackend) for an executable example.
94    fn catalog(
95        &self,
96        request: CatalogRequest,
97    ) -> impl Future<Output = Result<CatalogResponse, StorageError>> + Send + '_;
98
99    /// Executes a retained data operation.
100    ///
101    /// Ordinary writes are not rollback-capable multi-request transactions. A
102    /// native failure or timeout can leave an uncertain or partial outcome and
103    /// must not trigger automatic replay. Partition replacement retains its
104    /// explicit atomic contract.
105    ///
106    /// # Errors
107    ///
108    /// Returns [`StorageError`] for invalid input, unsupported request combinations,
109    /// read-only policy, serialization failures, and native execution/deadline
110    /// failures. Unsupported behavior must be rejected before mutation.
111    ///
112    /// See the [local collection workflow](crate::database) for an executable example.
113    fn data(
114        &self,
115        request: DataRequest,
116    ) -> impl Future<Output = Result<DataResponse, StorageError>> + Send + '_;
117
118    /// Executes a retained query operation.
119    ///
120    /// Return continuation tokens unchanged to the same applicable operation.
121    /// Scores preserve their explicit kind; a missing score is not zero.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`StorageError`] for invalid scope, vectors, fields, filters, or
126    /// tuning, unsupported query combinations, and native execution/deadline
127    /// failures. Optional capability bits do not override request validation.
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// use storage_kit::{Database, DatabaseBackend};
133    /// use storage_kit::database::config::{DatabaseConfig, LanceConfig};
134    /// use storage_kit::database::StorageLocator;
135    /// use storage_kit::database::catalog::{CatalogRequest, CreateCollectionRequest, CreateCollectionMode};
136    /// use storage_kit::database::data::DataRecord;
137    /// use storage_kit::database::query::{QueryRequest, ScrollRequest, Projection};
138    /// # tokio::runtime::Runtime::new()?.block_on(async {
139    /// let database = Database::connect(DatabaseConfig::lance(LanceConfig::embedded_path("memory://"))).await?;
140    /// let locator = StorageLocator::new("documents");
141    /// database.catalog(CatalogRequest::CreateCollection(CreateCollectionRequest {
142    ///     locator: locator.clone(), schema: None,
143    ///     records: vec![DataRecord::without_id().json_field("text", serde_json::json!("example"))],
144    ///     mode: CreateCollectionMode::Create,
145    /// })).await?;
146    /// let response = database.query(QueryRequest::Scroll(ScrollRequest {
147    ///     locator, filter: None, projection: Projection::All, limit: Some(10),
148    ///     offset: None, page_token: None,
149    /// })).await?;
150    /// assert_eq!(response.hits.len(), 1);
151    /// # Ok::<(), storage_kit::StorageError>(())
152    /// # })?;
153    /// # Ok::<(), Box<dyn std::error::Error>>(())
154    /// ```
155    fn query(
156        &self,
157        request: QueryRequest,
158    ) -> impl Future<Output = Result<QueryResponse, StorageError>> + Send + '_;
159
160    /// Executes a retained index operation.
161    ///
162    /// Creating an index and observing its readiness are separate operations.
163    /// Readiness requests must finish within both their requested wait bound and
164    /// the enclosing configured operation deadline.
165    ///
166    /// # Errors
167    ///
168    /// Returns [`StorageError`] for malformed index requests, unsupported kinds,
169    /// read-only policy, missing targets, unconfirmed completion, or native
170    /// execution/readiness deadline failures.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use storage_kit::{Database, DatabaseBackend};
176    /// use storage_kit::database::config::{DatabaseConfig, LanceConfig};
177    /// use storage_kit::database::StorageLocator;
178    /// use storage_kit::database::catalog::{CatalogRequest, CreateCollectionRequest, CreateCollectionMode};
179    /// use storage_kit::database::data::DataRecord;
180    /// use storage_kit::database::index::{IndexRequest, IndexResponse};
181    /// # tokio::runtime::Runtime::new()?.block_on(async {
182    /// let database = Database::connect(DatabaseConfig::lance(LanceConfig::embedded_path("memory://"))).await?;
183    /// let locator = StorageLocator::new("documents");
184    /// database.catalog(CatalogRequest::CreateCollection(CreateCollectionRequest {
185    ///     locator: locator.clone(), schema: None,
186    ///     records: vec![DataRecord::without_id().json_field("text", serde_json::json!("example"))],
187    ///     mode: CreateCollectionMode::Create,
188    /// })).await?;
189    /// let response = database.index(IndexRequest::List { locator }).await?;
190    /// assert!(matches!(response, IndexResponse::Indexes(_)));
191    /// # Ok::<(), storage_kit::StorageError>(())
192    /// # })?;
193    /// # Ok::<(), Box<dyn std::error::Error>>(())
194    /// ```
195    fn index(
196        &self,
197        request: IndexRequest,
198    ) -> impl Future<Output = Result<IndexResponse, StorageError>> + Send + '_;
199}
200
201/// Cloneable opaque database handle.
202///
203/// Clones share the same opened database. Import [`DatabaseBackend`] to call
204/// its catalog, data, query, index, and capability methods. This supplied
205/// handle contains only built-ins; caller implementations retain their own
206/// concrete type and implement the trait directly.
207///
208/// # Examples
209///
210/// ```
211/// use storage_kit::{Database, DatabaseBackend};
212/// use storage_kit::database::config::{DatabaseConfig, LanceConfig};
213/// # tokio::runtime::Runtime::new()?.block_on(async {
214/// let database = Database::connect(DatabaseConfig::lance(LanceConfig::embedded_path("memory://"))).await?;
215/// let shared = database.clone();
216/// assert_eq!(database.capabilities(), shared.capabilities());
217/// # Ok::<(), storage_kit::StorageError>(())
218/// # })?;
219/// # Ok::<(), Box<dyn std::error::Error>>(())
220/// ```
221#[derive(Clone)]
222pub struct Database {
223    inner: Arc<DatabaseInner>,
224}
225
226enum DatabaseInner {
227    Lance(LanceBackend),
228    #[cfg(feature = "surrealdb")]
229    Surreal(SurrealBackend),
230}
231
232impl Database {
233    /// Opens a built-in database.
234    ///
235    /// The configuration is validated before backend resources are opened.
236    /// LanceDB is always available. SurrealDB requires `surrealdb`.
237    /// Disabled implementations return
238    /// [`StorageError::InvalidConfig`] before opening native resources.
239    /// The example uses an in-memory LanceDB instance and needs no service.
240    ///
241    /// # Errors
242    ///
243    /// Returns [`StorageError::InvalidConfig`] for invalid configuration or
244    /// required capabilities the selected backend cannot provide, or the
245    /// owning connection, I/O, or timeout error.
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// use storage_kit::{Database, DatabaseBackend};
251    /// use storage_kit::database::config::{DatabaseConfig, LanceConfig};
252    /// use storage_kit::database::CapabilitySet;
253    /// # tokio::runtime::Runtime::new()?.block_on(async {
254    /// let database = Database::connect(DatabaseConfig::lance(LanceConfig::embedded_path("memory://"))).await?;
255    /// assert!(database.capabilities().contains(CapabilitySet::FILTERING));
256    /// # Ok::<(), storage_kit::StorageError>(())
257    /// # })?;
258    /// # Ok::<(), Box<dyn std::error::Error>>(())
259    /// ```
260    pub async fn connect(config: DatabaseConfig) -> Result<Self, StorageError> {
261        config.validate()?;
262        match config {
263            DatabaseConfig::Lance { config, common } => LanceBackend::connect(common, config)
264                .await
265                .map(|backend| Self {
266                    inner: Arc::new(DatabaseInner::Lance(backend)),
267                }),
268            DatabaseConfig::Surreal { config, common } => {
269                if cfg!(feature = "surrealdb") {
270                    #[cfg(feature = "surrealdb")]
271                    return SurrealBackend::connect(common, config)
272                        .await
273                        .map(|backend| Self {
274                            inner: Arc::new(DatabaseInner::Surreal(backend)),
275                        });
276                }
277                drop((config, common));
278                Err(StorageError::InvalidConfig(
279                    "SurrealDB requires the `surrealdb` feature".into(),
280                ))
281            }
282        }
283    }
284}
285
286impl fmt::Debug for Database {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        f.write_str("Database { .. }")
289    }
290}
291
292impl DatabaseBackend for Database {
293    fn capabilities(&self) -> CapabilitySet {
294        self.inner.capabilities()
295    }
296
297    // Keep the declared Send bound opaque to callers so their future proofs do
298    // not recursively expand the native database query implementation.
299    fn catalog(
300        &self,
301        request: CatalogRequest,
302    ) -> impl Future<Output = Result<CatalogResponse, StorageError>> + Send + '_ {
303        self.inner.catalog(request)
304    }
305
306    fn data(
307        &self,
308        request: DataRequest,
309    ) -> impl Future<Output = Result<DataResponse, StorageError>> + Send + '_ {
310        self.inner.data(request)
311    }
312
313    fn query(
314        &self,
315        request: QueryRequest,
316    ) -> impl Future<Output = Result<QueryResponse, StorageError>> + Send + '_ {
317        self.inner.query(request)
318    }
319
320    fn index(
321        &self,
322        request: IndexRequest,
323    ) -> impl Future<Output = Result<IndexResponse, StorageError>> + Send + '_ {
324        self.inner.index(request)
325    }
326}
327
328impl DatabaseBackend for DatabaseInner {
329    fn capabilities(&self) -> CapabilitySet {
330        match self {
331            Self::Lance(backend) => backend.capabilities(),
332            #[cfg(feature = "surrealdb")]
333            Self::Surreal(backend) => backend.capabilities(),
334        }
335    }
336
337    async fn catalog(&self, request: CatalogRequest) -> Result<CatalogResponse, StorageError> {
338        match self {
339            Self::Lance(backend) => backend.catalog(request).await,
340            #[cfg(feature = "surrealdb")]
341            Self::Surreal(backend) => backend.catalog(request).await,
342        }
343    }
344
345    async fn data(&self, request: DataRequest) -> Result<DataResponse, StorageError> {
346        request.validate()?;
347        match self {
348            Self::Lance(backend) => backend.data(request).await,
349            #[cfg(feature = "surrealdb")]
350            Self::Surreal(backend) => backend.data(request).await,
351        }
352    }
353
354    async fn query(&self, request: QueryRequest) -> Result<QueryResponse, StorageError> {
355        match self {
356            Self::Lance(backend) => backend.query(request).await,
357            #[cfg(feature = "surrealdb")]
358            Self::Surreal(backend) => backend.query(request).await,
359        }
360    }
361
362    async fn index(&self, request: IndexRequest) -> Result<IndexResponse, StorageError> {
363        request.validate()?;
364        match self {
365            Self::Lance(backend) => backend.index(request).await,
366            #[cfg(feature = "surrealdb")]
367            Self::Surreal(backend) => backend.index(request).await,
368        }
369    }
370}