Skip to main content

kevy_client/
scan.rs

1//! Key-space iteration: `KEYS`, `SCAN`, `RANDOMKEY`.
2//!
3//! Embedded backend uses `kevy_embedded::Store::with(|inner| ...)` to
4//! reach `kevy_store::Store::collect_keys`. `SCAN` on embed is a single
5//! shot — cursor always advances 0 → 0 (i.e., all keys returned at once).
6//! Use `KEYS` directly if you want the same data; `SCAN` exists so the
7//! same code can also run against a Redis server, where iteration matters.
8
9use crate::{KevyError, KevyResult};
10
11use kevy_resp::Reply;
12
13use crate::{Connection, array_to_bulks, string, unexpected};
14
15impl Connection {
16    /// `KEYS pattern` — every key matching `pattern` (glob: `*`, `?`,
17    /// `[abc]`). Use sparingly: O(N) over the whole keyspace.
18    pub fn keys(&mut self, pattern: &[u8]) -> KevyResult<Vec<Vec<u8>>> {
19        match self {
20            Self::Embedded(s) => Ok(s.with(|inner| inner.collect_keys(Some(pattern), None))),
21            Self::Remote(c) => match c.request(&[b"KEYS".to_vec(), pattern.to_vec()])? {
22                Reply::Array(items) => array_to_bulks(items),
23                Reply::Error(e) => Err(KevyError::Protocol(string(e))),
24                other => Err(unexpected(other)),
25            },
26        }
27    }
28
29    /// `SCAN cursor [MATCH pattern] [COUNT n]`. Returns `(next_cursor,
30    /// batch)`; iterate by re-calling with the returned cursor until
31    /// `next_cursor == 0`.
32    ///
33    /// On the embedded backend the iteration always finishes in one
34    /// call — `next_cursor` is `0` and `batch` holds every matching key.
35    /// `count` is taken as a hint only.
36    pub fn scan(
37        &mut self,
38        cursor: u64,
39        pattern: Option<&[u8]>,
40        count: Option<usize>,
41    ) -> KevyResult<(u64, Vec<Vec<u8>>)> {
42        match self {
43            Self::Embedded(s) => {
44                // Embed has no real cursor: any non-zero cursor means the caller
45                // already drained on a previous call.
46                if cursor != 0 {
47                    return Ok((0, Vec::new()));
48                }
49                let batch = s.with(|inner| inner.collect_keys(pattern, count));
50                Ok((0, batch))
51            }
52            Self::Remote(c) => {
53                let mut args: Vec<Vec<u8>> =
54                    vec![b"SCAN".to_vec(), cursor.to_string().into_bytes()];
55                if let Some(pat) = pattern {
56                    args.push(b"MATCH".to_vec());
57                    args.push(pat.to_vec());
58                }
59                if let Some(n) = count {
60                    args.push(b"COUNT".to_vec());
61                    args.push(n.to_string().into_bytes());
62                }
63                parse_scan_reply(c.request(&args)?)
64            }
65        }
66    }
67
68    /// `RANDOMKEY` — sample one key, or `None` if the keyspace is empty.
69    ///
70    /// Embed returns the lexicographically-first key (deterministic, no
71    /// RNG); the server returns a truly random key. Both honour empty.
72    pub fn randomkey(&mut self) -> KevyResult<Option<Vec<u8>>> {
73        match self {
74            Self::Embedded(s) => Ok(s.with(|inner| {
75                inner.collect_keys(None, Some(1)).into_iter().next()
76            })),
77            Self::Remote(c) => match c.request(&[b"RANDOMKEY".to_vec()])? {
78                Reply::Bulk(v) => Ok(Some(v)),
79                Reply::Nil => Ok(None),
80                Reply::Error(e) => Err(KevyError::Protocol(string(e))),
81                other => Err(unexpected(other)),
82            },
83        }
84    }
85}
86
87/// Unwrap the remote `SCAN` reply — a two-element array of
88/// `[next-cursor bulk, keys array]` — into `(next_cursor, keys)`.
89fn parse_scan_reply(reply: Reply) -> KevyResult<(u64, Vec<Vec<u8>>)> {
90    match reply {
91        Reply::Array(items) if items.len() == 2 => {
92            let mut it = items.into_iter();
93            let cursor_bulk = it.next().unwrap();
94            let keys_arr = it.next().unwrap();
95            let next_cursor = match cursor_bulk {
96                Reply::Bulk(b) => std::str::from_utf8(&b)
97                    .map_err(|_| KevyError::Protocol("non-utf8 SCAN cursor".into()))?
98                    .parse()
99                    .map_err(|_| KevyError::Protocol("bad SCAN cursor".into()))?,
100                other => return Err(unexpected(other)),
101            };
102            let keys = match keys_arr {
103                Reply::Array(items) => array_to_bulks(items)?,
104                other => return Err(unexpected(other)),
105            };
106            Ok((next_cursor, keys))
107        }
108        Reply::Error(e) => Err(KevyError::Protocol(string(e))),
109        other => Err(unexpected(other)),
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn embedded_keys_matches_glob() {
119        let mut c = Connection::connect("mem://").unwrap();
120        c.set(b"user:1", b"a").unwrap();
121        c.set(b"user:2", b"b").unwrap();
122        c.set(b"other", b"c").unwrap();
123        let mut keys = c.keys(b"user:*").unwrap();
124        keys.sort();
125        assert_eq!(keys, vec![b"user:1".to_vec(), b"user:2".to_vec()]);
126    }
127
128    #[test]
129    fn embedded_scan_returns_all_in_one_round() {
130        let mut c = Connection::connect("mem://").unwrap();
131        for i in 0..5 {
132            c.set(format!("k{i}").as_bytes(), b"v").unwrap();
133        }
134        let (next, batch) = c.scan(0, None, None).unwrap();
135        assert_eq!(next, 0);
136        assert_eq!(batch.len(), 5);
137        // Any non-zero cursor means "we already finished" on embed.
138        let (next2, batch2) = c.scan(123, None, None).unwrap();
139        assert_eq!(next2, 0);
140        assert!(batch2.is_empty());
141    }
142
143    #[test]
144    fn embedded_randomkey_empty_and_present() {
145        let mut c = Connection::connect("mem://").unwrap();
146        assert!(c.randomkey().unwrap().is_none());
147        c.set(b"only", b"x").unwrap();
148        assert_eq!(c.randomkey().unwrap(), Some(b"only".to_vec()));
149    }
150}