1use crate::shared_entity::{lookup, EntityFields, SharedEntity};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::cmp::Ordering;
11use std::collections::{BTreeMap, HashMap};
12use std::sync::Arc;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct SortKey {
19 sort_value: SortValue,
21 entity_key: String,
23 order: SortOrder,
25}
26
27impl PartialOrd for SortKey {
28 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
29 Some(self.cmp(other))
30 }
31}
32
33impl Ord for SortKey {
34 fn cmp(&self, other: &Self) -> Ordering {
35 if self.order != other.order {
36 return match (self.order, other.order) {
37 (SortOrder::Asc, SortOrder::Desc) => Ordering::Less,
38 (SortOrder::Desc, SortOrder::Asc) => Ordering::Greater,
39 _ => Ordering::Equal,
40 };
41 }
42
43 let sort_order = match (&self.sort_value, &other.sort_value, self.order) {
48 (SortValue::Null, SortValue::Null, _) => Ordering::Equal,
49 (SortValue::Null, _, _) => Ordering::Greater,
50 (_, SortValue::Null, _) => Ordering::Less,
51 (_, _, SortOrder::Asc) => self.sort_value.cmp(&other.sort_value),
52 (_, _, SortOrder::Desc) => self.sort_value.cmp(&other.sort_value).reverse(),
53 };
54
55 match sort_order {
56 Ordering::Equal => self.entity_key.cmp(&other.entity_key),
57 other => other,
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum SortValue {
65 Null,
66 Bool(bool),
67 Integer(i64),
68 Float(OrderedFloat),
69 String(String),
70}
71
72impl Ord for SortValue {
73 fn cmp(&self, other: &Self) -> Ordering {
74 match (self, other) {
75 (SortValue::Null, SortValue::Null) => Ordering::Equal,
76 (SortValue::Null, _) => Ordering::Less,
77 (_, SortValue::Null) => Ordering::Greater,
78 (SortValue::Bool(a), SortValue::Bool(b)) => a.cmp(b),
79 (SortValue::Integer(a), SortValue::Integer(b)) => a.cmp(b),
80 (SortValue::Float(a), SortValue::Float(b)) => a.cmp(b),
81 (SortValue::String(a), SortValue::String(b)) => {
82 compare_decimal_strings(a, b).unwrap_or_else(|| a.cmp(b))
83 }
84 (SortValue::Integer(_), SortValue::String(_)) => Ordering::Less,
86 (SortValue::String(_), SortValue::Integer(_)) => Ordering::Greater,
87 (SortValue::Float(_), SortValue::String(_)) => Ordering::Less,
88 (SortValue::String(_), SortValue::Float(_)) => Ordering::Greater,
89 (SortValue::Integer(a), SortValue::Float(b)) => OrderedFloat(*a as f64).cmp(b),
91 (SortValue::Float(a), SortValue::Integer(b)) => a.cmp(&OrderedFloat(*b as f64)),
92 (SortValue::Bool(_), _) => Ordering::Less,
94 (_, SortValue::Bool(_)) => Ordering::Greater,
95 }
96 }
97}
98
99impl PartialOrd for SortValue {
100 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
101 Some(self.cmp(other))
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq)]
107pub struct OrderedFloat(pub f64);
108
109impl Eq for OrderedFloat {}
110
111impl Ord for OrderedFloat {
112 fn cmp(&self, other: &Self) -> Ordering {
113 self.0.partial_cmp(&other.0).unwrap_or_else(|| {
114 if self.0.is_nan() && other.0.is_nan() {
115 Ordering::Equal
116 } else if self.0.is_nan() {
117 Ordering::Less
118 } else {
119 Ordering::Greater
120 }
121 })
122 }
123}
124
125impl PartialOrd for OrderedFloat {
126 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
127 Some(self.cmp(other))
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(rename_all = "lowercase")]
134pub enum SortOrder {
135 Asc,
136 Desc,
137}
138
139impl From<crate::materialized_view::SortOrder> for SortOrder {
140 fn from(order: crate::materialized_view::SortOrder) -> Self {
141 match order {
142 crate::materialized_view::SortOrder::Asc => SortOrder::Asc,
143 crate::materialized_view::SortOrder::Desc => SortOrder::Desc,
144 }
145 }
146}
147
148#[derive(Debug, Clone, PartialEq)]
150pub enum ViewDelta {
151 None,
153 Add { key: String, entity: Value },
155 Remove { key: String },
157 Update { key: String, entity: Value },
159}
160
161#[derive(Debug)]
186pub struct SortedViewCache {
187 view_id: String,
189 sort_field: Vec<String>,
191 order: SortOrder,
193 sorted: BTreeMap<SortKey, ()>,
195 entities: HashMap<String, (SortKey, SharedEntity)>,
197 keys_cache: Vec<String>,
199 cache_dirty: bool,
201}
202
203impl SortedViewCache {
204 pub fn new(view_id: String, sort_field: Vec<String>, order: SortOrder) -> Self {
205 Self {
206 view_id,
207 sort_field,
208 order,
209 sorted: BTreeMap::new(),
210 entities: HashMap::new(),
211 keys_cache: Vec::new(),
212 cache_dirty: true,
213 }
214 }
215
216 pub fn view_id(&self) -> &str {
217 &self.view_id
218 }
219
220 pub fn len(&self) -> usize {
221 self.entities.len()
222 }
223
224 pub fn is_empty(&self) -> bool {
225 self.entities.is_empty()
226 }
227
228 pub fn upsert(&mut self, entity_key: String, entity: impl Into<SharedEntity>) -> UpsertResult {
234 let entity = entity.into();
235 let sort_value = self.extract_sort_value(&entity);
236
237 if let Some((old_sort_key, old_entity)) = self.entities.remove(&entity_key) {
240 let effective_sort_value = if matches!(sort_value, SortValue::Null)
241 && !matches!(old_sort_key.sort_value, SortValue::Null)
242 {
243 old_sort_key.sort_value.clone()
244 } else {
245 sort_value
246 };
247
248 let new_sort_key = SortKey {
249 sort_value: effective_sort_value,
250 entity_key: entity_key.clone(),
251 order: self.order,
252 };
253
254 let merged_entity = Self::merge_entity(old_entity, entity);
256
257 if old_sort_key == new_sort_key {
258 self.entities
260 .insert(entity_key.clone(), (new_sort_key, merged_entity));
261 let position = self.find_position(&entity_key);
263 return UpsertResult::Updated { position };
264 }
265
266 self.sorted.remove(&old_sort_key);
268 self.sorted.insert(new_sort_key.clone(), ());
269 self.entities
270 .insert(entity_key.clone(), (new_sort_key, merged_entity));
271 self.cache_dirty = true;
272
273 let position = self.find_position(&entity_key);
274 return UpsertResult::Inserted { position };
275 }
276
277 let new_sort_key = SortKey {
278 sort_value,
279 entity_key: entity_key.clone(),
280 order: self.order,
281 };
282
283 self.sorted.insert(new_sort_key.clone(), ());
284 self.entities
285 .insert(entity_key.clone(), (new_sort_key, entity));
286 self.cache_dirty = true;
287
288 let position = self.find_position(&entity_key);
289
290 UpsertResult::Inserted { position }
291 }
292
293 pub fn would_keep<E: EntityFields + ?Sized>(
300 &self,
301 entity_key: &str,
302 entity: &E,
303 max_entries: usize,
304 ) -> bool {
305 if self.entities.contains_key(entity_key) || self.sorted.len() < max_entries {
306 return true;
307 }
308 let Some((last, ())) = self.sorted.last_key_value() else {
309 return true;
310 };
311 let candidate = SortKey {
312 sort_value: self.extract_sort_value(entity),
313 entity_key: entity_key.to_string(),
314 order: self.order,
315 };
316 candidate < *last
317 }
318
319 pub fn upsert_bounded(
325 &mut self,
326 entity_key: String,
327 entity: impl Into<SharedEntity>,
328 max_entries: usize,
329 ) -> UpsertResult {
330 let result = self.upsert(entity_key, entity);
331 self.trim_to_max_entries(max_entries);
332 result
333 }
334
335 pub fn trim_to_max_entries(&mut self, max_entries: usize) -> usize {
343 let mut evicted = 0;
344 while self.sorted.len() > max_entries {
345 let Some((sort_key, ())) = self.sorted.pop_last() else {
346 break;
347 };
348 self.entities.remove(&sort_key.entity_key);
349 evicted += 1;
350 }
351 if evicted > 0 && !self.cache_dirty {
352 self.keys_cache.truncate(self.sorted.len());
355 }
356 evicted
357 }
358
359 fn merge_entity(base: SharedEntity, patch: SharedEntity) -> SharedEntity {
363 let (base_fields, base_version) = base.into_parts();
364 let (patch_fields, patch_version) = patch.into_parts();
365 if !(base_fields.is_object() && patch_fields.is_object()) {
366 return SharedEntity::from_parts(patch_fields, patch_version);
367 }
368 let fields = if Arc::ptr_eq(&base_fields, &patch_fields)
369 || !keeps_fields(&base_fields, &patch_fields)
370 {
371 patch_fields
372 } else {
373 let base = Arc::try_unwrap(base_fields).unwrap_or_else(|shared| Value::clone(&shared));
374 Arc::new(Self::deep_merge(base, Value::clone(&patch_fields)))
375 };
376 let version = match (base_version, patch_version) {
377 (Some(base), Some(patch)) => Some(Self::deep_merge(base, patch)),
378 (base, None) => base,
379 (None, patch) => patch,
380 };
381 SharedEntity::from_parts(fields, version)
382 }
383
384 fn deep_merge(base: Value, patch: Value) -> Value {
385 match (base, patch) {
386 (Value::Object(mut base_map), Value::Object(patch_map)) => {
387 for (key, patch_value) in patch_map {
388 if let Some(base_value) = base_map.remove(&key) {
389 base_map.insert(key, Self::deep_merge(base_value, patch_value));
390 } else {
391 base_map.insert(key, patch_value);
392 }
393 }
394 Value::Object(base_map)
395 }
396 (_, patch) => patch,
397 }
398 }
399
400 pub fn clear(&mut self) {
402 self.sorted.clear();
403 self.entities.clear();
404 self.cache_dirty = true;
405 }
406
407 pub fn remove(&mut self, entity_key: &str) -> Option<usize> {
408 if let Some((sort_key, _)) = self.entities.remove(entity_key) {
409 let position = self.find_position_by_sort_key(&sort_key);
410 self.sorted.remove(&sort_key);
411 self.cache_dirty = true;
412 Some(position)
413 } else {
414 None
415 }
416 }
417
418 pub fn get(&self, entity_key: &str) -> Option<&SharedEntity> {
420 self.entities.get(entity_key).map(|(_, v)| v)
421 }
422
423 pub fn ordered_keys(&mut self) -> &[String] {
425 if self.cache_dirty {
426 self.rebuild_keys_cache();
427 }
428 &self.keys_cache
429 }
430
431 pub fn get_window(&mut self, skip: usize, take: usize) -> Vec<(String, Value)> {
433 if self.cache_dirty {
434 self.rebuild_keys_cache();
435 }
436
437 self.keys_cache
438 .iter()
439 .skip(skip)
440 .take(take)
441 .filter_map(|key| {
442 self.entities
443 .get(key)
444 .map(|(_, v)| (key.clone(), v.to_value()))
445 })
446 .collect()
447 }
448
449 pub fn get_all_ordered(&mut self) -> Vec<(String, Value)> {
451 self.ordered_entities()
452 .into_iter()
453 .map(|(key, entity)| (key, entity.into_value()))
454 .collect()
455 }
456
457 pub fn ordered_entities(&mut self) -> Vec<(String, SharedEntity)> {
460 if self.cache_dirty {
461 self.rebuild_keys_cache();
462 }
463
464 self.keys_cache
465 .iter()
466 .filter_map(|key| {
467 self.entities
468 .get(key)
469 .map(|(_, entity)| (key.clone(), entity.clone()))
470 })
471 .collect()
472 }
473
474 pub fn compute_window_deltas(
476 &mut self,
477 old_window_keys: &[String],
478 skip: usize,
479 take: usize,
480 ) -> Vec<ViewDelta> {
481 if self.cache_dirty {
482 self.rebuild_keys_cache();
483 }
484
485 let new_window_keys: Vec<&String> = self.keys_cache.iter().skip(skip).take(take).collect();
486
487 let old_set: std::collections::HashSet<&String> = old_window_keys.iter().collect();
488 let new_set: std::collections::HashSet<&String> = new_window_keys.iter().cloned().collect();
489
490 let mut deltas = Vec::new();
491
492 for key in old_set.difference(&new_set) {
494 deltas.push(ViewDelta::Remove {
495 key: (*key).clone(),
496 });
497 }
498
499 for key in new_set.difference(&old_set) {
501 if let Some((_, entity)) = self.entities.get(*key) {
502 deltas.push(ViewDelta::Add {
503 key: (*key).clone(),
504 entity: entity.to_value(),
505 });
506 }
507 }
508
509 deltas
510 }
511
512 fn extract_sort_value<E: EntityFields + ?Sized>(&self, entity: &E) -> SortValue {
513 match lookup(entity, &self.sort_field) {
514 Some(value) => value_to_sort_value(&value),
515 None => SortValue::Null,
516 }
517 }
518
519 fn find_position(&self, entity_key: &str) -> usize {
520 if let Some((sort_key, _)) = self.entities.get(entity_key) {
521 self.find_position_by_sort_key(sort_key)
522 } else {
523 0
524 }
525 }
526
527 fn find_position_by_sort_key(&self, sort_key: &SortKey) -> usize {
528 self.sorted.range(..sort_key).count()
529 }
530
531 fn rebuild_keys_cache(&mut self) {
532 self.keys_cache = self.sorted.keys().map(|sk| sk.entity_key.clone()).collect();
533 self.cache_dirty = false;
534 }
535}
536
537#[derive(Debug, Clone, PartialEq)]
539pub enum UpsertResult {
540 Inserted { position: usize },
542 Updated { position: usize },
544}
545
546fn keeps_fields(base: &Value, patch: &Value) -> bool {
549 match (base, patch) {
550 (Value::Object(base), Value::Object(patch)) => base
551 .iter()
552 .any(|(key, base)| patch.get(key).is_none_or(|patch| keeps_fields(base, patch))),
553 _ => false,
554 }
555}
556
557fn value_to_sort_value(v: &Value) -> SortValue {
558 match v {
559 Value::Null => SortValue::Null,
560 Value::Bool(b) => SortValue::Bool(*b),
561 Value::Number(n) => {
562 if let Some(i) = n.as_i64() {
563 SortValue::Integer(i)
564 } else if let Some(f) = n.as_f64() {
565 SortValue::Float(OrderedFloat(f))
566 } else {
567 SortValue::Null
568 }
569 }
570 Value::String(s) => SortValue::String(s.clone()),
571 _ => SortValue::Null,
572 }
573}
574
575fn compare_decimal_strings(left: &str, right: &str) -> Option<Ordering> {
576 fn parts(value: &str) -> Option<(bool, &str)> {
577 let (negative, digits) = match value.strip_prefix('-') {
578 Some(digits) => (true, digits),
579 None => (false, value),
580 };
581 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
582 return None;
583 }
584
585 let digits = digits.trim_start_matches('0');
586 let digits = if digits.is_empty() { "0" } else { digits };
587 Some((negative && digits != "0", digits))
588 }
589
590 let (left_negative, left_digits) = parts(left)?;
591 let (right_negative, right_digits) = parts(right)?;
592
593 match (left_negative, right_negative) {
594 (true, false) => Some(Ordering::Less),
595 (false, true) => Some(Ordering::Greater),
596 _ => {
597 let magnitude = left_digits
598 .len()
599 .cmp(&right_digits.len())
600 .then_with(|| left_digits.cmp(right_digits));
601 Some(if left_negative {
602 magnitude.reverse()
603 } else {
604 magnitude
605 })
606 }
607 }
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613 use serde_json::json;
614
615 #[test]
616 fn test_sorted_cache_basic() {
617 let mut cache = SortedViewCache::new(
618 "test/latest".to_string(),
619 vec!["id".to_string()],
620 SortOrder::Desc,
621 );
622
623 cache.upsert("a".to_string(), json!({"id": 1, "name": "first"}));
624 cache.upsert("b".to_string(), json!({"id": 3, "name": "third"}));
625 cache.upsert("c".to_string(), json!({"id": 2, "name": "second"}));
626
627 let keys = cache.ordered_keys();
628 assert_eq!(keys, vec!["b", "c", "a"]);
630 }
631
632 #[test]
633 fn test_sorted_cache_window() {
634 let mut cache = SortedViewCache::new(
635 "test/latest".to_string(),
636 vec!["id".to_string()],
637 SortOrder::Desc,
638 );
639
640 for i in 1..=10 {
641 cache.upsert(format!("e{}", i), json!({"id": i}));
642 }
643
644 let window = cache.get_window(0, 3);
646 assert_eq!(window.len(), 3);
647 assert_eq!(window[0].0, "e10");
648 assert_eq!(window[1].0, "e9");
649 assert_eq!(window[2].0, "e8");
650
651 let window = cache.get_window(3, 3);
652 assert_eq!(window[0].0, "e7");
653 }
654
655 #[test]
656 fn all_ordered_preserves_stable_sort_and_tie_breaking() {
657 let mut cache = SortedViewCache::new(
658 "test/latest".to_string(),
659 vec!["score".to_string()],
660 SortOrder::Desc,
661 );
662 cache.upsert("b".to_string(), json!({"score": 10}));
663 cache.upsert("a".to_string(), json!({"score": 10}));
664 cache.upsert("c".to_string(), json!({"score": 9}));
665
666 let keys: Vec<_> = cache
667 .get_all_ordered()
668 .into_iter()
669 .map(|(key, _)| key)
670 .collect();
671 assert_eq!(keys, ["a", "b", "c"]);
672 }
673
674 #[test]
675 fn test_sorted_cache_update_moves_position() {
676 let mut cache = SortedViewCache::new(
677 "test/latest".to_string(),
678 vec!["score".to_string()],
679 SortOrder::Desc,
680 );
681
682 cache.upsert("a".to_string(), json!({"score": 10}));
683 cache.upsert("b".to_string(), json!({"score": 20}));
684 cache.upsert("c".to_string(), json!({"score": 15}));
685
686 assert_eq!(cache.ordered_keys(), vec!["b", "c", "a"]);
688
689 cache.upsert("a".to_string(), json!({"score": 25}));
691
692 assert_eq!(cache.ordered_keys(), vec!["a", "b", "c"]);
694 }
695
696 #[test]
697 fn test_sorted_cache_remove() {
698 let mut cache = SortedViewCache::new(
699 "test/latest".to_string(),
700 vec!["id".to_string()],
701 SortOrder::Asc,
702 );
703
704 cache.upsert("a".to_string(), json!({"id": 1}));
705 cache.upsert("b".to_string(), json!({"id": 2}));
706 cache.upsert("c".to_string(), json!({"id": 3}));
707
708 assert_eq!(cache.len(), 3);
709
710 let pos = cache.remove("b");
711 assert_eq!(pos, Some(1));
712 assert_eq!(cache.len(), 2);
713 assert_eq!(cache.ordered_keys(), vec!["a", "c"]);
714 }
715
716 #[test]
717 fn test_compute_window_deltas() {
718 let mut cache = SortedViewCache::new(
719 "test/latest".to_string(),
720 vec!["id".to_string()],
721 SortOrder::Desc,
722 );
723
724 for i in 1..=5 {
726 cache.upsert(format!("e{}", i), json!({"id": i}));
727 }
728
729 let old_window: Vec<String> = vec!["e5".to_string(), "e4".to_string(), "e3".to_string()];
730
731 cache.upsert("e6".to_string(), json!({"id": 6}));
733
734 let deltas = cache.compute_window_deltas(&old_window, 0, 3);
737
738 assert_eq!(deltas.len(), 2);
739 assert!(deltas
741 .iter()
742 .any(|d| matches!(d, ViewDelta::Remove { key } if key == "e3")));
743 assert!(deltas
745 .iter()
746 .any(|d| matches!(d, ViewDelta::Add { key, .. } if key == "e6")));
747 }
748
749 fn keys(cache: &mut SortedViewCache) -> Vec<String> {
750 cache.ordered_keys().to_vec()
751 }
752
753 #[test]
754 fn bounded_upsert_evicts_bottom_of_desc_order() {
755 let mut cache = SortedViewCache::new(
756 "test/top".to_string(),
757 vec!["score".to_string()],
758 SortOrder::Desc,
759 );
760
761 for i in 1..=10 {
762 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 4);
763 assert!(cache.len() <= 4);
764 }
765
766 assert_eq!(cache.len(), 4);
767 assert_eq!(keys(&mut cache), ["e10", "e9", "e8", "e7"]);
768 assert!(cache.get("e1").is_none());
769 assert!(cache.get("e6").is_none());
770 }
771
772 #[test]
773 fn would_keep_matches_what_a_bounded_upsert_keeps() {
774 let mut cache = SortedViewCache::new(
775 "test/top".to_string(),
776 vec!["score".to_string()],
777 SortOrder::Desc,
778 );
779 for i in 1..=10 {
780 let entity = json!({"score": i});
781 let expected = cache.would_keep(&format!("e{i}"), &entity, 4);
782 cache.upsert_bounded(format!("e{i}"), entity, 4);
783 assert!(expected, "an entity above a full cache's tail is kept");
784 }
785 for (key, score, kept) in [("e0", 0, false), ("e6", 6, false), ("e11", 11, true)] {
787 let entity = json!({"score": score});
788 assert_eq!(cache.would_keep(key, &entity, 4), kept, "{key}");
789 let mut copy = SortedViewCache::new(
790 "test/top".to_string(),
791 vec!["score".to_string()],
792 SortOrder::Desc,
793 );
794 for existing in keys(&mut cache) {
795 let value = cache.get(&existing).unwrap().clone();
796 copy.upsert_bounded(existing, value, 4);
797 }
798 copy.upsert_bounded(key.to_string(), entity, 4);
799 assert_eq!(copy.get(key).is_some(), kept, "{key}");
800 }
801 assert!(cache.would_keep("e8", &json!({"score": -1}), 4));
803 }
804
805 #[test]
806 fn bounded_upsert_evicts_bottom_of_asc_order() {
807 let mut cache = SortedViewCache::new(
808 "test/bottom".to_string(),
809 vec!["score".to_string()],
810 SortOrder::Asc,
811 );
812
813 for i in (1..=10).rev() {
814 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 4);
815 assert!(cache.len() <= 4);
816 }
817
818 assert_eq!(cache.len(), 4);
819 assert_eq!(keys(&mut cache), ["e1", "e2", "e3", "e4"]);
820 assert!(cache.get("e10").is_none());
821 }
822
823 #[test]
824 fn bounded_upsert_does_not_evict_stale_top_entities() {
825 let mut cache = SortedViewCache::new(
826 "test/top".to_string(),
827 vec!["score".to_string()],
828 SortOrder::Desc,
829 );
830
831 cache.upsert_bounded("leader".to_string(), json!({"score": 1_000}), 3);
834 for i in 1..=20 {
835 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
836 }
837
838 assert_eq!(keys(&mut cache), ["leader", "e20", "e19"]);
839 }
840
841 #[test]
842 fn windows_within_cap_match_unbounded_cache() {
843 let mut bounded = SortedViewCache::new(
844 "test/top".to_string(),
845 vec!["score".to_string()],
846 SortOrder::Desc,
847 );
848 let mut unbounded = SortedViewCache::new(
849 "test/top".to_string(),
850 vec!["score".to_string()],
851 SortOrder::Desc,
852 );
853
854 for i in 0..200u64 {
859 let key = format!("e{}", i % 60);
860 let score = (i / 60) * 1_000 + (i * 37) % 101;
861 let entity = json!({"score": score, "n": i});
862 bounded.upsert_bounded(key.clone(), entity.clone(), 25);
863 unbounded.upsert(key, entity);
864 }
865
866 assert_eq!(bounded.len(), 25);
867 for (skip, take) in [(0, 25), (0, 10), (5, 20), (24, 1)] {
868 assert_eq!(
869 bounded.get_window(skip, take),
870 unbounded.get_window(skip, take),
871 "window skip={skip} take={take}"
872 );
873 }
874 }
875
876 #[test]
877 fn evicted_entity_is_missing_after_top_moves_down_until_it_updates() {
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..=4 {
885 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
886 }
887 assert_eq!(keys(&mut cache), ["e4", "e3", "e2"]);
888
889 cache.upsert_bounded("e4".to_string(), json!({"score": 0}), 3);
892 assert_eq!(keys(&mut cache), ["e3", "e2", "e4"]);
893
894 cache.upsert_bounded("e1".to_string(), json!({"score": 1}), 3);
896 assert_eq!(keys(&mut cache), ["e3", "e2", "e1"]);
897 }
898
899 #[test]
900 fn evicted_entity_reenters_when_upserted_again() {
901 let mut cache = SortedViewCache::new(
902 "test/top".to_string(),
903 vec!["score".to_string()],
904 SortOrder::Desc,
905 );
906
907 for i in 1..=5 {
908 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
909 }
910 assert!(cache.get("e1").is_none());
911
912 let result = cache.upsert_bounded("e1".to_string(), json!({"score": 100}), 3);
913 assert_eq!(result, UpsertResult::Inserted { position: 0 });
914 assert_eq!(keys(&mut cache), ["e1", "e5", "e4"]);
915 assert_eq!(cache.len(), 3);
916 }
917
918 #[test]
919 fn upsert_below_full_cap_is_evicted_immediately() {
920 let mut cache = SortedViewCache::new(
921 "test/top".to_string(),
922 vec!["score".to_string()],
923 SortOrder::Desc,
924 );
925
926 for i in 10..=12 {
927 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
928 }
929 let result = cache.upsert_bounded("low".to_string(), json!({"score": 1}), 3);
930
931 assert_eq!(result, UpsertResult::Inserted { position: 3 });
932 assert!(cache.get("low").is_none());
933 assert_eq!(keys(&mut cache), ["e12", "e11", "e10"]);
934 }
935
936 #[test]
937 fn trim_keeps_keys_cache_and_entities_consistent() {
938 let mut cache = SortedViewCache::new(
939 "test/top".to_string(),
940 vec!["score".to_string()],
941 SortOrder::Desc,
942 );
943
944 for i in 1..=10 {
945 cache.upsert(format!("e{i}"), json!({"score": i}));
946 }
947 assert_eq!(cache.ordered_keys().len(), 10);
949
950 assert_eq!(cache.trim_to_max_entries(4), 6);
951 assert_eq!(cache.trim_to_max_entries(4), 0);
952 assert_eq!(cache.len(), 4);
953 assert_eq!(keys(&mut cache), ["e10", "e9", "e8", "e7"]);
954 assert_eq!(cache.get_all_ordered().len(), 4);
955 assert_eq!(cache.remove("e7"), Some(3));
956 assert_eq!(keys(&mut cache), ["e10", "e9", "e8"]);
957 }
958
959 #[test]
960 fn test_nested_sort_field() {
961 let mut cache = SortedViewCache::new(
962 "test/latest".to_string(),
963 vec!["id".to_string(), "round_id".to_string()],
964 SortOrder::Desc,
965 );
966
967 cache.upsert("a".to_string(), json!({"id": {"round_id": 1}}));
968 cache.upsert("b".to_string(), json!({"id": {"round_id": 3}}));
969 cache.upsert("c".to_string(), json!({"id": {"round_id": 2}}));
970
971 let keys = cache.ordered_keys();
972 assert_eq!(keys, vec!["b", "c", "a"]);
973 }
974
975 #[test]
976 fn test_nested_decimal_string_sort_field() {
977 let mut cache = SortedViewCache::new(
978 "test/latest".to_string(),
979 vec!["id".to_string(), "round_id".to_string()],
980 SortOrder::Desc,
981 );
982
983 cache.upsert("9".to_string(), json!({"id": {"round_id": "9"}}));
984 cache.upsert("100".to_string(), json!({"id": {"round_id": "100"}}));
985 cache.upsert("10".to_string(), json!({"id": {"round_id": "10"}}));
986
987 assert_eq!(cache.ordered_keys(), vec!["100", "10", "9"]);
988 }
989
990 #[test]
991 fn test_descending_string_sort_field() {
992 let mut cache = SortedViewCache::new(
993 "test/latest".to_string(),
994 vec!["name".to_string()],
995 SortOrder::Desc,
996 );
997
998 cache.upsert("a".to_string(), json!({"name": "alpha"}));
999 cache.upsert("c".to_string(), json!({"name": "charlie"}));
1000 cache.upsert("b".to_string(), json!({"name": "bravo"}));
1001
1002 assert_eq!(cache.ordered_keys(), vec!["c", "b", "a"]);
1003 }
1004
1005 #[test]
1006 fn test_update_with_missing_sort_field_preserves_position() {
1007 let mut cache = SortedViewCache::new(
1008 "test/latest".to_string(),
1009 vec!["id".to_string(), "round_id".to_string()],
1010 SortOrder::Desc,
1011 );
1012
1013 cache.upsert(
1014 "100".to_string(),
1015 json!({"id": {"round_id": 100}, "data": "initial"}),
1016 );
1017 cache.upsert(
1018 "200".to_string(),
1019 json!({"id": {"round_id": 200}, "data": "initial"}),
1020 );
1021 cache.upsert(
1022 "300".to_string(),
1023 json!({"id": {"round_id": 300}, "data": "initial"}),
1024 );
1025
1026 assert_eq!(cache.ordered_keys(), vec!["300", "200", "100"]);
1027
1028 cache.upsert("200".to_string(), json!({"data": "updated_without_id"}));
1029
1030 assert_eq!(
1031 cache.ordered_keys(),
1032 vec!["300", "200", "100"],
1033 "Entity 200 should retain its position even when updated without sort field"
1034 );
1035
1036 let entity = cache.get("200").unwrap();
1037 assert_eq!(entity["data"], "updated_without_id");
1038 }
1039
1040 #[test]
1041 fn test_new_entity_with_missing_sort_field_sorts_last() {
1042 for order in [SortOrder::Desc, SortOrder::Asc] {
1043 let mut cache = SortedViewCache::new(
1044 "test/latest".to_string(),
1045 vec!["id".to_string(), "round_id".to_string()],
1046 order,
1047 );
1048
1049 cache.upsert("100".to_string(), json!({"id": {"round_id": 100}}));
1050 cache.upsert("200".to_string(), json!({"id": {"round_id": 200}}));
1051 cache.upsert("new".to_string(), json!({"data": "no_sort_field"}));
1052 cache.upsert("nil".to_string(), json!({"id": {"round_id": null}}));
1053
1054 let ranked = match order {
1055 SortOrder::Desc => ["200", "100"],
1056 SortOrder::Asc => ["100", "200"],
1057 };
1058 assert_eq!(
1059 cache.ordered_keys(),
1060 [ranked[0], ranked[1], "new", "nil"],
1061 "{order:?}: an entity without a sort value never outranks one with it; \
1062 among themselves they order by key"
1063 );
1064 }
1065 }
1066
1067 #[test]
1070 fn a_full_window_does_not_admit_an_entity_without_a_sort_value() {
1071 let mut cache = SortedViewCache::new(
1072 "test/latest".to_string(),
1073 vec!["id".to_string(), "round_id".to_string()],
1074 SortOrder::Desc,
1075 );
1076 for round in [100, 200, 300] {
1077 cache.upsert_bounded(round.to_string(), json!({"id": {"round_id": round}}), 3);
1078 }
1079 let partial = json!({"metrics": {"checkpoint_count": 7}});
1080 assert!(!cache.would_keep("1", &partial, 3));
1081 cache.upsert_bounded("1".to_string(), partial, 3);
1082 assert_eq!(cache.ordered_keys(), ["300", "200", "100"]);
1083 }
1084
1085 #[test]
1088 fn a_whole_update_shares_the_given_entity() {
1089 let mut cache = SortedViewCache::new(
1090 "test/top".to_string(),
1091 vec!["score".to_string()],
1092 SortOrder::Desc,
1093 );
1094 cache.upsert(
1095 "a".to_string(),
1096 json!({"score": 1, "name": "a", "_version": "e:1"}),
1097 );
1098 let update =
1099 SharedEntity::new(json!({"score": 2, "name": "a", "extra": true, "_version": "e:2"}));
1100 cache.upsert("a".to_string(), update.clone());
1101 let held = cache.get("a").unwrap();
1102 assert!(held.shares_fields_with(&update));
1103 assert_eq!(*held, update);
1104 }
1105
1106 #[test]
1109 fn an_update_lacking_held_fields_merges_into_its_own_copy() {
1110 let mut cache = SortedViewCache::new(
1111 "test/top".to_string(),
1112 vec!["score".to_string()],
1113 SortOrder::Desc,
1114 );
1115 cache.upsert(
1116 "a".to_string(),
1117 json!({"score": 1, "nested": {"x": 1, "y": 1}, "_version": "e:1"}),
1118 );
1119 let update = SharedEntity::new(json!({"score": 2, "nested": {"x": 2}}));
1120 cache.upsert("a".to_string(), update.clone());
1121 let held = cache.get("a").unwrap();
1122 assert!(!held.shares_fields_with(&update));
1123 assert_eq!(
1124 held.to_value(),
1125 json!({"score": 2, "nested": {"x": 2, "y": 1}, "_version": "e:1"})
1126 );
1127 assert_eq!(update.to_value(), json!({"score": 2, "nested": {"x": 2}}));
1128 }
1129
1130 #[test]
1133 fn a_version_sort_reads_each_copys_own_version() {
1134 let mut cache = SortedViewCache::new(
1135 "test/versions".to_string(),
1136 vec!["_version".to_string()],
1137 SortOrder::Desc,
1138 );
1139 let first = SharedEntity::new(json!({"id": 1, "_version": "e:1"}));
1140 let (fields, _) = first.clone().into_parts();
1141 cache.upsert("a".to_string(), first);
1142 cache.upsert(
1143 "b".to_string(),
1144 SharedEntity::from_parts(fields, Some(json!("e:2"))),
1145 );
1146 assert_eq!(cache.ordered_keys(), ["b", "a"]);
1147 }
1148}