Skip to main content

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;