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 remove(&mut self, entity_key: &str) -> Option<usize> {
363 if let Some((sort_key, _)) = self.entities.remove(entity_key) {
364 let position = self.find_position_by_sort_key(&sort_key);
365 self.sorted.remove(&sort_key);
366 self.cache_dirty = true;
367 Some(position)
368 } else {
369 None
370 }
371 }
372
373 pub fn get(&self, entity_key: &str) -> Option<&Value> {
375 self.entities.get(entity_key).map(|(_, v)| v)
376 }
377
378 pub fn ordered_keys(&mut self) -> &[String] {
380 if self.cache_dirty {
381 self.rebuild_keys_cache();
382 }
383 &self.keys_cache
384 }
385
386 pub fn get_window(&mut self, skip: usize, take: usize) -> Vec<(String, Value)> {
388 if self.cache_dirty {
389 self.rebuild_keys_cache();
390 }
391
392 self.keys_cache
393 .iter()
394 .skip(skip)
395 .take(take)
396 .filter_map(|key| {
397 self.entities
398 .get(key)
399 .map(|(_, v)| (key.clone(), v.clone()))
400 })
401 .collect()
402 }
403
404 pub fn get_all_ordered(&mut self) -> Vec<(String, Value)> {
406 if self.cache_dirty {
407 self.rebuild_keys_cache();
408 }
409
410 self.keys_cache
411 .iter()
412 .filter_map(|key| {
413 self.entities
414 .get(key)
415 .map(|(_, value)| (key.clone(), value.clone()))
416 })
417 .collect()
418 }
419
420 pub fn compute_window_deltas(
422 &mut self,
423 old_window_keys: &[String],
424 skip: usize,
425 take: usize,
426 ) -> Vec<ViewDelta> {
427 if self.cache_dirty {
428 self.rebuild_keys_cache();
429 }
430
431 let new_window_keys: Vec<&String> = self.keys_cache.iter().skip(skip).take(take).collect();
432
433 let old_set: std::collections::HashSet<&String> = old_window_keys.iter().collect();
434 let new_set: std::collections::HashSet<&String> = new_window_keys.iter().cloned().collect();
435
436 let mut deltas = Vec::new();
437
438 for key in old_set.difference(&new_set) {
440 deltas.push(ViewDelta::Remove {
441 key: (*key).clone(),
442 });
443 }
444
445 for key in new_set.difference(&old_set) {
447 if let Some((_, entity)) = self.entities.get(*key) {
448 deltas.push(ViewDelta::Add {
449 key: (*key).clone(),
450 entity: entity.clone(),
451 });
452 }
453 }
454
455 deltas
456 }
457
458 fn extract_sort_value(&self, entity: &Value) -> SortValue {
459 let mut current = entity;
460 for segment in &self.sort_field {
461 match current.get(segment) {
462 Some(v) => current = v,
463 None => return SortValue::Null,
464 }
465 }
466
467 value_to_sort_value(current)
468 }
469
470 fn find_position(&self, entity_key: &str) -> usize {
471 if let Some((sort_key, _)) = self.entities.get(entity_key) {
472 self.find_position_by_sort_key(sort_key)
473 } else {
474 0
475 }
476 }
477
478 fn find_position_by_sort_key(&self, sort_key: &SortKey) -> usize {
479 self.sorted.range(..sort_key).count()
480 }
481
482 fn rebuild_keys_cache(&mut self) {
483 self.keys_cache = self.sorted.keys().map(|sk| sk.entity_key.clone()).collect();
484 self.cache_dirty = false;
485 }
486}
487
488#[derive(Debug, Clone, PartialEq)]
490pub enum UpsertResult {
491 Inserted { position: usize },
493 Updated { position: usize },
495}
496
497fn value_to_sort_value(v: &Value) -> SortValue {
498 match v {
499 Value::Null => SortValue::Null,
500 Value::Bool(b) => SortValue::Bool(*b),
501 Value::Number(n) => {
502 if let Some(i) = n.as_i64() {
503 SortValue::Integer(i)
504 } else if let Some(f) = n.as_f64() {
505 SortValue::Float(OrderedFloat(f))
506 } else {
507 SortValue::Null
508 }
509 }
510 Value::String(s) => SortValue::String(s.clone()),
511 _ => SortValue::Null,
512 }
513}
514
515fn compare_decimal_strings(left: &str, right: &str) -> Option<Ordering> {
516 fn parts(value: &str) -> Option<(bool, &str)> {
517 let (negative, digits) = match value.strip_prefix('-') {
518 Some(digits) => (true, digits),
519 None => (false, value),
520 };
521 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
522 return None;
523 }
524
525 let digits = digits.trim_start_matches('0');
526 let digits = if digits.is_empty() { "0" } else { digits };
527 Some((negative && digits != "0", digits))
528 }
529
530 let (left_negative, left_digits) = parts(left)?;
531 let (right_negative, right_digits) = parts(right)?;
532
533 match (left_negative, right_negative) {
534 (true, false) => Some(Ordering::Less),
535 (false, true) => Some(Ordering::Greater),
536 _ => {
537 let magnitude = left_digits
538 .len()
539 .cmp(&right_digits.len())
540 .then_with(|| left_digits.cmp(right_digits));
541 Some(if left_negative {
542 magnitude.reverse()
543 } else {
544 magnitude
545 })
546 }
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use serde_json::json;
554
555 #[test]
556 fn test_sorted_cache_basic() {
557 let mut cache = SortedViewCache::new(
558 "test/latest".to_string(),
559 vec!["id".to_string()],
560 SortOrder::Desc,
561 );
562
563 cache.upsert("a".to_string(), json!({"id": 1, "name": "first"}));
564 cache.upsert("b".to_string(), json!({"id": 3, "name": "third"}));
565 cache.upsert("c".to_string(), json!({"id": 2, "name": "second"}));
566
567 let keys = cache.ordered_keys();
568 assert_eq!(keys, vec!["b", "c", "a"]);
570 }
571
572 #[test]
573 fn test_sorted_cache_window() {
574 let mut cache = SortedViewCache::new(
575 "test/latest".to_string(),
576 vec!["id".to_string()],
577 SortOrder::Desc,
578 );
579
580 for i in 1..=10 {
581 cache.upsert(format!("e{}", i), json!({"id": i}));
582 }
583
584 let window = cache.get_window(0, 3);
586 assert_eq!(window.len(), 3);
587 assert_eq!(window[0].0, "e10");
588 assert_eq!(window[1].0, "e9");
589 assert_eq!(window[2].0, "e8");
590
591 let window = cache.get_window(3, 3);
592 assert_eq!(window[0].0, "e7");
593 }
594
595 #[test]
596 fn all_ordered_preserves_stable_sort_and_tie_breaking() {
597 let mut cache = SortedViewCache::new(
598 "test/latest".to_string(),
599 vec!["score".to_string()],
600 SortOrder::Desc,
601 );
602 cache.upsert("b".to_string(), json!({"score": 10}));
603 cache.upsert("a".to_string(), json!({"score": 10}));
604 cache.upsert("c".to_string(), json!({"score": 9}));
605
606 let keys: Vec<_> = cache
607 .get_all_ordered()
608 .into_iter()
609 .map(|(key, _)| key)
610 .collect();
611 assert_eq!(keys, ["a", "b", "c"]);
612 }
613
614 #[test]
615 fn test_sorted_cache_update_moves_position() {
616 let mut cache = SortedViewCache::new(
617 "test/latest".to_string(),
618 vec!["score".to_string()],
619 SortOrder::Desc,
620 );
621
622 cache.upsert("a".to_string(), json!({"score": 10}));
623 cache.upsert("b".to_string(), json!({"score": 20}));
624 cache.upsert("c".to_string(), json!({"score": 15}));
625
626 assert_eq!(cache.ordered_keys(), vec!["b", "c", "a"]);
628
629 cache.upsert("a".to_string(), json!({"score": 25}));
631
632 assert_eq!(cache.ordered_keys(), vec!["a", "b", "c"]);
634 }
635
636 #[test]
637 fn test_sorted_cache_remove() {
638 let mut cache = SortedViewCache::new(
639 "test/latest".to_string(),
640 vec!["id".to_string()],
641 SortOrder::Asc,
642 );
643
644 cache.upsert("a".to_string(), json!({"id": 1}));
645 cache.upsert("b".to_string(), json!({"id": 2}));
646 cache.upsert("c".to_string(), json!({"id": 3}));
647
648 assert_eq!(cache.len(), 3);
649
650 let pos = cache.remove("b");
651 assert_eq!(pos, Some(1));
652 assert_eq!(cache.len(), 2);
653 assert_eq!(cache.ordered_keys(), vec!["a", "c"]);
654 }
655
656 #[test]
657 fn test_compute_window_deltas() {
658 let mut cache = SortedViewCache::new(
659 "test/latest".to_string(),
660 vec!["id".to_string()],
661 SortOrder::Desc,
662 );
663
664 for i in 1..=5 {
666 cache.upsert(format!("e{}", i), json!({"id": i}));
667 }
668
669 let old_window: Vec<String> = vec!["e5".to_string(), "e4".to_string(), "e3".to_string()];
670
671 cache.upsert("e6".to_string(), json!({"id": 6}));
673
674 let deltas = cache.compute_window_deltas(&old_window, 0, 3);
677
678 assert_eq!(deltas.len(), 2);
679 assert!(deltas
681 .iter()
682 .any(|d| matches!(d, ViewDelta::Remove { key } if key == "e3")));
683 assert!(deltas
685 .iter()
686 .any(|d| matches!(d, ViewDelta::Add { key, .. } if key == "e6")));
687 }
688
689 fn keys(cache: &mut SortedViewCache) -> Vec<String> {
690 cache.ordered_keys().to_vec()
691 }
692
693 #[test]
694 fn bounded_upsert_evicts_bottom_of_desc_order() {
695 let mut cache = SortedViewCache::new(
696 "test/top".to_string(),
697 vec!["score".to_string()],
698 SortOrder::Desc,
699 );
700
701 for i in 1..=10 {
702 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 4);
703 assert!(cache.len() <= 4);
704 }
705
706 assert_eq!(cache.len(), 4);
707 assert_eq!(keys(&mut cache), ["e10", "e9", "e8", "e7"]);
708 assert!(cache.get("e1").is_none());
709 assert!(cache.get("e6").is_none());
710 }
711
712 #[test]
713 fn would_keep_matches_what_a_bounded_upsert_keeps() {
714 let mut cache = SortedViewCache::new(
715 "test/top".to_string(),
716 vec!["score".to_string()],
717 SortOrder::Desc,
718 );
719 for i in 1..=10 {
720 let entity = json!({"score": i});
721 let expected = cache.would_keep(&format!("e{i}"), &entity, 4);
722 cache.upsert_bounded(format!("e{i}"), entity, 4);
723 assert!(expected, "an entity above a full cache's tail is kept");
724 }
725 for (key, score, kept) in [("e0", 0, false), ("e6", 6, false), ("e11", 11, true)] {
727 let entity = json!({"score": score});
728 assert_eq!(cache.would_keep(key, &entity, 4), kept, "{key}");
729 let mut copy = SortedViewCache::new(
730 "test/top".to_string(),
731 vec!["score".to_string()],
732 SortOrder::Desc,
733 );
734 for existing in keys(&mut cache) {
735 let value = cache.get(&existing).unwrap().clone();
736 copy.upsert_bounded(existing, value, 4);
737 }
738 copy.upsert_bounded(key.to_string(), entity, 4);
739 assert_eq!(copy.get(key).is_some(), kept, "{key}");
740 }
741 assert!(cache.would_keep("e8", &json!({"score": -1}), 4));
743 }
744
745 #[test]
746 fn bounded_upsert_evicts_bottom_of_asc_order() {
747 let mut cache = SortedViewCache::new(
748 "test/bottom".to_string(),
749 vec!["score".to_string()],
750 SortOrder::Asc,
751 );
752
753 for i in (1..=10).rev() {
754 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 4);
755 assert!(cache.len() <= 4);
756 }
757
758 assert_eq!(cache.len(), 4);
759 assert_eq!(keys(&mut cache), ["e1", "e2", "e3", "e4"]);
760 assert!(cache.get("e10").is_none());
761 }
762
763 #[test]
764 fn bounded_upsert_does_not_evict_stale_top_entities() {
765 let mut cache = SortedViewCache::new(
766 "test/top".to_string(),
767 vec!["score".to_string()],
768 SortOrder::Desc,
769 );
770
771 cache.upsert_bounded("leader".to_string(), json!({"score": 1_000}), 3);
774 for i in 1..=20 {
775 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
776 }
777
778 assert_eq!(keys(&mut cache), ["leader", "e20", "e19"]);
779 }
780
781 #[test]
782 fn windows_within_cap_match_unbounded_cache() {
783 let mut bounded = SortedViewCache::new(
784 "test/top".to_string(),
785 vec!["score".to_string()],
786 SortOrder::Desc,
787 );
788 let mut unbounded = SortedViewCache::new(
789 "test/top".to_string(),
790 vec!["score".to_string()],
791 SortOrder::Desc,
792 );
793
794 for i in 0..200u64 {
799 let key = format!("e{}", i % 60);
800 let score = (i / 60) * 1_000 + (i * 37) % 101;
801 let entity = json!({"score": score, "n": i});
802 bounded.upsert_bounded(key.clone(), entity.clone(), 25);
803 unbounded.upsert(key, entity);
804 }
805
806 assert_eq!(bounded.len(), 25);
807 for (skip, take) in [(0, 25), (0, 10), (5, 20), (24, 1)] {
808 assert_eq!(
809 bounded.get_window(skip, take),
810 unbounded.get_window(skip, take),
811 "window skip={skip} take={take}"
812 );
813 }
814 }
815
816 #[test]
817 fn evicted_entity_is_missing_after_top_moves_down_until_it_updates() {
818 let mut cache = SortedViewCache::new(
819 "test/top".to_string(),
820 vec!["score".to_string()],
821 SortOrder::Desc,
822 );
823
824 for i in 1..=4 {
825 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
826 }
827 assert_eq!(keys(&mut cache), ["e4", "e3", "e2"]);
828
829 cache.upsert_bounded("e4".to_string(), json!({"score": 0}), 3);
832 assert_eq!(keys(&mut cache), ["e3", "e2", "e4"]);
833
834 cache.upsert_bounded("e1".to_string(), json!({"score": 1}), 3);
836 assert_eq!(keys(&mut cache), ["e3", "e2", "e1"]);
837 }
838
839 #[test]
840 fn evicted_entity_reenters_when_upserted_again() {
841 let mut cache = SortedViewCache::new(
842 "test/top".to_string(),
843 vec!["score".to_string()],
844 SortOrder::Desc,
845 );
846
847 for i in 1..=5 {
848 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
849 }
850 assert!(cache.get("e1").is_none());
851
852 let result = cache.upsert_bounded("e1".to_string(), json!({"score": 100}), 3);
853 assert_eq!(result, UpsertResult::Inserted { position: 0 });
854 assert_eq!(keys(&mut cache), ["e1", "e5", "e4"]);
855 assert_eq!(cache.len(), 3);
856 }
857
858 #[test]
859 fn upsert_below_full_cap_is_evicted_immediately() {
860 let mut cache = SortedViewCache::new(
861 "test/top".to_string(),
862 vec!["score".to_string()],
863 SortOrder::Desc,
864 );
865
866 for i in 10..=12 {
867 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
868 }
869 let result = cache.upsert_bounded("low".to_string(), json!({"score": 1}), 3);
870
871 assert_eq!(result, UpsertResult::Inserted { position: 3 });
872 assert!(cache.get("low").is_none());
873 assert_eq!(keys(&mut cache), ["e12", "e11", "e10"]);
874 }
875
876 #[test]
877 fn trim_keeps_keys_cache_and_entities_consistent() {
878 let mut cache = SortedViewCache::new(
879 "test/top".to_string(),
880 vec!["score".to_string()],
881 SortOrder::Desc,
882 );
883
884 for i in 1..=10 {
885 cache.upsert(format!("e{i}"), json!({"score": i}));
886 }
887 assert_eq!(cache.ordered_keys().len(), 10);
889
890 assert_eq!(cache.trim_to_max_entries(4), 6);
891 assert_eq!(cache.trim_to_max_entries(4), 0);
892 assert_eq!(cache.len(), 4);
893 assert_eq!(keys(&mut cache), ["e10", "e9", "e8", "e7"]);
894 assert_eq!(cache.get_all_ordered().len(), 4);
895 assert_eq!(cache.remove("e7"), Some(3));
896 assert_eq!(keys(&mut cache), ["e10", "e9", "e8"]);
897 }
898
899 #[test]
900 fn test_nested_sort_field() {
901 let mut cache = SortedViewCache::new(
902 "test/latest".to_string(),
903 vec!["id".to_string(), "round_id".to_string()],
904 SortOrder::Desc,
905 );
906
907 cache.upsert("a".to_string(), json!({"id": {"round_id": 1}}));
908 cache.upsert("b".to_string(), json!({"id": {"round_id": 3}}));
909 cache.upsert("c".to_string(), json!({"id": {"round_id": 2}}));
910
911 let keys = cache.ordered_keys();
912 assert_eq!(keys, vec!["b", "c", "a"]);
913 }
914
915 #[test]
916 fn test_nested_decimal_string_sort_field() {
917 let mut cache = SortedViewCache::new(
918 "test/latest".to_string(),
919 vec!["id".to_string(), "round_id".to_string()],
920 SortOrder::Desc,
921 );
922
923 cache.upsert("9".to_string(), json!({"id": {"round_id": "9"}}));
924 cache.upsert("100".to_string(), json!({"id": {"round_id": "100"}}));
925 cache.upsert("10".to_string(), json!({"id": {"round_id": "10"}}));
926
927 assert_eq!(cache.ordered_keys(), vec!["100", "10", "9"]);
928 }
929
930 #[test]
931 fn test_descending_string_sort_field() {
932 let mut cache = SortedViewCache::new(
933 "test/latest".to_string(),
934 vec!["name".to_string()],
935 SortOrder::Desc,
936 );
937
938 cache.upsert("a".to_string(), json!({"name": "alpha"}));
939 cache.upsert("c".to_string(), json!({"name": "charlie"}));
940 cache.upsert("b".to_string(), json!({"name": "bravo"}));
941
942 assert_eq!(cache.ordered_keys(), vec!["c", "b", "a"]);
943 }
944
945 #[test]
946 fn test_update_with_missing_sort_field_preserves_position() {
947 let mut cache = SortedViewCache::new(
948 "test/latest".to_string(),
949 vec!["id".to_string(), "round_id".to_string()],
950 SortOrder::Desc,
951 );
952
953 cache.upsert(
954 "100".to_string(),
955 json!({"id": {"round_id": 100}, "data": "initial"}),
956 );
957 cache.upsert(
958 "200".to_string(),
959 json!({"id": {"round_id": 200}, "data": "initial"}),
960 );
961 cache.upsert(
962 "300".to_string(),
963 json!({"id": {"round_id": 300}, "data": "initial"}),
964 );
965
966 assert_eq!(cache.ordered_keys(), vec!["300", "200", "100"]);
967
968 cache.upsert("200".to_string(), json!({"data": "updated_without_id"}));
969
970 assert_eq!(
971 cache.ordered_keys(),
972 vec!["300", "200", "100"],
973 "Entity 200 should retain its position even when updated without sort field"
974 );
975
976 let entity = cache.get("200").unwrap();
977 assert_eq!(entity["data"], "updated_without_id");
978 }
979
980 #[test]
981 fn test_new_entity_with_missing_sort_field_sorts_last() {
982 for order in [SortOrder::Desc, SortOrder::Asc] {
983 let mut cache = SortedViewCache::new(
984 "test/latest".to_string(),
985 vec!["id".to_string(), "round_id".to_string()],
986 order,
987 );
988
989 cache.upsert("100".to_string(), json!({"id": {"round_id": 100}}));
990 cache.upsert("200".to_string(), json!({"id": {"round_id": 200}}));
991 cache.upsert("new".to_string(), json!({"data": "no_sort_field"}));
992 cache.upsert("nil".to_string(), json!({"id": {"round_id": null}}));
993
994 let ranked = match order {
995 SortOrder::Desc => ["200", "100"],
996 SortOrder::Asc => ["100", "200"],
997 };
998 assert_eq!(
999 cache.ordered_keys(),
1000 [ranked[0], ranked[1], "new", "nil"],
1001 "{order:?}: an entity without a sort value never outranks one with it; \
1002 among themselves they order by key"
1003 );
1004 }
1005 }
1006
1007 #[test]
1010 fn a_full_window_does_not_admit_an_entity_without_a_sort_value() {
1011 let mut cache = SortedViewCache::new(
1012 "test/latest".to_string(),
1013 vec!["id".to_string(), "round_id".to_string()],
1014 SortOrder::Desc,
1015 );
1016 for round in [100, 200, 300] {
1017 cache.upsert_bounded(round.to_string(), json!({"id": {"round_id": round}}), 3);
1018 }
1019 let partial = json!({"metrics": {"checkpoint_count": 7}});
1020 assert!(!cache.would_keep("1", &partial, 3));
1021 cache.upsert_bounded("1".to_string(), partial, 3);
1022 assert_eq!(cache.ordered_keys(), ["300", "200", "100"]);
1023 }
1024}