Skip to main content

arete_server/
sorted_cache.rs

1//! Sorted view cache for maintaining ordered entity collections.
2//!
3//! This module provides incremental maintenance of sorted entity views,
4//! enabling efficient windowed subscriptions (take/skip) with minimal
5//! recomputation on updates.
6
7use 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/// A sortable key that combines the sort value with entity key for stable ordering.
16/// Uses (sort_value, entity_key) tuple to ensure deterministic ordering even when
17/// sort values are equal.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct SortKey {
20    /// The extracted sort value (as comparable bytes)
21    sort_value: SortValue,
22    /// Entity key for tie-breaking
23    entity_key: String,
24    /// Direction to apply to the sort value comparison.
25    order: SortOrder,
26    /// A string sort value read as a decimal integer, once, rather than on
27    /// every comparison. Derived from `sort_value`, so it changes nothing
28    /// about equality.
29    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    /// Give the key another sort value.
43    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
70/// A [`SortKey`], borrowed: what ordering one reads, so a candidate can be
71/// compared without building (and allocating) a key of its own.
72struct Ranked<'a> {
73    sort_value: &'a SortValue,
74    decimal: Option<DecimalKey>,
75    entity_key: &'a str,
76    order: SortOrder,
77}
78
79impl Ranked<'_> {
80    /// [`SortValue::cmp`], using the pre-parsed decimals.
81    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        // An entity with no sort value (missing or `null`) cannot be ranked,
105        // so it sorts after every entity that has one in *both* directions:
106        // it must never take a window slot from a ranked entity. `desc`
107        // reverses only the comparison between two present values.
108        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/// Comparable sort value extracted from JSON
124#[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            // Cross-type comparisons: numbers < strings
144            (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            // Integer vs Float: convert to float
149            (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            // Bool vs others
152            (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/// Wrapper for f64 that implements Ord (treats NaN as less than all values)
165#[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/// Sort order for the cache
191#[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/// Delta representing a change to a client's windowed view
208#[derive(Debug, Clone, PartialEq)]
209pub enum ViewDelta {
210    /// No change to the client's window
211    None,
212    /// Entity was added to the window
213    Add { key: String, entity: Value },
214    /// Entity was removed from the window
215    Remove { key: String },
216    /// Entity in the window was updated
217    Update { key: String, entity: Value },
218}
219
220/// Sorted view cache maintaining entities in sort order.
221///
222/// # Bounding
223///
224/// The cache holds every entity it has been given. It shares an entity's
225/// fields with whoever gave it (see [`SharedEntity`]) rather than copying
226/// them, but it keeps them alive, so callers that feed it from a bounded
227/// source (the projector and snapshot restore feed it from the LRU-capped
228/// [`EntityCache`](crate::EntityCache)) should use
229/// [`upsert_bounded`](Self::upsert_bounded) or
230/// [`trim_to_max_entries`](Self::trim_to_max_entries) with that source's cap.
231///
232/// Entries are evicted from the *bottom* of the sort order, not by recency.
233/// Evicting the least-recently-updated entries (mirroring the entity cache)
234/// would drop a top-ranked entity that simply has not updated recently and
235/// break leaderboard-style `sort` + `take` views. Evicting everything beyond
236/// position `max_entries` keeps every window with `skip + take <= max_entries`
237/// exact. An evicted entity re-enters on its next update, because the
238/// projector re-reads the full entity from the entity cache before upserting.
239///
240/// Known edge: after entities are removed from (or move down out of) the top
241/// of the order, a previously evicted entity that has not updated since is
242/// missing from the cache until it next updates, so a window near the cap can
243/// under-fill or show a lower-ranked entity in its place until then.
244#[derive(Debug)]
245pub struct SortedViewCache {
246    /// View identifier
247    view_id: String,
248    /// Field path to sort by (e.g., ["id", "round_id"])
249    sort_field: Vec<String>,
250    /// Sort order
251    order: SortOrder,
252    /// Sorted entries: SortKey -> entity_key (for iteration in order)
253    sorted: BTreeMap<SortKey, ()>,
254    /// Entity data: entity_key -> (SortKey, entity)
255    entities: HashMap<String, (SortKey, SharedEntity)>,
256    /// Ordered keys cache (rebuilt on structural changes)
257    keys_cache: Vec<String>,
258    /// Whether keys_cache needs rebuild
259    cache_dirty: bool,
260    /// Stands in for an entity while it is merged in place.
261    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    /// Insert or update an entity, returns the position where it was inserted
291    ///
292    /// An update merges into the entity held, keeping fields the update lacks.
293    /// When it lacks none, which is the case for a whole entity, the cache
294    /// shares the update's fields instead of copying them.
295    ///
296    /// Finding the position walks the order up to it; [`Self::put`] does the
297    /// same upsert without it.
298    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    /// [`Self::upsert`] without reporting where the entity landed.
310    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    /// [`Self::upsert`] given the entity's sort value.
317    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            // Merge incoming entity with existing to preserve fields not in the update
325            let old_entity = std::mem::replace(held, self.placeholder.clone());
326            *held = Self::merge_entity(old_entity, entity);
327
328            // A missing sort value keeps the one held.
329            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                // Sort key unchanged - position unchanged, no structural change
333                return Placed::Unmoved;
334            }
335
336            // Sort key changed - need to reposition. The order's own copy of
337            // the key moves, so the entity key is not copied again.
338            let mut ordered = match self.sorted.remove_entry(sort_key) {
339                Some((ordered, ())) => ordered,
340                // Only an order that is not total can lose a held key. Drop
341                // whatever copy it holds rather than stop the view.
342                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    /// Whether upserting `entity` under `entity_key` into a cache bounded at
365    /// `max_entries` would keep it: the cache already holds the key, has
366    /// room, or the entity sorts before its current last entry.
367    ///
368    /// Lets a caller skip copying an entity that [`Self::upsert_bounded`]
369    /// would evict straight away, which in a busy view is most of them.
370    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    /// [`Self::would_keep`], handing back the sort value it read, if it read
381    /// one.
382    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    /// Upsert an entity, then evict from the bottom of the sort order so the
406    /// cache holds at most `max_entries` entities.
407    ///
408    /// If the upserted entity itself sorts beyond `max_entries` it is evicted
409    /// immediately and the returned position is `>= max_entries`.
410    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    /// [`Self::upsert_bounded`] for an entity [`Self::would_keep`] keeps,
422    /// without reporting its position: the cache ends up as it would after
423    /// `would_keep` and `upsert_bounded`, reading the entity's sort value
424    /// once and sharing (cloning) the entity only if the cache keeps it.
425    /// Returns whether it does.
426    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    /// Evict entities from the bottom of the sort order until at most
444    /// `max_entries` remain. Returns the number of evicted entities.
445    ///
446    /// Each eviction is an `O(log n)` pop from the end of the ordered index,
447    /// so a batch of evictions (e.g. after a bulk rebuild) costs no more than
448    /// the inserts that caused it. The ordered-keys cache is truncated in place
449    /// when it is current, so trimming does not force a full rebuild.
450    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            // Only the tail of the order was removed, so the prefix is still
461            // exact.
462            self.keys_cache.truncate(self.sorted.len());
463        }
464        evicted
465    }
466
467    /// [`Self::deep_merge`] for whole entities, `_version` included. The
468    /// result shares `patch`'s fields whenever merging adds nothing to them:
469    /// when `base` has no field, at any depth, that `patch` lacks.
470    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        // A top-level field both share adds nothing, so only the fields the
479        // update replaced are looked into.
480        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    /// Remove an entity, returns the position it was at
523    pub fn clear(&mut self) {
524        self.sorted.clear();
525        self.entities.clear();
526        self.cache_dirty = true;
527    }
528
529    ///
530    /// Finding the position walks the order up to it; [`Self::remove_key`]
531    /// removes without it.
532    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    /// Remove an entity, returning whether the cache held it.
540    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    /// Get entity by key
550    pub fn get(&self, entity_key: &str) -> Option<&SharedEntity> {
551        self.entities.get(entity_key).map(|(_, v)| v)
552    }
553
554    /// Get ordered keys (rebuilds cache if dirty)
555    ///
556    /// The other readers walk the order itself, so only this one pays for
557    /// copying every key after a change.
558    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    /// Get a window of entities, as copies.
566    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    /// Every entity in sort order, borrowed: a window read walks only as far
575    /// as it reads, `skip` included.
576    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    /// Copies of every entity in deterministic sort order.
585    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    /// Every entity in deterministic sort order for query-side filtering,
593    /// sharing their fields with the cache rather than copying them.
594    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    /// Compute deltas for a client with a specific window
601    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        // Removed from window
621        for key in old_set.difference(&new_set) {
622            deltas.push(ViewDelta::Remove {
623                key: (*key).clone(),
624            });
625        }
626
627        // Added to window
628        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
665/// Whether an upsert moved the entity in the order (or added it).
666enum Placed {
667    Unmoved,
668    Moved,
669}
670
671/// Result of an upsert operation
672#[derive(Debug, Clone, PartialEq)]
673pub enum UpsertResult {
674    /// Entity was inserted at a new position
675    Inserted { position: usize },
676    /// Entity was updated (may or may not have moved)
677    Updated { position: usize },
678}
679
680/// Whether merging `patch` into `base` keeps a field `patch` lacks: whether
681/// `base` has one, in an object where `patch` has an object too.
682fn 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/// A string that is a decimal integer (`-?[0-9]+`), as [`compare_decimal_strings`]
710/// reads it: its sign, where its significant digits start, and their value
711/// when it fits in a `u128`.
712#[derive(Debug, Clone, Copy, PartialEq, Eq)]
713struct DecimalKey {
714    negative: bool,
715    /// Where the digits start once leading zeros are dropped ("0" keeps one).
716    start: usize,
717    /// Those digits' value, when they fit.
718    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    /// [`compare_decimal_strings`] for `left` and `right`, whose keys these
751    /// are.
752    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
774/// How two string sort values order: decimal integers by value, before every
775/// other string, and other strings by their bytes.
776///
777/// Ranking decimals apart from the rest keeps the order total. Comparing a
778/// decimal with any string by bytes would not: `"2" < "10"` by value, but
779/// `"10" < "1a" < "2"` by bytes, and an order with a cycle loses entries.
780fn 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        // Desc order: 3, 2, 1
844        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        // Desc order: 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
860        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        // Order: b(20), c(15), a(10)
902        assert_eq!(cache.ordered_keys(), vec!["b", "c", "a"]);
903
904        // Update a to have highest score
905        cache.upsert("a".to_string(), json!({"score": 25}));
906
907        // New order: a(25), b(20), c(15)
908        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        // Initial: 5, 4, 3, 2, 1
940        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        // Add e6 (new top)
947        cache.upsert("e6".to_string(), json!({"id": 6}));
948
949        // New order: 6, 5, 4, 3, 2, 1
950        // New top 3: e6, e5, e4
951        let deltas = cache.compute_window_deltas(&old_window, 0, 3);
952
953        assert_eq!(deltas.len(), 2);
954        // e3 removed from window
955        assert!(deltas
956            .iter()
957            .any(|d| matches!(d, ViewDelta::Remove { key } if key == "e3")));
958        // e6 added to window
959        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        // e7 is the tail of [e10, e9, e8, e7].
1001        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        // A held entity is always kept, even when its new value sorts last.
1017        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        // The leader is inserted first and never updated again; recency-based
1047        // eviction would drop it.
1048        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        // Scores arrive out of order within each round and every entity moves
1070        // up on each later round. Entities never move down, so nothing
1071        // evicted can belong back inside the cap (see the edge-case test
1072        // below for what happens when they do).
1073        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        // The leader drops to the bottom; e1 would now rank third but was
1105        // evicted and has not updated, so e4 holds third place instead.
1106        cache.upsert_bounded("e4".to_string(), json!({"score": 0}), 3);
1107        assert_eq!(keys(&mut cache), ["e3", "e2", "e4"]);
1108
1109        // Once e1 updates it re-enters at its correct position.
1110        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        // Build the keys cache so the trim takes the in-place truncate path.
1163        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    /// Decimal and other strings in one view rank decimals first, so the
1206    /// order stays total and moving an entity never loses it.
1207    #[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    /// String ordering is transitive across decimal and other strings.
1231    #[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    /// The live `latest` failure: a window full of ranked entities must not
1331    /// admit an unranked one, since it would evict a real member.
1332    #[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    /// A whole entity, holding every field the cached copy has, is kept as
1349    /// given: the cache shares its fields instead of copying them.
1350    #[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    /// An update that lacks a field the cached copy has keeps that field, in
1370    /// a copy of its own: the entity it was given is left as it was.
1371    #[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    /// `_version` sorts like any other field, though copies keep it apart
1394    /// from the fields they share.
1395    #[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    /// Comparing pre-parsed decimal keys orders strings exactly as comparing
1413    /// the strings themselves does.
1414    #[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    /// The unpositioned upserts and removals the projector uses leave the
1467    /// cache exactly as the positioned ones do, in every order a window can
1468    /// read it.
1469    #[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                // The order itself, from `SortValue`'s own comparison: unranked
1548                // entities last either way, ties broken by key.
1549                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}