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