kevy_index/view.rs
1//! Views: named composition trees over declared indexes.
2//!
3//! Pure logic: [`ViewSpec`] (the declaration), [`eval_tree`] (the
4//! virtual-mode evaluator over segment closures), and
5//! [`MaterializedSet`] (the incremental ordered result set with the
6//! bounded top-K discipline). The runtime supplies segment access and
7//! wires maintenance to its write hook — nothing here does I/O.
8//!
9//! Locked structural rules: components are NAMED indexes (leaves carry
10//! a shape; the view layer holds no predicates of its own); a view
11//! stores MEMBERSHIP + ORDER only (never field values); AND/OR
12//! subtrees may be re-ordered by the engine (DIFF is fixed
13//! left-right).
14
15use crate::segment::Segment;
16use crate::value::IndexValue;
17
18use kevy_text::SortOrder;
19
20pub use crate::view_materialized::{MaterializedSet, Membership};
21pub use crate::view_mode::ViewMode;
22pub use crate::view_sidecar::{MAX_VIEWS, ViewCatalog};
23
24/// One leaf: a declared index + the shape it contributes.
25///
26/// ```
27/// use kevy_index::{IndexValue, Leaf};
28/// let adults = Leaf::new("age", IndexValue::I64(18), IndexValue::I64(i64::MAX));
29/// assert_eq!(adults.index, b"age");
30/// ```
31#[derive(Debug, Clone, PartialEq)]
32#[non_exhaustive]
33pub struct Leaf {
34 /// Index name (resolved by the runtime).
35 ///
36 /// ```
37 /// use kevy_index::{IndexValue, Leaf};
38 /// let leaf = Leaf::new("city", IndexValue::Str(b"a".to_vec()), IndexValue::Str(b"m".to_vec()));
39 /// assert_eq!(leaf.index, b"city");
40 /// ```
41 pub index: Vec<u8>,
42 /// Inclusive bounds (EQ = same min/max), already coerced to the
43 /// index's type by the runtime at CREATE time.
44 ///
45 /// ```
46 /// use kevy_index::{IndexValue, Leaf, Tree};
47 /// let leaf = Leaf::new("age", IndexValue::I64(18), IndexValue::I64(65));
48 /// assert_eq!(leaf.min, IndexValue::I64(18));
49 /// let t = Tree::Leaf(leaf);
50 /// assert!(t.contains_values(&|_: &[u8]| Some(IndexValue::I64(18))), "min is inclusive");
51 /// assert!(!t.contains_values(&|_: &[u8]| Some(IndexValue::I64(17))));
52 /// ```
53 pub min: IndexValue,
54 /// Upper bound.
55 ///
56 /// ```
57 /// use kevy_index::{IndexValue, Leaf, Tree};
58 /// let leaf = Leaf::new("age", IndexValue::I64(18), IndexValue::I64(65));
59 /// assert_eq!(leaf.max, IndexValue::I64(65));
60 /// let t = Tree::Leaf(leaf);
61 /// assert!(t.contains_values(&|_: &[u8]| Some(IndexValue::I64(65))), "max is inclusive");
62 /// assert!(!t.contains_values(&|_: &[u8]| Some(IndexValue::I64(66))));
63 /// ```
64 pub max: IndexValue,
65}
66
67impl Leaf {
68 /// Rows of index `index` whose value lies in `min..=max` (EQ is
69 /// `min == max`), bounds already coerced to the index's type.
70 ///
71 /// ```
72 /// use kevy_index::{IndexValue, Leaf};
73 /// let eq = Leaf::new("city", IndexValue::Str(b"kyoto".to_vec()), IndexValue::Str(b"kyoto".to_vec()));
74 /// assert_eq!(eq.min, eq.max);
75 /// ```
76 pub fn new(index: impl Into<Vec<u8>>, min: IndexValue, max: IndexValue) -> Leaf {
77 Leaf { index: index.into(), min, max }
78 }
79}
80
81/// The composition tree. Depth ≤ 3, leaves ≤ 4 (declarative caps,
82/// enforced at CREATE).
83///
84/// ```
85/// use kevy_index::{IndexValue, Leaf, Segment, Tree};
86///
87/// let mut age = Segment::new();
88/// age.set_key_dir(true);
89/// age.apply(b"u:1", None, Some(IndexValue::I64(30)));
90/// age.apply(b"u:2", None, Some(IndexValue::I64(70)));
91/// let mut city = Segment::new();
92/// city.set_key_dir(true);
93/// city.apply(b"u:1", None, Some(IndexValue::Str(b"kyoto".to_vec())));
94/// city.apply(b"u:2", None, Some(IndexValue::Str(b"kyoto".to_vec())));
95///
96/// let kyoto = IndexValue::Str(b"kyoto".to_vec());
97/// let t = Tree::And(
98/// Box::new(Tree::Leaf(Leaf::new("city", kyoto.clone(), kyoto))),
99/// Box::new(Tree::Leaf(Leaf::new("age", IndexValue::I64(18), IndexValue::I64(65)))),
100/// );
101/// let seg = |name: &[u8]| match name {
102/// b"age" => Some(&age),
103/// b"city" => Some(&city),
104/// _ => None,
105/// };
106/// assert_eq!(t.eval(&seg), vec![b"u:1".to_vec()]);
107/// assert!(t.contains(b"u:1", &seg) && !t.contains(b"u:2", &seg));
108/// assert_eq!((t.leaves(), t.depth()), (2, 2));
109///
110/// let mut names = Vec::new();
111/// t.each_leaf(&mut |l: &Leaf| names.push(l.index.clone()));
112/// assert_eq!(names, [b"city".to_vec(), b"age".to_vec()]);
113/// ```
114#[derive(Debug, Clone, PartialEq)]
115pub enum Tree {
116 /// A single index shape.
117 ///
118 /// ```
119 /// use kevy_index::{IndexValue, Leaf, Tree};
120 /// let t = Tree::Leaf(Leaf::new("age", IndexValue::I64(0), IndexValue::I64(9)));
121 /// assert_eq!((t.leaves(), t.depth()), (1, 1));
122 /// ```
123 Leaf(Leaf),
124 /// Intersection.
125 ///
126 /// ```
127 /// use kevy_index::{IndexValue, Leaf, Tree};
128 /// let leaf = |lo, hi| Box::new(Tree::Leaf(Leaf::new("n", IndexValue::I64(lo), IndexValue::I64(hi))));
129 /// let t = Tree::And(leaf(0, 10), leaf(5, 20));
130 /// assert!(t.contains_values(&|_: &[u8]| Some(IndexValue::I64(7))));
131 /// assert!(!t.contains_values(&|_: &[u8]| Some(IndexValue::I64(3))));
132 /// ```
133 And(Box<Tree>, Box<Tree>),
134 /// Union.
135 ///
136 /// ```
137 /// use kevy_index::{IndexValue, Leaf, Tree};
138 /// let leaf = |lo, hi| Box::new(Tree::Leaf(Leaf::new("n", IndexValue::I64(lo), IndexValue::I64(hi))));
139 /// let t = Tree::Or(leaf(0, 1), leaf(8, 9));
140 /// assert!(t.contains_values(&|_: &[u8]| Some(IndexValue::I64(9))));
141 /// assert!(!t.contains_values(&|_: &[u8]| Some(IndexValue::I64(5))));
142 /// ```
143 Or(Box<Tree>, Box<Tree>),
144 /// Left minus right (NOT commutative — order is fixed).
145 ///
146 /// ```
147 /// use kevy_index::{IndexValue, Leaf, Tree};
148 /// let leaf = |lo, hi| Box::new(Tree::Leaf(Leaf::new("n", IndexValue::I64(lo), IndexValue::I64(hi))));
149 /// let t = Tree::Diff(leaf(0, 10), leaf(5, 10));
150 /// assert!(t.contains_values(&|_: &[u8]| Some(IndexValue::I64(4))));
151 /// assert!(!t.contains_values(&|_: &[u8]| Some(IndexValue::I64(6))));
152 /// ```
153 Diff(Box<Tree>, Box<Tree>),
154}
155
156impl Tree {
157 /// Evaluate `tree` against one shard's segments: `seg` resolves an
158 /// index name to its [`Segment`] (None = unknown index → empty leaf —
159 /// the runtime validates names at CREATE, so this is defensive).
160 /// Returns the member keys (unordered set semantics).
161 ///
162 /// ```
163 /// use kevy_index::{IndexValue, Leaf, Segment, Tree};
164 /// let mut age = Segment::new();
165 /// age.set_key_dir(true);
166 /// age.apply(b"u:1", None, Some(IndexValue::I64(30)));
167 /// age.apply(b"u:2", None, Some(IndexValue::I64(70)));
168 /// let t = Tree::Leaf(Leaf::new("age", IndexValue::I64(18), IndexValue::I64(65)));
169 /// let seg = |name: &[u8]| (name == b"age").then_some(&age);
170 /// assert_eq!(t.eval(&seg), vec![b"u:1".to_vec()]);
171 /// assert!(t.contains(b"u:1", &seg) && !t.contains(b"u:2", &seg));
172 /// ```
173 pub fn eval<'a>(&self, seg: &impl Fn(&[u8]) -> Option<&'a Segment>) -> Vec<Vec<u8>> {
174 eval_tree(self, seg)
175 }
176
177 /// Re-evaluate ONE key's membership (the materialized write hook):
178 /// every leaf is a point probe in the segment's key directory
179 /// ([`Segment::set_key_dir`]); a segment without one holds no key.
180 pub fn contains<'a>(&self, key: &[u8], seg: &impl Fn(&[u8]) -> Option<&'a Segment>) -> bool {
181 key_in_tree(self, key, seg)
182 }
183
184 /// [`Tree::contains`] variant over PRE-FETCHED per-index values — the
185 /// write hook probes each referenced index ONCE per key and evaluates
186 /// every view against the same small table (bounds compares only; no
187 /// per-view re-hashing).
188 ///
189 /// ```
190 /// use kevy_index::{IndexValue, Leaf, Tree};
191 /// let t = Tree::Leaf(Leaf::new("age", IndexValue::I64(18), IndexValue::I64(65)));
192 /// assert!(t.contains_values(&|_: &[u8]| Some(IndexValue::I64(40))));
193 /// assert!(!t.contains_values(&|_: &[u8]| None));
194 /// ```
195 pub fn contains_values(&self, vals: &impl Fn(&[u8]) -> Option<IndexValue>) -> bool {
196 key_in_tree_vals(self, vals)
197 }
198
199 /// Number of leaves.
200 pub fn leaves(&self) -> usize {
201 match self {
202 Tree::Leaf(_) => 1,
203 Tree::And(a, b) | Tree::Or(a, b) | Tree::Diff(a, b) => a.leaves() + b.leaves(),
204 }
205 }
206
207 /// Depth (a leaf is 1).
208 pub fn depth(&self) -> usize {
209 match self {
210 Tree::Leaf(_) => 1,
211 Tree::And(a, b) | Tree::Or(a, b) | Tree::Diff(a, b) => 1 + a.depth().max(b.depth()),
212 }
213 }
214
215 /// Visit every leaf.
216 pub fn each_leaf<F: FnMut(&Leaf)>(&self, f: &mut F) {
217 match self {
218 Tree::Leaf(l) => f(l),
219 Tree::And(a, b) | Tree::Or(a, b) | Tree::Diff(a, b) => {
220 a.each_leaf(f);
221 b.each_leaf(f);
222 }
223 }
224 }
225}
226
227/// A declared view.
228///
229/// ```
230/// use kevy_index::{IndexValue, Leaf, SortOrder, Tree, ViewMode, ViewSpec};
231/// let leaf = Leaf::new("age", IndexValue::I64(18), IndexValue::I64(65));
232/// let v = ViewSpec::new("adults", Tree::Leaf(leaf), "age")
233/// .with_order(SortOrder::Desc)
234/// .with_mode(ViewMode::Materialized { top_k: 100 });
235/// assert!(v.validate().is_ok());
236/// ```
237#[derive(Debug, Clone, PartialEq)]
238#[non_exhaustive]
239pub struct ViewSpec {
240 /// Catalog name.
241 ///
242 /// ```
243 /// use kevy_index::{IndexValue, Leaf, Tree, ViewCatalog, ViewSpec};
244 /// let t = Tree::Leaf(Leaf::new("age", IndexValue::I64(0), IndexValue::I64(99)));
245 /// let mut views = ViewCatalog::new();
246 /// views.create(ViewSpec::new("adults", t, "age"))?;
247 /// assert_eq!(views.get(b"adults").map(|v| v.name.clone()), Some(b"adults".to_vec()));
248 /// # Ok::<(), Box<dyn std::error::Error>>(())
249 /// ```
250 pub name: Vec<u8>,
251 /// The composition.
252 ///
253 /// ```
254 /// use kevy_index::{IndexValue, Leaf, Tree, ViewSpec};
255 /// let t = Tree::Leaf(Leaf::new("age", IndexValue::I64(0), IndexValue::I64(99)));
256 /// let v = ViewSpec::new("v", t.clone(), "age");
257 /// assert_eq!(v.tree, t);
258 /// assert_eq!(v.tree.leaves(), 1);
259 /// ```
260 pub tree: Tree,
261 /// Index whose coerced value orders the view (a row absent from
262 /// this index is excluded — declaratively, counted).
263 ///
264 /// ```
265 /// use kevy_index::{IndexValue, Leaf, Tree, ViewSpec};
266 /// let city = |c: &[u8]| IndexValue::Str(c.to_vec());
267 /// let t = Tree::Leaf(Leaf::new("city", city(b"a"), city(b"z")));
268 /// let v = ViewSpec::new("by_age", t, "age");
269 /// assert_eq!(v.order_by, b"age", "the order index need not be in the tree");
270 /// ```
271 pub order_by: Vec<u8>,
272 /// Direction of that order.
273 ///
274 /// ```
275 /// use kevy_index::{IndexValue, Leaf, SortOrder, Tree, ViewSpec};
276 /// let t = Tree::Leaf(Leaf::new("age", IndexValue::I64(0), IndexValue::I64(99)));
277 /// let v = ViewSpec::new("v", t, "age");
278 /// assert_eq!(v.order, SortOrder::Asc);
279 /// assert_eq!(v.with_order(SortOrder::Desc).order, SortOrder::Desc);
280 /// ```
281 pub order: SortOrder,
282 /// Virtual or materialized.
283 ///
284 /// ```
285 /// use kevy_index::{IndexValue, Leaf, Tree, ViewMode, ViewSpec};
286 /// let t = Tree::Leaf(Leaf::new("age", IndexValue::I64(0), IndexValue::I64(99)));
287 /// let v = ViewSpec::new("v", t, "age").with_mode(ViewMode::Materialized { top_k: 50 });
288 /// assert_eq!(v.mode.name(), "materialized");
289 /// ```
290 pub mode: ViewMode,
291 /// Optional `VIA` hydration byte-template (`{key}` / `{key.N}`
292 /// placeholders; pure dereference, one template hop).
293 ///
294 /// ```
295 /// use kevy_index::{IndexValue, Leaf, Tree, ViewSpec};
296 /// let t = Tree::Leaf(Leaf::new("age", IndexValue::I64(0), IndexValue::I64(99)));
297 /// let v = ViewSpec::new("v", t, "age");
298 /// assert_eq!(v.via, None, "members are returned as keys unless VIA is declared");
299 /// ```
300 pub via: Option<Vec<u8>>,
301}
302
303/// Declarative caps (RFC §1).
304///
305/// ```
306/// use kevy_index::{IndexValue, Leaf, MAX_TREE_DEPTH, Tree, ViewError, ViewSpec};
307/// let leaf = || Box::new(Tree::Leaf(Leaf::new("n", IndexValue::I64(0), IndexValue::I64(1))));
308/// // And(And(Or(leaf, leaf), leaf), leaf) is four levels deep
309/// let deep = Tree::And(Box::new(Tree::And(Box::new(Tree::Or(leaf(), leaf())), leaf())), leaf());
310/// assert_eq!(deep.depth(), MAX_TREE_DEPTH + 1);
311/// assert_eq!(ViewSpec::new("v", deep, "n").validate(), Err(ViewError::TooDeep));
312/// ```
313pub const MAX_TREE_DEPTH: usize = 3;
314/// Max leaves per tree.
315///
316/// ```
317/// use kevy_index::{IndexValue, Leaf, MAX_TREE_LEAVES, Tree, ViewSpec};
318/// let leaf = || Box::new(Tree::Leaf(Leaf::new("n", IndexValue::I64(0), IndexValue::I64(1))));
319/// let pair = || Box::new(Tree::Or(leaf(), leaf()));
320/// let full = Tree::And(pair(), pair());
321/// assert_eq!(full.leaves(), MAX_TREE_LEAVES);
322/// assert!(ViewSpec::new("v", full, "n").validate().is_ok());
323///
324/// let wide = Tree::Or(Box::new(Tree::Or(pair(), pair())), leaf());
325/// assert_eq!(wide.leaves(), MAX_TREE_LEAVES + 1);
326/// assert!(ViewSpec::new("v", wide, "n").validate().is_err());
327/// ```
328pub const MAX_TREE_LEAVES: usize = 4;
329
330impl ViewSpec {
331 /// A virtual view `name` over `tree`, ascending by index `order_by`.
332 ///
333 /// ```
334 /// use kevy_index::{IndexValue, Leaf, SortOrder, Tree, ViewMode, ViewSpec};
335 /// let v = ViewSpec::new("v", Tree::Leaf(Leaf::new("a", IndexValue::I64(0), IndexValue::I64(9))), "a");
336 /// assert_eq!((v.order, v.mode, v.via), (SortOrder::Asc, ViewMode::Virtual, None));
337 /// ```
338 pub fn new(name: impl Into<Vec<u8>>, tree: Tree, order_by: impl Into<Vec<u8>>) -> ViewSpec {
339 ViewSpec {
340 name: name.into(),
341 tree,
342 order_by: order_by.into(),
343 order: SortOrder::Asc,
344 mode: ViewMode::Virtual,
345 via: None,
346 }
347 }
348
349 /// This view ordered in `order`.
350 ///
351 /// ```
352 /// # use kevy_index::{IndexValue, Leaf, SortOrder, Tree, ViewSpec};
353 /// # let t = Tree::Leaf(Leaf::new("a", IndexValue::I64(0), IndexValue::I64(9)));
354 /// assert_eq!(ViewSpec::new("v", t, "a").with_order(SortOrder::Desc).order, SortOrder::Desc);
355 /// ```
356 #[must_use]
357 pub fn with_order(mut self, order: SortOrder) -> ViewSpec {
358 self.order = order;
359 self
360 }
361
362 /// This view evaluated as `mode`.
363 ///
364 /// ```
365 /// # use kevy_index::{IndexValue, Leaf, Tree, ViewMode, ViewSpec};
366 /// # let t = Tree::Leaf(Leaf::new("a", IndexValue::I64(0), IndexValue::I64(9)));
367 /// let m = ViewMode::Materialized { top_k: 0 };
368 /// assert_eq!(ViewSpec::new("v", t, "a").with_mode(m).mode, m);
369 /// ```
370 #[must_use]
371 pub fn with_mode(mut self, mode: ViewMode) -> ViewSpec {
372 self.mode = mode;
373 self
374 }
375
376 /// This view hydrated through the `VIA` byte-template `via`.
377 ///
378 /// ```
379 /// # use kevy_index::{IndexValue, Leaf, Tree, ViewSpec};
380 /// # let t = Tree::Leaf(Leaf::new("a", IndexValue::I64(0), IndexValue::I64(9)));
381 /// let v = ViewSpec::new("v", t, "a").with_via(b"doc:{key}".to_vec());
382 /// assert_eq!(v.via.as_deref(), Some(&b"doc:{key}"[..]));
383 /// ```
384 #[must_use]
385 pub fn with_via(mut self, via: Vec<u8>) -> ViewSpec {
386 self.via = Some(via);
387 self
388 }
389
390 /// Validate the structural caps.
391 pub fn validate(&self) -> Result<(), crate::ViewError> {
392 if self.tree.depth() > MAX_TREE_DEPTH {
393 return Err(crate::ViewError::TooDeep);
394 }
395 if self.tree.leaves() > MAX_TREE_LEAVES {
396 return Err(crate::ViewError::TooManyLeaves);
397 }
398 Ok(())
399 }
400}
401
402pub(crate) fn eval_tree<'a>(
403 tree: &Tree,
404 seg: &impl Fn(&[u8]) -> Option<&'a Segment>,
405) -> Vec<Vec<u8>> {
406 match tree {
407 Tree::Leaf(l) => match seg(&l.index) {
408 Some(s) => {
409 let (hits, _) = s.range(&l.min, &l.max, None, usize::MAX);
410 hits.into_iter().map(|(k, _)| k).collect()
411 }
412 None => Vec::new(),
413 },
414 Tree::And(a, b) => {
415 // Engine may re-order (locked clause): drive the smaller
416 // side, probe the larger.
417 let (xa, xb) = (a.eval(seg), b.eval(seg));
418 let (mut drive, probe) = if xa.len() <= xb.len() { (xa, xb) } else { (xb, xa) };
419 let set: std::collections::HashSet<&[u8]> = probe.iter().map(Vec::as_slice).collect();
420 drive.retain(|k| set.contains(k.as_slice()));
421 drive
422 }
423 Tree::Or(a, b) => {
424 let mut xa = a.eval(seg);
425 xa.extend(b.eval(seg));
426 xa.sort();
427 xa.dedup();
428 xa
429 }
430 Tree::Diff(a, b) => {
431 let mut xa = a.eval(seg);
432 let xb = b.eval(seg);
433 let set: std::collections::HashSet<&[u8]> = xb.iter().map(Vec::as_slice).collect();
434 xa.retain(|k| !set.contains(k.as_slice()));
435 xa
436 }
437 }
438}
439
440pub(crate) fn key_in_tree<'a>(
441 tree: &Tree,
442 key: &[u8],
443 seg: &impl Fn(&[u8]) -> Option<&'a Segment>,
444) -> bool {
445 match tree {
446 Tree::Leaf(l) => seg(&l.index)
447 .and_then(|s| s.key_dir()?.get(key))
448 .is_some_and(|v| v >= l.min && v <= l.max),
449 Tree::And(a, b) => a.contains(key, seg) && b.contains(key, seg),
450 Tree::Or(a, b) => a.contains(key, seg) || b.contains(key, seg),
451 Tree::Diff(a, b) => a.contains(key, seg) && !b.contains(key, seg),
452 }
453}
454
455pub(crate) fn key_in_tree_vals(tree: &Tree, vals: &impl Fn(&[u8]) -> Option<IndexValue>) -> bool {
456 match tree {
457 Tree::Leaf(l) => vals(&l.index).is_some_and(|v| v >= l.min && v <= l.max),
458 Tree::And(a, b) => a.contains_values(vals) && b.contains_values(vals),
459 Tree::Or(a, b) => a.contains_values(vals) || b.contains_values(vals),
460 Tree::Diff(a, b) => a.contains_values(vals) && !b.contains_values(vals),
461 }
462}
463
464#[cfg(test)]
465#[path = "view_tests.rs"]
466mod tests;