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;