Skip to main content

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}