Skip to main content

glide/commands/
sorted_set.rs

1// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2//! Sorted-set commands. Mirrors Python's sorted-set command surface.
3#![allow(clippy::type_complexity)]
4
5use crate::ValkeyResult;
6use crate::cmd::Cmd;
7use crate::commands::options::Limit;
8use crate::executor::CommandExecutor;
9use crate::value::FromValkeyValue;
10use crate::value::ValkeyValue;
11use crate::value::to_glide_error;
12use crate::write::ToValkeyArgs;
13use crate::write::ValkeyWrite;
14use async_trait::async_trait;
15use bytes::Bytes;
16
17/// A score boundary for `ZRANGEBYSCORE`/`ZCOUNT` etc.
18///
19/// Mirrors Python `ScoreBoundary` + `InfBound`.
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum ScoreBound {
22    /// Negative infinity (`-inf`).
23    NegativeInfinity,
24    /// Positive infinity (`+inf`).
25    PositiveInfinity,
26    /// Inclusive bound at `value`.
27    Inclusive(f64),
28    /// Exclusive bound at `value` (`(value`).
29    Exclusive(f64),
30}
31
32impl ToValkeyArgs for ScoreBound {
33    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
34        match self {
35            ScoreBound::NegativeInfinity => out.write_arg(b"-inf"),
36            ScoreBound::PositiveInfinity => out.write_arg(b"+inf"),
37            ScoreBound::Inclusive(v) => out.write_arg_fmt(v),
38            ScoreBound::Exclusive(v) => out.write_arg_fmt(format_args!("({v}")),
39        }
40    }
41}
42
43/// A lexicographical boundary for `ZRANGEBYLEX`/`ZLEXCOUNT`.
44///
45/// Mirrors Python `LexBoundary` + `InfBound`.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum LexBound {
48    /// Smallest possible value (`-`).
49    NegativeInfinity,
50    /// Largest possible value (`+`).
51    PositiveInfinity,
52    /// Inclusive bound (`[value`).
53    Inclusive(Vec<u8>),
54    /// Exclusive bound (`(value`).
55    Exclusive(Vec<u8>),
56}
57
58impl ToValkeyArgs for LexBound {
59    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
60        // A bounded value is prefixed with `[`/`(` within the same argument.
61        let prefixed = |prefix: u8, value: &[u8]| {
62            let mut arg = Vec::with_capacity(1 + value.len());
63            arg.push(prefix);
64            arg.extend_from_slice(value);
65            arg
66        };
67        match self {
68            LexBound::NegativeInfinity => out.write_arg(b"-"),
69            LexBound::PositiveInfinity => out.write_arg(b"+"),
70            LexBound::Inclusive(v) => out.write_arg(&prefixed(b'[', v)),
71            LexBound::Exclusive(v) => out.write_arg(&prefixed(b'(', v)),
72        }
73    }
74}
75
76/// Aggregation mode for `ZUNIONSTORE`/`ZINTERSTORE`.
77///
78/// Mirrors Python `AggregationType`.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum AggregationType {
81    /// Sum the scores.
82    Sum,
83    /// Take the minimum score.
84    Min,
85    /// Take the maximum score.
86    Max,
87}
88
89impl AggregationType {
90    fn as_arg(&self) -> &'static str {
91        match self {
92            AggregationType::Sum => "SUM",
93            AggregationType::Min => "MIN",
94            AggregationType::Max => "MAX",
95        }
96    }
97}
98
99/// Sorted-set commands (`ZADD`, `ZRANGE`, `ZSCORE`, ...).
100#[async_trait]
101pub trait SortedSetCommands: CommandExecutor {
102    /// Increment the score of `member` by `increment` (`ZADD ... INCR`).
103    ///
104    /// This is the plain (unconditional) `INCR` form, so the result is always
105    /// `Some(new_score)`. The `Option` return is kept for the conditional
106    /// forms of the command: combined with `NX`/`XX`/`GT`/`LT` (reachable via
107    /// [`crate::CustomCommand::custom_command`]) the server replies nil when
108    /// the condition suppresses the update.
109    async fn zadd_incr<K: ToValkeyArgs + Send, M: ToValkeyArgs + Send>(
110        &self,
111        key: K,
112        member: M,
113        increment: f64,
114    ) -> ValkeyResult<Option<f64>> {
115        let mut cmd = Cmd::new();
116        cmd.arg("ZADD")
117            .arg(key)
118            .arg("INCR")
119            .arg(increment)
120            .arg(member);
121        Option::<f64>::from_owned_valkey_value(self.execute_command(cmd, None).await?)
122    }
123
124    /// Get the rank of `member` with its score, low to high (`ZRANK ... WITHSCORE`).
125    async fn zrank_withscore<K: ToValkeyArgs + Send, M: ToValkeyArgs + Send>(
126        &self,
127        key: K,
128        member: M,
129    ) -> ValkeyResult<Option<(i64, f64)>> {
130        let mut cmd = Cmd::new();
131        cmd.arg("ZRANK").arg(key).arg(member).arg("WITHSCORE");
132        parse_rank_withscore(self.execute_command(cmd, None).await?)
133    }
134
135    /// Get the rank of `member` with its score, high to low
136    /// (`ZREVRANK ... WITHSCORE`).
137    async fn zrevrank_withscore<K: ToValkeyArgs + Send, M: ToValkeyArgs + Send>(
138        &self,
139        key: K,
140        member: M,
141    ) -> ValkeyResult<Option<(i64, f64)>> {
142        let mut cmd = Cmd::new();
143        cmd.arg("ZREVRANK").arg(key).arg(member).arg("WITHSCORE");
144        parse_rank_withscore(self.execute_command(cmd, None).await?)
145    }
146
147    #[doc(hidden)]
148    async fn bzpop<K: ToValkeyArgs + Send + Sync>(
149        &self,
150        op: &'static str,
151        keys: &[K],
152        timeout: f64,
153    ) -> ValkeyResult<Option<(Bytes, Bytes, f64)>> {
154        let mut cmd = Cmd::new();
155        cmd.arg(op);
156        for k in keys {
157            cmd.arg(k);
158        }
159        cmd.arg(timeout);
160        match self.execute_command(cmd, None).await? {
161            ValkeyValue::Nil => Ok(None),
162            ValkeyValue::Array(mut items) if items.len() == 3 => {
163                let score = f64::from_owned_valkey_value(items.pop().unwrap())?;
164                let member = Bytes::from_owned_valkey_value(items.pop().unwrap())?;
165                let key = Bytes::from_owned_valkey_value(items.pop().unwrap())?;
166                Ok(Some((key, member, score)))
167            }
168            other => Err(to_glide_error(other, "Unexpected blocking zpop reply.")),
169        }
170    }
171
172    /// Store a range of a sorted set into `destination` (`ZRANGESTORE` by index).
173    /// Returns the number of elements stored.
174    async fn zrangestore_by_index<D: ToValkeyArgs + Send, S: ToValkeyArgs + Send>(
175        &self,
176        destination: D,
177        source: S,
178        start: i64,
179        stop: i64,
180        rev: bool,
181    ) -> ValkeyResult<i64> {
182        let mut cmd = Cmd::new();
183        cmd.arg("ZRANGESTORE")
184            .arg(destination)
185            .arg(source)
186            .arg(start)
187            .arg(stop);
188        if rev {
189            cmd.arg("REV");
190        }
191        i64::from_owned_valkey_value(self.execute_command(cmd, None).await?)
192    }
193
194    /// Store into `destination` the members of the sorted set `source` whose
195    /// scores are within `[min, max]` (`ZRANGESTORE ... BYSCORE`), returning the
196    /// number of elements stored.
197    ///
198    /// When `rev` is `true` the range is interpreted in reverse (highest scores
199    /// first); the bounds are emitted in the order the server requires for `REV`.
200    /// `limit` applies an optional `LIMIT offset count`.
201    async fn zrangestore_by_score<D: ToValkeyArgs + Send, S: ToValkeyArgs + Send>(
202        &self,
203        destination: D,
204        source: S,
205        min: ScoreBound,
206        max: ScoreBound,
207        rev: bool,
208        limit: Option<Limit>,
209    ) -> ValkeyResult<i64> {
210        // For a reverse range the server expects the high bound first.
211        let (first, second) = if rev { (max, min) } else { (min, max) };
212        let mut cmd = Cmd::new();
213        cmd.arg("ZRANGESTORE")
214            .arg(destination)
215            .arg(source)
216            .arg(first)
217            .arg(second)
218            .arg("BYSCORE");
219        if rev {
220            cmd.arg("REV");
221        }
222        if let Some(limit) = limit {
223            cmd.arg("LIMIT").arg(limit.offset).arg(limit.count);
224        }
225        i64::from_owned_valkey_value(self.execute_command(cmd, None).await?)
226    }
227
228    /// Store into `destination` the members of the sorted set `source` whose
229    /// lexicographical values are within `[min, max]`
230    /// (`ZRANGESTORE ... BYLEX`), returning the number of elements stored.
231    ///
232    /// When `rev` is `true` the range is interpreted in reverse; the bounds are
233    /// emitted in the order the server requires for `REV`. `limit` applies an
234    /// optional `LIMIT offset count`.
235    async fn zrangestore_by_lex<D: ToValkeyArgs + Send, S: ToValkeyArgs + Send>(
236        &self,
237        destination: D,
238        source: S,
239        min: &LexBound,
240        max: &LexBound,
241        rev: bool,
242        limit: Option<Limit>,
243    ) -> ValkeyResult<i64> {
244        let (first, second) = if rev { (max, min) } else { (min, max) };
245        let mut cmd = Cmd::new();
246        cmd.arg("ZRANGESTORE")
247            .arg(destination)
248            .arg(source)
249            .arg(first)
250            .arg(second)
251            .arg("BYLEX");
252        if rev {
253            cmd.arg("REV");
254        }
255        if let Some(limit) = limit {
256            cmd.arg("LIMIT").arg(limit.offset).arg(limit.count);
257        }
258        i64::from_owned_valkey_value(self.execute_command(cmd, None).await?)
259    }
260
261    /// Compute the difference of the given sorted sets (`ZDIFF`).
262    async fn zdiff<K: ToValkeyArgs + Send + Sync>(&self, keys: &[K]) -> ValkeyResult<Vec<Bytes>> {
263        let mut cmd = Cmd::new();
264        cmd.arg("ZDIFF").arg(keys.len());
265        for k in keys {
266            cmd.arg(k);
267        }
268        collect_bytes(self.execute_command(cmd, None).await?)
269    }
270
271    /// Compute the difference of the given sorted sets with scores
272    /// (`ZDIFF ... WITHSCORES`).
273    async fn zdiff_withscores<K: ToValkeyArgs + Send + Sync>(
274        &self,
275        keys: &[K],
276    ) -> ValkeyResult<Vec<(Bytes, f64)>> {
277        let mut cmd = Cmd::new();
278        cmd.arg("ZDIFF").arg(keys.len());
279        for k in keys {
280            cmd.arg(k);
281        }
282        cmd.arg("WITHSCORES");
283        collect_member_scores(self.execute_command(cmd, None).await?)
284    }
285
286    /// Store the difference of the given sorted sets into `destination`
287    /// (`ZDIFFSTORE`).
288    async fn zdiffstore<D: ToValkeyArgs + Send, K: ToValkeyArgs + Send + Sync>(
289        &self,
290        destination: D,
291        keys: &[K],
292    ) -> ValkeyResult<i64> {
293        let mut cmd = Cmd::new();
294        cmd.arg("ZDIFFSTORE").arg(destination).arg(keys.len());
295        for k in keys {
296            cmd.arg(k);
297        }
298        i64::from_owned_valkey_value(self.execute_command(cmd, None).await?)
299    }
300
301    /// Compute the union of the given sorted sets (`ZUNION`).
302    async fn zunion<K: ToValkeyArgs + Send + Sync>(
303        &self,
304        keys: &[K],
305        aggregate: Option<AggregationType>,
306    ) -> ValkeyResult<Vec<Bytes>> {
307        let mut cmd = Cmd::new();
308        cmd.arg("ZUNION").arg(keys.len());
309        for k in keys {
310            cmd.arg(k);
311        }
312        if let Some(agg) = aggregate {
313            cmd.arg("AGGREGATE").arg(agg.as_arg());
314        }
315        collect_bytes(self.execute_command(cmd, None).await?)
316    }
317
318    /// Compute the union of the given sorted sets with scores
319    /// (`ZUNION ... WITHSCORES`).
320    async fn zunion_withscores<K: ToValkeyArgs + Send + Sync>(
321        &self,
322        keys: &[K],
323        aggregate: Option<AggregationType>,
324    ) -> ValkeyResult<Vec<(Bytes, f64)>> {
325        let mut cmd = Cmd::new();
326        cmd.arg("ZUNION").arg(keys.len());
327        for k in keys {
328            cmd.arg(k);
329        }
330        if let Some(agg) = aggregate {
331            cmd.arg("AGGREGATE").arg(agg.as_arg());
332        }
333        cmd.arg("WITHSCORES");
334        collect_member_scores(self.execute_command(cmd, None).await?)
335    }
336
337    /// Compute the intersection of the given sorted sets (`ZINTER`).
338    async fn zinter<K: ToValkeyArgs + Send + Sync>(
339        &self,
340        keys: &[K],
341        aggregate: Option<AggregationType>,
342    ) -> ValkeyResult<Vec<Bytes>> {
343        let mut cmd = Cmd::new();
344        cmd.arg("ZINTER").arg(keys.len());
345        for k in keys {
346            cmd.arg(k);
347        }
348        if let Some(agg) = aggregate {
349            cmd.arg("AGGREGATE").arg(agg.as_arg());
350        }
351        collect_bytes(self.execute_command(cmd, None).await?)
352    }
353
354    /// Compute the intersection of the given sorted sets with scores
355    /// (`ZINTER ... WITHSCORES`).
356    async fn zinter_withscores<K: ToValkeyArgs + Send + Sync>(
357        &self,
358        keys: &[K],
359        aggregate: Option<AggregationType>,
360    ) -> ValkeyResult<Vec<(Bytes, f64)>> {
361        let mut cmd = Cmd::new();
362        cmd.arg("ZINTER").arg(keys.len());
363        for k in keys {
364            cmd.arg(k);
365        }
366        if let Some(agg) = aggregate {
367            cmd.arg("AGGREGATE").arg(agg.as_arg());
368        }
369        cmd.arg("WITHSCORES");
370        collect_member_scores(self.execute_command(cmd, None).await?)
371    }
372
373    /// Cardinality of the intersection of the given sorted sets (`ZINTERCARD`),
374    /// with an optional `LIMIT`.
375    async fn zintercard<K: ToValkeyArgs + Send + Sync>(
376        &self,
377        keys: &[K],
378        limit: Option<i64>,
379    ) -> ValkeyResult<i64> {
380        let mut cmd = Cmd::new();
381        cmd.arg("ZINTERCARD").arg(keys.len());
382        for k in keys {
383            cmd.arg(k);
384        }
385        if let Some(l) = limit {
386            cmd.arg("LIMIT").arg(l);
387        }
388        i64::from_owned_valkey_value(self.execute_command(cmd, None).await?)
389    }
390
391    #[doc(hidden)]
392    async fn zsetop_store<D: ToValkeyArgs + Send, K: ToValkeyArgs + Send + Sync>(
393        &self,
394        op: &'static str,
395        destination: D,
396        keys: &[K],
397        aggregate: Option<AggregationType>,
398    ) -> ValkeyResult<i64> {
399        let mut cmd = Cmd::new();
400        cmd.arg(op).arg(destination).arg(keys.len());
401        for k in keys {
402            cmd.arg(k);
403        }
404        if let Some(agg) = aggregate {
405            cmd.arg("AGGREGATE").arg(agg.as_arg());
406        }
407        i64::from_owned_valkey_value(self.execute_command(cmd, None).await?)
408    }
409}
410
411fn collect_bytes(v: ValkeyValue) -> ValkeyResult<Vec<Bytes>> {
412    match v {
413        ValkeyValue::Array(items) => items
414            .into_iter()
415            .map(Bytes::from_owned_valkey_value)
416            .collect(),
417        ValkeyValue::Nil => Ok(Vec::new()),
418        other => Ok(vec![Bytes::from_owned_valkey_value(other)?]),
419    }
420}
421
422/// Parse a `WITHSCORES`/`ZPOPMIN`-style reply into `(member, score)` pairs,
423/// handling both RESP2 flat arrays and RESP3 nested pairs.
424fn collect_member_scores(v: ValkeyValue) -> ValkeyResult<Vec<(Bytes, f64)>> {
425    match v {
426        ValkeyValue::Nil => Ok(Vec::new()),
427        // RESP3 returns a map of member -> score.
428        ValkeyValue::Map(pairs) => pairs
429            .into_iter()
430            .map(|(m, s)| {
431                Ok((
432                    Bytes::from_owned_valkey_value(m)?,
433                    f64::from_owned_valkey_value(s)?,
434                ))
435            })
436            .collect(),
437        ValkeyValue::Array(items) => {
438            // RESP3: array of [member, score] pairs.
439            if items
440                .iter()
441                .all(|it| matches!(it, ValkeyValue::Array(inner) if inner.len() == 2))
442            {
443                let mut out = Vec::with_capacity(items.len());
444                for it in items {
445                    if let ValkeyValue::Array(mut pair) = it {
446                        let score = f64::from_owned_valkey_value(pair.pop().unwrap())?;
447                        let member = Bytes::from_owned_valkey_value(pair.pop().unwrap())?;
448                        out.push((member, score));
449                    }
450                }
451                Ok(out)
452            } else {
453                // RESP2: flat [member, score, member, score, ...].
454                let mut out = Vec::with_capacity(items.len() / 2);
455                let mut iter = items.into_iter();
456                while let (Some(m), Some(s)) = (iter.next(), iter.next()) {
457                    out.push((
458                        Bytes::from_owned_valkey_value(m)?,
459                        f64::from_owned_valkey_value(s)?,
460                    ));
461                }
462                Ok(out)
463            }
464        }
465        other => Err(to_glide_error(other, "Unexpected sorted-set reply.")),
466    }
467}
468
469impl<T: CommandExecutor + ?Sized> SortedSetCommands for T {}
470
471/// Parse a `ZRANK ... WITHSCORE` reply (`[rank, score]` or nil).
472fn parse_rank_withscore(v: ValkeyValue) -> ValkeyResult<Option<(i64, f64)>> {
473    match v {
474        ValkeyValue::Nil => Ok(None),
475        ValkeyValue::Array(mut items) if items.len() == 2 => {
476            let score = f64::from_owned_valkey_value(items.pop().unwrap())?;
477            let rank = i64::from_owned_valkey_value(items.pop().unwrap())?;
478            Ok(Some((rank, score)))
479        }
480        other => Err(to_glide_error(other, "Unexpected ZRANK WITHSCORE reply.")),
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487    use crate::test_utils::assert_args;
488
489    #[test]
490    fn score_bound_args() {
491        assert_args(ScoreBound::NegativeInfinity, &["-inf"]);
492        assert_args(ScoreBound::PositiveInfinity, &["+inf"]);
493        assert_args(ScoreBound::Inclusive(1.5), &["1.5"]);
494        assert_args(ScoreBound::Exclusive(1.0), &["(1"]);
495    }
496
497    #[test]
498    fn lex_bound_args() {
499        assert_args(LexBound::NegativeInfinity, &["-"]);
500        assert_args(LexBound::PositiveInfinity, &["+"]);
501        assert_args(LexBound::Inclusive(b"a".to_vec()), &["[a"]);
502        assert_args(LexBound::Exclusive(b"b".to_vec()), &["(b"]);
503        // Binary values are kept byte-for-byte after the prefix.
504        assert_args(
505            LexBound::Inclusive(vec![0xFF, 0x00]),
506            &[[b'[', 0xFF, 0x00].as_slice()],
507        );
508    }
509}