Skip to main content

storage_kit/core/
index.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{DistanceMetric, StorageError, StorageLocator};
4
5use super::{ApplyDefaultLocator, HasLocator};
6
7/// Describes an index exposed by a backend.
8///
9/// # Examples
10///
11/// ```
12/// let info = storage_kit::database::index::IndexInfo {
13///     name: Some("idx_documents_vector".into()),
14///     columns: vec!["vector".into()],
15///     ready: Some(true),
16///     ..Default::default()
17/// };
18/// assert_eq!(info.ready, Some(true));
19/// ```
20#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
21pub struct IndexInfo {
22    /// Index name, when available.
23    pub name: Option<String>,
24    /// Indexed columns or fields covered by the index.
25    pub columns: Vec<String>,
26    /// Whether the index is ready for use.
27    pub ready: Option<bool>,
28}
29
30/// Portable index kind requested from a backend.
31///
32/// # Examples
33///
34/// ```
35/// let kind = storage_kit::database::index::IndexKind::Vector {
36///     dimension: 384,
37///     distance: Some(storage_kit::database::query::DistanceMetric::Cosine),
38/// };
39///
40/// assert!(matches!(
41///     kind,
42///     storage_kit::database::index::IndexKind::Vector { .. }
43/// ));
44/// ```
45///
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
47pub enum IndexKind {
48    /// Requests a scalar filter index using the backend's native scalar index.
49    Scalar,
50    /// Requests a full-text index using the portable `simple` analyzer.
51    FullText,
52    /// Requests a vector index while leaving the native algorithm private.
53    Vector {
54        /// Required vector dimensionality.
55        dimension: usize,
56        /// Distance metric to preserve across indexing and search.
57        distance: Option<DistanceMetric>,
58    },
59}
60
61/// Request to create an index.
62///
63/// Columns must be nonempty names; vector indexes require exactly one column
64/// and a positive dimension. A configured wait timeout must be positive.
65/// Request construction does not establish support for a kind or combination;
66/// [`crate::DatabaseBackend::index`] validates and executes it.
67///
68/// # Examples
69///
70/// ```
71/// let request = storage_kit::database::index::CreateIndexRequest {
72///     locator: storage_kit::database::StorageLocator::new("documents"),
73///     columns: vec!["title".into()],
74///     kind: storage_kit::database::index::IndexKind::FullText,
75///     wait_timeout_ms: Some(5_000),
76/// };
77/// assert!(matches!(
78///     request.kind,
79///     storage_kit::database::index::IndexKind::FullText
80/// ));
81/// ```
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83pub struct CreateIndexRequest {
84    /// Collection that should receive the new index.
85    pub locator: StorageLocator,
86    /// Columns or fields to index.
87    pub columns: Vec<String>,
88    /// Requested index kind.
89    pub kind: IndexKind,
90    /// Optional positive index-readiness wait timeout in milliseconds.
91    ///
92    /// Without a requested readiness wait, creation completion does not by
93    /// itself establish that the index is ready for queries.
94    pub wait_timeout_ms: Option<u64>,
95}
96
97impl HasLocator for CreateIndexRequest {
98    fn locator_mut(&mut self) -> &mut StorageLocator {
99        &mut self.locator
100    }
101}
102
103impl CreateIndexRequest {
104    pub(crate) fn validate(&self) -> Result<(), StorageError> {
105        if self.columns.is_empty() {
106            return Err(StorageError::InvalidRequest(
107                "index creation requires at least one column".into(),
108            ));
109        }
110        if self.columns.iter().any(|column| column.trim().is_empty()) {
111            return Err(StorageError::InvalidRequest(
112                "index columns must not be empty".into(),
113            ));
114        }
115        if let IndexKind::Vector { dimension, .. } = &self.kind {
116            if self.columns.len() != 1 {
117                return Err(StorageError::InvalidRequest(
118                    "vector index creation requires exactly one column".into(),
119                ));
120            }
121            if *dimension == 0 {
122                return Err(StorageError::InvalidRequest(
123                    "vector index dimension must be positive".into(),
124                ));
125            }
126        }
127        if self.wait_timeout_ms == Some(0) {
128            return Err(StorageError::InvalidRequest(
129                "index wait timeout must be positive when configured".into(),
130            ));
131        }
132        Ok(())
133    }
134}
135
136/// Request to wait until named indexes are ready.
137///
138/// At least one nonempty name and a positive timeout are required. An already
139/// ready index can complete immediately; an absent index remains a readiness
140/// failure. The requested wait bound applies in addition to the enclosing
141/// configured operation deadline.
142///
143/// # Examples
144///
145/// ```
146/// let request = storage_kit::database::index::WaitForIndexRequest {
147///     locator: storage_kit::database::StorageLocator::new("documents"),
148///     index_names: vec!["idx_documents_vector".into()],
149///     timeout_ms: 5_000,
150/// };
151/// assert_eq!(request.index_names.len(), 1);
152/// ```
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154pub struct WaitForIndexRequest {
155    /// Collection whose indexes should be observed.
156    pub locator: StorageLocator,
157    /// Index names to wait for.
158    pub index_names: Vec<String>,
159    /// Maximum wait time in milliseconds before the request should fail.
160    pub timeout_ms: u64,
161}
162
163impl HasLocator for WaitForIndexRequest {
164    fn locator_mut(&mut self) -> &mut StorageLocator {
165        &mut self.locator
166    }
167}
168
169impl WaitForIndexRequest {
170    fn validate(&self) -> Result<(), StorageError> {
171        if self.index_names.is_empty() {
172            return Err(StorageError::InvalidRequest(
173                "wait-for-index requires at least one index name".into(),
174            ));
175        }
176        if self.index_names.iter().any(|name| name.trim().is_empty()) {
177            return Err(StorageError::InvalidRequest(
178                "index names must not be empty".into(),
179            ));
180        }
181        if self.timeout_ms == 0 {
182            return Err(StorageError::InvalidRequest(
183                "wait-for-index timeout must be positive".into(),
184            ));
185        }
186        Ok(())
187    }
188}
189
190/// Portable index-management request.
191///
192/// Index creation and readiness waiting are separate operations. A completed
193/// creation does not imply every index is ready for use unless readiness was
194/// requested and established. Unsupported index kinds fail explicitly.
195///
196/// # Examples
197///
198/// ```
199/// use storage_kit::database::StorageLocator;
200/// use storage_kit::database::index::IndexRequest;
201/// let request = IndexRequest::List { locator: StorageLocator::new("documents") };
202/// assert!(matches!(request, IndexRequest::List { .. }));
203/// ```
204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
205pub enum IndexRequest {
206    /// Creates an index with the requested kind and tuning options.
207    Create(CreateIndexRequest),
208    /// Lists indexes that already exist for the target collection.
209    List {
210        /// Collection whose indexes should be listed.
211        locator: StorageLocator,
212    },
213    /// Drops a named index from the target collection.
214    Drop {
215        /// Collection whose index should be dropped.
216        locator: StorageLocator,
217        /// Index name that should be dropped.
218        name: String,
219    },
220    /// Waits for named indexes to become ready.
221    WaitFor(WaitForIndexRequest),
222}
223
224impl IndexRequest {
225    pub(crate) fn validate(&self) -> Result<(), StorageError> {
226        match self {
227            Self::Create(request) => request.validate(),
228            Self::Drop { name, .. } if name.trim().is_empty() => Err(StorageError::InvalidRequest(
229                "index name must not be empty".into(),
230            )),
231            Self::WaitFor(request) => request.validate(),
232            Self::List { .. } | Self::Drop { .. } => Ok(()),
233        }
234    }
235}
236
237impl ApplyDefaultLocator for IndexRequest {
238    fn apply_default_locator(self, default: &StorageLocator) -> Self {
239        match self {
240            Self::Create(req) => Self::Create(req.apply_default_locator(default)),
241            Self::List { locator } => Self::List {
242                locator: locator.with_defaults_from(default),
243            },
244            Self::Drop { locator, name } => Self::Drop {
245                locator: locator.with_defaults_from(default),
246                name,
247            },
248            Self::WaitFor(req) => Self::WaitFor(req.apply_default_locator(default)),
249        }
250    }
251}
252
253/// Portable index-management response.
254///
255/// # Examples
256///
257/// ```
258/// let response = storage_kit::database::index::IndexResponse::Indexes(Vec::new());
259/// assert!(matches!(response, storage_kit::database::index::IndexResponse::Indexes(_)));
260/// ```
261#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
262pub enum IndexResponse {
263    /// A mutation completed without a backend-native acknowledgement envelope.
264    Completed,
265    /// Returns index metadata for the target collection.
266    Indexes(Vec<IndexInfo>),
267}