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}