Skip to main content

kevy_embedded/
ops_index_opts.rs

1//! The optional clauses an embedded index query carries — the in-process
2//! twin of the wire's `FILTER` / `SORT` / `DISTINCT` / `FACET` / `OFFSET`
3//! (and, for text MATCH, `HIGHLIGHT` / `TYPO` / `IN`). A `#[path]` child
4//! of `ops_index.rs`, split out for the 500-LOC house rule.
5
6use kevy_index::SortOrder;
7
8/// One `FILTER` predicate: which stored value field it reads, and the
9/// test on it — the wire's `RANGE` / `EQ` shapes, in-process.
10///
11/// The bounds are raw bytes and are coerced with the type the field was
12/// DECLARED as, so a numeric range compares numerically rather than
13/// lexicographically.
14///
15/// ```
16/// use kevy_embedded::ValueFilter;
17///
18/// let cheap = ValueFilter::Range { field: b"price", min: b"0", max: b"10" };
19/// assert_ne!(cheap, ValueFilter::Eq { field: b"price", value: b"5" });
20/// ```
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[non_exhaustive]
23pub enum ValueFilter<'a> {
24    /// `field` between `min` and `max`, both inclusive.
25    ///
26    /// ```
27    /// # use kevy_embedded::*;
28    /// # let s = Store::open(Config::default())?;
29    /// # let st = [(&b"status"[..], IndexValType::Str), (b"total", IndexValType::I64)];
30    /// # s.idx_create_with_values(b"by_age", b"u:", b"age", IndexValType::I64, IndexKind::Range, &st)?;
31    /// # let rows = [(&b"u:1"[..], &b"30"[..], &b"paid"[..], &b"5"[..]), (b"u:2", b"40", b"paid", b"20"), (b"u:3", b"50", b"due", b"8")];
32    /// # for (k, age, status, total) in rows {
33    /// #     s.hset(k, &[(b"age", age), (b"status", status), (b"total", total)])?;
34    /// # }
35    /// # let (lo, hi) = (IndexValue::I64(0), IndexValue::I64(99));
36    /// # let keys = |opts| -> KevyResult<Vec<Vec<u8>>> {
37    /// #     Ok(s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?.rows.into_iter().map(|r| r.0).collect())
38    /// # };
39    /// // u:1 (age 30, paid, total 5), u:2 (40, paid, 20), u:3 (50, due, 8)
40    /// let f = [ValueFilter::Range { field: b"total", min: b"5", max: b"10" }];
41    /// assert_eq!(keys(ScalarQueryOpts::default().with_filters(&f))?, [b"u:1".to_vec(), b"u:3".to_vec()]);
42    /// # Ok::<(), kevy_embedded::KevyError>(())
43    /// ```
44    Range {
45        /// The declared value field to read.
46        ///
47        /// ```
48        /// # use kevy_embedded::*;
49        /// # let s = Store::open(Config::default())?;
50        /// # let st = [(&b"status"[..], IndexValType::Str), (b"total", IndexValType::I64)];
51        /// # s.idx_create_with_values(b"by_age", b"u:", b"age", IndexValType::I64, IndexKind::Range, &st)?;
52        /// # let rows = [(&b"u:1"[..], &b"30"[..], &b"paid"[..], &b"5"[..]), (b"u:2", b"40", b"paid", b"20"), (b"u:3", b"50", b"due", b"8")];
53        /// # for (k, age, status, total) in rows {
54        /// #     s.hset(k, &[(b"age", age), (b"status", status), (b"total", total)])?;
55        /// # }
56        /// # let (lo, hi) = (IndexValue::I64(0), IndexValue::I64(99));
57        /// # let keys = |opts| -> KevyResult<Vec<Vec<u8>>> {
58        /// #     Ok(s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?.rows.into_iter().map(|r| r.0).collect())
59        /// # };
60        /// // u:1 (age 30, paid, total 5), u:2 (40, paid, 20), u:3 (50, due, 8)
61        /// // `total` is declared I64, so the bounds compare as numbers, not bytes
62        /// let f = [ValueFilter::Range { field: b"total", min: b"9", max: b"100" }];
63        /// assert_eq!(keys(ScalarQueryOpts::default().with_filters(&f))?, [b"u:2".to_vec()]);
64        /// # Ok::<(), kevy_embedded::KevyError>(())
65        /// ```
66        field: &'a [u8],
67        /// Lower bound, inclusive.
68        ///
69        /// ```
70        /// # use kevy_embedded::*;
71        /// # let s = Store::open(Config::default())?;
72        /// # let st = [(&b"status"[..], IndexValType::Str), (b"total", IndexValType::I64)];
73        /// # s.idx_create_with_values(b"by_age", b"u:", b"age", IndexValType::I64, IndexKind::Range, &st)?;
74        /// # let rows = [(&b"u:1"[..], &b"30"[..], &b"paid"[..], &b"5"[..]), (b"u:2", b"40", b"paid", b"20"), (b"u:3", b"50", b"due", b"8")];
75        /// # for (k, age, status, total) in rows {
76        /// #     s.hset(k, &[(b"age", age), (b"status", status), (b"total", total)])?;
77        /// # }
78        /// # let (lo, hi) = (IndexValue::I64(0), IndexValue::I64(99));
79        /// # let keys = |opts| -> KevyResult<Vec<Vec<u8>>> {
80        /// #     Ok(s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?.rows.into_iter().map(|r| r.0).collect())
81        /// # };
82        /// // u:1 (age 30, paid, total 5), u:2 (40, paid, 20), u:3 (50, due, 8)
83        /// let f = [ValueFilter::Range { field: b"total", min: b"8", max: b"99" }];
84        /// assert_eq!(keys(ScalarQueryOpts::default().with_filters(&f))?.len(), 2, "8 itself is in");
85        /// # Ok::<(), kevy_embedded::KevyError>(())
86        /// ```
87        min: &'a [u8],
88        /// Upper bound, inclusive.
89        ///
90        /// ```
91        /// # use kevy_embedded::*;
92        /// # let s = Store::open(Config::default())?;
93        /// # let st = [(&b"status"[..], IndexValType::Str), (b"total", IndexValType::I64)];
94        /// # s.idx_create_with_values(b"by_age", b"u:", b"age", IndexValType::I64, IndexKind::Range, &st)?;
95        /// # let rows = [(&b"u:1"[..], &b"30"[..], &b"paid"[..], &b"5"[..]), (b"u:2", b"40", b"paid", b"20"), (b"u:3", b"50", b"due", b"8")];
96        /// # for (k, age, status, total) in rows {
97        /// #     s.hset(k, &[(b"age", age), (b"status", status), (b"total", total)])?;
98        /// # }
99        /// # let (lo, hi) = (IndexValue::I64(0), IndexValue::I64(99));
100        /// # let keys = |opts| -> KevyResult<Vec<Vec<u8>>> {
101        /// #     Ok(s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?.rows.into_iter().map(|r| r.0).collect())
102        /// # };
103        /// // u:1 (age 30, paid, total 5), u:2 (40, paid, 20), u:3 (50, due, 8)
104        /// let f = [ValueFilter::Range { field: b"total", min: b"0", max: b"8" }];
105        /// assert_eq!(keys(ScalarQueryOpts::default().with_filters(&f))?.len(), 2, "8 itself is in");
106        /// # Ok::<(), kevy_embedded::KevyError>(())
107        /// ```
108        max: &'a [u8],
109    },
110    /// `field` exactly `value`.
111    ///
112    /// ```
113    /// # use kevy_embedded::*;
114    /// # let s = Store::open(Config::default())?;
115    /// # let st = [(&b"status"[..], IndexValType::Str), (b"total", IndexValType::I64)];
116    /// # s.idx_create_with_values(b"by_age", b"u:", b"age", IndexValType::I64, IndexKind::Range, &st)?;
117    /// # let rows = [(&b"u:1"[..], &b"30"[..], &b"paid"[..], &b"5"[..]), (b"u:2", b"40", b"paid", b"20"), (b"u:3", b"50", b"due", b"8")];
118    /// # for (k, age, status, total) in rows {
119    /// #     s.hset(k, &[(b"age", age), (b"status", status), (b"total", total)])?;
120    /// # }
121    /// # let (lo, hi) = (IndexValue::I64(0), IndexValue::I64(99));
122    /// # let keys = |opts| -> KevyResult<Vec<Vec<u8>>> {
123    /// #     Ok(s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?.rows.into_iter().map(|r| r.0).collect())
124    /// # };
125    /// // u:1 (age 30, paid, total 5), u:2 (40, paid, 20), u:3 (50, due, 8)
126    /// let f = [ValueFilter::Eq { field: b"status", value: b"due" }];
127    /// assert_eq!(keys(ScalarQueryOpts::default().with_filters(&f))?, [b"u:3".to_vec()]);
128    /// # Ok::<(), kevy_embedded::KevyError>(())
129    /// ```
130    Eq {
131        /// The declared value field to read.
132        ///
133        /// ```
134        /// # use kevy_embedded::*;
135        /// # let s = Store::open(Config::default())?;
136        /// # let st = [(&b"status"[..], IndexValType::Str), (b"total", IndexValType::I64)];
137        /// # s.idx_create_with_values(b"by_age", b"u:", b"age", IndexValType::I64, IndexKind::Range, &st)?;
138        /// # let rows = [(&b"u:1"[..], &b"30"[..], &b"paid"[..], &b"5"[..]), (b"u:2", b"40", b"paid", b"20"), (b"u:3", b"50", b"due", b"8")];
139        /// # for (k, age, status, total) in rows {
140        /// #     s.hset(k, &[(b"age", age), (b"status", status), (b"total", total)])?;
141        /// # }
142        /// # let (lo, hi) = (IndexValue::I64(0), IndexValue::I64(99));
143        /// # let keys = |opts| -> KevyResult<Vec<Vec<u8>>> {
144        /// #     Ok(s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?.rows.into_iter().map(|r| r.0).collect())
145        /// # };
146        /// // u:1 (age 30, paid, total 5), u:2 (40, paid, 20), u:3 (50, due, 8)
147        /// // a field the index does not store is refused, not read as empty
148        /// let f = [ValueFilter::Eq { field: b"email", value: b"x" }];
149        /// assert!(keys(ScalarQueryOpts::default().with_filters(&f)).is_err());
150        /// # Ok::<(), kevy_embedded::KevyError>(())
151        /// ```
152        field: &'a [u8],
153        /// The value to match.
154        ///
155        /// ```
156        /// # use kevy_embedded::*;
157        /// # let s = Store::open(Config::default())?;
158        /// # let st = [(&b"status"[..], IndexValType::Str), (b"total", IndexValType::I64)];
159        /// # s.idx_create_with_values(b"by_age", b"u:", b"age", IndexValType::I64, IndexKind::Range, &st)?;
160        /// # let rows = [(&b"u:1"[..], &b"30"[..], &b"paid"[..], &b"5"[..]), (b"u:2", b"40", b"paid", b"20"), (b"u:3", b"50", b"due", b"8")];
161        /// # for (k, age, status, total) in rows {
162        /// #     s.hset(k, &[(b"age", age), (b"status", status), (b"total", total)])?;
163        /// # }
164        /// # let (lo, hi) = (IndexValue::I64(0), IndexValue::I64(99));
165        /// # let keys = |opts| -> KevyResult<Vec<Vec<u8>>> {
166        /// #     Ok(s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?.rows.into_iter().map(|r| r.0).collect())
167        /// # };
168        /// // u:1 (age 30, paid, total 5), u:2 (40, paid, 20), u:3 (50, due, 8)
169        /// let f = [ValueFilter::Eq { field: b"total", value: b"20" }];
170        /// assert_eq!(keys(ScalarQueryOpts::default().with_filters(&f))?, [b"u:2".to_vec()]);
171        /// # Ok::<(), kevy_embedded::KevyError>(())
172        /// ```
173        value: &'a [u8],
174    },
175}
176
177impl ValueFilter<'_> {
178    pub(crate) fn field(&self) -> &[u8] {
179        match self {
180            ValueFilter::Range { field, .. } | ValueFilter::Eq { field, .. } => field,
181        }
182    }
183}
184
185/// Everything a scalar RANGE/EQ query carries beyond its bounds and
186/// limit — the embedded twin of the wire's optional clauses.
187/// [`ScalarQueryOpts::default`] is the plain query; the `with_*` methods
188/// name the clauses a caller wants.
189///
190/// ```
191/// use kevy_embedded::{ScalarQueryOpts, SortOrder, ValueFilter};
192///
193/// let filters = [ValueFilter::Eq { field: b"status", value: b"paid" }];
194/// let opts = ScalarQueryOpts::default()
195///     .with_filters(&filters)
196///     .with_sort(b"total", SortOrder::Desc)
197///     .with_offset(20);
198/// assert_eq!(opts.sort, Some((&b"total"[..], SortOrder::Desc)));
199/// ```
200#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
201#[non_exhaustive]
202pub struct ScalarQueryOpts<'a> {
203    /// `FILTER …`: non-scoring predicates over stored values, ANDed. A
204    /// row without the stored value fails — absent is not a value.
205    ///
206    /// ```
207    /// # use kevy_embedded::*;
208    /// # let s = Store::open(Config::default())?;
209    /// # let st = [(&b"status"[..], IndexValType::Str), (b"total", IndexValType::I64)];
210    /// # s.idx_create_with_values(b"by_age", b"u:", b"age", IndexValType::I64, IndexKind::Range, &st)?;
211    /// # let rows = [(&b"u:1"[..], &b"30"[..], &b"paid"[..], &b"5"[..]), (b"u:2", b"40", b"paid", b"20"), (b"u:3", b"50", b"due", b"8")];
212    /// # for (k, age, status, total) in rows {
213    /// #     s.hset(k, &[(b"age", age), (b"status", status), (b"total", total)])?;
214    /// # }
215    /// # let (lo, hi) = (IndexValue::I64(0), IndexValue::I64(99));
216    /// # let keys = |opts| -> KevyResult<Vec<Vec<u8>>> {
217    /// #     Ok(s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?.rows.into_iter().map(|r| r.0).collect())
218    /// # };
219    /// // u:1 (age 30, paid, total 5), u:2 (40, paid, 20), u:3 (50, due, 8)
220    /// let paid = ValueFilter::Eq { field: b"status", value: b"paid" };
221    /// let big = ValueFilter::Range { field: b"total", min: b"10", max: b"99" };
222    /// let both = [paid, big];
223    /// let opts = ScalarQueryOpts::default().with_filters(&both);
224    /// assert_eq!(keys(opts)?, [b"u:2".to_vec()], "both predicates hold");
225    /// # Ok::<(), kevy_embedded::KevyError>(())
226    /// ```
227    pub filters: &'a [ValueFilter<'a>],
228    /// `SORT <field> ASC|DESC`: order the page by a stored value; a row
229    /// with no usable value sorts last in both directions.
230    ///
231    /// ```
232    /// # use kevy_embedded::*;
233    /// # let s = Store::open(Config::default())?;
234    /// # let st = [(&b"status"[..], IndexValType::Str), (b"total", IndexValType::I64)];
235    /// # s.idx_create_with_values(b"by_age", b"u:", b"age", IndexValType::I64, IndexKind::Range, &st)?;
236    /// # let rows = [(&b"u:1"[..], &b"30"[..], &b"paid"[..], &b"5"[..]), (b"u:2", b"40", b"paid", b"20"), (b"u:3", b"50", b"due", b"8")];
237    /// # for (k, age, status, total) in rows {
238    /// #     s.hset(k, &[(b"age", age), (b"status", status), (b"total", total)])?;
239    /// # }
240    /// # let (lo, hi) = (IndexValue::I64(0), IndexValue::I64(99));
241    /// # let keys = |opts| -> KevyResult<Vec<Vec<u8>>> {
242    /// #     Ok(s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?.rows.into_iter().map(|r| r.0).collect())
243    /// # };
244    /// // u:1 (age 30, paid, total 5), u:2 (40, paid, 20), u:3 (50, due, 8)
245    /// let opts = ScalarQueryOpts::default().with_sort(b"total", SortOrder::Desc);
246    /// assert_eq!(keys(opts)?, [b"u:2".to_vec(), b"u:3".to_vec(), b"u:1".to_vec()]);
247    /// # Ok::<(), kevy_embedded::KevyError>(())
248    /// ```
249    pub sort: Option<(&'a [u8], SortOrder)>,
250    /// `DISTINCT <field>`: at most one row per coerced value; a row
251    /// with no value is its own group.
252    ///
253    /// ```
254    /// # use kevy_embedded::*;
255    /// # let s = Store::open(Config::default())?;
256    /// # let st = [(&b"status"[..], IndexValType::Str), (b"total", IndexValType::I64)];
257    /// # s.idx_create_with_values(b"by_age", b"u:", b"age", IndexValType::I64, IndexKind::Range, &st)?;
258    /// # let rows = [(&b"u:1"[..], &b"30"[..], &b"paid"[..], &b"5"[..]), (b"u:2", b"40", b"paid", b"20"), (b"u:3", b"50", b"due", b"8")];
259    /// # for (k, age, status, total) in rows {
260    /// #     s.hset(k, &[(b"age", age), (b"status", status), (b"total", total)])?;
261    /// # }
262    /// # let (lo, hi) = (IndexValue::I64(0), IndexValue::I64(99));
263    /// # let keys = |opts| -> KevyResult<Vec<Vec<u8>>> {
264    /// #     Ok(s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?.rows.into_iter().map(|r| r.0).collect())
265    /// # };
266    /// // u:1 (age 30, paid, total 5), u:2 (40, paid, 20), u:3 (50, due, 8)
267    /// let opts = ScalarQueryOpts::default().with_distinct(b"status");
268    /// assert_eq!(keys(opts)?.len(), 2, "one paid row, one due row");
269    /// # Ok::<(), kevy_embedded::KevyError>(())
270    /// ```
271    pub distinct: Option<&'a [u8]>,
272    /// `FACET <field…>`: count each field's values over the whole match
273    /// set (FILTER reduces the counts; DISTINCT does not).
274    ///
275    /// ```
276    /// # use kevy_embedded::*;
277    /// # let s = Store::open(Config::default())?;
278    /// # let st = [(&b"status"[..], IndexValType::Str), (b"total", IndexValType::I64)];
279    /// # s.idx_create_with_values(b"by_age", b"u:", b"age", IndexValType::I64, IndexKind::Range, &st)?;
280    /// # let rows = [(&b"u:1"[..], &b"30"[..], &b"paid"[..], &b"5"[..]), (b"u:2", b"40", b"paid", b"20"), (b"u:3", b"50", b"due", b"8")];
281    /// # for (k, age, status, total) in rows {
282    /// #     s.hset(k, &[(b"age", age), (b"status", status), (b"total", total)])?;
283    /// # }
284    /// # let (lo, hi) = (IndexValue::I64(0), IndexValue::I64(99));
285    /// # let keys = |opts| -> KevyResult<Vec<Vec<u8>>> {
286    /// #     Ok(s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?.rows.into_iter().map(|r| r.0).collect())
287    /// # };
288    /// // u:1 (age 30, paid, total 5), u:2 (40, paid, 20), u:3 (50, due, 8)
289    /// let fields = [b"status".to_vec()];
290    /// let opts = ScalarQueryOpts::default().with_facets(&fields);
291    /// let page = s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?;
292    /// assert_eq!(page.facets[0], [(b"paid".to_vec(), 2), (b"due".to_vec(), 1)]);
293    /// # Ok::<(), kevy_embedded::KevyError>(())
294    /// ```
295    pub facets: &'a [Vec<u8>],
296    /// `OFFSET n`: rows to skip before `limit` takes effect.
297    ///
298    /// ```
299    /// # use kevy_embedded::*;
300    /// # let s = Store::open(Config::default())?;
301    /// # let st = [(&b"status"[..], IndexValType::Str), (b"total", IndexValType::I64)];
302    /// # s.idx_create_with_values(b"by_age", b"u:", b"age", IndexValType::I64, IndexKind::Range, &st)?;
303    /// # let rows = [(&b"u:1"[..], &b"30"[..], &b"paid"[..], &b"5"[..]), (b"u:2", b"40", b"paid", b"20"), (b"u:3", b"50", b"due", b"8")];
304    /// # for (k, age, status, total) in rows {
305    /// #     s.hset(k, &[(b"age", age), (b"status", status), (b"total", total)])?;
306    /// # }
307    /// # let (lo, hi) = (IndexValue::I64(0), IndexValue::I64(99));
308    /// # let keys = |opts| -> KevyResult<Vec<Vec<u8>>> {
309    /// #     Ok(s.idx_query_claused(b"by_age", &lo, &hi, None, 10, opts)?.rows.into_iter().map(|r| r.0).collect())
310    /// # };
311    /// // u:1 (age 30, paid, total 5), u:2 (40, paid, 20), u:3 (50, due, 8)
312    /// let opts = ScalarQueryOpts::default().with_offset(2);
313    /// assert_eq!(keys(opts)?, [b"u:3".to_vec()]);
314    /// # Ok::<(), kevy_embedded::KevyError>(())
315    /// ```
316    pub offset: usize,
317}
318
319impl<'a> ScalarQueryOpts<'a> {
320    /// Set [`Self::filters`].
321    ///
322    /// ```
323    /// let f = [kevy_embedded::ValueFilter::Eq { field: b"k", value: b"v" }];
324    /// assert_eq!(kevy_embedded::ScalarQueryOpts::default().with_filters(&f).filters.len(), 1);
325    /// ```
326    #[inline]
327    #[must_use]
328    pub fn with_filters(mut self, filters: &'a [ValueFilter<'a>]) -> Self {
329        self.filters = filters;
330        self
331    }
332
333    /// Set [`Self::sort`]: order by `field`, in `order`.
334    ///
335    /// ```
336    /// use kevy_embedded::{ScalarQueryOpts, SortOrder};
337    /// let o = ScalarQueryOpts::default().with_sort(b"ts", SortOrder::Asc);
338    /// assert!(o.sort.is_some());
339    /// ```
340    #[inline]
341    #[must_use]
342    pub fn with_sort(mut self, field: &'a [u8], order: SortOrder) -> Self {
343        self.sort = Some((field, order));
344        self
345    }
346
347    /// Set [`Self::distinct`].
348    ///
349    /// ```
350    /// let o = kevy_embedded::ScalarQueryOpts::default().with_distinct(b"user");
351    /// assert_eq!(o.distinct, Some(&b"user"[..]));
352    /// ```
353    #[inline]
354    #[must_use]
355    pub fn with_distinct(mut self, field: &'a [u8]) -> Self {
356        self.distinct = Some(field);
357        self
358    }
359
360    /// Set [`Self::facets`].
361    ///
362    /// ```
363    /// let fields = [b"status".to_vec()];
364    /// let o = kevy_embedded::ScalarQueryOpts::default().with_facets(&fields);
365    /// assert_eq!(o.facets.len(), 1);
366    /// ```
367    #[inline]
368    #[must_use]
369    pub fn with_facets(mut self, facets: &'a [Vec<u8>]) -> Self {
370        self.facets = facets;
371        self
372    }
373
374    /// Set [`Self::offset`].
375    ///
376    /// ```
377    /// assert_eq!(kevy_embedded::ScalarQueryOpts::default().with_offset(5).offset, 5);
378    /// ```
379    #[inline]
380    #[must_use]
381    pub fn with_offset(mut self, offset: usize) -> Self {
382        self.offset = offset;
383        self
384    }
385
386    /// Whether any clause reshapes the selection (the cursor-refusing
387    /// set — `FILTER` alone pages fine).
388    pub(crate) fn selects(&self) -> bool {
389        self.sort.is_some() || self.distinct.is_some() || !self.facets.is_empty() || self.offset > 0
390    }
391}
392
393#[cfg(feature = "text")]
394pub use super::match_opts::MatchOpts;