1use std::collections::HashMap;
17
18use roaring::RoaringBitmap;
19
20use nodedb_types::Value;
21
22#[derive(
30 Debug,
31 Clone,
32 PartialEq,
33 Eq,
34 PartialOrd,
35 Ord,
36 Hash,
37 serde::Serialize,
38 serde::Deserialize,
39 zerompk::ToMessagePack,
40 zerompk::FromMessagePack,
41)]
42#[non_exhaustive]
43pub enum PayloadKey {
44 Null,
45 Bool(bool),
46 Integer(i64),
47 Float(u64),
49 String(String),
50 Bytes(Vec<u8>),
51 DateTime(String),
53 Uuid(String),
54}
55
56impl PayloadKey {
57 pub fn from_value(v: &Value) -> Option<PayloadKey> {
63 match v {
64 Value::Null => Some(PayloadKey::Null),
65 Value::Bool(b) => Some(PayloadKey::Bool(*b)),
66 Value::Integer(i) => Some(PayloadKey::Integer(*i)),
67 Value::Float(f) => {
68 let bits = if f.is_nan() { u64::MAX } else { f.to_bits() };
70 Some(PayloadKey::Float(bits))
71 }
72 Value::String(s) => Some(PayloadKey::String(s.clone())),
73 Value::Bytes(b) => Some(PayloadKey::Bytes(b.clone())),
74 Value::Uuid(s) | Value::Ulid(s) | Value::Regex(s) => Some(PayloadKey::Uuid(s.clone())),
75 Value::DateTime(dt) | Value::NaiveDateTime(dt) => {
76 Some(PayloadKey::DateTime(format!("{dt:?}")))
77 }
78 Value::Decimal(d) => Some(PayloadKey::String(d.to_string())),
79 Value::Array(_)
81 | Value::Object(_)
82 | Value::Set(_)
83 | Value::Geometry(_)
84 | Value::Duration(_)
85 | Value::Range { .. }
86 | Value::Record { .. }
87 | Value::ArrayCell(_) => None,
88 _ => None,
91 }
92 }
93}
94
95pub use nodedb_types::PayloadIndexKind;
100
101#[derive(Debug, Clone)]
107#[non_exhaustive]
108pub enum PayloadIndexBitmaps {
109 Equality(HashMap<PayloadKey, RoaringBitmap>),
110 Range(std::collections::BTreeMap<PayloadKey, RoaringBitmap>),
111}
112
113impl PayloadIndexBitmaps {
114 fn for_kind(kind: PayloadIndexKind) -> Self {
115 match kind {
116 PayloadIndexKind::Range => Self::Range(std::collections::BTreeMap::new()),
117 _ => Self::Equality(HashMap::new()),
120 }
121 }
122
123 fn get(&self, key: &PayloadKey) -> Option<&RoaringBitmap> {
124 match self {
125 Self::Equality(m) => m.get(key),
126 Self::Range(m) => m.get(key),
127 }
128 }
129
130 fn entry_or_default(&mut self, key: PayloadKey) -> &mut RoaringBitmap {
131 match self {
132 Self::Equality(m) => m.entry(key).or_default(),
133 Self::Range(m) => m.entry(key).or_default(),
134 }
135 }
136
137 fn get_mut(&mut self, key: &PayloadKey) -> Option<&mut RoaringBitmap> {
138 match self {
139 Self::Equality(m) => m.get_mut(key),
140 Self::Range(m) => m.get_mut(key),
141 }
142 }
143
144 fn remove(&mut self, key: &PayloadKey) -> Option<RoaringBitmap> {
145 match self {
146 Self::Equality(m) => m.remove(key),
147 Self::Range(m) => m.remove(key),
148 }
149 }
150
151 fn clear(&mut self) {
152 match self {
153 Self::Equality(m) => m.clear(),
154 Self::Range(m) => m.clear(),
155 }
156 }
157
158 fn iter(&self) -> Box<dyn Iterator<Item = (&PayloadKey, &RoaringBitmap)> + '_> {
159 match self {
160 Self::Equality(m) => Box::new(m.iter()),
161 Self::Range(m) => Box::new(m.iter()),
162 }
163 }
164}
165
166#[derive(Debug, Clone)]
168pub struct PayloadIndex {
169 pub field: String,
171 pub kind: PayloadIndexKind,
173 pub bitmaps: PayloadIndexBitmaps,
175}
176
177impl Default for PayloadIndex {
178 fn default() -> Self {
179 Self::new(String::new(), PayloadIndexKind::Equality)
180 }
181}
182
183impl PayloadIndex {
184 pub fn new(field: impl Into<String>, kind: PayloadIndexKind) -> Self {
186 Self {
187 field: field.into(),
188 kind,
189 bitmaps: PayloadIndexBitmaps::for_kind(kind),
190 }
191 }
192
193 pub fn insert(&mut self, node_id: u32, value: &Value) {
195 let Some(key) = PayloadKey::from_value(value) else {
196 return;
197 };
198 self.bitmaps.entry_or_default(key).insert(node_id);
199 }
200
201 pub fn delete(&mut self, node_id: u32, value: &Value) {
203 let Some(key) = PayloadKey::from_value(value) else {
204 return;
205 };
206 if let Some(bm) = self.bitmaps.get_mut(&key) {
207 bm.remove(node_id);
208 if bm.is_empty() {
209 self.bitmaps.remove(&key);
210 }
211 }
212 }
213
214 pub fn equality(&self, value: &Value) -> Option<&RoaringBitmap> {
217 let key = PayloadKey::from_value(value)?;
218 self.bitmaps.get(&key)
219 }
220
221 pub fn range(
227 &self,
228 low: Option<&Value>,
229 low_inclusive: bool,
230 high: Option<&Value>,
231 high_inclusive: bool,
232 ) -> Option<RoaringBitmap> {
233 let PayloadIndexBitmaps::Range(map) = &self.bitmaps else {
234 return None;
235 };
236 use std::ops::Bound;
237 let low_key = low.and_then(PayloadKey::from_value);
238 let high_key = high.and_then(PayloadKey::from_value);
239 let low_bound = match (&low_key, low_inclusive) {
240 (Some(k), true) => Bound::Included(k.clone()),
241 (Some(k), false) => Bound::Excluded(k.clone()),
242 (None, _) => Bound::Unbounded,
243 };
244 let high_bound = match (&high_key, high_inclusive) {
245 (Some(k), true) => Bound::Included(k.clone()),
246 (Some(k), false) => Bound::Excluded(k.clone()),
247 (None, _) => Bound::Unbounded,
248 };
249 let mut acc = RoaringBitmap::new();
250 for (_, bm) in map.range((low_bound, high_bound)) {
251 acc |= bm;
252 }
253 Some(acc)
254 }
255}
256
257#[derive(Debug, Default)]
261pub struct PayloadIndexSet {
262 indexes: HashMap<String, PayloadIndex>,
263}
264
265impl PayloadIndexSet {
266 pub fn add_index(&mut self, field: impl Into<String>, kind: PayloadIndexKind) {
269 let field = field.into();
270 self.indexes
271 .entry(field.clone())
272 .or_insert_with(|| PayloadIndex::new(field, kind));
273 }
274
275 pub fn is_empty(&self) -> bool {
277 self.indexes.is_empty()
278 }
279
280 pub fn insert_row(&mut self, node_id: u32, fields: &HashMap<String, Value>) {
282 for (field, idx) in &mut self.indexes {
283 if let Some(value) = fields.get(field) {
284 idx.insert(node_id, value);
285 }
286 }
287 }
288
289 pub fn delete_row(&mut self, node_id: u32, fields: &HashMap<String, Value>) {
291 for (field, idx) in &mut self.indexes {
292 if let Some(value) = fields.get(field) {
293 idx.delete(node_id, value);
294 }
295 }
296 }
297
298 pub fn definitions_only(&self) -> Self {
300 Self {
301 indexes: self
302 .indexes
303 .iter()
304 .map(|(field, index)| {
305 (
306 field.clone(),
307 PayloadIndex::new(index.field.clone(), index.kind),
308 )
309 })
310 .collect(),
311 }
312 }
313
314 pub fn clear_rows(&mut self) {
317 for idx in self.indexes.values_mut() {
318 idx.bitmaps.clear();
319 }
320 }
321
322 pub fn pre_filter(&self, predicate: &FilterPredicate) -> Option<RoaringBitmap> {
330 match predicate {
331 FilterPredicate::Eq { field, value } => {
332 let idx = self.indexes.get(field)?;
333 Some(
334 idx.equality(value)
335 .cloned()
336 .unwrap_or_else(RoaringBitmap::new),
337 )
338 }
339 FilterPredicate::In { field, values } => {
340 let idx = self.indexes.get(field)?;
341 let mut acc = RoaringBitmap::new();
342 for v in values {
343 if let Some(bm) = idx.equality(v) {
344 acc |= bm;
345 }
346 }
347 Some(acc)
348 }
349 FilterPredicate::Range {
350 field,
351 low,
352 low_inclusive,
353 high,
354 high_inclusive,
355 } => {
356 let idx = self.indexes.get(field)?;
357 idx.range(low.as_ref(), *low_inclusive, high.as_ref(), *high_inclusive)
358 }
359 FilterPredicate::And(preds) => {
360 let mut result: Option<RoaringBitmap> = None;
361 for pred in preds {
362 let bm = self.pre_filter(pred)?;
363 result = Some(match result {
364 None => bm,
365 Some(acc) => acc & bm,
366 });
367 }
368 Some(result.unwrap_or_default())
369 }
370 FilterPredicate::Or(preds) => {
371 let mut result = RoaringBitmap::new();
372 for pred in preds {
373 let bm = self.pre_filter(pred)?;
374 result |= bm;
375 }
376 Some(result)
377 }
378 FilterPredicate::Not(pred) => {
379 let _ = pred;
384 None
385 }
386 }
387 }
388
389 pub fn field_names(&self) -> impl Iterator<Item = &str> {
391 self.indexes.keys().map(String::as_str)
392 }
393}
394
395#[derive(Debug, Clone)]
400#[non_exhaustive]
401pub enum FilterPredicate {
402 Eq { field: String, value: Value },
404 In { field: String, values: Vec<Value> },
407 Range {
410 field: String,
411 low: Option<Value>,
412 low_inclusive: bool,
413 high: Option<Value>,
414 high_inclusive: bool,
415 },
416 And(Vec<FilterPredicate>),
418 Or(Vec<FilterPredicate>),
420 Not(Box<FilterPredicate>),
422}
423
424#[derive(
428 serde::Serialize, serde::Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
429)]
430pub struct PayloadIndexSnapshot {
431 pub field: String,
432 pub kind: PayloadIndexKind,
433 pub entries: Vec<(Vec<u8>, Vec<u8>)>,
436}
437
438#[derive(
440 serde::Serialize, serde::Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
441)]
442pub struct PayloadIndexSetSnapshot {
443 pub indexes: Vec<PayloadIndexSnapshot>,
444}
445
446impl PayloadIndexSet {
447 pub fn to_snapshot(&self) -> PayloadIndexSetSnapshot {
449 let indexes = self
450 .indexes
451 .values()
452 .map(|idx| {
453 let entries = idx
454 .bitmaps
455 .iter()
456 .filter_map(|(key, bm)| {
457 let key_bytes = zerompk::to_msgpack_vec(key).ok()?;
458 let mut bm_bytes = Vec::new();
459 bm.serialize_into(&mut bm_bytes).ok()?;
460 Some((key_bytes, bm_bytes))
461 })
462 .collect();
463 PayloadIndexSnapshot {
464 field: idx.field.clone(),
465 kind: idx.kind,
466 entries,
467 }
468 })
469 .collect();
470 PayloadIndexSetSnapshot { indexes }
471 }
472
473 pub fn from_snapshot(snap: PayloadIndexSetSnapshot) -> Self {
475 let mut indexes = HashMap::new();
476 for idx_snap in snap.indexes {
477 let mut bitmaps = PayloadIndexBitmaps::for_kind(idx_snap.kind);
478 for (key_bytes, bm_bytes) in idx_snap.entries {
479 let Ok(key) = zerompk::from_msgpack::<PayloadKey>(&key_bytes) else {
480 continue;
481 };
482 let Ok(bm) = RoaringBitmap::deserialize_from(bm_bytes.as_slice()) else {
483 continue;
484 };
485 *bitmaps.entry_or_default(key) = bm;
486 }
487 let idx = PayloadIndex {
488 field: idx_snap.field.clone(),
489 kind: idx_snap.kind,
490 bitmaps,
491 };
492 indexes.insert(idx_snap.field, idx);
493 }
494 Self { indexes }
495 }
496}
497
498#[cfg(test)]
501mod tests {
502 use super::*;
503
504 fn string_val(s: &str) -> Value {
505 Value::String(s.to_string())
506 }
507 fn int_val(i: i64) -> Value {
508 Value::Integer(i)
509 }
510
511 #[test]
512 fn insert_and_equality_lookup() {
513 let mut idx = PayloadIndex::new("category", PayloadIndexKind::Equality);
514 idx.insert(0, &string_val("A"));
515 idx.insert(1, &string_val("B"));
516 idx.insert(2, &string_val("A"));
517
518 let a_bm = idx.equality(&string_val("A")).unwrap();
519 assert!(a_bm.contains(0));
520 assert!(a_bm.contains(2));
521 assert!(!a_bm.contains(1));
522
523 let b_bm = idx.equality(&string_val("B")).unwrap();
524 assert!(b_bm.contains(1));
525 }
526
527 #[test]
528 fn delete_removes_from_bitmap() {
529 let mut idx = PayloadIndex::new("category", PayloadIndexKind::Equality);
530 idx.insert(0, &string_val("A"));
531 idx.insert(1, &string_val("A"));
532 idx.delete(0, &string_val("A"));
533
534 let bm = idx.equality(&string_val("A")).unwrap();
535 assert!(!bm.contains(0));
536 assert!(bm.contains(1));
537 }
538
539 #[test]
540 fn delete_empty_bitmap_removed() {
541 let mut idx = PayloadIndex::new("x", PayloadIndexKind::Equality);
542 idx.insert(5, &string_val("v"));
543 idx.delete(5, &string_val("v"));
544 assert!(idx.equality(&string_val("v")).is_none());
546 }
547
548 #[test]
549 fn pre_filter_equality() {
550 let mut set = PayloadIndexSet::default();
551 set.add_index("category", PayloadIndexKind::Equality);
552
553 let mut fields = HashMap::new();
554 for i in 0u32..10 {
555 fields.clear();
556 fields.insert(
557 "category".to_string(),
558 string_val(if i < 5 { "A" } else { "B" }),
559 );
560 set.insert_row(i, &fields);
561 }
562
563 let pred = FilterPredicate::Eq {
564 field: "category".to_string(),
565 value: string_val("A"),
566 };
567 let bm = set.pre_filter(&pred).unwrap();
568 assert_eq!(bm.len(), 5);
569 for i in 0..5u32 {
570 assert!(bm.contains(i));
571 }
572 }
573
574 #[test]
575 fn pre_filter_and() {
576 let mut set = PayloadIndexSet::default();
577 set.add_index("category", PayloadIndexKind::Equality);
578 set.add_index("active", PayloadIndexKind::Boolean);
579
580 let mut fields = HashMap::new();
581 fields.insert("category".to_string(), string_val("A"));
583 fields.insert("active".to_string(), Value::Bool(true));
584 set.insert_row(0, &fields);
585 fields.insert("active".to_string(), Value::Bool(false));
587 set.insert_row(1, &fields);
588 fields.insert("category".to_string(), string_val("B"));
590 fields.insert("active".to_string(), Value::Bool(true));
591 set.insert_row(2, &fields);
592
593 let pred = FilterPredicate::And(vec![
594 FilterPredicate::Eq {
595 field: "category".to_string(),
596 value: string_val("A"),
597 },
598 FilterPredicate::Eq {
599 field: "active".to_string(),
600 value: Value::Bool(true),
601 },
602 ]);
603 let bm = set.pre_filter(&pred).unwrap();
604 assert_eq!(bm.len(), 1);
605 assert!(bm.contains(0));
606 }
607
608 #[test]
609 fn pre_filter_or() {
610 let mut set = PayloadIndexSet::default();
611 set.add_index("category", PayloadIndexKind::Equality);
612
613 let mut fields = HashMap::new();
614 for i in 0u32..9 {
615 fields.clear();
616 let cat = match i % 3 {
617 0 => "A",
618 1 => "B",
619 _ => "C",
620 };
621 fields.insert("category".to_string(), string_val(cat));
622 set.insert_row(i, &fields);
623 }
624
625 let pred = FilterPredicate::Or(vec![
626 FilterPredicate::Eq {
627 field: "category".to_string(),
628 value: string_val("A"),
629 },
630 FilterPredicate::Eq {
631 field: "category".to_string(),
632 value: string_val("B"),
633 },
634 ]);
635 let bm = set.pre_filter(&pred).unwrap();
636 assert_eq!(bm.len(), 6);
637 }
638
639 #[test]
640 fn pre_filter_missing_index_returns_none() {
641 let set = PayloadIndexSet::default(); let pred = FilterPredicate::Eq {
644 field: "category".to_string(),
645 value: string_val("A"),
646 };
647 assert!(set.pre_filter(&pred).is_none());
648 }
649
650 #[test]
651 fn pre_filter_not_returns_none() {
652 let mut set = PayloadIndexSet::default();
653 set.add_index("category", PayloadIndexKind::Equality);
654
655 let pred = FilterPredicate::Not(Box::new(FilterPredicate::Eq {
656 field: "category".to_string(),
657 value: string_val("A"),
658 }));
659 assert!(set.pre_filter(&pred).is_none());
661 }
662
663 #[test]
664 fn pre_filter_and_partial_index_returns_none() {
665 let mut set = PayloadIndexSet::default();
667 set.add_index("category", PayloadIndexKind::Equality);
668
669 let pred = FilterPredicate::And(vec![
670 FilterPredicate::Eq {
671 field: "category".to_string(),
672 value: string_val("A"),
673 },
674 FilterPredicate::Eq {
675 field: "unindexed_field".to_string(),
676 value: int_val(42),
677 },
678 ]);
679 assert!(set.pre_filter(&pred).is_none());
680 }
681
682 #[test]
683 fn nan_equality() {
684 let mut idx = PayloadIndex::new("score", PayloadIndexKind::Equality);
685 idx.insert(0, &Value::Float(f64::NAN));
686 idx.insert(1, &Value::Float(f64::NAN));
687 idx.insert(2, &Value::Float(1.0));
688
689 let bm = idx.equality(&Value::Float(f64::NAN)).unwrap();
691 assert_eq!(bm.len(), 2);
692 assert!(bm.contains(0));
693 assert!(bm.contains(1));
694 }
695
696 #[test]
697 fn null_value_indexed() {
698 let mut idx = PayloadIndex::new("f", PayloadIndexKind::Equality);
699 idx.insert(0, &Value::Null);
700 idx.insert(1, &Value::Null);
701 let bm = idx.equality(&Value::Null).unwrap();
702 assert_eq!(bm.len(), 2);
703 }
704
705 #[test]
706 fn snapshot_roundtrip() {
707 let mut set = PayloadIndexSet::default();
708 set.add_index("category", PayloadIndexKind::Equality);
709
710 let mut fields = HashMap::new();
711 for i in 0u32..6 {
712 fields.clear();
713 fields.insert(
714 "category".to_string(),
715 string_val(if i < 3 { "A" } else { "B" }),
716 );
717 set.insert_row(i, &fields);
718 }
719
720 let snap = set.to_snapshot();
721 let snap_bytes = zerompk::to_msgpack_vec(&snap).unwrap();
722 let restored_snap: PayloadIndexSetSnapshot = zerompk::from_msgpack(&snap_bytes).unwrap();
723 let restored = PayloadIndexSet::from_snapshot(restored_snap);
724
725 let pred = FilterPredicate::Eq {
726 field: "category".to_string(),
727 value: string_val("A"),
728 };
729 let bm = restored.pre_filter(&pred).unwrap();
730 assert_eq!(bm.len(), 3);
731 }
732}