1use crate::shared_entity::{lookup, EntityFields, Fields, SharedEntity};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::borrow::Cow;
11use std::cmp::Ordering;
12use std::collections::{BTreeMap, HashMap};
13use std::sync::Arc;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct SortKey {
20 sort_value: SortValue,
22 entity_key: String,
24 order: SortOrder,
26 decimal: Option<DecimalKey>,
30}
31
32impl SortKey {
33 fn new(sort_value: SortValue, entity_key: String, order: SortOrder) -> Self {
34 Self {
35 decimal: DecimalKey::of(&sort_value),
36 sort_value,
37 entity_key,
38 order,
39 }
40 }
41
42 fn set_sort_value(&mut self, sort_value: SortValue) {
44 self.decimal = DecimalKey::of(&sort_value);
45 self.sort_value = sort_value;
46 }
47
48 fn ranked(&self) -> Ranked<'_> {
49 Ranked {
50 sort_value: &self.sort_value,
51 decimal: self.decimal,
52 entity_key: &self.entity_key,
53 order: self.order,
54 }
55 }
56}
57
58impl PartialOrd for SortKey {
59 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
60 Some(self.cmp(other))
61 }
62}
63
64impl Ord for SortKey {
65 fn cmp(&self, other: &Self) -> Ordering {
66 self.ranked().cmp(&other.ranked())
67 }
68}
69
70struct Ranked<'a> {
73 sort_value: &'a SortValue,
74 decimal: Option<DecimalKey>,
75 entity_key: &'a str,
76 order: SortOrder,
77}
78
79impl Ranked<'_> {
80 fn cmp_values(&self, other: &Self) -> Ordering {
82 match (self.sort_value, other.sort_value) {
83 (SortValue::String(left), SortValue::String(right)) => {
84 match (&self.decimal, &other.decimal) {
85 (Some(a), Some(b)) => a.cmp(left, b, right),
86 (Some(_), None) => Ordering::Less,
87 (None, Some(_)) => Ordering::Greater,
88 (None, None) => left.cmp(right),
89 }
90 }
91 (left, right) => left.cmp(right),
92 }
93 }
94
95 fn cmp(&self, other: &Self) -> Ordering {
96 if self.order != other.order {
97 return match (self.order, other.order) {
98 (SortOrder::Asc, SortOrder::Desc) => Ordering::Less,
99 (SortOrder::Desc, SortOrder::Asc) => Ordering::Greater,
100 _ => Ordering::Equal,
101 };
102 }
103
104 let sort_order = match (self.sort_value, other.sort_value, self.order) {
109 (SortValue::Null, SortValue::Null, _) => Ordering::Equal,
110 (SortValue::Null, _, _) => Ordering::Greater,
111 (_, SortValue::Null, _) => Ordering::Less,
112 (_, _, SortOrder::Asc) => self.cmp_values(other),
113 (_, _, SortOrder::Desc) => self.cmp_values(other).reverse(),
114 };
115
116 match sort_order {
117 Ordering::Equal => self.entity_key.cmp(other.entity_key),
118 other => other,
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum SortValue {
126 Null,
127 Bool(bool),
128 Integer(i64),
129 Float(OrderedFloat),
130 String(String),
131}
132
133impl Ord for SortValue {
134 fn cmp(&self, other: &Self) -> Ordering {
135 match (self, other) {
136 (SortValue::Null, SortValue::Null) => Ordering::Equal,
137 (SortValue::Null, _) => Ordering::Less,
138 (_, SortValue::Null) => Ordering::Greater,
139 (SortValue::Bool(a), SortValue::Bool(b)) => a.cmp(b),
140 (SortValue::Integer(a), SortValue::Integer(b)) => a.cmp(b),
141 (SortValue::Float(a), SortValue::Float(b)) => a.cmp(b),
142 (SortValue::String(a), SortValue::String(b)) => compare_strings(a, b),
143 (SortValue::Integer(_), SortValue::String(_)) => Ordering::Less,
145 (SortValue::String(_), SortValue::Integer(_)) => Ordering::Greater,
146 (SortValue::Float(_), SortValue::String(_)) => Ordering::Less,
147 (SortValue::String(_), SortValue::Float(_)) => Ordering::Greater,
148 (SortValue::Integer(a), SortValue::Float(b)) => OrderedFloat(*a as f64).cmp(b),
150 (SortValue::Float(a), SortValue::Integer(b)) => a.cmp(&OrderedFloat(*b as f64)),
151 (SortValue::Bool(_), _) => Ordering::Less,
153 (_, SortValue::Bool(_)) => Ordering::Greater,
154 }
155 }
156}
157
158impl PartialOrd for SortValue {
159 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
160 Some(self.cmp(other))
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq)]
166pub struct OrderedFloat(pub f64);
167
168impl Eq for OrderedFloat {}
169
170impl Ord for OrderedFloat {
171 fn cmp(&self, other: &Self) -> Ordering {
172 self.0.partial_cmp(&other.0).unwrap_or_else(|| {
173 if self.0.is_nan() && other.0.is_nan() {
174 Ordering::Equal
175 } else if self.0.is_nan() {
176 Ordering::Less
177 } else {
178 Ordering::Greater
179 }
180 })
181 }
182}
183
184impl PartialOrd for OrderedFloat {
185 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
186 Some(self.cmp(other))
187 }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(rename_all = "lowercase")]
193pub enum SortOrder {
194 Asc,
195 Desc,
196}
197
198impl From<crate::materialized_view::SortOrder> for SortOrder {
199 fn from(order: crate::materialized_view::SortOrder) -> Self {
200 match order {
201 crate::materialized_view::SortOrder::Asc => SortOrder::Asc,
202 crate::materialized_view::SortOrder::Desc => SortOrder::Desc,
203 }
204 }
205}
206
207#[derive(Debug, Clone, PartialEq)]
209pub enum ViewDelta {
210 None,
212 Add { key: String, entity: Value },
214 Remove { key: String },
216 Update { key: String, entity: Value },
218}
219
220#[derive(Debug)]
245pub struct SortedViewCache {
246 view_id: String,
248 sort_field: Vec<String>,
250 order: SortOrder,
252 sorted: BTreeMap<SortKey, ()>,
254 entities: HashMap<String, (SortKey, SharedEntity)>,
256 keys_cache: Vec<String>,
258 cache_dirty: bool,
260 placeholder: SharedEntity,
262}
263
264impl SortedViewCache {
265 pub fn new(view_id: String, sort_field: Vec<String>, order: SortOrder) -> Self {
266 Self {
267 view_id,
268 sort_field,
269 order,
270 sorted: BTreeMap::new(),
271 entities: HashMap::new(),
272 keys_cache: Vec::new(),
273 cache_dirty: true,
274 placeholder: SharedEntity::new(Value::Null),
275 }
276 }
277
278 pub fn view_id(&self) -> &str {
279 &self.view_id
280 }
281
282 pub fn len(&self) -> usize {
283 self.entities.len()
284 }
285
286 pub fn is_empty(&self) -> bool {
287 self.entities.is_empty()
288 }
289
290 pub fn upsert(&mut self, entity_key: String, entity: impl Into<SharedEntity>) -> UpsertResult {
299 let entity = entity.into();
300 let sort_value = self.extract_sort_value(&entity);
301 let moved = self.put_with_sort_value(Cow::Borrowed(&entity_key), entity, sort_value);
302 let position = self.find_position(&entity_key);
303 match moved {
304 Placed::Unmoved => UpsertResult::Updated { position },
305 Placed::Moved => UpsertResult::Inserted { position },
306 }
307 }
308
309 pub fn put(&mut self, entity_key: String, entity: impl Into<SharedEntity>) {
311 let entity = entity.into();
312 let sort_value = self.extract_sort_value(&entity);
313 self.put_with_sort_value(Cow::Owned(entity_key), entity, sort_value);
314 }
315
316 fn put_with_sort_value(
318 &mut self,
319 entity_key: Cow<'_, str>,
320 entity: SharedEntity,
321 sort_value: SortValue,
322 ) -> Placed {
323 if let Some((sort_key, held)) = self.entities.get_mut(entity_key.as_ref()) {
324 let old_entity = std::mem::replace(held, self.placeholder.clone());
326 *held = Self::merge_entity(old_entity, entity);
327
328 let keeps_old_value = matches!(sort_value, SortValue::Null)
330 && !matches!(sort_key.sort_value, SortValue::Null);
331 if keeps_old_value || sort_key.sort_value == sort_value {
332 return Placed::Unmoved;
334 }
335
336 let mut ordered = match self.sorted.remove_entry(sort_key) {
339 Some((ordered, ())) => ordered,
340 None => {
343 let stale = sort_key.entity_key.as_str();
344 self.sorted.retain(|held, ()| held.entity_key != stale);
345 sort_key.clone()
346 }
347 };
348 ordered.set_sort_value(sort_value);
349 sort_key.sort_value = ordered.sort_value.clone();
350 sort_key.decimal = ordered.decimal;
351 self.sorted.insert(ordered, ());
352 self.cache_dirty = true;
353 return Placed::Moved;
354 }
355
356 let entity_key = entity_key.into_owned();
357 let new_sort_key = SortKey::new(sort_value, entity_key.clone(), self.order);
358 self.sorted.insert(new_sort_key.clone(), ());
359 self.entities.insert(entity_key, (new_sort_key, entity));
360 self.cache_dirty = true;
361 Placed::Moved
362 }
363
364 pub fn would_keep<E: EntityFields + ?Sized>(
371 &self,
372 entity_key: &str,
373 entity: &E,
374 max_entries: usize,
375 ) -> bool {
376 self.keeps(entity_key, || self.extract_sort_value(entity), max_entries)
377 .0
378 }
379
380 fn keeps(
383 &self,
384 entity_key: &str,
385 sort_value: impl FnOnce() -> SortValue,
386 max_entries: usize,
387 ) -> (bool, Option<SortValue>) {
388 if self.entities.contains_key(entity_key) || self.sorted.len() < max_entries {
389 return (true, None);
390 }
391 let Some((last, ())) = self.sorted.last_key_value() else {
392 return (true, None);
393 };
394 let sort_value = sort_value();
395 let candidate = Ranked {
396 sort_value: &sort_value,
397 decimal: DecimalKey::of(&sort_value),
398 entity_key,
399 order: self.order,
400 };
401 let kept = candidate.cmp(&last.ranked()).is_lt();
402 (kept, Some(sort_value))
403 }
404
405 pub fn upsert_bounded(
411 &mut self,
412 entity_key: String,
413 entity: impl Into<SharedEntity>,
414 max_entries: usize,
415 ) -> UpsertResult {
416 let result = self.upsert(entity_key, entity);
417 self.trim_to_max_entries(max_entries);
418 result
419 }
420
421 pub fn upsert_if_kept(
427 &mut self,
428 entity_key: &str,
429 entity: &SharedEntity,
430 max_entries: usize,
431 ) -> bool {
432 let (kept, sort_value) =
433 self.keeps(entity_key, || self.extract_sort_value(entity), max_entries);
434 if !kept {
435 return false;
436 }
437 let sort_value = sort_value.unwrap_or_else(|| self.extract_sort_value(entity));
438 self.put_with_sort_value(Cow::Borrowed(entity_key), entity.clone(), sort_value);
439 self.trim_to_max_entries(max_entries);
440 true
441 }
442
443 pub fn trim_to_max_entries(&mut self, max_entries: usize) -> usize {
451 let mut evicted = 0;
452 while self.sorted.len() > max_entries {
453 let Some((sort_key, ())) = self.sorted.pop_last() else {
454 break;
455 };
456 self.entities.remove(&sort_key.entity_key);
457 evicted += 1;
458 }
459 if evicted > 0 && !self.cache_dirty {
460 self.keys_cache.truncate(self.sorted.len());
463 }
464 evicted
465 }
466
467 fn merge_entity(base: SharedEntity, patch: SharedEntity) -> SharedEntity {
471 let (base_fields, base_version) = base.into_parts();
472 let (patch_fields, patch_version) = patch.into_parts();
473 let (Fields::Object(base_members), Fields::Object(patch_members)) =
474 (&*base_fields, &*patch_fields)
475 else {
476 return SharedEntity::from_parts(patch_fields, patch_version);
477 };
478 let keeps = !Arc::ptr_eq(&base_fields, &patch_fields)
481 && base_members.iter().any(|(key, base)| {
482 patch_members
483 .get(key)
484 .is_none_or(|patch| !Arc::ptr_eq(base, patch) && keeps_fields(base, patch))
485 });
486 let fields = if keeps {
487 let base = match Arc::try_unwrap(base_fields) {
488 Ok(fields) => fields.into_value(),
489 Err(shared) => shared.to_value(),
490 };
491 Arc::new(Fields::from_value(Self::deep_merge(
492 base,
493 patch_fields.to_value(),
494 )))
495 } else {
496 patch_fields
497 };
498 let version = match (base_version, patch_version) {
499 (Some(base), Some(patch)) => Some(Self::deep_merge(base, patch)),
500 (base, None) => base,
501 (None, patch) => patch,
502 };
503 SharedEntity::from_parts(fields, version)
504 }
505
506 fn deep_merge(base: Value, patch: Value) -> Value {
507 match (base, patch) {
508 (Value::Object(mut base_map), Value::Object(patch_map)) => {
509 for (key, patch_value) in patch_map {
510 if let Some(base_value) = base_map.remove(&key) {
511 base_map.insert(key, Self::deep_merge(base_value, patch_value));
512 } else {
513 base_map.insert(key, patch_value);
514 }
515 }
516 Value::Object(base_map)
517 }
518 (_, patch) => patch,
519 }
520 }
521
522 pub fn clear(&mut self) {
524 self.sorted.clear();
525 self.entities.clear();
526 self.cache_dirty = true;
527 }
528
529 pub fn remove(&mut self, entity_key: &str) -> Option<usize> {
533 let (sort_key, _) = self.entities.get(entity_key)?;
534 let position = self.find_position_by_sort_key(sort_key);
535 self.remove_key(entity_key);
536 Some(position)
537 }
538
539 pub fn remove_key(&mut self, entity_key: &str) -> bool {
541 let Some((sort_key, _)) = self.entities.remove(entity_key) else {
542 return false;
543 };
544 self.sorted.remove(&sort_key);
545 self.cache_dirty = true;
546 true
547 }
548
549 pub fn get(&self, entity_key: &str) -> Option<&SharedEntity> {
551 self.entities.get(entity_key).map(|(_, v)| v)
552 }
553
554 pub fn ordered_keys(&mut self) -> &[String] {
559 if self.cache_dirty {
560 self.rebuild_keys_cache();
561 }
562 &self.keys_cache
563 }
564
565 pub fn get_window(&mut self, skip: usize, take: usize) -> Vec<(String, Value)> {
567 self.iter_ordered()
568 .skip(skip)
569 .take(take)
570 .map(|(key, entity)| (key.to_string(), entity.to_value()))
571 .collect()
572 }
573
574 pub fn iter_ordered(&self) -> impl Iterator<Item = (&str, &SharedEntity)> + '_ {
577 self.sorted.keys().filter_map(|sort_key| {
578 self.entities
579 .get(&sort_key.entity_key)
580 .map(|(_, entity)| (sort_key.entity_key.as_str(), entity))
581 })
582 }
583
584 pub fn get_all_ordered(&mut self) -> Vec<(String, Value)> {
586 self.ordered_entities()
587 .into_iter()
588 .map(|(key, entity)| (key, entity.into_value()))
589 .collect()
590 }
591
592 pub fn ordered_entities(&mut self) -> Vec<(String, SharedEntity)> {
595 self.iter_ordered()
596 .map(|(key, entity)| (key.to_string(), entity.clone()))
597 .collect()
598 }
599
600 pub fn compute_window_deltas(
602 &mut self,
603 old_window_keys: &[String],
604 skip: usize,
605 take: usize,
606 ) -> Vec<ViewDelta> {
607 let new_window_keys: Vec<&String> = self
608 .sorted
609 .keys()
610 .map(|sort_key| &sort_key.entity_key)
611 .skip(skip)
612 .take(take)
613 .collect();
614
615 let old_set: std::collections::HashSet<&String> = old_window_keys.iter().collect();
616 let new_set: std::collections::HashSet<&String> = new_window_keys.iter().cloned().collect();
617
618 let mut deltas = Vec::new();
619
620 for key in old_set.difference(&new_set) {
622 deltas.push(ViewDelta::Remove {
623 key: (*key).clone(),
624 });
625 }
626
627 for key in new_set.difference(&old_set) {
629 if let Some((_, entity)) = self.entities.get(*key) {
630 deltas.push(ViewDelta::Add {
631 key: (*key).clone(),
632 entity: entity.to_value(),
633 });
634 }
635 }
636
637 deltas
638 }
639
640 fn extract_sort_value<E: EntityFields + ?Sized>(&self, entity: &E) -> SortValue {
641 match lookup(entity, &self.sort_field) {
642 Some(value) => value_to_sort_value(&value),
643 None => SortValue::Null,
644 }
645 }
646
647 fn find_position(&self, entity_key: &str) -> usize {
648 if let Some((sort_key, _)) = self.entities.get(entity_key) {
649 self.find_position_by_sort_key(sort_key)
650 } else {
651 0
652 }
653 }
654
655 fn find_position_by_sort_key(&self, sort_key: &SortKey) -> usize {
656 self.sorted.range(..sort_key).count()
657 }
658
659 fn rebuild_keys_cache(&mut self) {
660 self.keys_cache = self.sorted.keys().map(|sk| sk.entity_key.clone()).collect();
661 self.cache_dirty = false;
662 }
663}
664
665enum Placed {
667 Unmoved,
668 Moved,
669}
670
671#[derive(Debug, Clone, PartialEq)]
673pub enum UpsertResult {
674 Inserted { position: usize },
676 Updated { position: usize },
678}
679
680fn keeps_fields(base: &Value, patch: &Value) -> bool {
683 match (base, patch) {
684 (Value::Object(base), Value::Object(patch)) => base
685 .iter()
686 .any(|(key, base)| patch.get(key).is_none_or(|patch| keeps_fields(base, patch))),
687 _ => false,
688 }
689}
690
691fn value_to_sort_value(v: &Value) -> SortValue {
692 match v {
693 Value::Null => SortValue::Null,
694 Value::Bool(b) => SortValue::Bool(*b),
695 Value::Number(n) => {
696 if let Some(i) = n.as_i64() {
697 SortValue::Integer(i)
698 } else if let Some(f) = n.as_f64() {
699 SortValue::Float(OrderedFloat(f))
700 } else {
701 SortValue::Null
702 }
703 }
704 Value::String(s) => SortValue::String(s.clone()),
705 _ => SortValue::Null,
706 }
707}
708
709#[derive(Debug, Clone, Copy, PartialEq, Eq)]
713struct DecimalKey {
714 negative: bool,
715 start: usize,
717 value: Option<u128>,
719}
720
721impl DecimalKey {
722 fn of(sort_value: &SortValue) -> Option<Self> {
723 match sort_value {
724 SortValue::String(text) => Self::parse(text),
725 _ => None,
726 }
727 }
728
729 fn parse(text: &str) -> Option<Self> {
730 let (negative, digits) = match text.strip_prefix('-') {
731 Some(digits) => (true, digits),
732 None => (false, text),
733 };
734 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
735 return None;
736 }
737 let significant = digits.trim_start_matches('0');
738 let significant = if significant.is_empty() {
739 &digits[digits.len() - 1..]
740 } else {
741 significant
742 };
743 Some(Self {
744 negative: negative && significant != "0",
745 start: text.len() - significant.len(),
746 value: significant.parse().ok(),
747 })
748 }
749
750 fn cmp(&self, left: &str, other: &Self, right: &str) -> Ordering {
753 match (self.negative, other.negative) {
754 (true, false) => Ordering::Less,
755 (false, true) => Ordering::Greater,
756 _ => {
757 let magnitude = match (self.value, other.value) {
758 (Some(a), Some(b)) => a.cmp(&b),
759 _ => {
760 let (a, b) = (&left[self.start..], &right[other.start..]);
761 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
762 }
763 };
764 if self.negative {
765 magnitude.reverse()
766 } else {
767 magnitude
768 }
769 }
770 }
771 }
772}
773
774fn compare_strings(left: &str, right: &str) -> Ordering {
781 compare_decimal_strings(left, right).unwrap_or_else(|| {
782 let left_decimal = DecimalKey::parse(left).is_some();
783 let right_decimal = DecimalKey::parse(right).is_some();
784 right_decimal
785 .cmp(&left_decimal)
786 .then_with(|| left.cmp(right))
787 })
788}
789
790fn compare_decimal_strings(left: &str, right: &str) -> Option<Ordering> {
791 fn parts(value: &str) -> Option<(bool, &str)> {
792 let (negative, digits) = match value.strip_prefix('-') {
793 Some(digits) => (true, digits),
794 None => (false, value),
795 };
796 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
797 return None;
798 }
799
800 let digits = digits.trim_start_matches('0');
801 let digits = if digits.is_empty() { "0" } else { digits };
802 Some((negative && digits != "0", digits))
803 }
804
805 let (left_negative, left_digits) = parts(left)?;
806 let (right_negative, right_digits) = parts(right)?;
807
808 match (left_negative, right_negative) {
809 (true, false) => Some(Ordering::Less),
810 (false, true) => Some(Ordering::Greater),
811 _ => {
812 let magnitude = left_digits
813 .len()
814 .cmp(&right_digits.len())
815 .then_with(|| left_digits.cmp(right_digits));
816 Some(if left_negative {
817 magnitude.reverse()
818 } else {
819 magnitude
820 })
821 }
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828 use serde_json::json;
829
830 #[test]
831 fn test_sorted_cache_basic() {
832 let mut cache = SortedViewCache::new(
833 "test/latest".to_string(),
834 vec!["id".to_string()],
835 SortOrder::Desc,
836 );
837
838 cache.upsert("a".to_string(), json!({"id": 1, "name": "first"}));
839 cache.upsert("b".to_string(), json!({"id": 3, "name": "third"}));
840 cache.upsert("c".to_string(), json!({"id": 2, "name": "second"}));
841
842 let keys = cache.ordered_keys();
843 assert_eq!(keys, vec!["b", "c", "a"]);
845 }
846
847 #[test]
848 fn test_sorted_cache_window() {
849 let mut cache = SortedViewCache::new(
850 "test/latest".to_string(),
851 vec!["id".to_string()],
852 SortOrder::Desc,
853 );
854
855 for i in 1..=10 {
856 cache.upsert(format!("e{}", i), json!({"id": i}));
857 }
858
859 let window = cache.get_window(0, 3);
861 assert_eq!(window.len(), 3);
862 assert_eq!(window[0].0, "e10");
863 assert_eq!(window[1].0, "e9");
864 assert_eq!(window[2].0, "e8");
865
866 let window = cache.get_window(3, 3);
867 assert_eq!(window[0].0, "e7");
868 }
869
870 #[test]
871 fn all_ordered_preserves_stable_sort_and_tie_breaking() {
872 let mut cache = SortedViewCache::new(
873 "test/latest".to_string(),
874 vec!["score".to_string()],
875 SortOrder::Desc,
876 );
877 cache.upsert("b".to_string(), json!({"score": 10}));
878 cache.upsert("a".to_string(), json!({"score": 10}));
879 cache.upsert("c".to_string(), json!({"score": 9}));
880
881 let keys: Vec<_> = cache
882 .get_all_ordered()
883 .into_iter()
884 .map(|(key, _)| key)
885 .collect();
886 assert_eq!(keys, ["a", "b", "c"]);
887 }
888
889 #[test]
890 fn test_sorted_cache_update_moves_position() {
891 let mut cache = SortedViewCache::new(
892 "test/latest".to_string(),
893 vec!["score".to_string()],
894 SortOrder::Desc,
895 );
896
897 cache.upsert("a".to_string(), json!({"score": 10}));
898 cache.upsert("b".to_string(), json!({"score": 20}));
899 cache.upsert("c".to_string(), json!({"score": 15}));
900
901 assert_eq!(cache.ordered_keys(), vec!["b", "c", "a"]);
903
904 cache.upsert("a".to_string(), json!({"score": 25}));
906
907 assert_eq!(cache.ordered_keys(), vec!["a", "b", "c"]);
909 }
910
911 #[test]
912 fn test_sorted_cache_remove() {
913 let mut cache = SortedViewCache::new(
914 "test/latest".to_string(),
915 vec!["id".to_string()],
916 SortOrder::Asc,
917 );
918
919 cache.upsert("a".to_string(), json!({"id": 1}));
920 cache.upsert("b".to_string(), json!({"id": 2}));
921 cache.upsert("c".to_string(), json!({"id": 3}));
922
923 assert_eq!(cache.len(), 3);
924
925 let pos = cache.remove("b");
926 assert_eq!(pos, Some(1));
927 assert_eq!(cache.len(), 2);
928 assert_eq!(cache.ordered_keys(), vec!["a", "c"]);
929 }
930
931 #[test]
932 fn test_compute_window_deltas() {
933 let mut cache = SortedViewCache::new(
934 "test/latest".to_string(),
935 vec!["id".to_string()],
936 SortOrder::Desc,
937 );
938
939 for i in 1..=5 {
941 cache.upsert(format!("e{}", i), json!({"id": i}));
942 }
943
944 let old_window: Vec<String> = vec!["e5".to_string(), "e4".to_string(), "e3".to_string()];
945
946 cache.upsert("e6".to_string(), json!({"id": 6}));
948
949 let deltas = cache.compute_window_deltas(&old_window, 0, 3);
952
953 assert_eq!(deltas.len(), 2);
954 assert!(deltas
956 .iter()
957 .any(|d| matches!(d, ViewDelta::Remove { key } if key == "e3")));
958 assert!(deltas
960 .iter()
961 .any(|d| matches!(d, ViewDelta::Add { key, .. } if key == "e6")));
962 }
963
964 fn keys(cache: &mut SortedViewCache) -> Vec<String> {
965 cache.ordered_keys().to_vec()
966 }
967
968 #[test]
969 fn bounded_upsert_evicts_bottom_of_desc_order() {
970 let mut cache = SortedViewCache::new(
971 "test/top".to_string(),
972 vec!["score".to_string()],
973 SortOrder::Desc,
974 );
975
976 for i in 1..=10 {
977 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 4);
978 assert!(cache.len() <= 4);
979 }
980
981 assert_eq!(cache.len(), 4);
982 assert_eq!(keys(&mut cache), ["e10", "e9", "e8", "e7"]);
983 assert!(cache.get("e1").is_none());
984 assert!(cache.get("e6").is_none());
985 }
986
987 #[test]
988 fn would_keep_matches_what_a_bounded_upsert_keeps() {
989 let mut cache = SortedViewCache::new(
990 "test/top".to_string(),
991 vec!["score".to_string()],
992 SortOrder::Desc,
993 );
994 for i in 1..=10 {
995 let entity = json!({"score": i});
996 let expected = cache.would_keep(&format!("e{i}"), &entity, 4);
997 cache.upsert_bounded(format!("e{i}"), entity, 4);
998 assert!(expected, "an entity above a full cache's tail is kept");
999 }
1000 for (key, score, kept) in [("e0", 0, false), ("e6", 6, false), ("e11", 11, true)] {
1002 let entity = json!({"score": score});
1003 assert_eq!(cache.would_keep(key, &entity, 4), kept, "{key}");
1004 let mut copy = SortedViewCache::new(
1005 "test/top".to_string(),
1006 vec!["score".to_string()],
1007 SortOrder::Desc,
1008 );
1009 for existing in keys(&mut cache) {
1010 let value = cache.get(&existing).unwrap().clone();
1011 copy.upsert_bounded(existing, value, 4);
1012 }
1013 copy.upsert_bounded(key.to_string(), entity, 4);
1014 assert_eq!(copy.get(key).is_some(), kept, "{key}");
1015 }
1016 assert!(cache.would_keep("e8", &json!({"score": -1}), 4));
1018 }
1019
1020 #[test]
1021 fn bounded_upsert_evicts_bottom_of_asc_order() {
1022 let mut cache = SortedViewCache::new(
1023 "test/bottom".to_string(),
1024 vec!["score".to_string()],
1025 SortOrder::Asc,
1026 );
1027
1028 for i in (1..=10).rev() {
1029 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 4);
1030 assert!(cache.len() <= 4);
1031 }
1032
1033 assert_eq!(cache.len(), 4);
1034 assert_eq!(keys(&mut cache), ["e1", "e2", "e3", "e4"]);
1035 assert!(cache.get("e10").is_none());
1036 }
1037
1038 #[test]
1039 fn bounded_upsert_does_not_evict_stale_top_entities() {
1040 let mut cache = SortedViewCache::new(
1041 "test/top".to_string(),
1042 vec!["score".to_string()],
1043 SortOrder::Desc,
1044 );
1045
1046 cache.upsert_bounded("leader".to_string(), json!({"score": 1_000}), 3);
1049 for i in 1..=20 {
1050 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
1051 }
1052
1053 assert_eq!(keys(&mut cache), ["leader", "e20", "e19"]);
1054 }
1055
1056 #[test]
1057 fn windows_within_cap_match_unbounded_cache() {
1058 let mut bounded = SortedViewCache::new(
1059 "test/top".to_string(),
1060 vec!["score".to_string()],
1061 SortOrder::Desc,
1062 );
1063 let mut unbounded = SortedViewCache::new(
1064 "test/top".to_string(),
1065 vec!["score".to_string()],
1066 SortOrder::Desc,
1067 );
1068
1069 for i in 0..200u64 {
1074 let key = format!("e{}", i % 60);
1075 let score = (i / 60) * 1_000 + (i * 37) % 101;
1076 let entity = json!({"score": score, "n": i});
1077 bounded.upsert_bounded(key.clone(), entity.clone(), 25);
1078 unbounded.upsert(key, entity);
1079 }
1080
1081 assert_eq!(bounded.len(), 25);
1082 for (skip, take) in [(0, 25), (0, 10), (5, 20), (24, 1)] {
1083 assert_eq!(
1084 bounded.get_window(skip, take),
1085 unbounded.get_window(skip, take),
1086 "window skip={skip} take={take}"
1087 );
1088 }
1089 }
1090
1091 #[test]
1092 fn evicted_entity_is_missing_after_top_moves_down_until_it_updates() {
1093 let mut cache = SortedViewCache::new(
1094 "test/top".to_string(),
1095 vec!["score".to_string()],
1096 SortOrder::Desc,
1097 );
1098
1099 for i in 1..=4 {
1100 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
1101 }
1102 assert_eq!(keys(&mut cache), ["e4", "e3", "e2"]);
1103
1104 cache.upsert_bounded("e4".to_string(), json!({"score": 0}), 3);
1107 assert_eq!(keys(&mut cache), ["e3", "e2", "e4"]);
1108
1109 cache.upsert_bounded("e1".to_string(), json!({"score": 1}), 3);
1111 assert_eq!(keys(&mut cache), ["e3", "e2", "e1"]);
1112 }
1113
1114 #[test]
1115 fn evicted_entity_reenters_when_upserted_again() {
1116 let mut cache = SortedViewCache::new(
1117 "test/top".to_string(),
1118 vec!["score".to_string()],
1119 SortOrder::Desc,
1120 );
1121
1122 for i in 1..=5 {
1123 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
1124 }
1125 assert!(cache.get("e1").is_none());
1126
1127 let result = cache.upsert_bounded("e1".to_string(), json!({"score": 100}), 3);
1128 assert_eq!(result, UpsertResult::Inserted { position: 0 });
1129 assert_eq!(keys(&mut cache), ["e1", "e5", "e4"]);
1130 assert_eq!(cache.len(), 3);
1131 }
1132
1133 #[test]
1134 fn upsert_below_full_cap_is_evicted_immediately() {
1135 let mut cache = SortedViewCache::new(
1136 "test/top".to_string(),
1137 vec!["score".to_string()],
1138 SortOrder::Desc,
1139 );
1140
1141 for i in 10..=12 {
1142 cache.upsert_bounded(format!("e{i}"), json!({"score": i}), 3);
1143 }
1144 let result = cache.upsert_bounded("low".to_string(), json!({"score": 1}), 3);
1145
1146 assert_eq!(result, UpsertResult::Inserted { position: 3 });
1147 assert!(cache.get("low").is_none());
1148 assert_eq!(keys(&mut cache), ["e12", "e11", "e10"]);
1149 }
1150
1151 #[test]
1152 fn trim_keeps_keys_cache_and_entities_consistent() {
1153 let mut cache = SortedViewCache::new(
1154 "test/top".to_string(),
1155 vec!["score".to_string()],
1156 SortOrder::Desc,
1157 );
1158
1159 for i in 1..=10 {
1160 cache.upsert(format!("e{i}"), json!({"score": i}));
1161 }
1162 assert_eq!(cache.ordered_keys().len(), 10);
1164
1165 assert_eq!(cache.trim_to_max_entries(4), 6);
1166 assert_eq!(cache.trim_to_max_entries(4), 0);
1167 assert_eq!(cache.len(), 4);
1168 assert_eq!(keys(&mut cache), ["e10", "e9", "e8", "e7"]);
1169 assert_eq!(cache.get_all_ordered().len(), 4);
1170 assert_eq!(cache.remove("e7"), Some(3));
1171 assert_eq!(keys(&mut cache), ["e10", "e9", "e8"]);
1172 }
1173
1174 #[test]
1175 fn test_nested_sort_field() {
1176 let mut cache = SortedViewCache::new(
1177 "test/latest".to_string(),
1178 vec!["id".to_string(), "round_id".to_string()],
1179 SortOrder::Desc,
1180 );
1181
1182 cache.upsert("a".to_string(), json!({"id": {"round_id": 1}}));
1183 cache.upsert("b".to_string(), json!({"id": {"round_id": 3}}));
1184 cache.upsert("c".to_string(), json!({"id": {"round_id": 2}}));
1185
1186 let keys = cache.ordered_keys();
1187 assert_eq!(keys, vec!["b", "c", "a"]);
1188 }
1189
1190 #[test]
1191 fn test_nested_decimal_string_sort_field() {
1192 let mut cache = SortedViewCache::new(
1193 "test/latest".to_string(),
1194 vec!["id".to_string(), "round_id".to_string()],
1195 SortOrder::Desc,
1196 );
1197
1198 cache.upsert("9".to_string(), json!({"id": {"round_id": "9"}}));
1199 cache.upsert("100".to_string(), json!({"id": {"round_id": "100"}}));
1200 cache.upsert("10".to_string(), json!({"id": {"round_id": "10"}}));
1201
1202 assert_eq!(cache.ordered_keys(), vec!["100", "10", "9"]);
1203 }
1204
1205 #[test]
1208 fn test_mixed_decimal_and_text_sort_values() {
1209 let mut cache = SortedViewCache::new(
1210 "test/latest".to_string(),
1211 vec!["name".to_string()],
1212 SortOrder::Asc,
1213 );
1214
1215 cache.upsert("two".to_string(), json!({"name": "2"}));
1216 cache.upsert("ten".to_string(), json!({"name": "10"}));
1217 cache.upsert("text".to_string(), json!({"name": "1a"}));
1218 assert_eq!(cache.ordered_keys(), vec!["two", "ten", "text"]);
1219
1220 cache.put("ten".to_string(), json!({"name": "11"}));
1221 assert_eq!(cache.ordered_keys(), vec!["two", "ten", "text"]);
1222 cache.put("ten".to_string(), json!({"name": "1"}));
1223 assert_eq!(cache.ordered_keys(), vec!["ten", "two", "text"]);
1224 cache.put("text".to_string(), json!({"name": "0"}));
1225 assert_eq!(cache.ordered_keys(), vec!["text", "ten", "two"]);
1226 assert_eq!(cache.len(), 3);
1227 assert_eq!(cache.sorted.len(), 3);
1228 }
1229
1230 #[test]
1232 fn string_order_is_transitive() {
1233 let texts = [
1234 "", "-", "-1", "-10", "0", "007", "1", "10", "1a", "2", "a", "+5", "1.5",
1235 ];
1236 for a in texts {
1237 for b in texts {
1238 for c in texts {
1239 if compare_strings(a, b) != Ordering::Greater
1240 && compare_strings(b, c) != Ordering::Greater
1241 {
1242 assert_ne!(
1243 compare_strings(a, c),
1244 Ordering::Greater,
1245 "{a:?} <= {b:?} <= {c:?}"
1246 );
1247 }
1248 }
1249 }
1250 }
1251 }
1252
1253 #[test]
1254 fn test_descending_string_sort_field() {
1255 let mut cache = SortedViewCache::new(
1256 "test/latest".to_string(),
1257 vec!["name".to_string()],
1258 SortOrder::Desc,
1259 );
1260
1261 cache.upsert("a".to_string(), json!({"name": "alpha"}));
1262 cache.upsert("c".to_string(), json!({"name": "charlie"}));
1263 cache.upsert("b".to_string(), json!({"name": "bravo"}));
1264
1265 assert_eq!(cache.ordered_keys(), vec!["c", "b", "a"]);
1266 }
1267
1268 #[test]
1269 fn test_update_with_missing_sort_field_preserves_position() {
1270 let mut cache = SortedViewCache::new(
1271 "test/latest".to_string(),
1272 vec!["id".to_string(), "round_id".to_string()],
1273 SortOrder::Desc,
1274 );
1275
1276 cache.upsert(
1277 "100".to_string(),
1278 json!({"id": {"round_id": 100}, "data": "initial"}),
1279 );
1280 cache.upsert(
1281 "200".to_string(),
1282 json!({"id": {"round_id": 200}, "data": "initial"}),
1283 );
1284 cache.upsert(
1285 "300".to_string(),
1286 json!({"id": {"round_id": 300}, "data": "initial"}),
1287 );
1288
1289 assert_eq!(cache.ordered_keys(), vec!["300", "200", "100"]);
1290
1291 cache.upsert("200".to_string(), json!({"data": "updated_without_id"}));
1292
1293 assert_eq!(
1294 cache.ordered_keys(),
1295 vec!["300", "200", "100"],
1296 "Entity 200 should retain its position even when updated without sort field"
1297 );
1298
1299 let entity = cache.get("200").unwrap();
1300 assert_eq!(entity["data"], "updated_without_id");
1301 }
1302
1303 #[test]
1304 fn test_new_entity_with_missing_sort_field_sorts_last() {
1305 for order in [SortOrder::Desc, SortOrder::Asc] {
1306 let mut cache = SortedViewCache::new(
1307 "test/latest".to_string(),
1308 vec!["id".to_string(), "round_id".to_string()],
1309 order,
1310 );
1311
1312 cache.upsert("100".to_string(), json!({"id": {"round_id": 100}}));
1313 cache.upsert("200".to_string(), json!({"id": {"round_id": 200}}));
1314 cache.upsert("new".to_string(), json!({"data": "no_sort_field"}));
1315 cache.upsert("nil".to_string(), json!({"id": {"round_id": null}}));
1316
1317 let ranked = match order {
1318 SortOrder::Desc => ["200", "100"],
1319 SortOrder::Asc => ["100", "200"],
1320 };
1321 assert_eq!(
1322 cache.ordered_keys(),
1323 [ranked[0], ranked[1], "new", "nil"],
1324 "{order:?}: an entity without a sort value never outranks one with it; \
1325 among themselves they order by key"
1326 );
1327 }
1328 }
1329
1330 #[test]
1333 fn a_full_window_does_not_admit_an_entity_without_a_sort_value() {
1334 let mut cache = SortedViewCache::new(
1335 "test/latest".to_string(),
1336 vec!["id".to_string(), "round_id".to_string()],
1337 SortOrder::Desc,
1338 );
1339 for round in [100, 200, 300] {
1340 cache.upsert_bounded(round.to_string(), json!({"id": {"round_id": round}}), 3);
1341 }
1342 let partial = json!({"metrics": {"checkpoint_count": 7}});
1343 assert!(!cache.would_keep("1", &partial, 3));
1344 cache.upsert_bounded("1".to_string(), partial, 3);
1345 assert_eq!(cache.ordered_keys(), ["300", "200", "100"]);
1346 }
1347
1348 #[test]
1351 fn a_whole_update_shares_the_given_entity() {
1352 let mut cache = SortedViewCache::new(
1353 "test/top".to_string(),
1354 vec!["score".to_string()],
1355 SortOrder::Desc,
1356 );
1357 cache.upsert(
1358 "a".to_string(),
1359 json!({"score": 1, "name": "a", "_version": "e:1"}),
1360 );
1361 let update =
1362 SharedEntity::new(json!({"score": 2, "name": "a", "extra": true, "_version": "e:2"}));
1363 cache.upsert("a".to_string(), update.clone());
1364 let held = cache.get("a").unwrap();
1365 assert!(held.shares_fields_with(&update));
1366 assert_eq!(*held, update);
1367 }
1368
1369 #[test]
1372 fn an_update_lacking_held_fields_merges_into_its_own_copy() {
1373 let mut cache = SortedViewCache::new(
1374 "test/top".to_string(),
1375 vec!["score".to_string()],
1376 SortOrder::Desc,
1377 );
1378 cache.upsert(
1379 "a".to_string(),
1380 json!({"score": 1, "nested": {"x": 1, "y": 1}, "_version": "e:1"}),
1381 );
1382 let update = SharedEntity::new(json!({"score": 2, "nested": {"x": 2}}));
1383 cache.upsert("a".to_string(), update.clone());
1384 let held = cache.get("a").unwrap();
1385 assert!(!held.shares_fields_with(&update));
1386 assert_eq!(
1387 held.to_value(),
1388 json!({"score": 2, "nested": {"x": 2, "y": 1}, "_version": "e:1"})
1389 );
1390 assert_eq!(update.to_value(), json!({"score": 2, "nested": {"x": 2}}));
1391 }
1392
1393 #[test]
1396 fn a_version_sort_reads_each_copys_own_version() {
1397 let mut cache = SortedViewCache::new(
1398 "test/versions".to_string(),
1399 vec!["_version".to_string()],
1400 SortOrder::Desc,
1401 );
1402 let first = SharedEntity::new(json!({"id": 1, "_version": "e:1"}));
1403 let (fields, _) = first.clone().into_parts();
1404 cache.upsert("a".to_string(), first);
1405 cache.upsert(
1406 "b".to_string(),
1407 SharedEntity::from_parts(fields, Some(json!("e:2"))),
1408 );
1409 assert_eq!(cache.ordered_keys(), ["b", "a"]);
1410 }
1411
1412 #[test]
1415 fn pre_parsed_decimals_compare_as_the_strings_do() {
1416 let huge = "9".repeat(45);
1417 let huger = format!("1{}", "0".repeat(45));
1418 let u128_max = u128::MAX.to_string();
1419 let past_u128 = (u128::MAX - 1).to_string() + "0";
1420 let texts = [
1421 "0",
1422 "-0",
1423 "000",
1424 "-000",
1425 "7",
1426 "007",
1427 "-7",
1428 "-007",
1429 "10",
1430 "9",
1431 "-10",
1432 "-9",
1433 "18446744073709551616",
1434 "-18446744073709551616",
1435 &u128_max,
1436 &past_u128,
1437 &huge,
1438 &huger,
1439 "-",
1440 "",
1441 "+5",
1442 "5a",
1443 "a",
1444 "abc",
1445 " 1",
1446 "1.5",
1447 "-1.5",
1448 "0x10",
1449 ];
1450 for left in texts {
1451 for right in texts {
1452 let expected = compare_strings(left, right);
1453 for order in [SortOrder::Asc, SortOrder::Desc] {
1454 let a = SortKey::new(SortValue::String(left.to_string()), "k".into(), order);
1455 let b = SortKey::new(SortValue::String(right.to_string()), "k".into(), order);
1456 let expected = match order {
1457 SortOrder::Asc => expected,
1458 SortOrder::Desc => expected.reverse(),
1459 };
1460 assert_eq!(a.cmp(&b), expected, "{left:?} vs {right:?} ({order:?})");
1461 }
1462 }
1463 }
1464 }
1465
1466 #[test]
1470 fn unpositioned_writes_match_positioned_ones() {
1471 struct Rng(u64);
1472 impl Rng {
1473 fn below(&mut self, n: u64) -> u64 {
1474 self.0 = self
1475 .0
1476 .wrapping_mul(6364136223846793005)
1477 .wrapping_add(1442695040888963407);
1478 (self.0 >> 33) % n
1479 }
1480 }
1481 for (field, order) in [
1482 ("amount", SortOrder::Desc),
1483 ("amount", SortOrder::Asc),
1484 ("label", SortOrder::Asc),
1485 ] {
1486 let cache = || SortedViewCache::new("test/top".into(), vec![field.into()], order);
1487 let (mut positioned, mut unpositioned) = (cache(), cache());
1488 let mut rng = Rng(7);
1489 for step in 0..4_000 {
1490 let key = format!("k{}", rng.below(60));
1491 let max_entries = 1 + rng.below(40) as usize;
1492 let value = match rng.below(6) {
1493 0 => Value::Null,
1494 1 => json!(rng.below(1_000)),
1495 2 => json!(format!("{}", rng.below(1_000))),
1496 3 => json!(format!("-{:03}", rng.below(1_000))),
1497 4 => json!(format!("{}{}", "9".repeat(40), rng.below(10))),
1498 _ => json!(format!("x{}", rng.below(100))),
1499 };
1500 let entity = SharedEntity::new(json!({
1501 field: value,
1502 "step": step,
1503 "_version": format!("e:{step}"),
1504 }));
1505 match rng.below(4) {
1506 0 => {
1507 positioned.remove(&key);
1508 unpositioned.remove_key(&key);
1509 }
1510 1 => {
1511 positioned.upsert(key.clone(), entity.clone());
1512 unpositioned.put(key, entity);
1513 }
1514 _ => {
1515 let keeps = positioned.would_keep(&key, &entity, max_entries);
1516 if keeps {
1517 positioned.upsert_bounded(key.clone(), entity.clone(), max_entries);
1518 }
1519 assert_eq!(
1520 unpositioned.upsert_if_kept(&key, &entity, max_entries),
1521 keeps
1522 );
1523 }
1524 }
1525 let expected: Vec<(String, Value)> = positioned
1526 .ordered_keys()
1527 .to_vec()
1528 .into_iter()
1529 .map(|key| {
1530 let entity = positioned.get(&key).unwrap().to_value();
1531 (key, entity)
1532 })
1533 .collect();
1534 assert_eq!(unpositioned.get_all_ordered(), expected, "step {step}");
1535 let skip = rng.below(8) as usize;
1536 assert_eq!(
1537 unpositioned.get_window(skip, 5),
1538 expected
1539 .iter()
1540 .skip(skip)
1541 .take(5)
1542 .cloned()
1543 .collect::<Vec<_>>()
1544 );
1545 assert_eq!(unpositioned.ordered_keys(), positioned.ordered_keys());
1546
1547 let mut reference: Vec<(SortValue, String)> = expected
1550 .iter()
1551 .map(|(key, _)| {
1552 let (sort_key, _) = &unpositioned.entities[key];
1553 (sort_key.sort_value.clone(), key.clone())
1554 })
1555 .collect();
1556 reference.sort_by(|(a, a_key), (b, b_key)| {
1557 let by_value = match (a, b) {
1558 (SortValue::Null, SortValue::Null) => Ordering::Equal,
1559 (SortValue::Null, _) => Ordering::Greater,
1560 (_, SortValue::Null) => Ordering::Less,
1561 _ if order == SortOrder::Desc => a.cmp(b).reverse(),
1562 _ => a.cmp(b),
1563 };
1564 by_value.then_with(|| a_key.cmp(b_key))
1565 });
1566 let reference: Vec<&String> = reference.iter().map(|(_, key)| key).collect();
1567 let held: Vec<&String> = expected.iter().map(|(key, _)| key).collect();
1568 assert_eq!(held, reference, "step {step}");
1569 }
1570 }
1571 }
1572}