1use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::cmp::Ordering;
10use std::collections::{BTreeMap, HashMap};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct SortKey {
17 sort_value: SortValue,
19 entity_key: String,
21 order: SortOrder,
23}
24
25impl PartialOrd for SortKey {
26 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
27 Some(self.cmp(other))
28 }
29}
30
31impl Ord for SortKey {
32 fn cmp(&self, other: &Self) -> Ordering {
33 if self.order != other.order {
34 return match (self.order, other.order) {
35 (SortOrder::Asc, SortOrder::Desc) => Ordering::Less,
36 (SortOrder::Desc, SortOrder::Asc) => Ordering::Greater,
37 _ => Ordering::Equal,
38 };
39 }
40
41 let sort_order = match (&self.sort_value, &other.sort_value, self.order) {
46 (SortValue::Null, SortValue::Null, _) => Ordering::Equal,
47 (SortValue::Null, _, _) => Ordering::Greater,
48 (_, SortValue::Null, _) => Ordering::Less,
49 (_, _, SortOrder::Asc) => self.sort_value.cmp(&other.sort_value),
50 (_, _, SortOrder::Desc) => self.sort_value.cmp(&other.sort_value).reverse(),
51 };
52
53 match sort_order {
54 Ordering::Equal => self.entity_key.cmp(&other.entity_key),
55 other => other,
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum SortValue {
63 Null,
64 Bool(bool),
65 Integer(i64),
66 Float(OrderedFloat),
67 String(String),
68}
69
70impl Ord for SortValue {
71 fn cmp(&self, other: &Self) -> Ordering {
72 match (self, other) {
73 (SortValue::Null, SortValue::Null) => Ordering::Equal,
74 (SortValue::Null, _) => Ordering::Less,
75 (_, SortValue::Null) => Ordering::Greater,
76 (SortValue::Bool(a), SortValue::Bool(b)) => a.cmp(b),
77 (SortValue::Integer(a), SortValue::Integer(b)) => a.cmp(b),
78 (SortValue::Float(a), SortValue::Float(b)) => a.cmp(b),
79 (SortValue::String(a), SortValue::String(b)) => {
80 compare_decimal_strings(a, b).unwrap_or_else(|| a.cmp(b))
81 }
82 (SortValue::Integer(_), SortValue::String(_)) => Ordering::Less,
84 (SortValue::String(_), SortValue::Integer(_)) => Ordering::Greater,
85 (SortValue::Float(_), SortValue::String(_)) => Ordering::Less,
86 (SortValue::String(_), SortValue::Float(_)) => Ordering::Greater,
87 (SortValue::Integer(a), SortValue::Float(b)) => OrderedFloat(*a as f64).cmp(b),
89 (SortValue::Float(a), SortValue::Integer(b)) => a.cmp(&OrderedFloat(*b as f64)),
90 (SortValue::Bool(_), _) => Ordering::Less,
92 (_, SortValue::Bool(_)) => Ordering::Greater,
93 }
94 }
95}
96
97impl PartialOrd for SortValue {
98 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
99 Some(self.cmp(other))
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq)]
105pub struct OrderedFloat(pub f64);
106
107impl Eq for OrderedFloat {}
108
109impl Ord for OrderedFloat {
110 fn cmp(&self, other: &Self) -> Ordering {
111 self.0.partial_cmp(&other.0).unwrap_or_else(|| {
112 if self.0.is_nan() && other.0.is_nan() {
113 Ordering::Equal
114 } else if self.0.is_nan() {
115 Ordering::Less
116 } else {
117 Ordering::Greater
118 }
119 })
120 }
121}
122
123impl PartialOrd for OrderedFloat {
124 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
125 Some(self.cmp(other))
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(rename_all = "lowercase")]
132pub enum SortOrder {
133 Asc,
134 Desc,
135}
136
137impl From<crate::materialized_view::SortOrder> for SortOrder {
138 fn from(order: crate::materialized_view::SortOrder) -> Self {
139 match order {
140 crate::materialized_view::SortOrder::Asc => SortOrder::Asc,
141 crate::materialized_view::SortOrder::Desc => SortOrder::Desc,
142 }
143 }
144}
145
146#[derive(Debug, Clone, PartialEq)]
148pub enum ViewDelta {
149 None,
151 Add { key: String, entity: Value },
153 Remove { key: String },
155 Update { key: String, entity: Value },
157}
158
159#[derive(Debug)]
182pub struct SortedViewCache {
183 view_id: String,
185 sort_field: Vec<String>,
187 order: SortOrder,
189 sorted: BTreeMap<SortKey, ()>,
191 entities: HashMap<String, (SortKey, Value)>,
193 keys_cache: Vec<String>,
195 cache_dirty: bool,
197}
198
199impl SortedViewCache {
200 pub fn new(view_id: String, sort_field: Vec<String>, order: SortOrder) -> Self {
201 Self {
202 view_id,
203 sort_field,
204 order,
205 sorted: BTreeMap::new(),
206 entities: HashMap::new(),
207 keys_cache: Vec::new(),
208 cache_dirty: true,
209 }
210 }
211
212 pub fn view_id(&self) -> &str {
213 &self.view_id
214 }
215
216 pub fn len(&self) -> usize {
217 self.entities.len()
218 }
219
220 pub fn is_empty(&self) -> bool {
221 self.entities.is_empty()
222 }
223
224 pub fn upsert(&mut self, entity_key: String, entity: Value) -> UpsertResult {
226 let sort_value = self.extract_sort_value(&entity);
227
228 if let Some((old_sort_key, old_entity)) = self.entities.remove(&entity_key) {
231 let effective_sort_value = if matches!(sort_value, SortValue::Null)
232 && !matches!(old_sort_key.sort_value, SortValue::Null)
233 {
234 old_sort_key.sort_value.clone()
235 } else {
236 sort_value
237 };
238
239 let new_sort_key = SortKey {
240 sort_value: effective_sort_value,
241 entity_key: entity_key.clone(),
242 order: self.order,
243 };
244
245 let merged_entity = Self::deep_merge(old_entity, entity);
247
248 if old_sort_key == new_sort_key {
249 self.entities
251 .insert(entity_key.clone(), (new_sort_key, merged_entity));
252 let position = self.find_position(&entity_key);
254 return UpsertResult::Updated { position };
255 }
256
257 self.sorted.remove(&old_sort_key);
259 self.sorted.insert(new_sort_key.clone(), ());
260 self.entities
261 .insert(entity_key.clone(), (new_sort_key, merged_entity));
262 self.cache_dirty = true;
263
264 let position = self.find_position(&entity_key);
265 return UpsertResult::Inserted { position };
266 }
267
268 let new_sort_key = SortKey {
269 sort_value,
270 entity_key: entity_key.clone(),
271 order: self.order,
272 };
273
274 self.sorted.insert(new_sort_key.clone(), ());
275 self.entities
276 .insert(entity_key.clone(), (new_sort_key, entity));
277 self.cache_dirty = true;
278
279 let position = self.find_position(&entity_key);
280
281 UpsertResult::Inserted { position }
282 }
283
284 pub fn would_keep(&self, entity_key: &str, entity: &Value, max_entries: usize) -> bool {
291 if self.entities.contains_key(entity_key) || self.sorted.len() < max_entries {
292 return true;
293 }
294 let Some((last, ())) = self.sorted.last_key_value() else {
295 return true;
296 };
297 let candidate = SortKey {
298 sort_value: self.extract_sort_value(entity),
299 entity_key: entity_key.to_string(),
300 order: self.order,
301 };
302 candidate < *last
303 }
304
305 pub fn upsert_bounded(
311 &mut self,
312 entity_key: String,
313 entity: Value,
314 max_entries: usize,
315 ) -> UpsertResult {
316 let result = self.upsert(entity_key, entity);
317 self.trim_to_max_entries(max_entries);
318 result
319 }
320
321 pub fn trim_to_max_entries(&mut self, max_entries: usize) -> usize {
329 let mut evicted = 0;
330 while self.sorted.len() > max_entries {
331 let Some((sort_key, ())) = self.sorted.pop_last() else {
332 break;
333 };
334 self.entities.remove(&sort_key.entity_key);
335 evicted += 1;
336 }
337 if evicted > 0 && !self.cache_dirty {
338 self.keys_cache.truncate(self.sorted.len());
341 }
342 evicted
343 }
344
345 fn deep_merge(base: Value, patch: Value) -> Value {
346 match (base, patch) {
347 (Value::Object(mut base_map), Value::Object(patch_map)) => {
348 for (key, patch_value) in patch_map {
349 if let Some(base_value) = base_map.remove(&key) {
350 base_map.insert(key, Self::deep_merge(base_value, patch_value));
351 } else {
352 base_map.insert(key, patch_value);
353 }
354 }
355 Value::Object(base_map)
356 }
357 (_, patch) => patch,
358 }
359 }
360
361 pub fn clear(&mut self) {
363 self.sorted.clear();
364 self.entities.clear();
365 self.cache_dirty = true;
366 }
367
368 pub fn remove(&mut self, entity_key: &str) -> Option<usize> {
369 if let Some((sort_key, _)) = self.entities.remove(entity_key) {
370 let position = self.find_position_by_sort_key(&sort_key);
371 self.sorted.remove(&sort_key);
372 self.cache_dirty = true;
373 Some(position)
374 } else {
375 None
376 }
377 }
378
379 pub fn get(&self, entity_key: &str) -> Option<&Value> {
381 self.entities.get(entity_key).map(|(_, v)| v)
382 }
383
384 pub fn ordered_keys(&mut self) -> &[String] {
386 if self.cache_dirty {
387 self.rebuild_keys_cache();
388 }
389 &self.keys_cache
390 }
391
392 pub fn get_window(&mut self, skip: usize, take: usize) -> Vec<(String, Value)> {
394 if self.cache_dirty {
395 self.rebuild_keys_cache();
396 }
397
398 self.keys_cache
399 .iter()
400 .skip(skip)
401 .take(take)
402 .filter_map(|key| {
403 self.entities
404 .get(key)
405 .map(|(_, v)| (key.clone(), v.clone()))
406 })
407 .collect()
408 }
409
410 pub fn get_all_ordered(&mut self) -> Vec<(String, Value)> {
412 if self.cache_dirty {
413 self.rebuild_keys_cache();
414 }
415
416 self.keys_cache
417 .iter()
418 .filter_map(|key| {
419 self.entities
420 .get(key)
421 .map(|(_, value)| (key.clone(), value.clone()))
422 })
423 .collect()
424 }
425
426 pub fn compute_window_deltas(
428 &mut self,
429 old_window_keys: &[String],
430 skip: usize,
431 take: usize,
432 ) -> Vec<ViewDelta> {
433 if self.cache_dirty {
434 self.rebuild_keys_cache();
435 }
436
437 let new_window_keys: Vec<&String> = self.keys_cache.iter().skip(skip).take(take).collect();
438
439 let old_set: std::collections::HashSet<&String> = old_window_keys.iter().collect();
440 let new_set: std::collections::HashSet<&String> = new_window_keys.iter().cloned().collect();
441
442 let mut deltas = Vec::new();
443
444 for key in old_set.difference(&new_set) {
446 deltas.push(ViewDelta::Remove {
447 key: (*key).clone(),
448 });
449 }
450
451 for key in new_set.difference(&old_set) {
453 if let Some((_, entity)) = self.entities.get(*key) {
454 deltas.push(ViewDelta::Add {
455 key: (*key).clone(),
456 entity: entity.clone(),
457 });
458 }
459 }
460
461 deltas
462 }
463
464 fn extract_sort_value(&self, entity: &Value) -> SortValue {
465 let mut current = entity;
466 for segment in &self.sort_field {
467 match current.get(segment) {
468 Some(v) => current = v,
469 None => return SortValue::Null,
470 }
471 }
472
473 value_to_sort_value(current)
474 }
475
476 fn find_position(&self, entity_key: &str) -> usize {
477 if let Some((sort_key, _)) = self.entities.get(entity_key) {
478 self.find_position_by_sort_key(sort_key)
479 } else {
480 0
481 }
482 }
483
484 fn find_position_by_sort_key(&self, sort_key: &SortKey) -> usize {
485 self.sorted.range(..sort_key).count()
486 }
487
488 fn rebuild_keys_cache(&mut self) {
489 self.keys_cache = self.sorted.keys().map(|sk| sk.entity_key.clone()).collect();
490 self.cache_dirty = false;
491 }
492}
493
494#[derive(Debug, Clone, PartialEq)]
496pub enum UpsertResult {
497 Inserted { position: usize },
499 Updated { position: usize },
501}
502
503fn value_to_sort_value(v: &Value) -> SortValue {
504 match v {
505 Value::Null => SortValue::Null,
506 Value::Bool(b) => SortValue::Bool(*b),
507 Value::Number(n) => {
508 if let Some(i) = n.as_i64() {
509 SortValue::Integer(i)
510 } else if let Some(f) = n.as_f64() {
511 SortValue::Float(OrderedFloat(f))
512 } else {
513 SortValue::Null
514 }
515 }
516 Value::String(s) => SortValue::String(s.clone()),
517 _ => SortValue::Null,
518 }
519}
520
521fn compare_decimal_strings(left: &str, right: &str) -> Option<Ordering> {
522 fn parts(value: &str) -> Option<(bool, &str)> {
523 let (negative, digits) = match value.strip_prefix('-') {
524 Some(digits) => (true, digits),
525 None => (false, value),
526 };
527 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
528 return None;
529 }
530
531 let digits = digits.trim_start_matches('0');
532 let digits = if digits.is_empty() { "0" } else { digits };
533 Some((negative && digits != "0", digits))
534 }
535
536 let (left_negative, left_digits) = parts(left)?;
537 let (right_negative, right_digits) = parts(right)?;
538
539 match (left_negative, right_negative) {
540 (true, false) => Some(Ordering::Less),
541 (false, true) => Some(Ordering::Greater),
542 _ => {
543 let magnitude = left_digits
544 .len()
545 .cmp(&right_digits.len())
546 .then_with(|| left_digits.cmp(right_digits));
547 Some(if left_negative {
548 magnitude.reverse()
549 } else {
550 magnitude
551 })
552 }
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use serde_json::json;
560
561 #[test]
562 fn test_sorted_cache_basic() {
563 let mut cache = SortedViewCache::new(
564 "test/latest".to_string(),
565 vec!["id".to_string()],
566 SortOrder::Desc,
567 );
568
569 cache.upsert("a".to_string(), json!({"id": 1, "name": "first"}));
570 cache.upsert("b".to_string(), json!({"id": 3, "name": "third"}));
571 cache.upsert("c".to_string(), json!({"id": 2, "name": "second"}));
572
573 let keys = cache.ordered_keys();
574 assert_eq!(keys, vec!["b", "c", "a"]);
576 }
577
578 #[test]
579 fn test_sorted_cache_window() {
580 let mut cache = SortedViewCache::new(
581 "test/latest".to_string(),
582 vec!["id".to_string()],
583 SortOrder::Desc,
584 );
585
586 for i in 1..=10 {
587 cache.upsert(format!("e{}", i), json!({"id": i}));
588 }
589
590 let window = cache.get_window(0, 3);
592 assert_eq!(window.len(), 3);
593 assert_eq!(window[0].0, "e10");
594 assert_eq!(window[1].0, "e9");
595 assert_eq!(window[2].0, "e8");
596
597 let window = cache.get_window(3, 3);
598 assert_eq!(window[0].0, "e7");
599 }
600
601 #[test]
602 fn all_ordered_preserves_stable_sort_and_tie_breaking() {
603 let mut cache = SortedViewCache::new(
604 "test/latest".to_string(),
605 vec!["score".to_string()],
606 SortOrder::Desc,
607 );
608 cache.upsert("b".to_string(), json!({"score": 10}));
609 cache.upsert("a".to_string(), json!({"score": 10}));
610 cache.upsert("c".to_string(), json!({"score": 9}));
611
612 let keys: Vec<_> = cache
613 .get_all_ordered()
614 .into_iter()
615 .map(|(key, _)| key)
616 .collect();
617 assert_eq!(keys, ["a", "b", "c"]);
618 }
619
620 #[test]
621 fn test_sorted_cache_update_moves_position() {
622 let mut cache = SortedViewCache::new(
623 "test/latest".to_string(),
624 vec!["score".to_string()],
625 SortOrder::Desc,
626 );
627
628 cache.upsert("a".to_string(), json!({"score": 10}));
629 cache.upsert("b".to_string(), json!({"score": 20}));
630 cache.upsert("c".to_string(), json!({"score": 15}));
631
632 assert_eq!(cache.ordered_keys(), vec!["b", "c", "a"]);
634
635 cache.upsert("a".to_string(), json!({"score": 25}));
637
638 assert_eq!(cache.ordered_keys(), vec!["a", "b", "c"]);
640 }
641
642 #[test]
643 fn test_sorted_cache_remove() {
644 let mut cache = SortedViewCache::new(
645 "test/latest".to_string(),
646 vec!["id".to_string()],
647 SortOrder::Asc,
648 );
649
650 cache.upsert("a".to_string(), json!({"id": 1}));
651 cache.upsert("b".to_string(), json!({"id": 2}));
652 cache.upsert("c".to_string(), json!({"id": 3}));
653
654 assert_eq!(cache.len(), 3);
655
656 let pos = cache.remove("b");
657 assert_eq!(pos, Some(1));
658 assert_eq!(cache.len(), 2);
659 assert_eq!(cache.ordered_keys(), vec!["a", "c"]);
660 }
661
662 #[test]
663 fn test_compute_window_deltas() {
664 let mut cache = SortedViewCache::new(
665 "test/latest".to_string(),
666 vec!["id".to_string()],
667 SortOrder::Desc,
668 );
669
670 for i in 1..=5 {
672 cache.upsert(format!("e{}", i), json!({"id": i}));
673 }
674
675 let old_window: Vec<String> = vec!["e5".to_string(), "e4".to_string(), "e3".to_string()];
676
677 cache.upsert("e6".to_string(), json!({"id": 6}));
679
680 let deltas = cache.compute_window_deltas(&old_window, 0, 3);
683
684 assert_eq!(deltas.len(), 2);
685 assert!(deltas
687 .iter()
688 .any(|d| matches!(d, ViewDelta::Remove { key } if key == "e3")));
689 assert!(deltas
691 .iter()
692 .any(|d| matches!(d, ViewDelta::Add { key, .. } if key == "e6")));
693 }
694
695 fn keys(cache: &mut SortedViewCache) -> Vec<String> {
696 cache.ordered_keys().to_vec()
697 }
698
699 #[test]
700 fn bounded_upsert_evicts_bottom_of_desc_order() {
701 let mut cache = SortedViewCache::new(
702 "test/top".to_string(),
703 vec!["score".to_string()],
704 SortOrder::Desc,
705 );
706
707 for i in 1..=10 {
708 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 4);
709 assert!(cache.len() <= 4);
710 }
711
712 assert_eq!(cache.len(), 4);
713 assert_eq!(keys(&mut cache), ["e10", "e9", "e8", "e7"]);
714 assert!(cache.get("e1").is_none());
715 assert!(cache.get("e6").is_none());
716 }
717
718 #[test]
719 fn would_keep_matches_what_a_bounded_upsert_keeps() {
720 let mut cache = SortedViewCache::new(
721 "test/top".to_string(),
722 vec!["score".to_string()],
723 SortOrder::Desc,
724 );
725 for i in 1..=10 {
726 let entity = json!({"score": i});
727 let expected = cache.would_keep(&format!("e{i}"), &entity, 4);
728 cache.upsert_bounded(format!("e{i}"), entity, 4);
729 assert!(expected, "an entity above a full cache's tail is kept");
730 }
731 for (key, score, kept) in [("e0", 0, false), ("e6", 6, false), ("e11", 11, true)] {
733 let entity = json!({"score": score});
734 assert_eq!(cache.would_keep(key, &entity, 4), kept, "{key}");
735 let mut copy = SortedViewCache::new(
736 "test/top".to_string(),
737 vec!["score".to_string()],
738 SortOrder::Desc,
739 );
740 for existing in keys(&mut cache) {
741 let value = cache.get(&existing).unwrap().clone();
742 copy.upsert_bounded(existing, value, 4);
743 }
744 copy.upsert_bounded(key.to_string(), entity, 4);
745 assert_eq!(copy.get(key).is_some(), kept, "{key}");
746 }
747 assert!(cache.would_keep("e8", &json!({"score": -1}), 4));
749 }
750
751 #[test]
752 fn bounded_upsert_evicts_bottom_of_asc_order() {
753 let mut cache = SortedViewCache::new(
754 "test/bottom".to_string(),
755 vec!["score".to_string()],
756 SortOrder::Asc,
757 );
758
759 for i in (1..=10).rev() {
760 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 4);
761 assert!(cache.len() <= 4);
762 }
763
764 assert_eq!(cache.len(), 4);
765 assert_eq!(keys(&mut cache), ["e1", "e2", "e3", "e4"]);
766 assert!(cache.get("e10").is_none());
767 }
768
769 #[test]
770 fn bounded_upsert_does_not_evict_stale_top_entities() {
771 let mut cache = SortedViewCache::new(
772 "test/top".to_string(),
773 vec!["score".to_string()],
774 SortOrder::Desc,
775 );
776
777 cache.upsert_bounded("leader".to_string(), json!({"score": 1_000}), 3);
780 for i in 1..=20 {
781 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
782 }
783
784 assert_eq!(keys(&mut cache), ["leader", "e20", "e19"]);
785 }
786
787 #[test]
788 fn windows_within_cap_match_unbounded_cache() {
789 let mut bounded = SortedViewCache::new(
790 "test/top".to_string(),
791 vec!["score".to_string()],
792 SortOrder::Desc,
793 );
794 let mut unbounded = SortedViewCache::new(
795 "test/top".to_string(),
796 vec!["score".to_string()],
797 SortOrder::Desc,
798 );
799
800 for i in 0..200u64 {
805 let key = format!("e{}", i % 60);
806 let score = (i / 60) * 1_000 + (i * 37) % 101;
807 let entity = json!({"score": score, "n": i});
808 bounded.upsert_bounded(key.clone(), entity.clone(), 25);
809 unbounded.upsert(key, entity);
810 }
811
812 assert_eq!(bounded.len(), 25);
813 for (skip, take) in [(0, 25), (0, 10), (5, 20), (24, 1)] {
814 assert_eq!(
815 bounded.get_window(skip, take),
816 unbounded.get_window(skip, take),
817 "window skip={skip} take={take}"
818 );
819 }
820 }
821
822 #[test]
823 fn evicted_entity_is_missing_after_top_moves_down_until_it_updates() {
824 let mut cache = SortedViewCache::new(
825 "test/top".to_string(),
826 vec!["score".to_string()],
827 SortOrder::Desc,
828 );
829
830 for i in 1..=4 {
831 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
832 }
833 assert_eq!(keys(&mut cache), ["e4", "e3", "e2"]);
834
835 cache.upsert_bounded("e4".to_string(), json!({"score": 0}), 3);
838 assert_eq!(keys(&mut cache), ["e3", "e2", "e4"]);
839
840 cache.upsert_bounded("e1".to_string(), json!({"score": 1}), 3);
842 assert_eq!(keys(&mut cache), ["e3", "e2", "e1"]);
843 }
844
845 #[test]
846 fn evicted_entity_reenters_when_upserted_again() {
847 let mut cache = SortedViewCache::new(
848 "test/top".to_string(),
849 vec!["score".to_string()],
850 SortOrder::Desc,
851 );
852
853 for i in 1..=5 {
854 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
855 }
856 assert!(cache.get("e1").is_none());
857
858 let result = cache.upsert_bounded("e1".to_string(), json!({"score": 100}), 3);
859 assert_eq!(result, UpsertResult::Inserted { position: 0 });
860 assert_eq!(keys(&mut cache), ["e1", "e5", "e4"]);
861 assert_eq!(cache.len(), 3);
862 }
863
864 #[test]
865 fn upsert_below_full_cap_is_evicted_immediately() {
866 let mut cache = SortedViewCache::new(
867 "test/top".to_string(),
868 vec!["score".to_string()],
869 SortOrder::Desc,
870 );
871
872 for i in 10..=12 {
873 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
874 }
875 let result = cache.upsert_bounded("low".to_string(), json!({"score": 1}), 3);
876
877 assert_eq!(result, UpsertResult::Inserted { position: 3 });
878 assert!(cache.get("low").is_none());
879 assert_eq!(keys(&mut cache), ["e12", "e11", "e10"]);
880 }
881
882 #[test]
883 fn trim_keeps_keys_cache_and_entities_consistent() {
884 let mut cache = SortedViewCache::new(
885 "test/top".to_string(),
886 vec!["score".to_string()],
887 SortOrder::Desc,
888 );
889
890 for i in 1..=10 {
891 cache.upsert(format!("e{i}"), json!({"score": i}));
892 }
893 assert_eq!(cache.ordered_keys().len(), 10);
895
896 assert_eq!(cache.trim_to_max_entries(4), 6);
897 assert_eq!(cache.trim_to_max_entries(4), 0);
898 assert_eq!(cache.len(), 4);
899 assert_eq!(keys(&mut cache), ["e10", "e9", "e8", "e7"]);
900 assert_eq!(cache.get_all_ordered().len(), 4);
901 assert_eq!(cache.remove("e7"), Some(3));
902 assert_eq!(keys(&mut cache), ["e10", "e9", "e8"]);
903 }
904
905 #[test]
906 fn test_nested_sort_field() {
907 let mut cache = SortedViewCache::new(
908 "test/latest".to_string(),
909 vec!["id".to_string(), "round_id".to_string()],
910 SortOrder::Desc,
911 );
912
913 cache.upsert("a".to_string(), json!({"id": {"round_id": 1}}));
914 cache.upsert("b".to_string(), json!({"id": {"round_id": 3}}));
915 cache.upsert("c".to_string(), json!({"id": {"round_id": 2}}));
916
917 let keys = cache.ordered_keys();
918 assert_eq!(keys, vec!["b", "c", "a"]);
919 }
920
921 #[test]
922 fn test_nested_decimal_string_sort_field() {
923 let mut cache = SortedViewCache::new(
924 "test/latest".to_string(),
925 vec!["id".to_string(), "round_id".to_string()],
926 SortOrder::Desc,
927 );
928
929 cache.upsert("9".to_string(), json!({"id": {"round_id": "9"}}));
930 cache.upsert("100".to_string(), json!({"id": {"round_id": "100"}}));
931 cache.upsert("10".to_string(), json!({"id": {"round_id": "10"}}));
932
933 assert_eq!(cache.ordered_keys(), vec!["100", "10", "9"]);
934 }
935
936 #[test]
937 fn test_descending_string_sort_field() {
938 let mut cache = SortedViewCache::new(
939 "test/latest".to_string(),
940 vec!["name".to_string()],
941 SortOrder::Desc,
942 );
943
944 cache.upsert("a".to_string(), json!({"name": "alpha"}));
945 cache.upsert("c".to_string(), json!({"name": "charlie"}));
946 cache.upsert("b".to_string(), json!({"name": "bravo"}));
947
948 assert_eq!(cache.ordered_keys(), vec!["c", "b", "a"]);
949 }
950
951 #[test]
952 fn test_update_with_missing_sort_field_preserves_position() {
953 let mut cache = SortedViewCache::new(
954 "test/latest".to_string(),
955 vec!["id".to_string(), "round_id".to_string()],
956 SortOrder::Desc,
957 );
958
959 cache.upsert(
960 "100".to_string(),
961 json!({"id": {"round_id": 100}, "data": "initial"}),
962 );
963 cache.upsert(
964 "200".to_string(),
965 json!({"id": {"round_id": 200}, "data": "initial"}),
966 );
967 cache.upsert(
968 "300".to_string(),
969 json!({"id": {"round_id": 300}, "data": "initial"}),
970 );
971
972 assert_eq!(cache.ordered_keys(), vec!["300", "200", "100"]);
973
974 cache.upsert("200".to_string(), json!({"data": "updated_without_id"}));
975
976 assert_eq!(
977 cache.ordered_keys(),
978 vec!["300", "200", "100"],
979 "Entity 200 should retain its position even when updated without sort field"
980 );
981
982 let entity = cache.get("200").unwrap();
983 assert_eq!(entity["data"], "updated_without_id");
984 }
985
986 #[test]
987 fn test_new_entity_with_missing_sort_field_sorts_last() {
988 for order in [SortOrder::Desc, SortOrder::Asc] {
989 let mut cache = SortedViewCache::new(
990 "test/latest".to_string(),
991 vec!["id".to_string(), "round_id".to_string()],
992 order,
993 );
994
995 cache.upsert("100".to_string(), json!({"id": {"round_id": 100}}));
996 cache.upsert("200".to_string(), json!({"id": {"round_id": 200}}));
997 cache.upsert("new".to_string(), json!({"data": "no_sort_field"}));
998 cache.upsert("nil".to_string(), json!({"id": {"round_id": null}}));
999
1000 let ranked = match order {
1001 SortOrder::Desc => ["200", "100"],
1002 SortOrder::Asc => ["100", "200"],
1003 };
1004 assert_eq!(
1005 cache.ordered_keys(),
1006 [ranked[0], ranked[1], "new", "nil"],
1007 "{order:?}: an entity without a sort value never outranks one with it; \
1008 among themselves they order by key"
1009 );
1010 }
1011 }
1012
1013 #[test]
1016 fn a_full_window_does_not_admit_an_entity_without_a_sort_value() {
1017 let mut cache = SortedViewCache::new(
1018 "test/latest".to_string(),
1019 vec!["id".to_string(), "round_id".to_string()],
1020 SortOrder::Desc,
1021 );
1022 for round in [100, 200, 300] {
1023 cache.upsert_bounded(round.to_string(), json!({"id": {"round_id": round}}), 3);
1024 }
1025 let partial = json!({"metrics": {"checkpoint_count": 7}});
1026 assert!(!cache.would_keep("1", &partial, 3));
1027 cache.upsert_bounded("1".to_string(), partial, 3);
1028 assert_eq!(cache.ordered_keys(), ["300", "200", "100"]);
1029 }
1030}