1use std::collections::HashMap;
9
10const BYTES_PER_MB: u64 = 1_048_576;
16
17const SECS_PER_DAY: u64 = 86_400;
19
20#[derive(Clone, Debug, PartialEq, Eq, Hash)]
26pub enum IndexKey {
27 ContentType(String),
29 SizeBucket(u64),
31 DayBucket(u64),
33 Tag(String),
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct IndexEntry {
44 pub cid: String,
46 pub size_bytes: u64,
48 pub content_type: String,
50 pub created_at_secs: u64,
52 pub tags: Vec<String>,
54}
55
56impl IndexEntry {
57 #[inline]
59 pub fn size_bucket(&self) -> IndexKey {
60 IndexKey::SizeBucket(self.size_bytes / BYTES_PER_MB)
61 }
62
63 #[inline]
65 pub fn day_bucket(&self) -> IndexKey {
66 IndexKey::DayBucket(self.created_at_secs / SECS_PER_DAY)
67 }
68}
69
70#[derive(Clone, Debug, Default)]
79pub struct IndexQuery {
80 pub content_type: Option<String>,
82 pub min_size_bytes: Option<u64>,
84 pub max_size_bytes: Option<u64>,
86 pub created_after_secs: Option<u64>,
88 pub tag: Option<String>,
90}
91
92#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct IndexStats {
99 pub total_entries: usize,
101 pub unique_content_types: usize,
103 pub unique_tags: usize,
105 pub total_size_bytes: u64,
107}
108
109pub struct StorageBlockIndex {
129 pub entries: HashMap<String, IndexEntry>,
131 pub index: HashMap<IndexKey, Vec<String>>,
133}
134
135impl StorageBlockIndex {
136 pub fn new() -> Self {
138 Self {
139 entries: HashMap::new(),
140 index: HashMap::new(),
141 }
142 }
143
144 pub fn insert(&mut self, entry: IndexEntry) {
151 self.remove(&entry.cid.clone());
153
154 let cid = entry.cid.clone();
155
156 let keys = Self::keys_for_entry(&entry);
158
159 self.entries.insert(cid.clone(), entry);
161
162 for key in keys {
164 self.index.entry(key).or_default().push(cid.clone());
165 }
166 }
167
168 pub fn remove(&mut self, cid: &str) -> bool {
174 let entry = match self.entries.remove(cid) {
175 Some(e) => e,
176 None => return false,
177 };
178
179 let keys = Self::keys_for_entry(&entry);
180
181 for key in keys {
182 if let Some(cids) = self.index.get_mut(&key) {
183 cids.retain(|c| c != cid);
184 }
187 }
188
189 true
190 }
191
192 pub fn query<'a>(&'a self, q: &IndexQuery) -> Vec<&'a IndexEntry> {
200 let mut results: Vec<&IndexEntry> = self
201 .entries
202 .values()
203 .filter(|e| {
204 if let Some(ref ct) = q.content_type {
205 if &e.content_type != ct {
206 return false;
207 }
208 }
209 if let Some(min) = q.min_size_bytes {
210 if e.size_bytes < min {
211 return false;
212 }
213 }
214 if let Some(max) = q.max_size_bytes {
215 if e.size_bytes > max {
216 return false;
217 }
218 }
219 if let Some(after) = q.created_after_secs {
220 if e.created_at_secs <= after {
221 return false;
222 }
223 }
224 if let Some(ref tag) = q.tag {
225 if !e.tags.contains(tag) {
226 return false;
227 }
228 }
229 true
230 })
231 .collect();
232
233 results.sort_by(|a, b| {
235 b.created_at_secs
236 .cmp(&a.created_at_secs)
237 .then_with(|| a.cid.cmp(&b.cid))
238 });
239
240 results
241 }
242
243 pub fn entries_for_key<'a>(&'a self, key: &IndexKey) -> Vec<&'a IndexEntry> {
249 let cids = match self.index.get(key) {
250 Some(v) => v,
251 None => return Vec::new(),
252 };
253
254 let mut entries: Vec<&IndexEntry> = cids
255 .iter()
256 .filter_map(|cid| self.entries.get(cid.as_str()))
257 .collect();
258
259 entries.sort_by(|a, b| a.cid.cmp(&b.cid));
260 entries
261 }
262
263 pub fn stats(&self) -> IndexStats {
267 let total_entries = self.entries.len();
268
269 let mut content_types = std::collections::HashSet::new();
270 let mut tags = std::collections::HashSet::new();
271 let mut total_size_bytes: u64 = 0;
272
273 for entry in self.entries.values() {
274 content_types.insert(entry.content_type.as_str());
275 for tag in &entry.tags {
276 tags.insert(tag.as_str());
277 }
278 total_size_bytes = total_size_bytes.saturating_add(entry.size_bytes);
279 }
280
281 IndexStats {
282 total_entries,
283 unique_content_types: content_types.len(),
284 unique_tags: tags.len(),
285 total_size_bytes,
286 }
287 }
288
289 fn keys_for_entry(entry: &IndexEntry) -> Vec<IndexKey> {
293 let mut keys = Vec::with_capacity(3 + entry.tags.len());
294 keys.push(IndexKey::ContentType(entry.content_type.clone()));
295 keys.push(entry.size_bucket());
296 keys.push(entry.day_bucket());
297 for tag in &entry.tags {
298 keys.push(IndexKey::Tag(tag.clone()));
299 }
300 keys
301 }
302}
303
304impl Default for StorageBlockIndex {
305 fn default() -> Self {
306 Self::new()
307 }
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
317pub struct BlockIndexEntry {
318 pub cid: String,
320 pub size_bytes: u64,
322 pub codec: String,
324 pub created_tick: u64,
326 pub tags: Vec<String>,
328}
329
330#[derive(Debug, Clone, PartialEq, Eq)]
336pub struct BlockIndexStats {
337 pub entry_count: usize,
339 pub total_bytes: u64,
341 pub unique_codecs: usize,
343 pub unique_tags: usize,
345}
346
347pub struct SecondaryBlockIndex {
359 entries: HashMap<String, BlockIndexEntry>,
361 by_codec: HashMap<String, Vec<String>>,
363 by_tag: HashMap<String, Vec<String>>,
365 total_bytes: u64,
367}
368
369impl SecondaryBlockIndex {
370 pub fn new() -> Self {
372 Self {
373 entries: HashMap::new(),
374 by_codec: HashMap::new(),
375 by_tag: HashMap::new(),
376 total_bytes: 0,
377 }
378 }
379
380 pub fn insert(&mut self, entry: BlockIndexEntry) {
387 let cid_clone = entry.cid.clone();
389 self.remove(&cid_clone);
390
391 self.total_bytes = self.total_bytes.saturating_add(entry.size_bytes);
393
394 self.by_codec
396 .entry(entry.codec.clone())
397 .or_default()
398 .push(entry.cid.clone());
399
400 for tag in &entry.tags {
402 self.by_tag
403 .entry(tag.clone())
404 .or_default()
405 .push(entry.cid.clone());
406 }
407
408 self.entries.insert(entry.cid.clone(), entry);
410 }
411
412 pub fn remove(&mut self, cid: &str) -> Option<BlockIndexEntry> {
418 let entry = self.entries.remove(cid)?;
419
420 self.total_bytes = self.total_bytes.saturating_sub(entry.size_bytes);
422
423 if let Some(cids) = self.by_codec.get_mut(&entry.codec) {
425 cids.retain(|c| c != cid);
426 }
427
428 for tag in &entry.tags {
430 if let Some(cids) = self.by_tag.get_mut(tag) {
431 cids.retain(|c| c != cid);
432 }
433 }
434
435 Some(entry)
436 }
437
438 pub fn get(&self, cid: &str) -> Option<&BlockIndexEntry> {
442 self.entries.get(cid)
443 }
444
445 pub fn find_by_codec(&self, codec: &str) -> Vec<&BlockIndexEntry> {
450 let cids = match self.by_codec.get(codec) {
451 Some(v) => v,
452 None => return Vec::new(),
453 };
454 let mut results: Vec<&BlockIndexEntry> = cids
455 .iter()
456 .filter_map(|c| self.entries.get(c.as_str()))
457 .collect();
458 results.sort_by(|a, b| a.cid.cmp(&b.cid));
459 results
460 }
461
462 pub fn find_by_tag(&self, tag: &str) -> Vec<&BlockIndexEntry> {
466 let cids = match self.by_tag.get(tag) {
467 Some(v) => v,
468 None => return Vec::new(),
469 };
470 let mut results: Vec<&BlockIndexEntry> = cids
471 .iter()
472 .filter_map(|c| self.entries.get(c.as_str()))
473 .collect();
474 results.sort_by(|a, b| a.cid.cmp(&b.cid));
475 results
476 }
477
478 pub fn find_by_size_range(&self, min: u64, max: u64) -> Vec<&BlockIndexEntry> {
483 let mut results: Vec<&BlockIndexEntry> = self
484 .entries
485 .values()
486 .filter(|e| e.size_bytes >= min && e.size_bytes <= max)
487 .collect();
488 results.sort_by(|a, b| a.cid.cmp(&b.cid));
489 results
490 }
491
492 pub fn find_by_created_range(&self, min_tick: u64, max_tick: u64) -> Vec<&BlockIndexEntry> {
497 let mut results: Vec<&BlockIndexEntry> = self
498 .entries
499 .values()
500 .filter(|e| e.created_tick >= min_tick && e.created_tick <= max_tick)
501 .collect();
502 results.sort_by(|a, b| a.cid.cmp(&b.cid));
503 results
504 }
505
506 #[inline]
510 pub fn entry_count(&self) -> usize {
511 self.entries.len()
512 }
513
514 #[inline]
518 pub fn total_bytes(&self) -> u64 {
519 self.total_bytes
520 }
521
522 pub fn unique_codecs(&self) -> Vec<String> {
526 let mut codecs: Vec<String> = self
527 .by_codec
528 .iter()
529 .filter(|(_, cids)| !cids.is_empty())
530 .map(|(codec, _)| codec.clone())
531 .collect();
532 codecs.sort();
533 codecs
534 }
535
536 pub fn unique_tags(&self) -> Vec<String> {
540 let mut tags: Vec<String> = self
541 .by_tag
542 .iter()
543 .filter(|(_, cids)| !cids.is_empty())
544 .map(|(tag, _)| tag.clone())
545 .collect();
546 tags.sort();
547 tags
548 }
549
550 pub fn stats(&self) -> BlockIndexStats {
554 BlockIndexStats {
555 entry_count: self.entry_count(),
556 total_bytes: self.total_bytes,
557 unique_codecs: self.unique_codecs().len(),
558 unique_tags: self.unique_tags().len(),
559 }
560 }
561}
562
563impl Default for SecondaryBlockIndex {
564 fn default() -> Self {
565 Self::new()
566 }
567}
568
569#[cfg(test)]
574mod tests {
575 use super::*;
576
577 fn make_entry(
580 cid: &str,
581 content_type: &str,
582 size_bytes: u64,
583 created_at_secs: u64,
584 tags: &[&str],
585 ) -> IndexEntry {
586 IndexEntry {
587 cid: cid.to_owned(),
588 content_type: content_type.to_owned(),
589 size_bytes,
590 created_at_secs,
591 tags: tags.iter().map(|t| t.to_string()).collect(),
592 }
593 }
594
595 fn populated_index() -> StorageBlockIndex {
597 let mut idx = StorageBlockIndex::new();
598 idx.insert(make_entry(
600 "cid-a",
601 "image/png",
602 2 * BYTES_PER_MB,
603 0,
604 &["public"],
605 ));
606 idx.insert(make_entry(
608 "cid-b",
609 "video/mp4",
610 10 * BYTES_PER_MB,
611 SECS_PER_DAY,
612 &["public", "featured"],
613 ));
614 idx.insert(make_entry(
616 "cid-c",
617 "image/png",
618 512_000,
619 2 * SECS_PER_DAY,
620 &["private"],
621 ));
622 idx.insert(make_entry("cid-d", "text/plain", 1, 3 * SECS_PER_DAY, &[]));
624 idx
625 }
626
627 #[test]
630 fn test_new_is_empty() {
631 let idx = StorageBlockIndex::new();
632 assert!(idx.entries.is_empty());
633 assert!(idx.index.is_empty());
634 }
635
636 #[test]
639 fn test_insert_stores_entry() {
640 let mut idx = StorageBlockIndex::new();
641 let e = make_entry("cid-1", "text/plain", 100, 1000, &[]);
642 idx.insert(e.clone());
643 assert_eq!(idx.entries.get("cid-1"), Some(&e));
644 }
645
646 #[test]
649 fn test_insert_updates_content_type_index() {
650 let mut idx = StorageBlockIndex::new();
651 idx.insert(make_entry("cid-1", "text/plain", 100, 0, &[]));
652 let key = IndexKey::ContentType("text/plain".to_owned());
653 assert!(idx
654 .index
655 .get(&key)
656 .is_some_and(|v| v.contains(&"cid-1".to_owned())));
657 }
658
659 #[test]
660 fn test_insert_updates_size_bucket_index() {
661 let mut idx = StorageBlockIndex::new();
662 idx.insert(make_entry("cid-1", "text/plain", 3 * BYTES_PER_MB, 0, &[]));
664 let key = IndexKey::SizeBucket(3);
665 assert!(idx
666 .index
667 .get(&key)
668 .is_some_and(|v| v.contains(&"cid-1".to_owned())));
669 }
670
671 #[test]
672 fn test_insert_updates_day_bucket_index() {
673 let mut idx = StorageBlockIndex::new();
674 idx.insert(make_entry(
676 "cid-1",
677 "text/plain",
678 1,
679 5 * SECS_PER_DAY + 3600,
680 &[],
681 ));
682 let key = IndexKey::DayBucket(5);
683 assert!(idx
684 .index
685 .get(&key)
686 .is_some_and(|v| v.contains(&"cid-1".to_owned())));
687 }
688
689 #[test]
690 fn test_insert_updates_tag_indexes() {
691 let mut idx = StorageBlockIndex::new();
692 idx.insert(make_entry("cid-1", "text/plain", 1, 0, &["alpha", "beta"]));
693 assert!(idx
694 .index
695 .get(&IndexKey::Tag("alpha".to_owned()))
696 .is_some_and(|v| v.contains(&"cid-1".to_owned())));
697 assert!(idx
698 .index
699 .get(&IndexKey::Tag("beta".to_owned()))
700 .is_some_and(|v| v.contains(&"cid-1".to_owned())));
701 }
702
703 #[test]
706 fn test_remove_removes_from_entries() {
707 let mut idx = populated_index();
708 assert!(idx.remove("cid-a"));
709 assert!(!idx.entries.contains_key("cid-a"));
710 }
711
712 #[test]
715 fn test_remove_cleans_content_type_bucket() {
716 let mut idx = populated_index();
717 idx.remove("cid-a");
718 let key = IndexKey::ContentType("image/png".to_owned());
719 let cids = idx.index.get(&key).cloned().unwrap_or_default();
721 assert!(!cids.contains(&"cid-a".to_owned()));
722 }
723
724 #[test]
725 fn test_remove_cleans_size_bucket() {
726 let mut idx = populated_index();
727 idx.remove("cid-a");
729 let key = IndexKey::SizeBucket(2);
730 let cids = idx.index.get(&key).cloned().unwrap_or_default();
731 assert!(!cids.contains(&"cid-a".to_owned()));
732 }
733
734 #[test]
735 fn test_remove_cleans_tag_bucket() {
736 let mut idx = populated_index();
737 idx.remove("cid-a");
738 let key = IndexKey::Tag("public".to_owned());
739 let cids = idx.index.get(&key).cloned().unwrap_or_default();
740 assert!(!cids.contains(&"cid-a".to_owned()));
741 assert!(cids.contains(&"cid-b".to_owned()));
743 }
744
745 #[test]
748 fn test_remove_unknown_cid_returns_false() {
749 let mut idx = StorageBlockIndex::new();
750 assert!(!idx.remove("nonexistent"));
751 }
752
753 #[test]
756 fn test_query_content_type_filter() {
757 let idx = populated_index();
758 let q = IndexQuery {
759 content_type: Some("image/png".to_owned()),
760 ..Default::default()
761 };
762 let results = idx.query(&q);
763 assert_eq!(results.len(), 2);
764 for e in &results {
765 assert_eq!(e.content_type, "image/png");
766 }
767 }
768
769 #[test]
772 fn test_query_min_size_filter() {
773 let idx = populated_index();
774 let q = IndexQuery {
775 min_size_bytes: Some(5 * BYTES_PER_MB),
776 ..Default::default()
777 };
778 let results = idx.query(&q);
779 assert_eq!(results.len(), 1);
781 assert_eq!(results[0].cid, "cid-b");
782 }
783
784 #[test]
787 fn test_query_max_size_filter() {
788 let idx = populated_index();
789 let q = IndexQuery {
790 max_size_bytes: Some(BYTES_PER_MB),
791 ..Default::default()
792 };
793 let results = idx.query(&q);
794 assert_eq!(results.len(), 2);
796 for e in &results {
797 assert!(e.size_bytes <= BYTES_PER_MB);
798 }
799 }
800
801 #[test]
804 fn test_query_size_range() {
805 let idx = populated_index();
806 let q = IndexQuery {
807 min_size_bytes: Some(BYTES_PER_MB),
808 max_size_bytes: Some(5 * BYTES_PER_MB),
809 ..Default::default()
810 };
811 let results = idx.query(&q);
812 assert_eq!(results.len(), 1);
814 assert_eq!(results[0].cid, "cid-a");
815 }
816
817 #[test]
820 fn test_query_created_after_filter() {
821 let idx = populated_index();
822 let q = IndexQuery {
824 created_after_secs: Some(SECS_PER_DAY),
825 ..Default::default()
826 };
827 let results = idx.query(&q);
828 assert_eq!(results.len(), 2);
829 for e in &results {
830 assert!(e.created_at_secs > SECS_PER_DAY);
831 }
832 }
833
834 #[test]
837 fn test_query_tag_filter() {
838 let idx = populated_index();
839 let q = IndexQuery {
840 tag: Some("featured".to_owned()),
841 ..Default::default()
842 };
843 let results = idx.query(&q);
844 assert_eq!(results.len(), 1);
845 assert_eq!(results[0].cid, "cid-b");
846 }
847
848 #[test]
851 fn test_query_combined_filters() {
852 let idx = populated_index();
853 let q = IndexQuery {
854 content_type: Some("image/png".to_owned()),
855 max_size_bytes: Some(BYTES_PER_MB),
856 ..Default::default()
857 };
858 let results = idx.query(&q);
859 assert_eq!(results.len(), 1);
861 assert_eq!(results[0].cid, "cid-c");
862 }
863
864 #[test]
867 fn test_query_sorted_by_created_at_desc() {
868 let idx = populated_index();
869 let results = idx.query(&IndexQuery::default());
870 assert_eq!(results.len(), 4);
871 let times: Vec<u64> = results.iter().map(|e| e.created_at_secs).collect();
872 for i in 1..times.len() {
874 assert!(
875 times[i - 1] >= times[i],
876 "Expected descending order at index {i}: {times:?}"
877 );
878 }
879 }
880
881 #[test]
884 fn test_query_no_match_returns_empty() {
885 let idx = populated_index();
886 let q = IndexQuery {
887 content_type: Some("application/octet-stream".to_owned()),
888 ..Default::default()
889 };
890 assert!(idx.query(&q).is_empty());
891 }
892
893 #[test]
896 fn test_entries_for_key_content_type() {
897 let idx = populated_index();
898 let key = IndexKey::ContentType("image/png".to_owned());
899 let entries = idx.entries_for_key(&key);
900 assert_eq!(entries.len(), 2);
901 let cids: Vec<&str> = entries.iter().map(|e| e.cid.as_str()).collect();
902 assert!(cids.contains(&"cid-a"));
903 assert!(cids.contains(&"cid-c"));
904 }
905
906 #[test]
909 fn test_entries_for_key_tag() {
910 let idx = populated_index();
911 let key = IndexKey::Tag("public".to_owned());
912 let entries = idx.entries_for_key(&key);
913 assert_eq!(entries.len(), 2);
914 let cids: Vec<&str> = entries.iter().map(|e| e.cid.as_str()).collect();
915 assert!(cids.contains(&"cid-a"));
916 assert!(cids.contains(&"cid-b"));
917 }
918
919 #[test]
922 fn test_entries_for_key_size_bucket() {
923 let idx = populated_index();
924 let key = IndexKey::SizeBucket(2);
926 let entries = idx.entries_for_key(&key);
927 assert_eq!(entries.len(), 1);
928 assert_eq!(entries[0].cid, "cid-a");
929 }
930
931 #[test]
934 fn test_entries_for_key_day_bucket() {
935 let idx = populated_index();
936 let key = IndexKey::DayBucket(1);
938 let entries = idx.entries_for_key(&key);
939 assert_eq!(entries.len(), 1);
940 assert_eq!(entries[0].cid, "cid-b");
941 }
942
943 #[test]
946 fn test_stats_total_entries() {
947 let idx = populated_index();
948 assert_eq!(idx.stats().total_entries, 4);
949 }
950
951 #[test]
954 fn test_stats_unique_content_types() {
955 let idx = populated_index();
956 assert_eq!(idx.stats().unique_content_types, 3);
958 }
959
960 #[test]
963 fn test_stats_total_size_bytes() {
964 let idx = populated_index();
965 let expected = 2 * BYTES_PER_MB + 10 * BYTES_PER_MB + 512_000 + 1; assert_eq!(idx.stats().total_size_bytes, expected);
970 }
971
972 #[test]
975 fn test_stats_unique_tags() {
976 let idx = populated_index();
977 assert_eq!(idx.stats().unique_tags, 3);
979 }
980
981 #[test]
984 fn test_default_is_empty() {
985 let idx = StorageBlockIndex::default();
986 assert!(idx.entries.is_empty());
987 }
988
989 #[test]
992 fn test_reinsert_replaces_entry() {
993 let mut idx = StorageBlockIndex::new();
994 idx.insert(make_entry("cid-1", "text/plain", 100, 1000, &["old"]));
995 idx.insert(make_entry("cid-1", "image/png", 200, 2000, &["new"]));
996
997 assert_eq!(idx.entries.len(), 1);
998 let e = idx.entries.get("cid-1").expect("entry must exist");
999 assert_eq!(e.content_type, "image/png");
1000
1001 let old_key = IndexKey::ContentType("text/plain".to_owned());
1003 let old_cids = idx.index.get(&old_key).cloned().unwrap_or_default();
1004 assert!(!old_cids.contains(&"cid-1".to_owned()));
1005
1006 let old_tag = IndexKey::Tag("old".to_owned());
1008 let old_tag_cids = idx.index.get(&old_tag).cloned().unwrap_or_default();
1009 assert!(!old_tag_cids.contains(&"cid-1".to_owned()));
1010 }
1011}
1012
1013#[cfg(test)]
1018mod secondary_tests {
1019 use super::*;
1020
1021 fn entry(cid: &str, size: u64, codec: &str, tick: u64, tags: &[&str]) -> BlockIndexEntry {
1022 BlockIndexEntry {
1023 cid: cid.to_owned(),
1024 size_bytes: size,
1025 codec: codec.to_owned(),
1026 created_tick: tick,
1027 tags: tags.iter().map(|t| t.to_string()).collect(),
1028 }
1029 }
1030
1031 fn populated() -> SecondaryBlockIndex {
1032 let mut idx = SecondaryBlockIndex::new();
1033 idx.insert(entry("cid-a", 1024, "raw", 10, &["pin"]));
1034 idx.insert(entry("cid-b", 2048, "dag-cbor", 20, &["pin", "important"]));
1035 idx.insert(entry("cid-c", 512, "raw", 30, &["temp"]));
1036 idx.insert(entry("cid-d", 4096, "dag-pb", 40, &[]));
1037 idx
1038 }
1039
1040 #[test]
1043 fn test_secondary_new_is_empty() {
1044 let idx = SecondaryBlockIndex::new();
1045 assert_eq!(idx.entry_count(), 0);
1046 assert_eq!(idx.total_bytes(), 0);
1047 }
1048
1049 #[test]
1052 fn test_secondary_default_is_empty() {
1053 let idx = SecondaryBlockIndex::default();
1054 assert_eq!(idx.entry_count(), 0);
1055 }
1056
1057 #[test]
1060 fn test_secondary_insert_and_get() {
1061 let mut idx = SecondaryBlockIndex::new();
1062 let e = entry("cid-1", 100, "raw", 5, &["x"]);
1063 idx.insert(e.clone());
1064 let got = idx.get("cid-1");
1065 assert!(got.is_some());
1066 assert_eq!(got.map(|g| &g.cid), Some(&"cid-1".to_owned()));
1067 assert_eq!(got.map(|g| g.size_bytes), Some(100));
1068 }
1069
1070 #[test]
1073 fn test_secondary_get_missing() {
1074 let idx = SecondaryBlockIndex::new();
1075 assert!(idx.get("nope").is_none());
1076 }
1077
1078 #[test]
1081 fn test_secondary_remove_returns_entry() {
1082 let mut idx = populated();
1083 let removed = idx.remove("cid-a");
1084 assert!(removed.is_some());
1085 assert_eq!(removed.map(|r| r.cid), Some("cid-a".to_owned()));
1086 assert!(idx.get("cid-a").is_none());
1087 }
1088
1089 #[test]
1092 fn test_secondary_remove_missing() {
1093 let mut idx = SecondaryBlockIndex::new();
1094 assert!(idx.remove("nope").is_none());
1095 }
1096
1097 #[test]
1100 fn test_secondary_remove_updates_total_bytes() {
1101 let mut idx = SecondaryBlockIndex::new();
1102 idx.insert(entry("a", 100, "raw", 1, &[]));
1103 idx.insert(entry("b", 200, "raw", 2, &[]));
1104 assert_eq!(idx.total_bytes(), 300);
1105 idx.remove("a");
1106 assert_eq!(idx.total_bytes(), 200);
1107 }
1108
1109 #[test]
1112 fn test_secondary_remove_updates_codec_index() {
1113 let mut idx = populated();
1114 idx.remove("cid-a"); let raw_entries = idx.find_by_codec("raw");
1116 let cids: Vec<&str> = raw_entries.iter().map(|e| e.cid.as_str()).collect();
1117 assert!(!cids.contains(&"cid-a"));
1118 assert!(cids.contains(&"cid-c")); }
1120
1121 #[test]
1124 fn test_secondary_remove_updates_tag_index() {
1125 let mut idx = populated();
1126 idx.remove("cid-a"); let pin_entries = idx.find_by_tag("pin");
1128 let cids: Vec<&str> = pin_entries.iter().map(|e| e.cid.as_str()).collect();
1129 assert!(!cids.contains(&"cid-a"));
1130 assert!(cids.contains(&"cid-b")); }
1132
1133 #[test]
1136 fn test_secondary_find_by_codec() {
1137 let idx = populated();
1138 let raw = idx.find_by_codec("raw");
1139 assert_eq!(raw.len(), 2);
1140 let cids: Vec<&str> = raw.iter().map(|e| e.cid.as_str()).collect();
1141 assert!(cids.contains(&"cid-a"));
1142 assert!(cids.contains(&"cid-c"));
1143 }
1144
1145 #[test]
1148 fn test_secondary_find_by_codec_empty() {
1149 let idx = populated();
1150 assert!(idx.find_by_codec("dag-json").is_empty());
1151 }
1152
1153 #[test]
1156 fn test_secondary_find_by_tag() {
1157 let idx = populated();
1158 let pinned = idx.find_by_tag("pin");
1159 assert_eq!(pinned.len(), 2);
1160 let cids: Vec<&str> = pinned.iter().map(|e| e.cid.as_str()).collect();
1161 assert!(cids.contains(&"cid-a"));
1162 assert!(cids.contains(&"cid-b"));
1163 }
1164
1165 #[test]
1168 fn test_secondary_find_by_tag_empty() {
1169 let idx = populated();
1170 assert!(idx.find_by_tag("nonexistent").is_empty());
1171 }
1172
1173 #[test]
1176 fn test_secondary_find_by_tag_single() {
1177 let idx = populated();
1178 let important = idx.find_by_tag("important");
1179 assert_eq!(important.len(), 1);
1180 assert_eq!(important[0].cid, "cid-b");
1181 }
1182
1183 #[test]
1186 fn test_secondary_find_by_size_range() {
1187 let idx = populated();
1188 let results = idx.find_by_size_range(512, 2048);
1189 assert_eq!(results.len(), 3); for e in &results {
1191 assert!(e.size_bytes >= 512 && e.size_bytes <= 2048);
1192 }
1193 }
1194
1195 #[test]
1198 fn test_secondary_find_by_size_range_none() {
1199 let idx = populated();
1200 assert!(idx.find_by_size_range(10000, 20000).is_empty());
1201 }
1202
1203 #[test]
1206 fn test_secondary_find_by_size_range_exact() {
1207 let idx = populated();
1208 let results = idx.find_by_size_range(4096, 4096);
1209 assert_eq!(results.len(), 1);
1210 assert_eq!(results[0].cid, "cid-d");
1211 }
1212
1213 #[test]
1216 fn test_secondary_find_by_created_range() {
1217 let idx = populated();
1218 let results = idx.find_by_created_range(15, 35);
1219 assert_eq!(results.len(), 2); for e in &results {
1221 assert!(e.created_tick >= 15 && e.created_tick <= 35);
1222 }
1223 }
1224
1225 #[test]
1228 fn test_secondary_find_by_created_range_none() {
1229 let idx = populated();
1230 assert!(idx.find_by_created_range(100, 200).is_empty());
1231 }
1232
1233 #[test]
1236 fn test_secondary_entry_count() {
1237 let idx = populated();
1238 assert_eq!(idx.entry_count(), 4);
1239 }
1240
1241 #[test]
1244 fn test_secondary_total_bytes() {
1245 let idx = populated();
1246 assert_eq!(idx.total_bytes(), 1024 + 2048 + 512 + 4096);
1247 }
1248
1249 #[test]
1252 fn test_secondary_unique_codecs() {
1253 let idx = populated();
1254 let codecs = idx.unique_codecs();
1255 assert_eq!(codecs.len(), 3);
1256 assert!(codecs.contains(&"raw".to_owned()));
1257 assert!(codecs.contains(&"dag-cbor".to_owned()));
1258 assert!(codecs.contains(&"dag-pb".to_owned()));
1259 }
1260
1261 #[test]
1264 fn test_secondary_unique_tags() {
1265 let idx = populated();
1266 let tags = idx.unique_tags();
1267 assert_eq!(tags.len(), 3);
1268 assert!(tags.contains(&"pin".to_owned()));
1269 assert!(tags.contains(&"important".to_owned()));
1270 assert!(tags.contains(&"temp".to_owned()));
1271 }
1272
1273 #[test]
1276 fn test_secondary_stats() {
1277 let idx = populated();
1278 let s = idx.stats();
1279 assert_eq!(s.entry_count, 4);
1280 assert_eq!(s.total_bytes, 1024 + 2048 + 512 + 4096);
1281 assert_eq!(s.unique_codecs, 3);
1282 assert_eq!(s.unique_tags, 3);
1283 }
1284
1285 #[test]
1288 fn test_secondary_reinsert_replaces() {
1289 let mut idx = SecondaryBlockIndex::new();
1290 idx.insert(entry("cid-1", 100, "raw", 1, &["old"]));
1291 idx.insert(entry("cid-1", 200, "dag-cbor", 2, &["new"]));
1292
1293 assert_eq!(idx.entry_count(), 1);
1294 let e = idx.get("cid-1");
1295 assert!(e.is_some());
1296 let e = e.expect("checked above");
1297 assert_eq!(e.codec, "dag-cbor");
1298 assert_eq!(e.size_bytes, 200);
1299 assert_eq!(idx.total_bytes(), 200);
1300
1301 assert!(idx.find_by_codec("raw").is_empty());
1303 assert!(idx.find_by_tag("old").is_empty());
1305 assert_eq!(idx.find_by_codec("dag-cbor").len(), 1);
1307 assert_eq!(idx.find_by_tag("new").len(), 1);
1308 }
1309
1310 #[test]
1313 fn test_secondary_multiple_same_codec() {
1314 let mut idx = SecondaryBlockIndex::new();
1315 idx.insert(entry("a", 10, "raw", 1, &[]));
1316 idx.insert(entry("b", 20, "raw", 2, &[]));
1317 idx.insert(entry("c", 30, "raw", 3, &[]));
1318 let results = idx.find_by_codec("raw");
1319 assert_eq!(results.len(), 3);
1320 }
1321
1322 #[test]
1325 fn test_secondary_empty_index_operations() {
1326 let idx = SecondaryBlockIndex::new();
1327 assert!(idx.find_by_codec("raw").is_empty());
1328 assert!(idx.find_by_tag("any").is_empty());
1329 assert!(idx.find_by_size_range(0, u64::MAX).is_empty());
1330 assert!(idx.find_by_created_range(0, u64::MAX).is_empty());
1331 assert!(idx.unique_codecs().is_empty());
1332 assert!(idx.unique_tags().is_empty());
1333 let s = idx.stats();
1334 assert_eq!(s.entry_count, 0);
1335 assert_eq!(s.total_bytes, 0);
1336 assert_eq!(s.unique_codecs, 0);
1337 assert_eq!(s.unique_tags, 0);
1338 }
1339
1340 #[test]
1343 fn test_secondary_unique_codecs_after_remove() {
1344 let mut idx = SecondaryBlockIndex::new();
1345 idx.insert(entry("a", 10, "raw", 1, &[]));
1346 assert_eq!(idx.unique_codecs().len(), 1);
1347 idx.remove("a");
1348 assert!(idx.unique_codecs().is_empty());
1349 }
1350
1351 #[test]
1354 fn test_secondary_unique_tags_after_remove() {
1355 let mut idx = SecondaryBlockIndex::new();
1356 idx.insert(entry("a", 10, "raw", 1, &["only"]));
1357 assert_eq!(idx.unique_tags().len(), 1);
1358 idx.remove("a");
1359 assert!(idx.unique_tags().is_empty());
1360 }
1361
1362 #[test]
1365 fn test_secondary_find_sorted_by_cid() {
1366 let mut idx = SecondaryBlockIndex::new();
1367 idx.insert(entry("z", 10, "raw", 1, &["t"]));
1368 idx.insert(entry("a", 20, "raw", 2, &["t"]));
1369 idx.insert(entry("m", 30, "raw", 3, &["t"]));
1370
1371 let by_codec = idx.find_by_codec("raw");
1372 let cids: Vec<&str> = by_codec.iter().map(|e| e.cid.as_str()).collect();
1373 assert_eq!(cids, vec!["a", "m", "z"]);
1374
1375 let by_tag = idx.find_by_tag("t");
1376 let cids: Vec<&str> = by_tag.iter().map(|e| e.cid.as_str()).collect();
1377 assert_eq!(cids, vec!["a", "m", "z"]);
1378 }
1379}