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.get(&entity_key).cloned() {
226 let effective_sort_value = if matches!(sort_value, SortValue::Null)
227 && !matches!(old_sort_key.sort_value, SortValue::Null)
228 {
229 old_sort_key.sort_value.clone()
230 } else {
231 sort_value
232 };
233
234 let new_sort_key = SortKey {
235 sort_value: effective_sort_value,
236 entity_key: entity_key.clone(),
237 order: self.order,
238 };
239
240 let merged_entity = Self::deep_merge(old_entity, entity);
242
243 if old_sort_key == new_sort_key {
244 self.entities
246 .insert(entity_key.clone(), (new_sort_key, merged_entity));
247 let position = self.find_position(&entity_key);
249 return UpsertResult::Updated { position };
250 }
251
252 self.sorted.remove(&old_sort_key);
254 self.sorted.insert(new_sort_key.clone(), ());
255 self.entities
256 .insert(entity_key.clone(), (new_sort_key, merged_entity));
257 self.cache_dirty = true;
258
259 let position = self.find_position(&entity_key);
260 return UpsertResult::Inserted { position };
261 }
262
263 let new_sort_key = SortKey {
264 sort_value,
265 entity_key: entity_key.clone(),
266 order: self.order,
267 };
268
269 self.sorted.insert(new_sort_key.clone(), ());
270 self.entities
271 .insert(entity_key.clone(), (new_sort_key, entity));
272 self.cache_dirty = true;
273
274 let position = self.find_position(&entity_key);
275
276 UpsertResult::Inserted { position }
277 }
278
279 pub fn upsert_bounded(
285 &mut self,
286 entity_key: String,
287 entity: Value,
288 max_entries: usize,
289 ) -> UpsertResult {
290 let result = self.upsert(entity_key, entity);
291 self.trim_to_max_entries(max_entries);
292 result
293 }
294
295 pub fn trim_to_max_entries(&mut self, max_entries: usize) -> usize {
303 let mut evicted = 0;
304 while self.sorted.len() > max_entries {
305 let Some((sort_key, ())) = self.sorted.pop_last() else {
306 break;
307 };
308 self.entities.remove(&sort_key.entity_key);
309 evicted += 1;
310 }
311 if evicted > 0 && !self.cache_dirty {
312 self.keys_cache.truncate(self.sorted.len());
315 }
316 evicted
317 }
318
319 fn deep_merge(base: Value, patch: Value) -> Value {
320 match (base, patch) {
321 (Value::Object(mut base_map), Value::Object(patch_map)) => {
322 for (key, patch_value) in patch_map {
323 if let Some(base_value) = base_map.remove(&key) {
324 base_map.insert(key, Self::deep_merge(base_value, patch_value));
325 } else {
326 base_map.insert(key, patch_value);
327 }
328 }
329 Value::Object(base_map)
330 }
331 (_, patch) => patch,
332 }
333 }
334
335 pub fn remove(&mut self, entity_key: &str) -> Option<usize> {
337 if let Some((sort_key, _)) = self.entities.remove(entity_key) {
338 let position = self.find_position_by_sort_key(&sort_key);
339 self.sorted.remove(&sort_key);
340 self.cache_dirty = true;
341 Some(position)
342 } else {
343 None
344 }
345 }
346
347 pub fn get(&self, entity_key: &str) -> Option<&Value> {
349 self.entities.get(entity_key).map(|(_, v)| v)
350 }
351
352 pub fn ordered_keys(&mut self) -> &[String] {
354 if self.cache_dirty {
355 self.rebuild_keys_cache();
356 }
357 &self.keys_cache
358 }
359
360 pub fn get_window(&mut self, skip: usize, take: usize) -> Vec<(String, Value)> {
362 if self.cache_dirty {
363 self.rebuild_keys_cache();
364 }
365
366 self.keys_cache
367 .iter()
368 .skip(skip)
369 .take(take)
370 .filter_map(|key| {
371 self.entities
372 .get(key)
373 .map(|(_, v)| (key.clone(), v.clone()))
374 })
375 .collect()
376 }
377
378 pub fn get_all_ordered(&mut self) -> Vec<(String, Value)> {
380 if self.cache_dirty {
381 self.rebuild_keys_cache();
382 }
383
384 self.keys_cache
385 .iter()
386 .filter_map(|key| {
387 self.entities
388 .get(key)
389 .map(|(_, value)| (key.clone(), value.clone()))
390 })
391 .collect()
392 }
393
394 pub fn compute_window_deltas(
396 &mut self,
397 old_window_keys: &[String],
398 skip: usize,
399 take: usize,
400 ) -> Vec<ViewDelta> {
401 if self.cache_dirty {
402 self.rebuild_keys_cache();
403 }
404
405 let new_window_keys: Vec<&String> = self.keys_cache.iter().skip(skip).take(take).collect();
406
407 let old_set: std::collections::HashSet<&String> = old_window_keys.iter().collect();
408 let new_set: std::collections::HashSet<&String> = new_window_keys.iter().cloned().collect();
409
410 let mut deltas = Vec::new();
411
412 for key in old_set.difference(&new_set) {
414 deltas.push(ViewDelta::Remove {
415 key: (*key).clone(),
416 });
417 }
418
419 for key in new_set.difference(&old_set) {
421 if let Some((_, entity)) = self.entities.get(*key) {
422 deltas.push(ViewDelta::Add {
423 key: (*key).clone(),
424 entity: entity.clone(),
425 });
426 }
427 }
428
429 deltas
430 }
431
432 fn extract_sort_value(&self, entity: &Value) -> SortValue {
433 let mut current = entity;
434 for segment in &self.sort_field {
435 match current.get(segment) {
436 Some(v) => current = v,
437 None => return SortValue::Null,
438 }
439 }
440
441 value_to_sort_value(current)
442 }
443
444 fn find_position(&self, entity_key: &str) -> usize {
445 if let Some((sort_key, _)) = self.entities.get(entity_key) {
446 self.find_position_by_sort_key(sort_key)
447 } else {
448 0
449 }
450 }
451
452 fn find_position_by_sort_key(&self, sort_key: &SortKey) -> usize {
453 self.sorted.range(..sort_key).count()
454 }
455
456 fn rebuild_keys_cache(&mut self) {
457 self.keys_cache = self.sorted.keys().map(|sk| sk.entity_key.clone()).collect();
458 self.cache_dirty = false;
459 }
460}
461
462#[derive(Debug, Clone, PartialEq)]
464pub enum UpsertResult {
465 Inserted { position: usize },
467 Updated { position: usize },
469}
470
471fn value_to_sort_value(v: &Value) -> SortValue {
472 match v {
473 Value::Null => SortValue::Null,
474 Value::Bool(b) => SortValue::Bool(*b),
475 Value::Number(n) => {
476 if let Some(i) = n.as_i64() {
477 SortValue::Integer(i)
478 } else if let Some(f) = n.as_f64() {
479 SortValue::Float(OrderedFloat(f))
480 } else {
481 SortValue::Null
482 }
483 }
484 Value::String(s) => SortValue::String(s.clone()),
485 _ => SortValue::Null,
486 }
487}
488
489fn compare_decimal_strings(left: &str, right: &str) -> Option<Ordering> {
490 fn parts(value: &str) -> Option<(bool, &str)> {
491 let (negative, digits) = match value.strip_prefix('-') {
492 Some(digits) => (true, digits),
493 None => (false, value),
494 };
495 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
496 return None;
497 }
498
499 let digits = digits.trim_start_matches('0');
500 let digits = if digits.is_empty() { "0" } else { digits };
501 Some((negative && digits != "0", digits))
502 }
503
504 let (left_negative, left_digits) = parts(left)?;
505 let (right_negative, right_digits) = parts(right)?;
506
507 match (left_negative, right_negative) {
508 (true, false) => Some(Ordering::Less),
509 (false, true) => Some(Ordering::Greater),
510 _ => {
511 let magnitude = left_digits
512 .len()
513 .cmp(&right_digits.len())
514 .then_with(|| left_digits.cmp(right_digits));
515 Some(if left_negative {
516 magnitude.reverse()
517 } else {
518 magnitude
519 })
520 }
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use serde_json::json;
528
529 #[test]
530 fn test_sorted_cache_basic() {
531 let mut cache = SortedViewCache::new(
532 "test/latest".to_string(),
533 vec!["id".to_string()],
534 SortOrder::Desc,
535 );
536
537 cache.upsert("a".to_string(), json!({"id": 1, "name": "first"}));
538 cache.upsert("b".to_string(), json!({"id": 3, "name": "third"}));
539 cache.upsert("c".to_string(), json!({"id": 2, "name": "second"}));
540
541 let keys = cache.ordered_keys();
542 assert_eq!(keys, vec!["b", "c", "a"]);
544 }
545
546 #[test]
547 fn test_sorted_cache_window() {
548 let mut cache = SortedViewCache::new(
549 "test/latest".to_string(),
550 vec!["id".to_string()],
551 SortOrder::Desc,
552 );
553
554 for i in 1..=10 {
555 cache.upsert(format!("e{}", i), json!({"id": i}));
556 }
557
558 let window = cache.get_window(0, 3);
560 assert_eq!(window.len(), 3);
561 assert_eq!(window[0].0, "e10");
562 assert_eq!(window[1].0, "e9");
563 assert_eq!(window[2].0, "e8");
564
565 let window = cache.get_window(3, 3);
566 assert_eq!(window[0].0, "e7");
567 }
568
569 #[test]
570 fn all_ordered_preserves_stable_sort_and_tie_breaking() {
571 let mut cache = SortedViewCache::new(
572 "test/latest".to_string(),
573 vec!["score".to_string()],
574 SortOrder::Desc,
575 );
576 cache.upsert("b".to_string(), json!({"score": 10}));
577 cache.upsert("a".to_string(), json!({"score": 10}));
578 cache.upsert("c".to_string(), json!({"score": 9}));
579
580 let keys: Vec<_> = cache
581 .get_all_ordered()
582 .into_iter()
583 .map(|(key, _)| key)
584 .collect();
585 assert_eq!(keys, ["a", "b", "c"]);
586 }
587
588 #[test]
589 fn test_sorted_cache_update_moves_position() {
590 let mut cache = SortedViewCache::new(
591 "test/latest".to_string(),
592 vec!["score".to_string()],
593 SortOrder::Desc,
594 );
595
596 cache.upsert("a".to_string(), json!({"score": 10}));
597 cache.upsert("b".to_string(), json!({"score": 20}));
598 cache.upsert("c".to_string(), json!({"score": 15}));
599
600 assert_eq!(cache.ordered_keys(), vec!["b", "c", "a"]);
602
603 cache.upsert("a".to_string(), json!({"score": 25}));
605
606 assert_eq!(cache.ordered_keys(), vec!["a", "b", "c"]);
608 }
609
610 #[test]
611 fn test_sorted_cache_remove() {
612 let mut cache = SortedViewCache::new(
613 "test/latest".to_string(),
614 vec!["id".to_string()],
615 SortOrder::Asc,
616 );
617
618 cache.upsert("a".to_string(), json!({"id": 1}));
619 cache.upsert("b".to_string(), json!({"id": 2}));
620 cache.upsert("c".to_string(), json!({"id": 3}));
621
622 assert_eq!(cache.len(), 3);
623
624 let pos = cache.remove("b");
625 assert_eq!(pos, Some(1));
626 assert_eq!(cache.len(), 2);
627 assert_eq!(cache.ordered_keys(), vec!["a", "c"]);
628 }
629
630 #[test]
631 fn test_compute_window_deltas() {
632 let mut cache = SortedViewCache::new(
633 "test/latest".to_string(),
634 vec!["id".to_string()],
635 SortOrder::Desc,
636 );
637
638 for i in 1..=5 {
640 cache.upsert(format!("e{}", i), json!({"id": i}));
641 }
642
643 let old_window: Vec<String> = vec!["e5".to_string(), "e4".to_string(), "e3".to_string()];
644
645 cache.upsert("e6".to_string(), json!({"id": 6}));
647
648 let deltas = cache.compute_window_deltas(&old_window, 0, 3);
651
652 assert_eq!(deltas.len(), 2);
653 assert!(deltas
655 .iter()
656 .any(|d| matches!(d, ViewDelta::Remove { key } if key == "e3")));
657 assert!(deltas
659 .iter()
660 .any(|d| matches!(d, ViewDelta::Add { key, .. } if key == "e6")));
661 }
662
663 fn keys(cache: &mut SortedViewCache) -> Vec<String> {
664 cache.ordered_keys().to_vec()
665 }
666
667 #[test]
668 fn bounded_upsert_evicts_bottom_of_desc_order() {
669 let mut cache = SortedViewCache::new(
670 "test/top".to_string(),
671 vec!["score".to_string()],
672 SortOrder::Desc,
673 );
674
675 for i in 1..=10 {
676 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 4);
677 assert!(cache.len() <= 4);
678 }
679
680 assert_eq!(cache.len(), 4);
681 assert_eq!(keys(&mut cache), ["e10", "e9", "e8", "e7"]);
682 assert!(cache.get("e1").is_none());
683 assert!(cache.get("e6").is_none());
684 }
685
686 #[test]
687 fn bounded_upsert_evicts_bottom_of_asc_order() {
688 let mut cache = SortedViewCache::new(
689 "test/bottom".to_string(),
690 vec!["score".to_string()],
691 SortOrder::Asc,
692 );
693
694 for i in (1..=10).rev() {
695 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 4);
696 assert!(cache.len() <= 4);
697 }
698
699 assert_eq!(cache.len(), 4);
700 assert_eq!(keys(&mut cache), ["e1", "e2", "e3", "e4"]);
701 assert!(cache.get("e10").is_none());
702 }
703
704 #[test]
705 fn bounded_upsert_does_not_evict_stale_top_entities() {
706 let mut cache = SortedViewCache::new(
707 "test/top".to_string(),
708 vec!["score".to_string()],
709 SortOrder::Desc,
710 );
711
712 cache.upsert_bounded("leader".to_string(), json!({"score": 1_000}), 3);
715 for i in 1..=20 {
716 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
717 }
718
719 assert_eq!(keys(&mut cache), ["leader", "e20", "e19"]);
720 }
721
722 #[test]
723 fn windows_within_cap_match_unbounded_cache() {
724 let mut bounded = SortedViewCache::new(
725 "test/top".to_string(),
726 vec!["score".to_string()],
727 SortOrder::Desc,
728 );
729 let mut unbounded = SortedViewCache::new(
730 "test/top".to_string(),
731 vec!["score".to_string()],
732 SortOrder::Desc,
733 );
734
735 for i in 0..200u64 {
740 let key = format!("e{}", i % 60);
741 let score = (i / 60) * 1_000 + (i * 37) % 101;
742 let entity = json!({"score": score, "n": i});
743 bounded.upsert_bounded(key.clone(), entity.clone(), 25);
744 unbounded.upsert(key, entity);
745 }
746
747 assert_eq!(bounded.len(), 25);
748 for (skip, take) in [(0, 25), (0, 10), (5, 20), (24, 1)] {
749 assert_eq!(
750 bounded.get_window(skip, take),
751 unbounded.get_window(skip, take),
752 "window skip={skip} take={take}"
753 );
754 }
755 }
756
757 #[test]
758 fn evicted_entity_is_missing_after_top_moves_down_until_it_updates() {
759 let mut cache = SortedViewCache::new(
760 "test/top".to_string(),
761 vec!["score".to_string()],
762 SortOrder::Desc,
763 );
764
765 for i in 1..=4 {
766 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
767 }
768 assert_eq!(keys(&mut cache), ["e4", "e3", "e2"]);
769
770 cache.upsert_bounded("e4".to_string(), json!({"score": 0}), 3);
773 assert_eq!(keys(&mut cache), ["e3", "e2", "e4"]);
774
775 cache.upsert_bounded("e1".to_string(), json!({"score": 1}), 3);
777 assert_eq!(keys(&mut cache), ["e3", "e2", "e1"]);
778 }
779
780 #[test]
781 fn evicted_entity_reenters_when_upserted_again() {
782 let mut cache = SortedViewCache::new(
783 "test/top".to_string(),
784 vec!["score".to_string()],
785 SortOrder::Desc,
786 );
787
788 for i in 1..=5 {
789 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
790 }
791 assert!(cache.get("e1").is_none());
792
793 let result = cache.upsert_bounded("e1".to_string(), json!({"score": 100}), 3);
794 assert_eq!(result, UpsertResult::Inserted { position: 0 });
795 assert_eq!(keys(&mut cache), ["e1", "e5", "e4"]);
796 assert_eq!(cache.len(), 3);
797 }
798
799 #[test]
800 fn upsert_below_full_cap_is_evicted_immediately() {
801 let mut cache = SortedViewCache::new(
802 "test/top".to_string(),
803 vec!["score".to_string()],
804 SortOrder::Desc,
805 );
806
807 for i in 10..=12 {
808 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
809 }
810 let result = cache.upsert_bounded("low".to_string(), json!({"score": 1}), 3);
811
812 assert_eq!(result, UpsertResult::Inserted { position: 3 });
813 assert!(cache.get("low").is_none());
814 assert_eq!(keys(&mut cache), ["e12", "e11", "e10"]);
815 }
816
817 #[test]
818 fn trim_keeps_keys_cache_and_entities_consistent() {
819 let mut cache = SortedViewCache::new(
820 "test/top".to_string(),
821 vec!["score".to_string()],
822 SortOrder::Desc,
823 );
824
825 for i in 1..=10 {
826 cache.upsert(format!("e{i}"), json!({"score": i}));
827 }
828 assert_eq!(cache.ordered_keys().len(), 10);
830
831 assert_eq!(cache.trim_to_max_entries(4), 6);
832 assert_eq!(cache.trim_to_max_entries(4), 0);
833 assert_eq!(cache.len(), 4);
834 assert_eq!(keys(&mut cache), ["e10", "e9", "e8", "e7"]);
835 assert_eq!(cache.get_all_ordered().len(), 4);
836 assert_eq!(cache.remove("e7"), Some(3));
837 assert_eq!(keys(&mut cache), ["e10", "e9", "e8"]);
838 }
839
840 #[test]
841 fn test_nested_sort_field() {
842 let mut cache = SortedViewCache::new(
843 "test/latest".to_string(),
844 vec!["id".to_string(), "round_id".to_string()],
845 SortOrder::Desc,
846 );
847
848 cache.upsert("a".to_string(), json!({"id": {"round_id": 1}}));
849 cache.upsert("b".to_string(), json!({"id": {"round_id": 3}}));
850 cache.upsert("c".to_string(), json!({"id": {"round_id": 2}}));
851
852 let keys = cache.ordered_keys();
853 assert_eq!(keys, vec!["b", "c", "a"]);
854 }
855
856 #[test]
857 fn test_nested_decimal_string_sort_field() {
858 let mut cache = SortedViewCache::new(
859 "test/latest".to_string(),
860 vec!["id".to_string(), "round_id".to_string()],
861 SortOrder::Desc,
862 );
863
864 cache.upsert("9".to_string(), json!({"id": {"round_id": "9"}}));
865 cache.upsert("100".to_string(), json!({"id": {"round_id": "100"}}));
866 cache.upsert("10".to_string(), json!({"id": {"round_id": "10"}}));
867
868 assert_eq!(cache.ordered_keys(), vec!["100", "10", "9"]);
869 }
870
871 #[test]
872 fn test_descending_string_sort_field() {
873 let mut cache = SortedViewCache::new(
874 "test/latest".to_string(),
875 vec!["name".to_string()],
876 SortOrder::Desc,
877 );
878
879 cache.upsert("a".to_string(), json!({"name": "alpha"}));
880 cache.upsert("c".to_string(), json!({"name": "charlie"}));
881 cache.upsert("b".to_string(), json!({"name": "bravo"}));
882
883 assert_eq!(cache.ordered_keys(), vec!["c", "b", "a"]);
884 }
885
886 #[test]
887 fn test_update_with_missing_sort_field_preserves_position() {
888 let mut cache = SortedViewCache::new(
889 "test/latest".to_string(),
890 vec!["id".to_string(), "round_id".to_string()],
891 SortOrder::Desc,
892 );
893
894 cache.upsert(
895 "100".to_string(),
896 json!({"id": {"round_id": 100}, "data": "initial"}),
897 );
898 cache.upsert(
899 "200".to_string(),
900 json!({"id": {"round_id": 200}, "data": "initial"}),
901 );
902 cache.upsert(
903 "300".to_string(),
904 json!({"id": {"round_id": 300}, "data": "initial"}),
905 );
906
907 assert_eq!(cache.ordered_keys(), vec!["300", "200", "100"]);
908
909 cache.upsert("200".to_string(), json!({"data": "updated_without_id"}));
910
911 assert_eq!(
912 cache.ordered_keys(),
913 vec!["300", "200", "100"],
914 "Entity 200 should retain its position even when updated without sort field"
915 );
916
917 let entity = cache.get("200").unwrap();
918 assert_eq!(entity["data"], "updated_without_id");
919 }
920
921 #[test]
922 fn test_new_entity_with_missing_sort_field_gets_null_position() {
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("100".to_string(), json!({"id": {"round_id": 100}}));
930 cache.upsert("200".to_string(), json!({"id": {"round_id": 200}}));
931
932 cache.upsert("new".to_string(), json!({"data": "no_sort_field"}));
933
934 let keys = cache.ordered_keys();
935 assert_eq!(
936 keys.first().unwrap(),
937 "new",
938 "New entity without sort field gets Null which sorts first (Null < any value)"
939 );
940 }
941}