Skip to main content

kevy_client/
zalgebra.rs

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