1use std::collections::{HashMap, HashSet};
9
10#[derive(Clone, Debug, PartialEq, Eq, Hash)]
16pub enum MetadataField {
17 Tag(String),
19 ContentType(String),
21 Owner(String),
23 SizeBucket(u64),
25 TickBucket(u64),
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct MetadataIndexEntry {
36 pub block_id: u64,
38 pub cid: String,
40 pub fields: Vec<MetadataField>,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum MetadataSortField {
51 BlockId,
53 Cid,
55}
56
57#[derive(Clone, Debug, Default)]
63pub struct MetadataQuery {
64 pub must_have: Vec<MetadataField>,
66 pub must_not_have: Vec<MetadataField>,
68 pub sort_by: Option<MetadataSortField>,
70 pub limit: Option<usize>,
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
80pub struct MetadataQueryResult {
81 pub block_id: u64,
83 pub cid: String,
85 pub matched_fields: Vec<MetadataField>,
87}
88
89#[derive(Clone, Debug, Default, PartialEq, Eq)]
95pub struct MetadataIndexStats {
96 pub total_entries: usize,
98 pub total_fields_indexed: usize,
100 pub total_queries: u64,
102}
103
104pub struct StorageMetadataIndex {
123 pub entries: HashMap<u64, MetadataIndexEntry>,
125 pub field_index: HashMap<MetadataField, Vec<u64>>,
127 pub stats: MetadataIndexStats,
129}
130
131impl StorageMetadataIndex {
132 pub fn new() -> Self {
136 Self {
137 entries: HashMap::new(),
138 field_index: HashMap::new(),
139 stats: MetadataIndexStats::default(),
140 }
141 }
142
143 pub fn insert(&mut self, entry: MetadataIndexEntry) {
150 if let Some(old) = self.entries.remove(&entry.block_id) {
152 self.stats.total_entries -= 1;
153 self.stats.total_fields_indexed -= old.fields.len();
154 for field in &old.fields {
155 if let Some(ids) = self.field_index.get_mut(field) {
156 ids.retain(|&id| id != old.block_id);
157 if ids.is_empty() {
158 self.field_index.remove(field);
159 }
160 }
161 }
162 }
163
164 let block_id = entry.block_id;
166 let field_count = entry.fields.len();
167 for field in &entry.fields {
168 self.field_index
169 .entry(field.clone())
170 .or_default()
171 .push(block_id);
172 }
173
174 self.entries.insert(block_id, entry);
175 self.stats.total_entries += 1;
176 self.stats.total_fields_indexed += field_count;
177 }
178
179 pub fn remove(&mut self, block_id: u64) -> bool {
183 match self.entries.remove(&block_id) {
184 None => false,
185 Some(old) => {
186 self.stats.total_entries -= 1;
187 self.stats.total_fields_indexed -= old.fields.len();
188 for field in &old.fields {
189 if let Some(ids) = self.field_index.get_mut(field) {
190 ids.retain(|&id| id != block_id);
191 if ids.is_empty() {
192 self.field_index.remove(field);
193 }
194 }
195 }
196 true
197 }
198 }
199 }
200
201 pub fn query(&mut self, q: &MetadataQuery) -> Vec<MetadataQueryResult> {
218 let candidates: HashSet<u64> = if q.must_have.is_empty() {
220 self.entries.keys().copied().collect()
221 } else {
222 let mut iter = q.must_have.iter();
225 let first_field = iter.next().expect("must_have checked non-empty");
227 let initial: HashSet<u64> = self
228 .field_index
229 .get(first_field)
230 .map(|v| v.iter().copied().collect())
231 .unwrap_or_default();
232
233 iter.fold(initial, |acc, field| {
234 let posting: HashSet<u64> = self
235 .field_index
236 .get(field)
237 .map(|v| v.iter().copied().collect())
238 .unwrap_or_default();
239 acc.intersection(&posting).copied().collect()
240 })
241 };
242
243 let exclude_set: HashSet<&MetadataField> = q.must_not_have.iter().collect();
245
246 let mut results: Vec<MetadataQueryResult> = candidates
248 .into_iter()
249 .filter_map(|block_id| {
250 let entry = self.entries.get(&block_id)?;
251 let excluded = entry.fields.iter().any(|f| exclude_set.contains(f));
253 if excluded {
254 return None;
255 }
256 let matched_fields: Vec<MetadataField> = q
258 .must_have
259 .iter()
260 .filter(|f| entry.fields.contains(f))
261 .cloned()
262 .collect();
263 Some(MetadataQueryResult {
264 block_id: entry.block_id,
265 cid: entry.cid.clone(),
266 matched_fields,
267 })
268 })
269 .collect();
270
271 match q.sort_by {
273 Some(MetadataSortField::BlockId) => {
274 results.sort_by_key(|r| r.block_id);
275 }
276 Some(MetadataSortField::Cid) => {
277 results.sort_by(|a, b| a.cid.cmp(&b.cid));
278 }
279 None => {}
280 }
281
282 if let Some(limit) = q.limit {
284 results.truncate(limit);
285 }
286
287 self.stats.total_queries += 1;
289
290 results
291 }
292
293 pub fn get(&self, block_id: u64) -> Option<&MetadataIndexEntry> {
297 self.entries.get(&block_id)
298 }
299
300 pub fn stats(&self) -> &MetadataIndexStats {
302 &self.stats
303 }
304}
305
306impl Default for StorageMetadataIndex {
307 fn default() -> Self {
308 Self::new()
309 }
310}
311
312#[cfg(test)]
317mod tests {
318 use super::*;
319
320 fn make_entry(block_id: u64, cid: &str, fields: Vec<MetadataField>) -> MetadataIndexEntry {
323 MetadataIndexEntry {
324 block_id,
325 cid: cid.to_string(),
326 fields,
327 }
328 }
329
330 fn png_entry(block_id: u64) -> MetadataIndexEntry {
331 make_entry(
332 block_id,
333 &format!("cid-{block_id}"),
334 vec![
335 MetadataField::ContentType("image/png".to_string()),
336 MetadataField::Tag("photo".to_string()),
337 MetadataField::Owner("alice".to_string()),
338 MetadataField::SizeBucket(block_id % 4),
339 MetadataField::TickBucket(block_id / 5),
340 ],
341 )
342 }
343
344 #[test]
347 fn test_insert_creates_entry() {
348 let mut idx = StorageMetadataIndex::new();
349 idx.insert(png_entry(1));
350 assert!(idx.get(1).is_some());
351 assert_eq!(idx.stats().total_entries, 1);
352 }
353
354 #[test]
355 fn test_insert_updates_field_index() {
356 let mut idx = StorageMetadataIndex::new();
357 idx.insert(png_entry(1));
358 let key = MetadataField::ContentType("image/png".to_string());
359 assert!(idx.field_index.contains_key(&key));
360 assert!(idx.field_index[&key].contains(&1));
361 }
362
363 #[test]
364 fn test_insert_multiple_entries_share_field() {
365 let mut idx = StorageMetadataIndex::new();
366 idx.insert(png_entry(1));
367 idx.insert(png_entry(2));
368 let key = MetadataField::ContentType("image/png".to_string());
369 let ids = &idx.field_index[&key];
370 assert!(ids.contains(&1));
371 assert!(ids.contains(&2));
372 }
373
374 #[test]
375 fn test_insert_replaces_existing_entry() {
376 let mut idx = StorageMetadataIndex::new();
377 idx.insert(make_entry(
378 10,
379 "cid-old",
380 vec![MetadataField::Tag("old-tag".to_string())],
381 ));
382 idx.insert(make_entry(
384 10,
385 "cid-new",
386 vec![MetadataField::Tag("new-tag".to_string())],
387 ));
388 assert_eq!(idx.stats().total_entries, 1);
389 assert_eq!(idx.get(10).unwrap().cid, "cid-new");
390 let old_key = MetadataField::Tag("old-tag".to_string());
392 assert!(!idx.field_index.contains_key(&old_key));
393 let new_key = MetadataField::Tag("new-tag".to_string());
395 assert!(idx.field_index.contains_key(&new_key));
396 }
397
398 #[test]
399 fn test_insert_replace_updates_stats_correctly() {
400 let mut idx = StorageMetadataIndex::new();
401 idx.insert(make_entry(
402 5,
403 "cid-5",
404 vec![
405 MetadataField::Tag("a".to_string()),
406 MetadataField::Tag("b".to_string()),
407 ],
408 ));
409 assert_eq!(idx.stats().total_fields_indexed, 2);
410 idx.insert(make_entry(
412 5,
413 "cid-5",
414 vec![
415 MetadataField::Tag("x".to_string()),
416 MetadataField::Tag("y".to_string()),
417 MetadataField::Tag("z".to_string()),
418 ],
419 ));
420 assert_eq!(idx.stats().total_entries, 1);
421 assert_eq!(idx.stats().total_fields_indexed, 3);
422 }
423
424 #[test]
427 fn test_remove_returns_true_when_present() {
428 let mut idx = StorageMetadataIndex::new();
429 idx.insert(png_entry(1));
430 assert!(idx.remove(1));
431 }
432
433 #[test]
434 fn test_remove_returns_false_when_absent() {
435 let mut idx = StorageMetadataIndex::new();
436 assert!(!idx.remove(999));
437 }
438
439 #[test]
440 fn test_remove_cleans_field_index() {
441 let mut idx = StorageMetadataIndex::new();
442 idx.insert(png_entry(1));
443 idx.remove(1);
444 let key = MetadataField::ContentType("image/png".to_string());
445 assert!(!idx.field_index.contains_key(&key));
446 }
447
448 #[test]
449 fn test_remove_partial_field_index_cleanup() {
450 let mut idx = StorageMetadataIndex::new();
451 idx.insert(png_entry(1));
452 idx.insert(png_entry(2));
453 idx.remove(1);
454 let key = MetadataField::ContentType("image/png".to_string());
455 assert!(idx.field_index.contains_key(&key));
457 assert!(!idx.field_index[&key].contains(&1));
458 assert!(idx.field_index[&key].contains(&2));
459 }
460
461 #[test]
462 fn test_remove_updates_stats() {
463 let mut idx = StorageMetadataIndex::new();
464 idx.insert(png_entry(1));
465 let initial_fields = idx.stats().total_fields_indexed;
466 idx.remove(1);
467 assert_eq!(idx.stats().total_entries, 0);
468 assert_eq!(idx.stats().total_fields_indexed, 0);
469 assert!(initial_fields > 0);
470 }
471
472 #[test]
475 fn test_get_some() {
476 let mut idx = StorageMetadataIndex::new();
477 idx.insert(png_entry(42));
478 assert!(idx.get(42).is_some());
479 assert_eq!(idx.get(42).unwrap().cid, "cid-42");
480 }
481
482 #[test]
483 fn test_get_none() {
484 let idx = StorageMetadataIndex::new();
485 assert!(idx.get(0).is_none());
486 }
487
488 #[test]
491 fn test_query_must_have_single_field_matches() {
492 let mut idx = StorageMetadataIndex::new();
493 idx.insert(png_entry(1));
494 idx.insert(png_entry(2));
495 idx.insert(make_entry(
496 3,
497 "cid-3",
498 vec![MetadataField::ContentType("text/plain".to_string())],
499 ));
500
501 let q = MetadataQuery {
502 must_have: vec![MetadataField::ContentType("image/png".to_string())],
503 must_not_have: vec![],
504 sort_by: Some(MetadataSortField::BlockId),
505 limit: None,
506 };
507 let results = idx.query(&q);
508 assert_eq!(results.len(), 2);
509 assert_eq!(results[0].block_id, 1);
510 assert_eq!(results[1].block_id, 2);
511 }
512
513 #[test]
514 fn test_query_must_have_single_field_no_match() {
515 let mut idx = StorageMetadataIndex::new();
516 idx.insert(png_entry(1));
517 let q = MetadataQuery {
518 must_have: vec![MetadataField::Tag("nonexistent".to_string())],
519 ..Default::default()
520 };
521 let results = idx.query(&q);
522 assert!(results.is_empty());
523 }
524
525 #[test]
528 fn test_query_must_have_multiple_fields_and_semantics() {
529 let mut idx = StorageMetadataIndex::new();
530 idx.insert(png_entry(1));
532 idx.insert(make_entry(
534 2,
535 "cid-2",
536 vec![MetadataField::ContentType("image/png".to_string())],
537 ));
538
539 let q = MetadataQuery {
540 must_have: vec![
541 MetadataField::ContentType("image/png".to_string()),
542 MetadataField::Tag("photo".to_string()),
543 ],
544 ..Default::default()
545 };
546 let results = idx.query(&q);
547 assert_eq!(results.len(), 1);
548 assert_eq!(results[0].block_id, 1);
549 }
550
551 #[test]
552 fn test_query_must_have_matched_fields_populated() {
553 let mut idx = StorageMetadataIndex::new();
554 idx.insert(png_entry(1));
555 let q = MetadataQuery {
556 must_have: vec![
557 MetadataField::ContentType("image/png".to_string()),
558 MetadataField::Tag("photo".to_string()),
559 ],
560 sort_by: Some(MetadataSortField::BlockId),
561 ..Default::default()
562 };
563 let results = idx.query(&q);
564 assert_eq!(results.len(), 1);
565 assert!(results[0]
566 .matched_fields
567 .contains(&MetadataField::ContentType("image/png".to_string())));
568 assert!(results[0]
569 .matched_fields
570 .contains(&MetadataField::Tag("photo".to_string())));
571 }
572
573 #[test]
576 fn test_query_must_not_have_excludes_entries() {
577 let mut idx = StorageMetadataIndex::new();
578 idx.insert(png_entry(1)); idx.insert(make_entry(
580 2,
581 "cid-2",
582 vec![
583 MetadataField::ContentType("image/png".to_string()),
584 MetadataField::Owner("bob".to_string()),
585 ],
586 ));
587
588 let q = MetadataQuery {
589 must_have: vec![MetadataField::ContentType("image/png".to_string())],
590 must_not_have: vec![MetadataField::Owner("alice".to_string())],
591 sort_by: Some(MetadataSortField::BlockId),
592 limit: None,
593 };
594 let results = idx.query(&q);
595 assert_eq!(results.len(), 1);
596 assert_eq!(results[0].block_id, 2);
597 }
598
599 #[test]
600 fn test_query_must_not_have_excludes_all() {
601 let mut idx = StorageMetadataIndex::new();
602 idx.insert(png_entry(1));
603 let q = MetadataQuery {
604 must_have: vec![MetadataField::ContentType("image/png".to_string())],
605 must_not_have: vec![MetadataField::Tag("photo".to_string())],
606 ..Default::default()
607 };
608 let results = idx.query(&q);
609 assert!(results.is_empty());
610 }
611
612 #[test]
615 fn test_query_sort_by_block_id_ascending() {
616 let mut idx = StorageMetadataIndex::new();
617 for id in [5u64, 3, 1, 4, 2] {
618 idx.insert(make_entry(
619 id,
620 &format!("cid-{id}"),
621 vec![MetadataField::Tag("common".to_string())],
622 ));
623 }
624 let q = MetadataQuery {
625 must_have: vec![MetadataField::Tag("common".to_string())],
626 sort_by: Some(MetadataSortField::BlockId),
627 ..Default::default()
628 };
629 let results = idx.query(&q);
630 let ids: Vec<u64> = results.iter().map(|r| r.block_id).collect();
631 assert_eq!(ids, vec![1, 2, 3, 4, 5]);
632 }
633
634 #[test]
635 fn test_query_sort_by_cid_alphabetical_ascending() {
636 let mut idx = StorageMetadataIndex::new();
637 let entries = vec![
638 make_entry(1, "zed", vec![MetadataField::Tag("t".to_string())]),
639 make_entry(2, "alpha", vec![MetadataField::Tag("t".to_string())]),
640 make_entry(3, "mango", vec![MetadataField::Tag("t".to_string())]),
641 ];
642 for e in entries {
643 idx.insert(e);
644 }
645 let q = MetadataQuery {
646 must_have: vec![MetadataField::Tag("t".to_string())],
647 sort_by: Some(MetadataSortField::Cid),
648 ..Default::default()
649 };
650 let results = idx.query(&q);
651 let cids: Vec<&str> = results.iter().map(|r| r.cid.as_str()).collect();
652 assert_eq!(cids, vec!["alpha", "mango", "zed"]);
653 }
654
655 #[test]
658 fn test_query_limit_truncates_results() {
659 let mut idx = StorageMetadataIndex::new();
660 for id in 1..=10u64 {
661 idx.insert(make_entry(
662 id,
663 &format!("cid-{id}"),
664 vec![MetadataField::Tag("bulk".to_string())],
665 ));
666 }
667 let q = MetadataQuery {
668 must_have: vec![MetadataField::Tag("bulk".to_string())],
669 sort_by: Some(MetadataSortField::BlockId),
670 limit: Some(3),
671 ..Default::default()
672 };
673 let results = idx.query(&q);
674 assert_eq!(results.len(), 3);
675 assert_eq!(results[0].block_id, 1);
677 assert_eq!(results[1].block_id, 2);
678 assert_eq!(results[2].block_id, 3);
679 }
680
681 #[test]
682 fn test_query_limit_zero_returns_empty() {
683 let mut idx = StorageMetadataIndex::new();
684 idx.insert(png_entry(1));
685 let q = MetadataQuery {
686 must_have: vec![MetadataField::ContentType("image/png".to_string())],
687 limit: Some(0),
688 ..Default::default()
689 };
690 let results = idx.query(&q);
691 assert!(results.is_empty());
692 }
693
694 #[test]
697 fn test_query_empty_must_have_returns_all() {
698 let mut idx = StorageMetadataIndex::new();
699 idx.insert(png_entry(1));
700 idx.insert(png_entry(2));
701 idx.insert(make_entry(
702 3,
703 "cid-3",
704 vec![MetadataField::Owner("eve".to_string())],
705 ));
706
707 let q = MetadataQuery {
708 must_have: vec![],
709 sort_by: Some(MetadataSortField::BlockId),
710 ..Default::default()
711 };
712 let results = idx.query(&q);
713 assert_eq!(results.len(), 3);
714 }
715
716 #[test]
717 fn test_query_empty_must_have_matched_fields_is_empty() {
718 let mut idx = StorageMetadataIndex::new();
719 idx.insert(png_entry(1));
720 let q = MetadataQuery::default();
721 let results = idx.query(&q);
722 assert_eq!(results.len(), 1);
723 assert!(results[0].matched_fields.is_empty());
724 }
725
726 #[test]
729 fn test_stats_total_fields_indexed() {
730 let mut idx = StorageMetadataIndex::new();
731 idx.insert(make_entry(
732 1,
733 "a",
734 vec![
735 MetadataField::Tag("x".to_string()),
736 MetadataField::SizeBucket(1),
737 ],
738 ));
739 idx.insert(make_entry(
740 2,
741 "b",
742 vec![MetadataField::Tag("y".to_string())],
743 ));
744 assert_eq!(idx.stats().total_fields_indexed, 3);
745 }
746
747 #[test]
748 fn test_stats_total_queries_increments() {
749 let mut idx = StorageMetadataIndex::new();
750 idx.insert(png_entry(1));
751 let q = MetadataQuery::default();
752 idx.query(&q);
753 idx.query(&q);
754 idx.query(&q);
755 assert_eq!(idx.stats().total_queries, 3);
756 }
757
758 #[test]
759 fn test_stats_total_entries_after_operations() {
760 let mut idx = StorageMetadataIndex::new();
761 idx.insert(png_entry(1));
762 idx.insert(png_entry(2));
763 idx.insert(png_entry(3));
764 idx.remove(2);
765 assert_eq!(idx.stats().total_entries, 2);
766 }
767
768 #[test]
771 fn test_query_owner_field() {
772 let mut idx = StorageMetadataIndex::new();
773 idx.insert(make_entry(
774 1,
775 "c1",
776 vec![MetadataField::Owner("alice".to_string())],
777 ));
778 idx.insert(make_entry(
779 2,
780 "c2",
781 vec![MetadataField::Owner("bob".to_string())],
782 ));
783 let q = MetadataQuery {
784 must_have: vec![MetadataField::Owner("alice".to_string())],
785 ..Default::default()
786 };
787 let results = idx.query(&q);
788 assert_eq!(results.len(), 1);
789 assert_eq!(results[0].block_id, 1);
790 }
791
792 #[test]
793 fn test_query_size_bucket_field() {
794 let mut idx = StorageMetadataIndex::new();
795 idx.insert(make_entry(1, "c1", vec![MetadataField::SizeBucket(0)]));
796 idx.insert(make_entry(2, "c2", vec![MetadataField::SizeBucket(1)]));
797 idx.insert(make_entry(3, "c3", vec![MetadataField::SizeBucket(0)]));
798 let q = MetadataQuery {
799 must_have: vec![MetadataField::SizeBucket(0)],
800 sort_by: Some(MetadataSortField::BlockId),
801 ..Default::default()
802 };
803 let results = idx.query(&q);
804 assert_eq!(results.len(), 2);
805 assert_eq!(results[0].block_id, 1);
806 assert_eq!(results[1].block_id, 3);
807 }
808
809 #[test]
810 fn test_query_tick_bucket_field() {
811 let mut idx = StorageMetadataIndex::new();
812 idx.insert(make_entry(1, "c1", vec![MetadataField::TickBucket(0)]));
813 idx.insert(make_entry(2, "c2", vec![MetadataField::TickBucket(1)]));
814 let q = MetadataQuery {
815 must_have: vec![MetadataField::TickBucket(1)],
816 ..Default::default()
817 };
818 let results = idx.query(&q);
819 assert_eq!(results.len(), 1);
820 assert_eq!(results[0].block_id, 2);
821 }
822
823 #[test]
826 fn test_query_on_empty_index() {
827 let mut idx = StorageMetadataIndex::new();
828 let q = MetadataQuery::default();
829 let results = idx.query(&q);
830 assert!(results.is_empty());
831 }
832
833 #[test]
834 fn test_insert_entry_with_no_fields() {
835 let mut idx = StorageMetadataIndex::new();
836 idx.insert(make_entry(1, "c1", vec![]));
837 assert_eq!(idx.stats().total_entries, 1);
838 assert_eq!(idx.stats().total_fields_indexed, 0);
839 let q = MetadataQuery {
841 sort_by: Some(MetadataSortField::BlockId),
842 ..Default::default()
843 };
844 let results = idx.query(&q);
845 assert_eq!(results.len(), 1);
846 }
847
848 #[test]
849 fn test_default_impl() {
850 let idx = StorageMetadataIndex::default();
851 assert_eq!(idx.stats().total_entries, 0);
852 }
853}