storage_kit/core/query/common.rs
1use serde::{Deserialize, Serialize};
2
3use crate::core::{ApplyDefaultLocator, HasLocator};
4use crate::{DistanceMetric, FilterExpr, StorageLocator};
5
6/// Fields returned for a record.
7///
8/// # Examples
9///
10/// ```
11/// use storage_kit::database::query::Projection;
12/// let projection = Projection::Columns(vec!["title".to_owned()]);
13/// assert!(matches!(projection, Projection::Columns(columns) if columns == ["title"]));
14/// ```
15#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
16pub enum Projection {
17 /// Returns every portable field.
18 #[default]
19 All,
20 /// Returns a fixed field subset.
21 Columns(Vec<String>),
22}
23
24/// Shared collection and result scope.
25///
26/// The scope is shared by vector and text requests. Construction only builds
27/// a request; the selected backend validates fields and supported combinations
28/// when executing it.
29///
30/// # Examples
31///
32/// ```
33/// use storage_kit::database::StorageLocator;
34/// use storage_kit::database::data::FilterExpr;
35/// use storage_kit::database::query::{Projection, QueryScope};
36/// let scope = QueryScope::new(StorageLocator::new("documents"))
37/// .filter(FilterExpr::eq("published", true))
38/// .projection(Projection::Columns(vec!["title".to_owned()]));
39/// assert!(scope.filter.is_some());
40/// ```
41#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
42pub struct QueryScope {
43 /// Logical collection.
44 pub locator: StorageLocator,
45 /// Optional structured filter.
46 pub filter: Option<FilterExpr>,
47 /// Returned fields.
48 pub projection: Projection,
49}
50
51impl QueryScope {
52 /// Creates an unfiltered all-field scope.
53 ///
54 /// # Examples
55 ///
56 /// ```
57 /// use storage_kit::database::StorageLocator;
58 /// use storage_kit::database::query::{Projection, QueryScope};
59 /// let scope = QueryScope::new(StorageLocator::new("documents"));
60 /// assert_eq!(scope.projection, Projection::All);
61 /// assert!(scope.filter.is_none());
62 /// ```
63 #[must_use]
64 pub fn new(locator: StorageLocator) -> Self {
65 Self {
66 locator,
67 filter: None,
68 projection: Projection::All,
69 }
70 }
71
72 /// Applies a filter.
73 ///
74 /// # Examples
75 ///
76 /// ```
77 /// use storage_kit::database::StorageLocator;
78 /// use storage_kit::database::data::FilterExpr;
79 /// use storage_kit::database::query::QueryScope;
80 /// let scope = QueryScope::new(StorageLocator::new("documents"))
81 /// .filter(FilterExpr::eq("published", true));
82 /// assert!(scope.filter.is_some());
83 /// ```
84 #[must_use]
85 pub fn filter(mut self, filter: FilterExpr) -> Self {
86 self.filter = Some(filter);
87 self
88 }
89
90 /// Replaces returned fields.
91 ///
92 /// # Examples
93 ///
94 /// ```
95 /// use storage_kit::database::StorageLocator;
96 /// use storage_kit::database::query::{Projection, QueryScope};
97 /// let scope = QueryScope::new(StorageLocator::new("documents"))
98 /// .projection(Projection::Columns(vec!["title".to_owned()]));
99 /// assert_eq!(scope.projection, Projection::Columns(vec!["title".to_owned()]));
100 /// ```
101 #[must_use]
102 pub fn projection(mut self, projection: Projection) -> Self {
103 self.projection = projection;
104 self
105 }
106}
107
108impl From<StorageLocator> for QueryScope {
109 fn from(locator: StorageLocator) -> Self {
110 Self::new(locator)
111 }
112}
113
114impl HasLocator for QueryScope {
115 fn locator_mut(&mut self) -> &mut StorageLocator {
116 &mut self.locator
117 }
118}
119
120/// Shared scope plus query-family options.
121///
122/// Use [`Self::new`] with explicit options or the `vector` and `text`
123/// constructors, then embed the result in a concrete query request. This value
124/// does not execute work or contain a database connection.
125///
126/// # Examples
127///
128/// ```
129/// use storage_kit::database::StorageLocator;
130/// use storage_kit::database::query::ScopedQuery;
131/// let query = ScopedQuery::text(StorageLocator::new("documents"))
132/// .text_field("body").limit(5);
133/// assert_eq!(query.options.text_field.as_deref(), Some("body"));
134/// assert_eq!(query.options.limit, Some(5));
135/// ```
136#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
137pub struct ScopedQuery<Options> {
138 /// Collection and result scope.
139 pub scope: QueryScope,
140 /// Query-family options.
141 pub options: Options,
142}
143
144impl<Options> ScopedQuery<Options> {
145 /// Creates a scoped query.
146 ///
147 /// # Examples
148 ///
149 /// ```
150 /// use storage_kit::database::StorageLocator;
151 /// use storage_kit::database::query::{ScopedQuery, TextQueryOptions};
152 /// let query = ScopedQuery::new(StorageLocator::new("documents"), TextQueryOptions::default());
153 /// assert_eq!(query.scope.locator.collection, "documents");
154 /// assert!(query.options.text_field.is_none());
155 /// ```
156 #[must_use]
157 pub fn new(scope: impl Into<QueryScope>, options: Options) -> Self {
158 Self {
159 scope: scope.into(),
160 options,
161 }
162 }
163
164 pub(crate) fn collection(&self) -> &str {
165 &self.scope.locator.collection
166 }
167
168 /// Replaces the shared scope.
169 ///
170 /// # Examples
171 ///
172 /// ```
173 /// use storage_kit::database::StorageLocator;
174 /// use storage_kit::database::query::{QueryScope, ScopedQuery};
175 /// let scope = QueryScope::new(StorageLocator::new("published"));
176 /// let query = ScopedQuery::text(StorageLocator::new("drafts")).with_scope(scope);
177 /// assert_eq!(query.scope.locator.collection, "published");
178 /// ```
179 #[must_use]
180 pub fn with_scope(mut self, scope: QueryScope) -> Self {
181 self.scope = scope;
182 self
183 }
184
185 /// Applies a filter.
186 ///
187 /// # Examples
188 ///
189 /// ```
190 /// use storage_kit::database::StorageLocator;
191 /// use storage_kit::database::data::FilterExpr;
192 /// use storage_kit::database::query::ScopedQuery;
193 /// let query = ScopedQuery::text(StorageLocator::new("documents"))
194 /// .filter(FilterExpr::eq("published", true));
195 /// assert!(query.scope.filter.is_some());
196 /// ```
197 #[must_use]
198 pub fn filter(mut self, filter: FilterExpr) -> Self {
199 self.scope = self.scope.filter(filter);
200 self
201 }
202
203 /// Replaces returned fields.
204 ///
205 /// # Examples
206 ///
207 /// ```
208 /// use storage_kit::database::StorageLocator;
209 /// use storage_kit::database::query::{Projection, ScopedQuery};
210 /// let query = ScopedQuery::vector(StorageLocator::new("documents"))
211 /// .projection(Projection::Columns(vec!["title".to_owned()]));
212 /// assert_eq!(query.scope.projection, Projection::Columns(vec!["title".to_owned()]));
213 /// ```
214 #[must_use]
215 pub fn projection(mut self, projection: Projection) -> Self {
216 self.scope = self.scope.projection(projection);
217 self
218 }
219
220 /// Mutates family options.
221 ///
222 /// # Examples
223 ///
224 /// ```
225 /// use storage_kit::database::StorageLocator;
226 /// use storage_kit::database::query::ScopedQuery;
227 /// let query = ScopedQuery::text(StorageLocator::new("documents"))
228 /// .map_options(|options| { options.set_field("body"); options.set_limit(5); });
229 /// assert_eq!(query.options.limit, Some(5));
230 /// ```
231 #[must_use]
232 pub fn map_options(mut self, update: impl FnOnce(&mut Options)) -> Self {
233 update(&mut self.options);
234 self
235 }
236}
237
238impl<Options> ApplyDefaultLocator for ScopedQuery<Options> {
239 fn apply_default_locator(mut self, default: &StorageLocator) -> Self {
240 self.scope = self.scope.apply_default_locator(default);
241 self
242 }
243}
244
245/// Vector search shaping and portable tuning.
246///
247/// Defaults request 10 hits and no offset or explicit distance/search breadth.
248/// Tuning support depends on the backend and query;
249/// a capability bit does not promise acceptance of every combination.
250///
251/// # Examples
252///
253/// ```
254/// use storage_kit::database::query::VectorSearchParams;
255/// let params = VectorSearchParams { limit: 5, ..Default::default() };
256/// assert_eq!(params.limit, 5);
257/// assert_eq!(params.offset, None);
258/// ```
259#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
260pub struct VectorSearchParams {
261 /// Maximum hits.
262 pub limit: usize,
263 /// Hit offset.
264 pub offset: Option<usize>,
265 /// Requested distance metric.
266 pub distance: Option<DistanceMetric>,
267 /// HNSW search breadth.
268 pub ef: Option<usize>,
269}
270
271impl Default for VectorSearchParams {
272 fn default() -> Self {
273 Self {
274 limit: 10,
275 offset: None,
276 distance: None,
277 ef: None,
278 }
279 }
280}
281
282/// Vector field and result options.
283///
284/// An absent field selects the configured backend default where supported.
285/// The default parameters come from [`VectorSearchParams::default`].
286///
287/// # Examples
288///
289/// ```
290/// use storage_kit::database::query::VectorQueryOptions;
291/// let mut options = VectorQueryOptions::default();
292/// options.set_field("embedding");
293/// options.set_limit(5);
294/// assert_eq!(options.vector_field.as_deref(), Some("embedding"));
295/// assert_eq!(options.params.limit, 5);
296/// ```
297#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
298pub struct VectorQueryOptions {
299 /// Vector field.
300 pub vector_field: Option<String>,
301 /// Search shaping.
302 pub params: VectorSearchParams,
303}
304
305impl VectorQueryOptions {
306 /// Selects a vector field.
307 ///
308 /// # Examples
309 ///
310 /// ```
311 /// use storage_kit::database::query::VectorQueryOptions;
312 /// let mut options = VectorQueryOptions::default();
313 /// options.set_field("embedding");
314 /// assert_eq!(options.vector_field.as_deref(), Some("embedding"));
315 /// ```
316 pub fn set_field(&mut self, field: impl Into<String>) {
317 self.vector_field = Some(field.into());
318 }
319
320 /// Sets the maximum hits.
321 ///
322 /// # Examples
323 ///
324 /// ```
325 /// use storage_kit::database::query::VectorQueryOptions;
326 /// let mut options = VectorQueryOptions::default();
327 /// options.set_limit(5);
328 /// assert_eq!(options.params.limit, 5);
329 /// ```
330 pub fn set_limit(&mut self, limit: usize) {
331 self.params.limit = limit;
332 }
333
334 /// Replaces search shaping.
335 ///
336 /// # Examples
337 ///
338 /// ```
339 /// use storage_kit::database::query::{VectorQueryOptions, VectorSearchParams};
340 /// let mut options = VectorQueryOptions::default();
341 /// options.set_params(VectorSearchParams { limit: 5, ..Default::default() });
342 /// assert_eq!(options.params.limit, 5);
343 /// ```
344 pub fn set_params(&mut self, params: VectorSearchParams) {
345 self.params = params;
346 }
347}
348
349impl ScopedQuery<VectorQueryOptions> {
350 /// Creates a vector query.
351 ///
352 /// # Examples
353 ///
354 /// ```
355 /// use storage_kit::database::StorageLocator;
356 /// use storage_kit::database::query::ScopedQuery;
357 /// let query = ScopedQuery::vector(StorageLocator::new("documents"));
358 /// assert_eq!(query.options.params.limit, 10);
359 /// assert!(query.options.vector_field.is_none());
360 /// ```
361 #[must_use]
362 pub fn vector(locator: StorageLocator) -> Self {
363 Self::new(locator, VectorQueryOptions::default())
364 }
365
366 /// Selects a vector field.
367 ///
368 /// # Examples
369 ///
370 /// ```
371 /// use storage_kit::database::StorageLocator;
372 /// use storage_kit::database::query::ScopedQuery;
373 /// let query = ScopedQuery::vector(StorageLocator::new("documents")).vector_field("embedding");
374 /// assert_eq!(query.options.vector_field.as_deref(), Some("embedding"));
375 /// ```
376 #[must_use]
377 pub fn vector_field(self, field: impl Into<String>) -> Self {
378 self.map_options(|options| options.set_field(field))
379 }
380
381 /// Sets the maximum hits.
382 ///
383 /// # Examples
384 ///
385 /// ```
386 /// use storage_kit::database::StorageLocator;
387 /// use storage_kit::database::query::ScopedQuery;
388 /// let query = ScopedQuery::vector(StorageLocator::new("documents")).limit(5);
389 /// assert_eq!(query.options.params.limit, 5);
390 /// ```
391 #[must_use]
392 pub fn limit(self, limit: usize) -> Self {
393 self.map_options(|options| options.set_limit(limit))
394 }
395
396 /// Replaces search shaping.
397 ///
398 /// # Examples
399 ///
400 /// ```
401 /// use storage_kit::database::StorageLocator;
402 /// use storage_kit::database::query::{ScopedQuery, VectorSearchParams};
403 /// let query = ScopedQuery::vector(StorageLocator::new("documents"))
404 /// .params(VectorSearchParams { limit: 5, offset: Some(2), ..Default::default() });
405 /// assert_eq!(query.options.params.offset, Some(2));
406 /// ```
407 #[must_use]
408 pub fn params(self, params: VectorSearchParams) -> Self {
409 self.map_options(|options| options.set_params(params))
410 }
411}
412
413/// Text field and result options.
414///
415/// The default leaves field, limit, and offset unset. Select a text field on
416/// the request when the backend requires one; configuration does not provide
417/// a universal text-field fallback.
418///
419/// # Examples
420///
421/// ```
422/// use storage_kit::database::query::TextQueryOptions;
423/// let mut options = TextQueryOptions::default();
424/// options.set_field("body");
425/// options.set_limit(5);
426/// assert_eq!(options.text_field.as_deref(), Some("body"));
427/// assert_eq!(options.limit, Some(5));
428/// ```
429#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
430pub struct TextQueryOptions {
431 /// Text field.
432 pub text_field: Option<String>,
433 /// Maximum hits.
434 pub limit: Option<usize>,
435 /// Hit offset.
436 pub offset: Option<usize>,
437}
438
439impl TextQueryOptions {
440 /// Selects a text field.
441 ///
442 /// # Examples
443 ///
444 /// ```
445 /// use storage_kit::database::query::TextQueryOptions;
446 /// let mut options = TextQueryOptions::default();
447 /// options.set_field("body");
448 /// assert_eq!(options.text_field.as_deref(), Some("body"));
449 /// ```
450 pub fn set_field(&mut self, field: impl Into<String>) {
451 self.text_field = Some(field.into());
452 }
453
454 /// Sets maximum hits.
455 ///
456 /// # Examples
457 ///
458 /// ```
459 /// use storage_kit::database::query::TextQueryOptions;
460 /// let mut options = TextQueryOptions::default();
461 /// options.set_limit(5);
462 /// assert_eq!(options.limit, Some(5));
463 /// ```
464 pub fn set_limit(&mut self, limit: usize) {
465 self.limit = Some(limit);
466 }
467
468 /// Sets hit offset.
469 ///
470 /// # Examples
471 ///
472 /// ```
473 /// use storage_kit::database::query::TextQueryOptions;
474 /// let mut options = TextQueryOptions::default();
475 /// options.set_offset(10);
476 /// assert_eq!(options.offset, Some(10));
477 /// ```
478 pub fn set_offset(&mut self, offset: usize) {
479 self.offset = Some(offset);
480 }
481}
482
483impl ScopedQuery<TextQueryOptions> {
484 /// Creates a text query.
485 ///
486 /// # Examples
487 ///
488 /// ```
489 /// use storage_kit::database::StorageLocator;
490 /// use storage_kit::database::query::ScopedQuery;
491 /// let query = ScopedQuery::text(StorageLocator::new("documents"));
492 /// assert!(query.options.text_field.is_none());
493 /// assert!(query.options.limit.is_none());
494 /// ```
495 #[must_use]
496 pub fn text(locator: StorageLocator) -> Self {
497 Self::new(locator, TextQueryOptions::default())
498 }
499
500 /// Selects a text field.
501 ///
502 /// # Examples
503 ///
504 /// ```
505 /// use storage_kit::database::StorageLocator;
506 /// use storage_kit::database::query::ScopedQuery;
507 /// let query = ScopedQuery::text(StorageLocator::new("documents")).text_field("body");
508 /// assert_eq!(query.options.text_field.as_deref(), Some("body"));
509 /// ```
510 #[must_use]
511 pub fn text_field(self, field: impl Into<String>) -> Self {
512 self.map_options(|options| options.set_field(field))
513 }
514
515 /// Sets maximum hits.
516 ///
517 /// # Examples
518 ///
519 /// ```
520 /// use storage_kit::database::StorageLocator;
521 /// use storage_kit::database::query::ScopedQuery;
522 /// let query = ScopedQuery::text(StorageLocator::new("documents")).limit(5);
523 /// assert_eq!(query.options.limit, Some(5));
524 /// ```
525 #[must_use]
526 pub fn limit(self, limit: usize) -> Self {
527 self.map_options(|options| options.set_limit(limit))
528 }
529
530 /// Sets hit offset.
531 ///
532 /// # Examples
533 ///
534 /// ```
535 /// use storage_kit::database::StorageLocator;
536 /// use storage_kit::database::query::ScopedQuery;
537 /// let query = ScopedQuery::text(StorageLocator::new("documents")).offset(10);
538 /// assert_eq!(query.options.offset, Some(10));
539 /// ```
540 #[must_use]
541 pub fn offset(self, offset: usize) -> Self {
542 self.map_options(|options| options.set_offset(offset))
543 }
544}