Skip to main content

kevy_client/
zalgebra.rs

1//! Sorted-set algebra: `ZINTERSTORE` / `ZUNIONSTORE` / `ZINTERCARD`.
2//!
3//! The store forms take the full server option face — `WEIGHTS` and
4//! `AGGREGATE SUM|MIN|MAX` ([`ZAggregate`], re-exported from
5//! kevy-embedded) — via the `_with` variants; the plain forms are the
6//! common unweighted-SUM shorthand.
7
8use crate::{KevyError, KevyResult};
9
10use kevy_embedded::ZAggregate;
11use kevy_resp::Reply;
12use kevy_resp_client::RespClient;
13
14use crate::{Connection, string, unexpected};
15
16impl Connection {
17    /// `ZINTERSTORE dest numkeys key…` — store the intersection of the
18    /// given sorted sets into `dest` (unweighted, `AGGREGATE SUM`).
19    /// Returns the destination's cardinality.
20    pub fn zinterstore(&mut self, dest: &[u8], keys: &[&[u8]]) -> KevyResult<usize> {
21        self.zinterstore_with(dest, keys, None, ZAggregate::Sum)
22    }
23
24    /// `ZINTERSTORE dest numkeys key… [WEIGHTS w…] [AGGREGATE SUM|MIN|MAX]`
25    /// — full option face. `weights`, when given, must have one entry
26    /// per key (the backend rejects a mismatch).
27    pub fn zinterstore_with(
28        &mut self,
29        dest: &[u8],
30        keys: &[&[u8]],
31        weights: Option<&[f64]>,
32        aggregate: ZAggregate,
33    ) -> KevyResult<usize> {
34        check_keys(keys)?;
35        match self {
36            Self::Embedded(s) => s.zinterstore(dest, keys, weights, aggregate),
37            Self::Remote(c) => zstore_request(c, b"ZINTERSTORE", dest, keys, weights, aggregate),
38        }
39    }
40
41    /// `ZUNIONSTORE dest numkeys key…` — store the union of the given
42    /// sorted sets into `dest` (unweighted, `AGGREGATE SUM`). Returns
43    /// the destination's cardinality.
44    pub fn zunionstore(&mut self, dest: &[u8], keys: &[&[u8]]) -> KevyResult<usize> {
45        self.zunionstore_with(dest, keys, None, ZAggregate::Sum)
46    }
47
48    /// `ZUNIONSTORE dest numkeys key… [WEIGHTS w…] [AGGREGATE SUM|MIN|MAX]`
49    /// — full option face, as in [`Self::zinterstore_with`].
50    pub fn zunionstore_with(
51        &mut self,
52        dest: &[u8],
53        keys: &[&[u8]],
54        weights: Option<&[f64]>,
55        aggregate: ZAggregate,
56    ) -> KevyResult<usize> {
57        check_keys(keys)?;
58        match self {
59            Self::Embedded(s) => s.zunionstore(dest, keys, weights, aggregate),
60            Self::Remote(c) => zstore_request(c, b"ZUNIONSTORE", dest, keys, weights, aggregate),
61        }
62    }
63
64    /// `ZINTERCARD numkeys key… [LIMIT n]` — cardinality of the
65    /// intersection without materialising it. `limit = None` counts
66    /// everything; `Some(n)` short-circuits at `n`.
67    pub fn zintercard(&mut self, keys: &[&[u8]], limit: Option<usize>) -> KevyResult<usize> {
68        check_keys(keys)?;
69        match self {
70            Self::Embedded(s) => s.zintercard(keys, limit.unwrap_or(0)),
71            Self::Remote(c) => {
72                let mut args = Vec::with_capacity(keys.len() + 4);
73                args.push(b"ZINTERCARD".to_vec());
74                args.push(keys.len().to_string().into_bytes());
75                args.extend(keys.iter().map(|k| k.to_vec()));
76                if let Some(n) = limit {
77                    args.push(b"LIMIT".to_vec());
78                    args.push(n.to_string().into_bytes());
79                }
80                int_reply(c.request(&args)?)
81            }
82        }
83    }
84}
85
86/// The verbs require `numkeys ≥ 1` — surface that before the wire.
87fn check_keys(keys: &[&[u8]]) -> KevyResult<()> {
88    if keys.is_empty() {
89        return Err(KevyError::InvalidInput("zset algebra needs at least one source key".into()));
90    }
91    Ok(())
92}
93
94fn aggregate_tag(aggregate: ZAggregate) -> &'static [u8] {
95    match aggregate {
96        ZAggregate::Sum => b"SUM",
97        ZAggregate::Min => b"MIN",
98        ZAggregate::Max => b"MAX",
99    }
100}
101
102/// Build `verb dest numkeys key… [WEIGHTS w…] [AGGREGATE tag]` and
103/// parse the cardinality reply. `AGGREGATE` is emitted only for the
104/// non-default modes, keeping the wire identical to the plain forms
105/// when nothing was customised.
106fn zstore_request(
107    c: &mut RespClient,
108    verb: &[u8],
109    dest: &[u8],
110    keys: &[&[u8]],
111    weights: Option<&[f64]>,
112    aggregate: ZAggregate,
113) -> KevyResult<usize> {
114    let mut args = Vec::with_capacity(keys.len() * 2 + 6);
115    args.push(verb.to_vec());
116    args.push(dest.to_vec());
117    args.push(keys.len().to_string().into_bytes());
118    args.extend(keys.iter().map(|k| k.to_vec()));
119    if let Some(ws) = weights {
120        args.push(b"WEIGHTS".to_vec());
121        args.extend(ws.iter().map(|w| format!("{w}").into_bytes()));
122    }
123    if aggregate != ZAggregate::Sum {
124        args.push(b"AGGREGATE".to_vec());
125        args.push(aggregate_tag(aggregate).to_vec());
126    }
127    int_reply(c.request(&args)?)
128}
129
130fn int_reply(reply: Reply) -> KevyResult<usize> {
131    match reply {
132        Reply::Int(n) if n >= 0 => Ok(n as usize),
133        Reply::Error(e) => Err(KevyError::Protocol(string(e))),
134        other => Err(unexpected(other)),
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    fn seed(c: &mut Connection) {
143        c.zadd(b"za", &[(1.0, b"x".as_ref()), (2.0, b"y".as_ref())])
144            .unwrap();
145        c.zadd(b"zb", &[(10.0, b"y".as_ref()), (20.0, b"z".as_ref())])
146            .unwrap();
147    }
148
149    #[test]
150    fn embedded_zinterstore_and_zunionstore() {
151        let mut c = Connection::connect("mem://").unwrap();
152        seed(&mut c);
153        assert_eq!(c.zinterstore(b"zi", &[&b"za"[..], &b"zb"[..]]).unwrap(), 1);
154        assert_eq!(c.zscore(b"zi", b"y").unwrap(), Some(12.0)); // SUM
155        assert_eq!(c.zunionstore(b"zu", &[&b"za"[..], &b"zb"[..]]).unwrap(), 3);
156    }
157
158    #[test]
159    fn embedded_zstore_with_weights_and_aggregate() {
160        let mut c = Connection::connect("mem://").unwrap();
161        seed(&mut c);
162        let n = c
163            .zunionstore_with(
164                b"zw",
165                &[&b"za"[..], &b"zb"[..]],
166                Some(&[2.0, 1.0]),
167                ZAggregate::Max,
168            )
169            .unwrap();
170        assert_eq!(n, 3);
171        // y: max(2*2, 10*1) = 10
172        assert_eq!(c.zscore(b"zw", b"y").unwrap(), Some(10.0));
173    }
174
175    #[test]
176    fn embedded_zintercard_with_and_without_limit() {
177        let mut c = Connection::connect("mem://").unwrap();
178        c.zadd(b"za", &[(1.0, b"a".as_ref()), (2.0, b"b".as_ref()), (3.0, b"c".as_ref())])
179            .unwrap();
180        c.zadd(b"zb", &[(1.0, b"a".as_ref()), (2.0, b"b".as_ref()), (9.0, b"q".as_ref())])
181            .unwrap();
182        assert_eq!(c.zintercard(&[&b"za"[..], &b"zb"[..]], None).unwrap(), 2);
183        assert_eq!(c.zintercard(&[&b"za"[..], &b"zb"[..]], Some(1)).unwrap(), 1);
184    }
185
186    #[test]
187    fn empty_keys_rejected() {
188        let mut c = Connection::connect("mem://").unwrap();
189        let err = c.zintercard(&[], None).unwrap_err();
190        assert!(matches!(err, KevyError::InvalidInput(_)));
191    }
192}