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