1use std::cell::RefCell;
14use std::collections::{BTreeMap, HashMap};
15use std::fmt;
16
17use nodedb_types::Surrogate;
18
19use crate::backend::FtsBackend;
20use crate::posting::Posting;
21use crate::scope::IndexScope;
22
23#[derive(Debug)]
25pub struct MemoryError(String);
26
27impl fmt::Display for MemoryError {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 write!(f, "memory backend: {}", self.0)
30 }
31}
32
33type IndexKey = (u64, u64, String, String);
35type SubKey = (IndexKey, String);
37type DocLenKey = (IndexKey, Surrogate);
39type PostingList = BTreeMap<Surrogate, Posting>;
42
43#[derive(Debug, Default)]
49pub struct MemoryBackend {
50 postings: RefCell<HashMap<SubKey, PostingList>>,
52 doc_lengths: RefCell<HashMap<DocLenKey, u32>>,
54 stats: RefCell<HashMap<IndexKey, (u32, u64)>>,
56 meta: RefCell<HashMap<SubKey, Vec<u8>>>,
58 segments: RefCell<HashMap<SubKey, Vec<u8>>>,
60}
61
62impl MemoryBackend {
63 pub fn new() -> Self {
64 Self::default()
65 }
66}
67
68fn index_key(database_id: u64, tid: u64, index: IndexScope<'_>) -> IndexKey {
69 (
70 database_id,
71 tid,
72 index.collection().to_string(),
73 index.field_key().to_string(),
74 )
75}
76
77fn sub_key(database_id: u64, tid: u64, index: IndexScope<'_>, sub: &str) -> SubKey {
78 (index_key(database_id, tid, index), sub.to_string())
79}
80
81fn doc_len_key(database_id: u64, tid: u64, index: IndexScope<'_>, doc_id: Surrogate) -> DocLenKey {
82 (index_key(database_id, tid, index), doc_id)
83}
84
85fn owned_by(key: &IndexKey, database_id: u64, tid: u64, collection: Option<&str>) -> bool {
87 key.0 == database_id && key.1 == tid && collection.is_none_or(|c| key.2 == c)
88}
89
90impl MemoryBackend {
91 fn purge_owned(&self, database_id: u64, tid: u64, collection: Option<&str>) -> usize {
93 let mut postings = self.postings.borrow_mut();
94 let mut doc_lengths = self.doc_lengths.borrow_mut();
95 let before = postings.len() + doc_lengths.len();
96 postings.retain(|k, _| !owned_by(&k.0, database_id, tid, collection));
97 doc_lengths.retain(|k, _| !owned_by(&k.0, database_id, tid, collection));
98 self.stats
99 .borrow_mut()
100 .retain(|k, _| !owned_by(k, database_id, tid, collection));
101 self.meta
102 .borrow_mut()
103 .retain(|k, _| !owned_by(&k.0, database_id, tid, collection));
104 self.segments
105 .borrow_mut()
106 .retain(|k, _| !owned_by(&k.0, database_id, tid, collection));
107 let after = postings.len() + doc_lengths.len();
108 before - after
109 }
110}
111
112impl FtsBackend for MemoryBackend {
113 type Error = MemoryError;
114
115 fn read_postings(
116 &self,
117 database_id: u64,
118 tid: u64,
119 index: IndexScope<'_>,
120 term: &str,
121 ) -> Result<Vec<Posting>, Self::Error> {
122 Ok(self
123 .postings
124 .borrow()
125 .get(&sub_key(database_id, tid, index, term))
126 .map(|list| list.values().cloned().collect())
127 .unwrap_or_default())
128 }
129
130 fn write_postings(
131 &self,
132 database_id: u64,
133 tid: u64,
134 index: IndexScope<'_>,
135 term: &str,
136 postings: &[Posting],
137 ) -> Result<(), Self::Error> {
138 let key = sub_key(database_id, tid, index, term);
139 let mut map = self.postings.borrow_mut();
140 if postings.is_empty() {
141 map.remove(&key);
142 } else {
143 let list: PostingList = postings.iter().map(|p| (p.doc_id, p.clone())).collect();
144 map.insert(key, list);
145 }
146 Ok(())
147 }
148
149 fn upsert_posting(
150 &self,
151 database_id: u64,
152 tid: u64,
153 index: IndexScope<'_>,
154 term: &str,
155 posting: &Posting,
156 ) -> Result<(), Self::Error> {
157 self.postings
158 .borrow_mut()
159 .entry(sub_key(database_id, tid, index, term))
160 .or_default()
161 .insert(posting.doc_id, posting.clone());
162 Ok(())
163 }
164
165 fn remove_posting(
166 &self,
167 database_id: u64,
168 tid: u64,
169 index: IndexScope<'_>,
170 term: &str,
171 doc_id: Surrogate,
172 ) -> Result<(), Self::Error> {
173 let key = sub_key(database_id, tid, index, term);
174 let mut map = self.postings.borrow_mut();
175 let emptied = match map.get_mut(&key) {
176 Some(list) => {
177 list.remove(&doc_id);
178 list.is_empty()
179 }
180 None => false,
181 };
182 if emptied {
183 map.remove(&key);
184 }
185 Ok(())
186 }
187
188 fn remove_postings(
189 &self,
190 database_id: u64,
191 tid: u64,
192 index: IndexScope<'_>,
193 term: &str,
194 ) -> Result<(), Self::Error> {
195 self.postings
196 .borrow_mut()
197 .remove(&sub_key(database_id, tid, index, term));
198 Ok(())
199 }
200
201 fn read_doc_length(
202 &self,
203 database_id: u64,
204 tid: u64,
205 index: IndexScope<'_>,
206 doc_id: Surrogate,
207 ) -> Result<Option<u32>, Self::Error> {
208 Ok(self
209 .doc_lengths
210 .borrow()
211 .get(&doc_len_key(database_id, tid, index, doc_id))
212 .copied())
213 }
214
215 fn write_doc_length(
216 &self,
217 database_id: u64,
218 tid: u64,
219 index: IndexScope<'_>,
220 doc_id: Surrogate,
221 length: u32,
222 ) -> Result<(), Self::Error> {
223 self.doc_lengths
224 .borrow_mut()
225 .insert(doc_len_key(database_id, tid, index, doc_id), length);
226 Ok(())
227 }
228
229 fn remove_doc_length(
230 &self,
231 database_id: u64,
232 tid: u64,
233 index: IndexScope<'_>,
234 doc_id: Surrogate,
235 ) -> Result<(), Self::Error> {
236 self.doc_lengths
237 .borrow_mut()
238 .remove(&doc_len_key(database_id, tid, index, doc_id));
239 Ok(())
240 }
241
242 fn collection_terms(
243 &self,
244 database_id: u64,
245 tid: u64,
246 index: IndexScope<'_>,
247 ) -> Result<Vec<String>, Self::Error> {
248 let key = index_key(database_id, tid, index);
249 Ok(self
250 .postings
251 .borrow()
252 .keys()
253 .filter(|(k, _)| *k == key)
254 .map(|(_, term)| term.clone())
255 .collect())
256 }
257
258 fn collection_stats(
259 &self,
260 database_id: u64,
261 tid: u64,
262 index: IndexScope<'_>,
263 ) -> Result<(u32, u64), Self::Error> {
264 Ok(self
265 .stats
266 .borrow()
267 .get(&index_key(database_id, tid, index))
268 .copied()
269 .unwrap_or((0, 0)))
270 }
271
272 fn increment_stats(
273 &self,
274 database_id: u64,
275 tid: u64,
276 index: IndexScope<'_>,
277 doc_len: u32,
278 ) -> Result<(), Self::Error> {
279 let mut stats = self.stats.borrow_mut();
280 let entry = stats
281 .entry(index_key(database_id, tid, index))
282 .or_insert((0, 0));
283 entry.0 += 1;
284 entry.1 += doc_len as u64;
285 Ok(())
286 }
287
288 fn decrement_stats(
289 &self,
290 database_id: u64,
291 tid: u64,
292 index: IndexScope<'_>,
293 doc_len: u32,
294 ) -> Result<(), Self::Error> {
295 let mut stats = self.stats.borrow_mut();
296 let entry = stats
297 .entry(index_key(database_id, tid, index))
298 .or_insert((0, 0));
299 entry.0 = entry.0.saturating_sub(1);
300 entry.1 = entry.1.saturating_sub(doc_len as u64);
301 Ok(())
302 }
303
304 fn read_meta(
305 &self,
306 database_id: u64,
307 tid: u64,
308 index: IndexScope<'_>,
309 subkey: &str,
310 ) -> Result<Option<Vec<u8>>, Self::Error> {
311 Ok(self
312 .meta
313 .borrow()
314 .get(&sub_key(database_id, tid, index, subkey))
315 .cloned())
316 }
317
318 fn write_meta(
319 &self,
320 database_id: u64,
321 tid: u64,
322 index: IndexScope<'_>,
323 subkey: &str,
324 value: &[u8],
325 ) -> Result<(), Self::Error> {
326 self.meta
327 .borrow_mut()
328 .insert(sub_key(database_id, tid, index, subkey), value.to_vec());
329 Ok(())
330 }
331
332 fn write_segment(
333 &self,
334 database_id: u64,
335 tid: u64,
336 index: IndexScope<'_>,
337 segment_id: &str,
338 data: &[u8],
339 ) -> Result<(), Self::Error> {
340 self.segments
341 .borrow_mut()
342 .insert(sub_key(database_id, tid, index, segment_id), data.to_vec());
343 Ok(())
344 }
345
346 fn read_segment(
347 &self,
348 database_id: u64,
349 tid: u64,
350 index: IndexScope<'_>,
351 segment_id: &str,
352 ) -> Result<Option<Vec<u8>>, Self::Error> {
353 Ok(self
354 .segments
355 .borrow()
356 .get(&sub_key(database_id, tid, index, segment_id))
357 .cloned())
358 }
359
360 fn list_segments(
361 &self,
362 database_id: u64,
363 tid: u64,
364 index: IndexScope<'_>,
365 ) -> Result<Vec<String>, Self::Error> {
366 let key = index_key(database_id, tid, index);
367 Ok(self
368 .segments
369 .borrow()
370 .keys()
371 .filter(|(k, _)| *k == key)
372 .map(|(_, seg)| seg.clone())
373 .collect())
374 }
375
376 fn remove_segment(
377 &self,
378 database_id: u64,
379 tid: u64,
380 index: IndexScope<'_>,
381 segment_id: &str,
382 ) -> Result<(), Self::Error> {
383 self.segments
384 .borrow_mut()
385 .remove(&sub_key(database_id, tid, index, segment_id));
386 Ok(())
387 }
388
389 fn purge_collection(
390 &self,
391 database_id: u64,
392 tid: u64,
393 collection: &str,
394 ) -> Result<usize, Self::Error> {
395 Ok(self.purge_owned(database_id, tid, Some(collection)))
396 }
397
398 fn purge_tenant(&self, database_id: u64, tid: u64) -> Result<usize, Self::Error> {
399 Ok(self.purge_owned(database_id, tid, None))
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 const DB: u64 = 0;
408 const T: u64 = 1;
409 const COL: IndexScope<'static> = IndexScope::document("col");
410 const OTHER: IndexScope<'static> = IndexScope::document("other");
411
412 fn posting(position: u32) -> Vec<Posting> {
413 vec![Posting {
414 doc_id: Surrogate(1),
415 term_freq: 1,
416 positions: vec![position],
417 }]
418 }
419
420 #[test]
421 fn roundtrip_postings() {
422 let backend = MemoryBackend::new();
423 let postings = vec![Posting {
424 doc_id: Surrogate(1),
425 term_freq: 2,
426 positions: vec![0, 5],
427 }];
428 backend
429 .write_postings(DB, T, COL, "hello", &postings)
430 .unwrap();
431
432 let read = backend.read_postings(DB, T, COL, "hello").unwrap();
433 assert_eq!(read.len(), 1);
434 assert_eq!(read[0].doc_id, Surrogate(1));
435 }
436
437 #[test]
438 fn upsert_replaces_one_document_and_remove_empties_the_list() {
439 let backend = MemoryBackend::new();
440 let doc = |id: u32, freq: u32| Posting {
441 doc_id: Surrogate(id),
442 term_freq: freq,
443 positions: vec![0],
444 };
445 for (id, freq) in [(3, 1), (1, 1), (2, 1), (1, 4)] {
446 backend
447 .upsert_posting(DB, T, COL, "ab", &doc(id, freq))
448 .unwrap();
449 }
450 backend
451 .upsert_posting(DB, T, COL, "abc", &doc(9, 1))
452 .unwrap();
453
454 let ab = backend.read_postings(DB, T, COL, "ab").unwrap();
455 let ids: Vec<(u32, u32)> = ab.iter().map(|p| (p.doc_id.0, p.term_freq)).collect();
456 assert_eq!(ids, vec![(1, 4), (2, 1), (3, 1)]);
457 assert_eq!(backend.read_postings(DB, T, COL, "abc").unwrap().len(), 1);
458
459 for id in [1, 2, 3] {
460 backend
461 .remove_posting(DB, T, COL, "ab", Surrogate(id))
462 .unwrap();
463 }
464 assert!(backend.read_postings(DB, T, COL, "ab").unwrap().is_empty());
465 assert_eq!(backend.collection_terms(DB, T, COL).unwrap(), vec!["abc"]);
466 backend
467 .remove_posting(DB, T, COL, "missing", Surrogate(1))
468 .unwrap();
469 }
470
471 #[test]
472 fn roundtrip_doc_lengths() {
473 let backend = MemoryBackend::new();
474 backend
475 .write_doc_length(DB, T, COL, Surrogate(1), 42)
476 .unwrap();
477 assert_eq!(
478 backend.read_doc_length(DB, T, COL, Surrogate(1)).unwrap(),
479 Some(42)
480 );
481
482 backend.remove_doc_length(DB, T, COL, Surrogate(1)).unwrap();
483 assert_eq!(
484 backend.read_doc_length(DB, T, COL, Surrogate(1)).unwrap(),
485 None
486 );
487 }
488
489 #[test]
490 fn incremental_stats() {
491 let backend = MemoryBackend::new();
492 backend.increment_stats(DB, T, COL, 10).unwrap();
493 backend.increment_stats(DB, T, COL, 20).unwrap();
494 assert_eq!(backend.collection_stats(DB, T, COL).unwrap(), (2, 30));
495
496 backend.decrement_stats(DB, T, COL, 10).unwrap();
497 assert_eq!(backend.collection_stats(DB, T, COL).unwrap(), (1, 20));
498 }
499
500 #[test]
501 fn stats_saturating_sub() {
502 let backend = MemoryBackend::new();
503 backend.decrement_stats(DB, T, COL, 100).unwrap();
504 assert_eq!(backend.collection_stats(DB, T, COL).unwrap(), (0, 0));
505 }
506
507 #[test]
508 fn field_scopes_are_isolated_from_the_document_scope() {
509 let backend = MemoryBackend::new();
510 let title = IndexScope::field("col", "title").unwrap();
511 backend
512 .write_postings(DB, T, title, "rust", &posting(0))
513 .unwrap();
514 backend.increment_stats(DB, T, title, 3).unwrap();
515 backend
516 .write_doc_length(DB, T, title, Surrogate(1), 3)
517 .unwrap();
518
519 assert!(
520 backend
521 .read_postings(DB, T, COL, "rust")
522 .unwrap()
523 .is_empty()
524 );
525 assert_eq!(backend.collection_stats(DB, T, COL).unwrap(), (0, 0));
526 assert_eq!(
527 backend.read_doc_length(DB, T, COL, Surrogate(1)).unwrap(),
528 None
529 );
530 assert_eq!(
531 backend.collection_terms(DB, T, title).unwrap(),
532 vec!["rust"]
533 );
534 assert!(backend.collection_terms(DB, T, COL).unwrap().is_empty());
535 }
536
537 #[test]
538 fn purge_clears_stats_and_isolates_collections() {
539 let backend = MemoryBackend::new();
540 let title = IndexScope::field("col", "title").unwrap();
541 backend.increment_stats(DB, T, COL, 10).unwrap();
542 backend.increment_stats(DB, T, title, 2).unwrap();
543 backend
544 .write_doc_length(DB, T, COL, Surrogate(1), 10)
545 .unwrap();
546 backend
547 .write_postings(DB, T, COL, "hello", &posting(0))
548 .unwrap();
549 backend
550 .write_postings(DB, T, title, "hello", &posting(0))
551 .unwrap();
552
553 backend.increment_stats(DB, T, OTHER, 7).unwrap();
554 backend
555 .write_doc_length(DB, T, OTHER, Surrogate(1), 7)
556 .unwrap();
557 backend
558 .write_postings(DB, T, OTHER, "world", &posting(0))
559 .unwrap();
560
561 backend.purge_collection(DB, T, "col").unwrap();
562 assert_eq!(backend.collection_stats(DB, T, COL).unwrap(), (0, 0));
563 assert_eq!(backend.collection_stats(DB, T, title).unwrap(), (0, 0));
564 assert!(
565 backend
566 .read_postings(DB, T, COL, "hello")
567 .unwrap()
568 .is_empty()
569 );
570 assert!(
571 backend
572 .read_postings(DB, T, title, "hello")
573 .unwrap()
574 .is_empty()
575 );
576 assert_eq!(
577 backend.read_doc_length(DB, T, COL, Surrogate(1)).unwrap(),
578 None
579 );
580
581 assert_eq!(backend.collection_stats(DB, T, OTHER).unwrap(), (1, 7));
582 assert_eq!(
583 backend.read_postings(DB, T, OTHER, "world").unwrap().len(),
584 1
585 );
586 assert_eq!(
587 backend.read_doc_length(DB, T, OTHER, Surrogate(1)).unwrap(),
588 Some(7)
589 );
590 }
591
592 #[test]
593 fn collection_terms() {
594 let backend = MemoryBackend::new();
595 backend
596 .write_postings(DB, T, COL, "hello", &posting(0))
597 .unwrap();
598 backend
599 .write_postings(DB, T, COL, "world", &posting(1))
600 .unwrap();
601
602 let mut terms = backend.collection_terms(DB, T, COL).unwrap();
603 terms.sort();
604 assert_eq!(terms, vec!["hello", "world"]);
605 }
606
607 #[test]
608 fn segment_roundtrip() {
609 let backend = MemoryBackend::new();
610 let data = b"compressed segment bytes";
611 backend.write_segment(DB, T, COL, "id1", data).unwrap();
612 assert_eq!(
613 backend.read_segment(DB, T, COL, "id1").unwrap(),
614 Some(data.to_vec())
615 );
616 assert_eq!(backend.read_segment(DB, T, COL, "missing").unwrap(), None);
617 }
618
619 #[test]
620 fn segment_list_filters_by_index() {
621 let backend = MemoryBackend::new();
622 let title = IndexScope::field("col", "title").unwrap();
623 backend.write_segment(DB, T, COL, "a", b"a").unwrap();
624 backend.write_segment(DB, T, COL, "b", b"b").unwrap();
625 backend.write_segment(DB, T, OTHER, "c", b"c").unwrap();
626 backend.write_segment(DB, T, title, "d", b"d").unwrap();
627
628 let mut segs = backend.list_segments(DB, T, COL).unwrap();
629 segs.sort();
630 assert_eq!(segs, vec!["a", "b"]);
631
632 assert_eq!(backend.list_segments(DB, T, OTHER).unwrap(), vec!["c"]);
633 assert_eq!(backend.list_segments(DB, T, title).unwrap(), vec!["d"]);
634 }
635
636 #[test]
637 fn segment_remove() {
638 let backend = MemoryBackend::new();
639 backend.write_segment(DB, T, COL, "id1", b"data").unwrap();
640 backend.remove_segment(DB, T, COL, "id1").unwrap();
641 assert_eq!(backend.read_segment(DB, T, COL, "id1").unwrap(), None);
642 }
643
644 #[test]
645 fn purge_clears_segments() {
646 let backend = MemoryBackend::new();
647 backend.write_segment(DB, T, COL, "a", b"a").unwrap();
648 backend.write_segment(DB, T, OTHER, "b", b"b").unwrap();
649
650 backend.purge_collection(DB, T, "col").unwrap();
651 assert!(backend.list_segments(DB, T, COL).unwrap().is_empty());
652 assert_eq!(backend.list_segments(DB, T, OTHER).unwrap().len(), 1);
653 }
654
655 #[test]
656 fn purge_tenant_isolates_tenants() {
657 let backend = MemoryBackend::new();
658 backend.increment_stats(DB, 1, COL, 5).unwrap();
659 backend.increment_stats(DB, 2, COL, 7).unwrap();
660 backend
661 .write_postings(DB, 1, COL, "t", &posting(0))
662 .unwrap();
663 backend
664 .write_postings(DB, 2, COL, "t", &posting(0))
665 .unwrap();
666
667 backend.purge_tenant(DB, 1).unwrap();
668 assert_eq!(backend.collection_stats(DB, 1, COL).unwrap(), (0, 0));
669 assert!(backend.read_postings(DB, 1, COL, "t").unwrap().is_empty());
670 assert_eq!(backend.collection_stats(DB, 2, COL).unwrap(), (1, 7));
671 assert_eq!(backend.read_postings(DB, 2, COL, "t").unwrap().len(), 1);
672 }
673
674 #[test]
675 fn databases_isolated() {
676 let backend = MemoryBackend::new();
677 backend.increment_stats(0, T, COL, 5).unwrap();
678 backend.increment_stats(9, T, COL, 7).unwrap();
679 backend.write_postings(0, T, COL, "t", &posting(0)).unwrap();
680 backend.write_postings(9, T, COL, "t", &posting(0)).unwrap();
681
682 backend.purge_tenant(0, T).unwrap();
683 assert_eq!(backend.collection_stats(0, T, COL).unwrap(), (0, 0));
684 assert!(backend.read_postings(0, T, COL, "t").unwrap().is_empty());
685 assert_eq!(backend.collection_stats(9, T, COL).unwrap(), (1, 7));
687 assert_eq!(backend.read_postings(9, T, COL, "t").unwrap().len(), 1);
688 }
689}