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}