1use crate::segment::Segment;
17use crate::value::IndexValue;
18
19#[derive(Debug, Clone, PartialEq)]
21pub struct Leaf {
22 pub index: Vec<u8>,
24 pub min: IndexValue,
27 pub max: IndexValue,
29}
30
31#[derive(Debug, Clone, PartialEq)]
34pub enum Tree {
35 Leaf(Leaf),
37 And(Box<Tree>, Box<Tree>),
39 Or(Box<Tree>, Box<Tree>),
41 Diff(Box<Tree>, Box<Tree>),
43}
44
45impl Tree {
46 pub fn leaves(&self) -> usize {
48 match self {
49 Tree::Leaf(_) => 1,
50 Tree::And(a, b) | Tree::Or(a, b) | Tree::Diff(a, b) => a.leaves() + b.leaves(),
51 }
52 }
53
54 pub fn depth(&self) -> usize {
56 match self {
57 Tree::Leaf(_) => 1,
58 Tree::And(a, b) | Tree::Or(a, b) | Tree::Diff(a, b) => 1 + a.depth().max(b.depth()),
59 }
60 }
61
62 pub fn each_leaf<F: FnMut(&Leaf)>(&self, f: &mut F) {
64 match self {
65 Tree::Leaf(l) => f(l),
66 Tree::And(a, b) | Tree::Or(a, b) | Tree::Diff(a, b) => {
67 a.each_leaf(f);
68 b.each_leaf(f);
69 }
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum ViewMode {
77 Virtual,
79 Materialized {
81 top_k: u32,
83 },
84}
85
86#[derive(Debug, Clone, PartialEq)]
88pub struct ViewSpec {
89 pub name: Vec<u8>,
91 pub tree: Tree,
93 pub order_by: Vec<u8>,
96 pub desc: bool,
98 pub mode: ViewMode,
100 pub via: Option<Vec<u8>>,
103}
104
105pub const MAX_TREE_DEPTH: usize = 3;
107pub const MAX_TREE_LEAVES: usize = 4;
109
110impl ViewSpec {
111 pub fn validate(&self) -> Result<(), &'static str> {
113 if self.tree.depth() > MAX_TREE_DEPTH {
114 return Err("ERR view tree deeper than 3");
115 }
116 if self.tree.leaves() > MAX_TREE_LEAVES {
117 return Err("ERR view tree has more than 4 leaves");
118 }
119 Ok(())
120 }
121}
122
123pub fn eval_tree<'a>(
128 tree: &Tree,
129 seg: &impl Fn(&[u8]) -> Option<&'a Segment>,
130) -> Vec<Vec<u8>> {
131 match tree {
132 Tree::Leaf(l) => match seg(&l.index) {
133 Some(s) => {
134 let (hits, _) = s.range(&l.min, &l.max, None, usize::MAX);
135 hits.into_iter().map(|(k, _)| k).collect()
136 }
137 None => Vec::new(),
138 },
139 Tree::And(a, b) => {
140 let (xa, xb) = (eval_tree(a, seg), eval_tree(b, seg));
143 let (mut drive, probe) = if xa.len() <= xb.len() { (xa, xb) } else { (xb, xa) };
144 let set: std::collections::HashSet<&[u8]> =
145 probe.iter().map(Vec::as_slice).collect();
146 drive.retain(|k| set.contains(k.as_slice()));
147 drive
148 }
149 Tree::Or(a, b) => {
150 let mut xa = eval_tree(a, seg);
151 xa.extend(eval_tree(b, seg));
152 xa.sort();
153 xa.dedup();
154 xa
155 }
156 Tree::Diff(a, b) => {
157 let mut xa = eval_tree(a, seg);
158 let xb = eval_tree(b, seg);
159 let set: std::collections::HashSet<&[u8]> = xb.iter().map(Vec::as_slice).collect();
160 xa.retain(|k| !set.contains(k.as_slice()));
161 xa
162 }
163 }
164}
165
166pub fn key_in_tree<'a>(
169 tree: &Tree,
170 key: &[u8],
171 seg: &impl Fn(&[u8]) -> Option<&'a Segment>,
172) -> bool {
173 match tree {
174 Tree::Leaf(l) => seg(&l.index)
175 .and_then(|s| s.verify_entry(key))
176 .is_some_and(|v| *v >= l.min && *v <= l.max),
177 Tree::And(a, b) => key_in_tree(a, key, seg) && key_in_tree(b, key, seg),
178 Tree::Or(a, b) => key_in_tree(a, key, seg) || key_in_tree(b, key, seg),
179 Tree::Diff(a, b) => key_in_tree(a, key, seg) && !key_in_tree(b, key, seg),
180 }
181}
182
183pub fn key_in_tree_vals(
188 tree: &Tree,
189 vals: &impl Fn(&[u8]) -> Option<IndexValue>,
190) -> bool {
191 match tree {
192 Tree::Leaf(l) => vals(&l.index).is_some_and(|v| v >= l.min && v <= l.max),
193 Tree::And(a, b) => key_in_tree_vals(a, vals) && key_in_tree_vals(b, vals),
194 Tree::Or(a, b) => key_in_tree_vals(a, vals) || key_in_tree_vals(b, vals),
195 Tree::Diff(a, b) => key_in_tree_vals(a, vals) && !key_in_tree_vals(b, vals),
196 }
197}
198
199#[derive(Debug, Default)]
204pub struct MaterializedSet {
205 set: std::collections::BTreeSet<(IndexValue, Vec<u8>)>,
206 back: std::collections::HashMap<Vec<u8>, IndexValue>,
207 top_k: u32,
209 desc: bool,
212 pub order_excluded: u64,
214}
215
216impl MaterializedSet {
217 pub fn new(top_k: u32, desc: bool) -> Self {
220 Self { top_k, desc, ..Default::default() }
221 }
222
223 fn cap(&self) -> usize {
224 if self.top_k == 0 {
225 usize::MAX
226 } else {
227 (self.top_k + self.top_k / 4) as usize
228 }
229 }
230
231 pub fn apply(&mut self, key: &[u8], member: bool, order: Option<IndexValue>) -> bool {
235 if self.top_k != 0
241 && member
242 && !self.back.contains_key(key)
243 && self.set.len() >= self.cap()
244 && let Some(v) = &order
245 {
246 let enters = if self.desc {
247 self.set.iter().next().is_some_and(|(worst, _)| v > worst)
248 } else {
249 self.set.iter().next_back().is_some_and(|(worst, _)| v < worst)
250 };
251 if !enters {
252 return false;
253 }
254 }
255 if let Some(old) = self.back.remove(key) {
256 self.set.remove(&(old, key.to_vec()));
257 }
258 match (member, order) {
259 (true, Some(v)) => {
260 self.back.insert(key.to_vec(), v.clone());
261 self.set.insert((v, key.to_vec()));
262 if self.set.len() > self.cap() {
265 let worst = if self.desc {
266 self.set.iter().next().cloned()
267 } else {
268 self.set.iter().next_back().cloned()
269 };
270 if let Some(w) = worst {
271 self.set.remove(&w);
272 self.back.remove(&w.1);
273 }
274 }
275 false
276 }
277 (true, None) => {
278 self.order_excluded += 1;
279 false
280 }
281 _ => {
282 self.top_k != 0 && self.set.len() < self.top_k as usize
283 }
284 }
285 }
286
287 pub fn page(
292 &self,
293 after: Option<&(IndexValue, Vec<u8>)>,
294 limit: usize,
295 desc: bool,
296 ) -> Vec<(IndexValue, Vec<u8>)> {
297 if desc {
298 let iter: Box<dyn Iterator<Item = &(IndexValue, Vec<u8>)>> = match after {
299 Some(c) => Box::new(
300 self.set
301 .range((std::ops::Bound::Unbounded, std::ops::Bound::Excluded(c.clone())))
302 .rev(),
303 ),
304 None => Box::new(self.set.iter().rev()),
305 };
306 return iter.take(limit).cloned().collect();
307 }
308 let iter: Box<dyn Iterator<Item = &(IndexValue, Vec<u8>)>> = match after {
309 Some(c) => Box::new(self.set.range((
310 std::ops::Bound::Excluded(c.clone()),
311 std::ops::Bound::Unbounded,
312 ))),
313 None => Box::new(self.set.iter()),
314 };
315 iter.take(limit).cloned().collect()
316 }
317
318 pub fn len(&self) -> usize {
320 self.set.len()
321 }
322
323 pub fn is_empty(&self) -> bool {
325 self.set.is_empty()
326 }
327
328 pub fn clear(&mut self) {
330 self.set.clear();
331 self.back.clear();
332 }
333
334 pub fn approx_bytes(&self) -> u64 {
336 self.set
337 .iter()
338 .map(|(v, k)| (v.approx_bytes() + k.len() + 48) as u64)
339 .sum()
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 fn seg_ab() -> (Segment, Segment) {
348 let mut a = Segment::new();
349 let mut b = Segment::new();
350 for i in 0..10 {
351 a.apply(format!("k{i}").as_bytes(), Some(IndexValue::I64(i)));
352 if i % 2 == 0 {
353 b.apply(format!("k{i}").as_bytes(), Some(IndexValue::Str(b"eng".to_vec())));
354 }
355 }
356 (a, b)
357 }
358
359 fn leaf(idx: &str, min: IndexValue, max: IndexValue) -> Tree {
360 Tree::Leaf(Leaf { index: idx.into(), min, max })
361 }
362
363 #[test]
364 fn tree_eval_and_or_diff() {
365 let (a, b) = seg_ab();
366 let seg = |n: &[u8]| -> Option<&Segment> {
367 match n {
368 b"age" => Some(&a),
369 b"dept" => Some(&b),
370 _ => None,
371 }
372 };
373 let age = leaf("age", IndexValue::I64(2), IndexValue::I64(7));
374 let eng = leaf(
375 "dept",
376 IndexValue::Str(b"eng".to_vec()),
377 IndexValue::Str(b"eng".to_vec()),
378 );
379 let and = Tree::And(Box::new(age.clone()), Box::new(eng.clone()));
380 let mut got = eval_tree(&and, &seg);
381 got.sort();
382 assert_eq!(got, vec![b"k2".to_vec(), b"k4".to_vec(), b"k6".to_vec()]);
383
384 let or = Tree::Or(Box::new(age.clone()), Box::new(eng.clone()));
385 assert_eq!(eval_tree(&or, &seg).len(), 8, "2..=7 ∪ evens = 8");
386
387 let diff = Tree::Diff(Box::new(age.clone()), Box::new(eng.clone()));
388 let mut got = eval_tree(&diff, &seg);
389 got.sort();
390 assert_eq!(got, vec![b"k3".to_vec(), b"k5".to_vec(), b"k7".to_vec()]);
391
392 assert!(key_in_tree(&and, b"k4", &seg));
394 assert!(!key_in_tree(&and, b"k3", &seg));
395 assert!(key_in_tree(&diff, b"k5", &seg));
396 assert!(!key_in_tree(&diff, b"k4", &seg));
397 }
398
399 #[test]
400 fn caps_validate() {
401 let l = leaf("a", IndexValue::I64(0), IndexValue::I64(1));
402 let deep = Tree::And(
403 Box::new(Tree::And(
404 Box::new(Tree::And(Box::new(l.clone()), Box::new(l.clone()))),
405 Box::new(l.clone()),
406 )),
407 Box::new(l.clone()),
408 );
409 let spec = ViewSpec {
410 name: b"v".to_vec(),
411 tree: deep,
412 order_by: b"a".to_vec(),
413 desc: false,
414 mode: ViewMode::Virtual,
415 via: None,
416 };
417 assert!(spec.validate().is_err(), "depth 4 rejected");
418 }
419
420 #[test]
421 fn materialized_bounds_and_underflow() {
422 let mut m = MaterializedSet::new(4, false); for i in 0..8 {
424 let under = m.apply(
425 format!("k{i}").as_bytes(),
426 true,
427 Some(IndexValue::I64(i)),
428 );
429 assert!(!under);
430 }
431 assert_eq!(m.len(), 5, "bounded at K+Δ");
432 let page = m.page(None, 10, false);
433 assert_eq!(page[0].1, b"k0".to_vec(), "best kept");
434 assert_eq!(page.last().unwrap().1, b"k4".to_vec(), "worst evicted");
435
436 }
437
438 #[test]
439 fn materialized_desc_bound_keeps_largest() {
440 let mut m = MaterializedSet::new(4, true); for i in 0..8 {
442 m.apply(format!("k{i}").as_bytes(), true, Some(IndexValue::I64(i)));
443 }
444 assert_eq!(m.len(), 5);
445 let page = m.page(None, 10, true);
446 assert_eq!(page[0].1, b"k7".to_vec(), "largest kept on top");
447 assert_eq!(page.last().unwrap().1, b"k3".to_vec(), "smallest evicted");
448 }
449
450 #[test]
451 fn view_catalog_sidecar_roundtrip() {
452 let tree = Tree::Diff(
453 Box::new(Tree::And(
454 Box::new(leaf("age", IndexValue::I64(1), IndexValue::I64(9))),
455 Box::new(leaf(
456 "dept",
457 IndexValue::Str(b"e )n%g".to_vec()),
458 IndexValue::Str(b"e )n%g".to_vec()),
459 )),
460 )),
461 Box::new(leaf("flag", IndexValue::F64(-0.5), IndexValue::F64(2.5))),
462 );
463 let spec = ViewSpec {
464 name: b"v one".to_vec(),
465 tree,
466 order_by: b"age".to_vec(),
467 desc: true,
468 mode: ViewMode::Materialized { top_k: 50 },
469 via: Some(b"user:{key.1}".to_vec()),
470 };
471 let mut c = ViewCatalog::new();
472 c.create(spec.clone()).unwrap();
473 c.create(ViewSpec {
474 name: b"v2".to_vec(),
475 tree: leaf("age", IndexValue::I64(0), IndexValue::I64(1)),
476 order_by: b"age".to_vec(),
477 desc: false,
478 mode: ViewMode::Virtual,
479 via: None,
480 })
481 .unwrap();
482 let text = c.to_sidecar();
483 let c2 = ViewCatalog::from_sidecar(&text).expect("parse");
484 assert_eq!(c2.len(), 2);
485 assert_eq!(c2.get(b"v one").unwrap(), &spec);
486 assert!(ViewCatalog::from_sidecar("junk").is_none());
487 }
488
489 #[test]
490 fn materialized_underflow_signals() {
491 let mut m = MaterializedSet::new(4, false);
492 for i in 0..5 {
493 m.apply(format!("k{i}").as_bytes(), true, Some(IndexValue::I64(i)));
494 }
495 assert_eq!(m.len(), 5);
496 assert!(!m.apply(b"k0", false, None), "5→4 = still K");
497 assert!(m.apply(b"k1", false, None), "4→3 < K → underflow signal");
498 m.apply(b"kx", true, None);
500 assert_eq!(m.order_excluded, 1);
501 let mut u = MaterializedSet::new(0, false);
503 u.apply(b"a", true, Some(IndexValue::I64(1)));
504 assert!(!u.apply(b"a", false, None));
505 }
506}
507
508#[derive(Debug, Clone, Default)]
511pub struct ViewCatalog {
512 specs: Vec<ViewSpec>,
513}
514
515pub const MAX_VIEWS: usize = 64;
517
518impl ViewCatalog {
519 pub fn new() -> Self {
521 Self::default()
522 }
523
524 pub fn create(&mut self, spec: ViewSpec) -> Result<(), &'static str> {
526 spec.validate()?;
527 if self.specs.len() >= MAX_VIEWS {
528 return Err("ERR view limit reached (64)");
529 }
530 if self.specs.iter().any(|s| s.name == spec.name) {
531 return Err("ERR view already exists");
532 }
533 self.specs.push(spec);
534 Ok(())
535 }
536
537 pub fn drop_view(&mut self, name: &[u8]) -> bool {
539 let n = self.specs.len();
540 self.specs.retain(|s| s.name != name);
541 self.specs.len() != n
542 }
543
544 pub fn get(&self, name: &[u8]) -> Option<&ViewSpec> {
546 self.specs.iter().find(|s| s.name == name)
547 }
548
549 pub fn iter(&self) -> impl Iterator<Item = &ViewSpec> {
551 self.specs.iter()
552 }
553
554 pub fn len(&self) -> usize {
556 self.specs.len()
557 }
558
559 pub fn is_empty(&self) -> bool {
561 self.specs.is_empty()
562 }
563
564 pub fn to_sidecar(&self) -> String {
566 let mut out = String::from("kevy-view-catalog v1\n");
567 for s in &self.specs {
568 out.push_str(&s.to_line());
569 out.push('\n');
570 }
571 out
572 }
573
574 pub fn from_sidecar(text: &str) -> Option<ViewCatalog> {
576 let mut lines = text.lines();
577 if lines.next()? != "kevy-view-catalog v1" {
578 return None;
579 }
580 let mut c = ViewCatalog::new();
581 for line in lines {
582 if line.is_empty() {
583 continue;
584 }
585 c.create(ViewSpec::from_line(line)?).ok()?;
586 }
587 Some(c)
588 }
589}
590
591fn esc(b: &[u8]) -> String {
592 let mut out = String::with_capacity(b.len());
593 for &c in b {
594 if c == b' ' || c == b'\t' || c == b'\n' || c == b'%' || c == b'(' || c == b')' || !(33..127).contains(&c) {
595 out.push_str(&format!("%{c:02X}"));
596 } else {
597 out.push(c as char);
598 }
599 }
600 if out.is_empty() { "%".into() } else { out }
601}
602
603fn unesc(s: &str) -> Option<Vec<u8>> {
604 if s == "%" {
605 return Some(Vec::new());
606 }
607 let mut out = Vec::with_capacity(s.len());
608 let b = s.as_bytes();
609 let mut i = 0;
610 while i < b.len() {
611 if b[i] == b'%' {
612 out.push(u8::from_str_radix(s.get(i + 1..i + 3)?, 16).ok()?);
613 i += 3;
614 } else {
615 out.push(b[i]);
616 i += 1;
617 }
618 }
619 Some(out)
620}
621
622fn val_ser(v: &IndexValue) -> String {
623 match v {
624 IndexValue::I64(i) => format!("i{i}"),
625 IndexValue::F64(f) => format!("f{}", f.to_bits()),
626 IndexValue::Str(s) => format!("s{}", esc(s)),
627 }
628}
629
630fn val_de(s: &str) -> Option<IndexValue> {
631 let (tag, rest) = s.split_at(1);
632 match tag {
633 "i" => rest.parse().ok().map(IndexValue::I64),
634 "f" => rest.parse::<u64>().ok().map(|b| IndexValue::F64(f64::from_bits(b))),
635 "s" => unesc(rest).map(IndexValue::Str),
636 _ => None,
637 }
638}
639
640fn tree_ser(t: &Tree, out: &mut String) {
641 match t {
642 Tree::Leaf(l) => {
643 out.push_str(&format!("(L {} {} {})", esc(&l.index), val_ser(&l.min), val_ser(&l.max)));
644 }
645 Tree::And(a, b) | Tree::Or(a, b) | Tree::Diff(a, b) => {
646 let op = match t {
647 Tree::And(..) => "A",
648 Tree::Or(..) => "O",
649 _ => "D",
650 };
651 out.push_str(&format!("({op} "));
652 tree_ser(a, out);
653 out.push(' ');
654 tree_ser(b, out);
655 out.push(')');
656 }
657 }
658}
659
660fn tree_de(toks: &[&str], pos: &mut usize) -> Option<Tree> {
661 let t = toks.get(*pos)?;
662 *pos += 1;
663 match *t {
664 "(L" => {
665 let idx = unesc(toks.get(*pos)?)?;
666 let min = val_de(toks.get(*pos + 1)?)?;
667 let max = val_de(toks.get(*pos + 2)?.trim_end_matches(')'))?;
668 *pos += 3;
669 Some(Tree::Leaf(Leaf { index: idx, min, max }))
670 }
671 "(A" | "(O" | "(D" => {
672 let a = tree_de(toks, pos)?;
673 let b = tree_de(toks, pos)?;
674 let tree = match *t {
675 "(A" => Tree::And(Box::new(a), Box::new(b)),
676 "(O" => Tree::Or(Box::new(a), Box::new(b)),
677 _ => Tree::Diff(Box::new(a), Box::new(b)),
678 };
679 Some(tree)
680 }
681 _ => None,
682 }
683}
684
685impl ViewSpec {
686 pub fn to_line(&self) -> String {
688 let (mode, k) = match self.mode {
689 ViewMode::Virtual => ("v", 0),
690 ViewMode::Materialized { top_k } => ("m", top_k),
691 };
692 let via = self.via.as_deref().map(esc).unwrap_or_else(|| "-".into());
693 let mut tree = String::new();
694 tree_ser(&self.tree, &mut tree);
695 format!(
696 "{} {} {} {} {} {} {}",
697 esc(&self.name),
698 esc(&self.order_by),
699 u8::from(self.desc),
700 mode,
701 k,
702 via,
703 tree
704 )
705 }
706
707 pub fn from_line(line: &str) -> Option<ViewSpec> {
709 let toks: Vec<&str> = line.split(' ').collect();
710 if toks.len() < 7 {
711 return None;
712 }
713 let mode = match toks[3] {
714 "v" => ViewMode::Virtual,
715 "m" => ViewMode::Materialized { top_k: toks[4].parse().ok()? },
716 _ => return None,
717 };
718 let via = if toks[5] == "-" { None } else { Some(unesc(toks[5])?) };
719 let mut pos = 6;
720 let tree = tree_de_root(&toks, &mut pos)?;
723 Some(ViewSpec {
724 name: unesc(toks[0])?,
725 order_by: unesc(toks[1])?,
726 desc: toks[2] == "1",
727 mode,
728 via,
729 tree,
730 })
731 }
732}
733
734fn tree_de_root(toks: &[&str], pos: &mut usize) -> Option<Tree> {
735 tree_de(toks, pos)
740}