Skip to main content

kevy_store/
zset.rs

1//! `Store` sorted-set write-path commands (`ZADD` / `ZREM` / `ZINCRBY` /
2//! `ZSCORE` / `ZCARD` / `ZRANK`). The range / pop / range-removal family
3//! lives in `zset_range.rs` (500-LOC house cap).
4
5#[cfg(not(feature = "std"))]
6use crate::nostd_prelude::*;
7use crate::small_zset::{self, AddResult as ZAddResult, SmallZSetData};
8use crate::value::{SmallBytes, Value, ZSetData, zset_member_weight};
9use crate::zset_seg::{SegZSetData, Z_PROMOTE};
10use crate::{Entry, Store, StoreError};
11use alloc::sync::Arc;
12
13/// `-0.0` and `0.0` are one score, as they are in Redis.
14///
15/// Folded at the one door every score passes through to reach a sorted set,
16/// rather than inside `Score`'s comparison — that would be a branch in the
17/// innermost step of every tree descent, paid on reads as well as writes, to
18/// settle something that can be settled once on the way in.
19///
20/// Redis keys its skiplist on a plain `double` compare, where the two zeros
21/// are equal and the member breaks the tie. This rank tree is a B-tree,
22/// whose key needs a total order that `f64: PartialOrd` is not, and `Score`
23/// orders by `total_cmp` — which is total precisely because it separates
24/// them. Measured against a real `redis:8` on one host,
25/// `ZADD z 0 a; ZADD z -0 b; ZRANGE z 0 -1` answered `a b` there and `b a`
26/// here. See the finding under bench/.
27#[inline]
28fn fold_zero_sign(score: f64) -> f64 {
29    if score == 0.0 { 0.0 } else { score }
30}
31
32impl Store {
33    // ---- sorted sets ---------------------------------------------------
34
35    /// Borrow the key's zset mutably; promotes inline → flat, and flat →
36    /// segmented at the threshold (so ZINCRBY-only workloads cross the
37    /// boundary too).
38    fn zset_mut(&mut self, key: &[u8], create: bool) -> Result<Option<ZRefMut<'_>>, StoreError> {
39        if self.live_entry_mut(key).is_none() {
40            if !create {
41                return Ok(None);
42            }
43            self.insert_entry(
44                SmallBytes::from_slice(key),
45                Entry::new(Value::ZSet(Arc::default()), None),
46            );
47        }
48        // A.8: see hash.rs::hash_mut — promote out-of-scope.
49        let needs = match self.map.get(key).map(|e| &e.value) {
50            Some(Value::SmallZSetInline(_)) => true,
51            Some(Value::ZSet(z)) => z.len() >= Z_PROMOTE,
52            _ => false,
53        };
54        if needs {
55            self.promote_zset_encoding(key);
56        }
57        match &mut self.map.get_mut(key).expect("present").value {
58            Value::ZSet(z) => Ok(Some(ZRefMut::Flat(Arc::make_mut(z)))),
59            Value::SegZSet(z) => Ok(Some(ZRefMut::Seg(Arc::make_mut(z)))),
60            _ => Err(StoreError::WrongType),
61        }
62    }
63
64    /// One promotion step: inline → flat, or flat-at-threshold →
65    /// segmented. Reweighs the entry.
66    fn promote_zset_encoding(&mut self, key: &[u8]) {
67        let Some(e) = self.map.get_mut(key) else { return };
68        match &mut e.value {
69            Value::SmallZSetInline(s) => {
70                e.value = Value::ZSet(Arc::new(small_zset::promote(s)));
71            }
72            Value::ZSet(z) => {
73                e.value = Value::SegZSet(Arc::new(SegZSetData::from_flat(z)));
74            }
75            _ => return,
76        }
77        self.reweigh_entry(key);
78    }
79
80    /// A.8: read the key's zset slot for ZADD. None when absent.
81    fn zset_value_for_set(&mut self, key: &[u8]) -> Result<Option<&mut Value>, StoreError> {
82        match self.live_entry_mut(key) {
83            None => Ok(None),
84            Some(e) => match &e.value {
85                Value::ZSet(_) | Value::SegZSet(_) | Value::SmallZSetInline(_) => {
86                    Ok(Some(&mut e.value))
87                }
88                _ => Err(StoreError::WrongType),
89            },
90        }
91    }
92
93    fn drop_if_empty_zset(&mut self, key: &[u8]) {
94        let empty = match self.map.get(key).map(|e| &e.value) {
95            Some(Value::ZSet(z)) => z.len() == 0,
96            Some(Value::SegZSet(z)) => z.is_empty(),
97            Some(Value::SmallZSetInline(z)) => z.is_empty(),
98            _ => false,
99        };
100        if empty {
101            self.remove_entry(key);
102        }
103    }
104
105    /// `ZADD` — returns the count of newly-added members. Borrowed
106    /// argv: no per-member allocation; routes through the
107    /// encoding-switch path.
108    pub fn zadd(&mut self, key: &[u8], pairs: &[(f64, &[u8])]) -> Result<usize, StoreError> {
109        if pairs.is_empty() {
110            return Ok(0);
111        }
112        let mut added = 0usize;
113        let mut delta: i64 = 0;
114        for (score, m) in pairs {
115            match self.zadd_one(key, m, *score)? {
116                ZaddOutcome::AddedInline => added += 1,
117                ZaddOutcome::UpdatedInline => {}
118                ZaddOutcome::AddedHeap(w) => {
119                    added += 1;
120                    delta += w;
121                }
122                ZaddOutcome::UpdatedHeap => {}
123            }
124        }
125        self.account_delta(key, delta);
126        Ok(added)
127    }
128
129    /// One member's score. `Ok(None)` for a missing key or a missing
130    /// member — ZSCORE does not distinguish them.
131    pub fn zscore(&mut self, key: &[u8], member: &[u8]) -> Result<Option<f64>, StoreError> {
132        match self.live_entry(key) {
133            None => Ok(None),
134            Some(e) => match &e.value {
135                Value::ZSet(z) => Ok(z.by_member.get(member).copied()),
136                Value::SegZSet(z) => Ok(z.score_of(member)),
137                Value::SmallZSetInline(z) => Ok(z.score(member)),
138                _ => Err(StoreError::WrongType),
139            },
140        }
141    }
142
143    /// Member count. A missing key is 0, matching ZCARD.
144    pub fn zcard(&mut self, key: &[u8]) -> Result<usize, StoreError> {
145        match self.live_entry(key) {
146            None => Ok(0),
147            Some(e) => match &e.value {
148                Value::ZSet(z) => Ok(z.len()),
149                Value::SegZSet(z) => Ok(z.len()),
150                Value::SmallZSetInline(z) => Ok(z.len()),
151                _ => Err(StoreError::WrongType),
152            },
153        }
154    }
155
156    /// `ZREM` — returns the count of members removed.
157    pub fn zrem(&mut self, key: &[u8], members: &[&[u8]]) -> Result<usize, StoreError> {
158        let (removed, delta) = {
159            let mut r = 0usize;
160            let mut d: i64 = 0;
161            if let Some(e) = self.live_entry_mut(key) {
162                match &mut e.value {
163                    Value::ZSet(z) => {
164                        // G-A3: hoist Arc::make_mut OUT of loop.
165                        let z = Arc::make_mut(z);
166                        for m in members {
167                            if z.remove(m) {
168                                r += 1;
169                                d -= zset_member_weight(&SmallBytes::from_slice(m)) as i64;
170                            }
171                        }
172                    }
173                    Value::SegZSet(z) => {
174                        let z = Arc::make_mut(z);
175                        for m in members {
176                            if z.remove(m) {
177                                r += 1;
178                                d -= zset_member_weight(&SmallBytes::from_slice(m)) as i64;
179                            }
180                        }
181                    }
182                    Value::SmallZSetInline(z) => {
183                        for m in members {
184                            if z.try_remove(m) {
185                                r += 1;
186                            }
187                        }
188                    }
189                    _ => return Err(StoreError::WrongType),
190                }
191            }
192            (r, d)
193        };
194        self.account_delta(key, delta);
195        self.drop_if_empty_zset(key);
196        Ok(removed)
197    }
198
199    /// `ZRANK` — 0-based position in ascending order. O(log N): a hash
200    /// lookup for the score, then one order-statistic tree descent.
201    pub fn zrank(&mut self, key: &[u8], member: &[u8]) -> Result<Option<usize>, StoreError> {
202        match self.live_entry(key) {
203            None => Ok(None),
204            Some(e) => match &e.value {
205                Value::ZSet(z) => {
206                    Ok(z.by_member.get(member).copied().and_then(|sc| z.rank_of(member, sc)))
207                }
208                Value::SegZSet(z) => Ok(z.score_of(member).and_then(|sc| z.rank_of(member, sc))),
209                Value::SmallZSetInline(z) => {
210                    // Inline holds at most 2 entries; sort by score (then
211                    // bytes) so ZRANK matches ZRANGE order.
212                    let mut entries: Vec<(&[u8], f64)> = z.iter().collect();
213                    entries.sort_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(b.0)));
214                    Ok(entries.iter().position(|(m, _)| *m == member))
215                }
216                _ => Err(StoreError::WrongType),
217            },
218        }
219    }
220
221    /// `ZINCRBY` — add `incr` to a member's score; returns the new score.
222    pub fn zincrby(&mut self, key: &[u8], incr: f64, member: &[u8]) -> Result<f64, StoreError> {
223        let mut z = self.zset_mut(key, true)?.expect("created");
224        let cur = z.score_of(member).unwrap_or(0.0);
225        let next = cur + incr;
226        let smb = SmallBytes::from_slice(member);
227        let is_new = !z.contains_member(member);
228        z.insert(member, next);
229        let d = if is_new { zset_member_weight(&smb) as i64 } else { 0 };
230        self.account_delta(key, d);
231        Ok(next)
232    }
233
234    /// A.8 core: set one `(member, score)` pair via encoding-switch.
235    fn zadd_one(&mut self, key: &[u8], m: &[u8], score: f64) -> Result<ZaddOutcome, StoreError> {
236        let score = fold_zero_sign(score);
237        if self.zset_value_for_set(key)?.is_none() {
238            return Ok(self.zadd_create(key, m, score));
239        }
240        let v = self.zset_value_for_set(key)?.expect("present and a zset");
241        match v {
242            Value::SmallZSetInline(z) => match z.try_set(m, score) {
243                ZAddResult::Added => Ok(ZaddOutcome::AddedInline),
244                ZAddResult::Updated => Ok(ZaddOutcome::UpdatedInline),
245                ZAddResult::NoRoom => {
246                    let outcome = promote_inline_zset_and_add(v, m, score);
247                    self.reweigh_entry(key);
248                    Ok(outcome)
249                }
250            },
251            Value::ZSet(z) if z.len() >= Z_PROMOTE => {
252                let is_new = promote_flat_zset_and_add(v, m, score);
253                self.reweigh_entry(key);
254                // Reweighed from scratch — swallow the per-member delta.
255                if is_new { Ok(ZaddOutcome::AddedHeap(0)) } else { Ok(ZaddOutcome::UpdatedHeap) }
256            }
257            Value::ZSet(z) => {
258                let z = Arc::make_mut(z);
259                let smb = SmallBytes::from_slice(m);
260                let w = zset_member_weight(&smb) as i64;
261                if z.insert(m, score) {
262                    Ok(ZaddOutcome::AddedHeap(w))
263                } else {
264                    Ok(ZaddOutcome::UpdatedHeap)
265                }
266            }
267            Value::SegZSet(z) => {
268                let z = Arc::make_mut(z);
269                let smb = SmallBytes::from_slice(m);
270                let w = zset_member_weight(&smb) as i64;
271                if z.insert(m, score) {
272                    Ok(ZaddOutcome::AddedHeap(w))
273                } else {
274                    Ok(ZaddOutcome::UpdatedHeap)
275                }
276            }
277            _ => Err(StoreError::WrongType),
278        }
279    }
280
281    /// Create a fresh entry holding one `(member, score)` pair.
282    fn zadd_create(&mut self, key: &[u8], m: &[u8], score: f64) -> ZaddOutcome {
283        if let Some(inline) = SmallZSetData::with_one(m, score) {
284            self.insert_entry(
285                SmallBytes::from_slice(key),
286                Entry::new(Value::SmallZSetInline(inline), None),
287            );
288            ZaddOutcome::AddedInline
289        } else {
290            let mut z = ZSetData::default();
291            z.insert(m, score);
292            self.insert_entry(
293                SmallBytes::from_slice(key),
294                Entry::new(Value::ZSet(Arc::new(z)), None),
295            );
296            ZaddOutcome::AddedInline
297        }
298    }
299}
300
301/// Inline zset out of room: promote to the flat heap encoding, then
302/// set the spilling pair. Caller reweighs the entry.
303fn promote_inline_zset_and_add(v: &mut Value, m: &[u8], score: f64) -> ZaddOutcome {
304    let Value::SmallZSetInline(z) = v else { unreachable!("matched inline") };
305    let mut promoted = small_zset::promote(z);
306    let smb = SmallBytes::from_slice(m);
307    let is_new = !promoted.by_member.contains_key(m);
308    let w = zset_member_weight(&smb) as i64;
309    promoted.insert(m, score);
310    *v = Value::ZSet(Arc::new(promoted));
311    if is_new { ZaddOutcome::AddedHeap(w) } else { ZaddOutcome::UpdatedHeap }
312}
313
314/// Flat zset at the threshold: segment, then set. One-time
315/// O(Z_PROMOTE) rebuild (or clone, if a view pins it now). Returns
316/// whether the member was new; caller reweighs.
317fn promote_flat_zset_and_add(v: &mut Value, m: &[u8], score: f64) -> bool {
318    let Value::ZSet(z) = v else { unreachable!("matched ZSet") };
319    let mut seg = SegZSetData::from_flat(z);
320    let is_new = seg.insert(m, score);
321    *v = Value::SegZSet(Arc::new(seg));
322    is_new
323}
324
325/// A mutable borrow of either heap zset encoding — the read-modify-
326/// write entry point (`zincrby`) stays encoding-blind.
327enum ZRefMut<'a> {
328    Flat(&'a mut ZSetData),
329    Seg(&'a mut SegZSetData),
330}
331
332impl ZRefMut<'_> {
333    fn score_of(&self, member: &[u8]) -> Option<f64> {
334        match self {
335            Self::Flat(z) => z.by_member.get(member).copied(),
336            Self::Seg(z) => z.score_of(member),
337        }
338    }
339    fn contains_member(&self, member: &[u8]) -> bool {
340        match self {
341            Self::Flat(z) => z.by_member.contains_key(member),
342            Self::Seg(z) => z.contains_member(member),
343        }
344    }
345    fn insert(&mut self, member: &[u8], score: f64) -> bool {
346        match self {
347            Self::Flat(z) => z.insert(member, score),
348            Self::Seg(z) => z.insert(member, score),
349        }
350    }
351}
352
353enum ZaddOutcome {
354    AddedInline,
355    UpdatedInline,
356    AddedHeap(i64),
357    UpdatedHeap,
358}