Skip to main content

kevy_index/
view.rs

1//! v2.6 — views: named composition trees over declared indexes
2//! (RFC 2026-07-04, LOCKED).
3//!
4//! Pure logic: [`ViewSpec`] (the declaration), [`eval_tree`] (the
5//! virtual-mode evaluator over segment closures), and
6//! [`MaterializedSet`] (the incremental ordered result set with the
7//! bounded top-K discipline). The runtime supplies segment access and
8//! wires maintenance to its write hook — nothing here does I/O.
9//!
10//! Locked structural rules: components are NAMED indexes (leaves carry
11//! a shape; the view layer holds no predicates of its own); a view
12//! stores MEMBERSHIP + ORDER only (never field values); AND/OR
13//! subtrees may be re-ordered by the engine (DIFF is fixed
14//! left-right).
15
16use crate::segment::Segment;
17use crate::value::IndexValue;
18
19pub use crate::view_sidecar::{MAX_VIEWS, ViewCatalog};
20
21/// One leaf: a declared index + the shape it contributes.
22#[derive(Debug, Clone, PartialEq)]
23pub struct Leaf {
24    /// Index name (resolved by the runtime).
25    pub index: Vec<u8>,
26    /// Inclusive bounds (EQ = same min/max), already coerced to the
27    /// index's type by the runtime at CREATE time.
28    pub min: IndexValue,
29    /// Upper bound.
30    pub max: IndexValue,
31}
32
33/// The composition tree. Depth ≤ 3, leaves ≤ 4 (declarative caps,
34/// enforced at CREATE).
35#[derive(Debug, Clone, PartialEq)]
36pub enum Tree {
37    /// A single index shape.
38    Leaf(Leaf),
39    /// Intersection.
40    And(Box<Tree>, Box<Tree>),
41    /// Union.
42    Or(Box<Tree>, Box<Tree>),
43    /// Left minus right (NOT commutative — order is fixed).
44    Diff(Box<Tree>, Box<Tree>),
45}
46
47impl Tree {
48    /// Number of leaves.
49    pub fn leaves(&self) -> usize {
50        match self {
51            Tree::Leaf(_) => 1,
52            Tree::And(a, b) | Tree::Or(a, b) | Tree::Diff(a, b) => a.leaves() + b.leaves(),
53        }
54    }
55
56    /// Depth (a leaf is 1).
57    pub fn depth(&self) -> usize {
58        match self {
59            Tree::Leaf(_) => 1,
60            Tree::And(a, b) | Tree::Or(a, b) | Tree::Diff(a, b) => 1 + a.depth().max(b.depth()),
61        }
62    }
63
64    /// Visit every leaf.
65    pub fn each_leaf<F: FnMut(&Leaf)>(&self, f: &mut F) {
66        match self {
67            Tree::Leaf(l) => f(l),
68            Tree::And(a, b) | Tree::Or(a, b) | Tree::Diff(a, b) => {
69                a.each_leaf(f);
70                b.each_leaf(f);
71            }
72        }
73    }
74}
75
76/// View mode.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum ViewMode {
79    /// Evaluate the tree at query time.
80    Virtual,
81    /// Maintain an incremental result set; `top_k = 0` = unbounded.
82    Materialized {
83        /// Bounded size (0 = keep every member).
84        top_k: u32,
85    },
86}
87
88/// A declared view.
89#[derive(Debug, Clone, PartialEq)]
90pub struct ViewSpec {
91    /// Catalog name.
92    pub name: Vec<u8>,
93    /// The composition.
94    pub tree: Tree,
95    /// Index whose coerced value orders the view (a row absent from
96    /// this index is excluded — declaratively, counted).
97    pub order_by: Vec<u8>,
98    /// Descending order?
99    pub desc: bool,
100    /// Virtual or materialized.
101    pub mode: ViewMode,
102    /// Optional `VIA` hydration byte-template (`{key}` / `{key.N}`
103    /// placeholders; pure dereference, one template hop).
104    pub via: Option<Vec<u8>>,
105}
106
107/// Declarative caps (RFC §1).
108pub const MAX_TREE_DEPTH: usize = 3;
109/// Max leaves per tree.
110pub const MAX_TREE_LEAVES: usize = 4;
111
112impl ViewSpec {
113    /// Validate the structural caps.
114    pub fn validate(&self) -> Result<(), &'static str> {
115        if self.tree.depth() > MAX_TREE_DEPTH {
116            return Err("ERR view tree deeper than 3");
117        }
118        if self.tree.leaves() > MAX_TREE_LEAVES {
119            return Err("ERR view tree has more than 4 leaves");
120        }
121        Ok(())
122    }
123}
124
125/// Evaluate `tree` against one shard's segments: `seg` resolves an
126/// index name to its [`Segment`] (None = unknown index → empty leaf —
127/// the runtime validates names at CREATE, so this is defensive).
128/// Returns the member keys (unordered set semantics).
129pub fn eval_tree<'a>(
130    tree: &Tree,
131    seg: &impl Fn(&[u8]) -> Option<&'a Segment>,
132) -> Vec<Vec<u8>> {
133    match tree {
134        Tree::Leaf(l) => match seg(&l.index) {
135            Some(s) => {
136                let (hits, _) = s.range(&l.min, &l.max, None, usize::MAX);
137                hits.into_iter().map(|(k, _)| k).collect()
138            }
139            None => Vec::new(),
140        },
141        Tree::And(a, b) => {
142            // Engine may re-order (locked clause): drive the smaller
143            // side, probe the larger.
144            let (xa, xb) = (eval_tree(a, seg), eval_tree(b, seg));
145            let (mut drive, probe) = if xa.len() <= xb.len() { (xa, xb) } else { (xb, xa) };
146            let set: std::collections::HashSet<&[u8]> =
147                probe.iter().map(Vec::as_slice).collect();
148            drive.retain(|k| set.contains(k.as_slice()));
149            drive
150        }
151        Tree::Or(a, b) => {
152            let mut xa = eval_tree(a, seg);
153            xa.extend(eval_tree(b, seg));
154            xa.sort();
155            xa.dedup();
156            xa
157        }
158        Tree::Diff(a, b) => {
159            let mut xa = eval_tree(a, seg);
160            let xb = eval_tree(b, seg);
161            let set: std::collections::HashSet<&[u8]> = xb.iter().map(Vec::as_slice).collect();
162            xa.retain(|k| !set.contains(k.as_slice()));
163            xa
164        }
165    }
166}
167
168/// Re-evaluate ONE key's membership (the materialized write hook):
169/// every leaf is a point probe via the segment's reverse map.
170pub fn key_in_tree<'a>(
171    tree: &Tree,
172    key: &[u8],
173    seg: &impl Fn(&[u8]) -> Option<&'a Segment>,
174) -> bool {
175    match tree {
176        Tree::Leaf(l) => seg(&l.index)
177            .and_then(|s| s.verify_entry(key))
178            .is_some_and(|v| *v >= l.min && *v <= l.max),
179        Tree::And(a, b) => key_in_tree(a, key, seg) && key_in_tree(b, key, seg),
180        Tree::Or(a, b) => key_in_tree(a, key, seg) || key_in_tree(b, key, seg),
181        Tree::Diff(a, b) => key_in_tree(a, key, seg) && !key_in_tree(b, key, seg),
182    }
183}
184
185/// [`key_in_tree`] variant over PRE-FETCHED per-index values — the
186/// write hook probes each referenced index ONCE per key and evaluates
187/// every view against the same small table (bounds compares only; no
188/// per-view re-hashing).
189pub fn key_in_tree_vals(
190    tree: &Tree,
191    vals: &impl Fn(&[u8]) -> Option<IndexValue>,
192) -> bool {
193    match tree {
194        Tree::Leaf(l) => vals(&l.index).is_some_and(|v| v >= l.min && v <= l.max),
195        Tree::And(a, b) => key_in_tree_vals(a, vals) && key_in_tree_vals(b, vals),
196        Tree::Or(a, b) => key_in_tree_vals(a, vals) || key_in_tree_vals(b, vals),
197        Tree::Diff(a, b) => key_in_tree_vals(a, vals) && !key_in_tree_vals(b, vals),
198    }
199}
200
201/// One shard's materialized result set: ordered `(order_value, key)`
202/// members with the bounded top-K discipline (keep `K + Δ` where
203/// `Δ = K/4`; underflow requests a local rebuild from the base
204/// indexes — RFC §2).
205#[derive(Debug, Default)]
206pub struct MaterializedSet {
207    set: std::collections::BTreeSet<(IndexValue, Vec<u8>)>,
208    back: std::collections::HashMap<Vec<u8>, IndexValue>,
209    /// 0 = unbounded.
210    top_k: u32,
211    /// DESC view: the bound keeps the LARGEST members (evict the
212    /// smallest past the cap); ASC keeps the smallest.
213    desc: bool,
214    /// Members excluded because they're absent from the order index.
215    pub order_excluded: u64,
216}
217
218impl MaterializedSet {
219    /// New set with the declared bound (0 = unbounded) and order
220    /// direction (the bound evicts from the view's WORST end).
221    pub fn new(top_k: u32, desc: bool) -> Self {
222        Self { top_k, desc, ..Default::default() }
223    }
224
225    fn cap(&self) -> usize {
226        if self.top_k == 0 {
227            usize::MAX
228        } else {
229            (self.top_k + self.top_k / 4) as usize
230        }
231    }
232
233    /// Apply one key's membership verdict + order value. Returns
234    /// `true` if the set UNDERFLOWED below K after a removal (the
235    /// caller must schedule a local rebuild).
236    pub fn apply(&mut self, key: &[u8], member: bool, order: Option<IndexValue>) -> bool {
237        // Bounded fast path: a NON-member of a full top-K set whose
238        // value is worse than the current worst can neither enter nor
239        // change anything — one comparison, no tree ops, no allocs.
240        // This is the write-tax fast path for hot-list views (most
241        // writes touch rows outside the top K).
242        if self.top_k != 0
243            && member
244            && !self.back.contains_key(key)
245            && self.set.len() >= self.cap()
246            && let Some(v) = &order
247        {
248            let enters = if self.desc {
249                self.set.iter().next().is_some_and(|(worst, _)| v > worst)
250            } else {
251                self.set.iter().next_back().is_some_and(|(worst, _)| v < worst)
252            };
253            if !enters {
254                return false;
255            }
256        }
257        if let Some(old) = self.back.remove(key) {
258            self.set.remove(&(old, key.to_vec()));
259        }
260        match (member, order) {
261            (true, Some(v)) => {
262                self.back.insert(key.to_vec(), v.clone());
263                self.set.insert((v, key.to_vec()));
264                self.evict_past_cap();
265                false
266            }
267            (true, None) => {
268                self.order_excluded += 1;
269                false
270            }
271            _ => {
272                self.top_k != 0 && self.set.len() < self.top_k as usize
273            }
274        }
275    }
276
277    /// Bound: evict the view's WORST member past K+Δ — the largest
278    /// for ASC, the SMALLEST for DESC.
279    fn evict_past_cap(&mut self) {
280        if self.set.len() > self.cap() {
281            let worst = if self.desc {
282                self.set.iter().next().cloned()
283            } else {
284                self.set.iter().next_back().cloned()
285            };
286            if let Some(w) = worst {
287                self.set.remove(&w);
288                self.back.remove(&w.1);
289            }
290        }
291    }
292
293    /// Ordered page. `desc = false`: ascending from just past `after`;
294    /// `desc = true`: DESCENDING from just below `after` (a DESC view
295    /// must take each shard's LARGEST members — taking the ascending
296    /// head and reversing at the merge yields the wrong member set).
297    pub fn page(
298        &self,
299        after: Option<&(IndexValue, Vec<u8>)>,
300        limit: usize,
301        desc: bool,
302    ) -> Vec<(IndexValue, Vec<u8>)> {
303        if desc {
304            let iter: Box<dyn Iterator<Item = &(IndexValue, Vec<u8>)>> = match after {
305                Some(c) => Box::new(
306                    self.set
307                        .range((std::ops::Bound::Unbounded, std::ops::Bound::Excluded(c.clone())))
308                        .rev(),
309                ),
310                None => Box::new(self.set.iter().rev()),
311            };
312            return iter.take(limit).cloned().collect();
313        }
314        let iter: Box<dyn Iterator<Item = &(IndexValue, Vec<u8>)>> = match after {
315            Some(c) => Box::new(self.set.range((
316                std::ops::Bound::Excluded(c.clone()),
317                std::ops::Bound::Unbounded,
318            ))),
319            None => Box::new(self.set.iter()),
320        };
321        iter.take(limit).cloned().collect()
322    }
323
324    /// Member count.
325    pub fn len(&self) -> usize {
326        self.set.len()
327    }
328
329    /// Empty?
330    pub fn is_empty(&self) -> bool {
331        self.set.is_empty()
332    }
333
334    /// Wipe (rebuild path).
335    pub fn clear(&mut self) {
336        self.set.clear();
337        self.back.clear();
338    }
339
340    /// Approximate heap bytes (RFC §5 formula's measured side).
341    pub fn approx_bytes(&self) -> u64 {
342        self.set
343            .iter()
344            .map(|(v, k)| (v.approx_bytes() + k.len() + 48) as u64)
345            .sum()
346    }
347}
348
349#[cfg(test)]
350#[path = "view_tests.rs"]
351mod tests;