Skip to main content

glide/commands/
scan.rs

1// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2//! GLIDE's cursor-driven scan iterators, returned by the `scan*` methods of
3//! [`crate::AsyncCommands`] / `glide::Commands`.
4//!
5//! These ride the unified API's owned-send dispatch (each page is one command
6//! built fresh and handed to glide-core **by value**), so scanning never
7//! touches the `redis` connection-object machinery. Call-site shape matches
8//! the familiar redis-rs iterators:
9//!
10//! ```rust,no_run
11//! # use glide::AsyncCommands;
12//! # async fn demo(client: glide::GlideClient) -> glide::ValkeyResult<()> {
13//! let mut iter = client.scan_match::<_, String>("prefix:*").await?;
14//! while let Some(key) = iter.next_item().await {
15//!     println!("{}", key?);
16//! }
17//! # Ok(()) }
18//! ```
19//!
20//! The first page is fetched eagerly, so errors there surface at the `scan*`
21//! call. A failure while fetching a later page surfaces as an error from
22//! `next_item`, after which the iteration ends.
23
24use crate::ValkeyResult;
25use crate::cmd::Cmd;
26use crate::commands::core::CommandDispatch;
27use crate::value::FromValkeyValue;
28
29#[cfg(feature = "sync")]
30use crate::commands::core::SyncCommandDispatch;
31
32/// Argument layout of one scan page: `prefix… <cursor> suffix…`
33/// (e.g. `HSCAN key <cursor> MATCH pattern`).
34#[derive(Debug)]
35struct PageSpec {
36    prefix: Vec<Vec<u8>>,
37    suffix: Vec<Vec<u8>>,
38}
39
40impl PageSpec {
41    fn to_cmd(&self, cursor: u64) -> Cmd {
42        let mut cmd = Cmd::new();
43        for a in &self.prefix {
44            cmd.arg(&a[..]);
45        }
46        cmd.arg(cursor);
47        for a in &self.suffix {
48            cmd.arg(&a[..]);
49        }
50        cmd
51    }
52}
53
54/// An in-progress async scan iteration.
55///
56/// ```rust,no_run
57/// # use glide::AsyncCommands;
58/// # async fn demo(client: glide::GlideClient) -> glide::ValkeyResult<()> {
59/// let mut iter = client.scan_match::<_, String>("prefix:*").await?;
60/// while let Some(key) = iter.next_item().await {
61///     println!("{}", key?);
62/// }
63/// # Ok(()) }
64/// ```
65pub struct ScanIter<'a, C: ?Sized, RV> {
66    con: &'a C,
67    spec: PageSpec,
68    cursor: u64,
69    batch: std::vec::IntoIter<RV>,
70}
71
72impl<'a, C: CommandDispatch, RV: FromValkeyValue> ScanIter<'a, C, RV> {
73    /// Returns an iterator for the scan, or
74    /// an error if fetching the first page fails.
75    pub(crate) async fn new(
76        con: &'a C,
77        prefix: Vec<Vec<u8>>,
78        suffix: Vec<Vec<u8>>,
79    ) -> ValkeyResult<ScanIter<'a, C, RV>> {
80        let spec = PageSpec { prefix, suffix };
81
82        // Fetch first page immediately.
83        let (cursor, batch): (u64, Vec<RV>) = FromValkeyValue::from_owned_valkey_value(
84            con.glide_dispatch_command(spec.to_cmd(0)).await?,
85        )?;
86        Ok(ScanIter {
87            con,
88            spec,
89            cursor,
90            batch: batch.into_iter(),
91        })
92    }
93
94    /// The next element, an error if fetching a page fails, or `None` if the
95    /// scan completed. An error ends the iteration and subsequent calls return
96    /// `None`.
97    pub async fn next_item(&mut self) -> Option<ValkeyResult<RV>> {
98        // Page may be empty, so keep fetching until an
99        // item is produced or the cursor wraps to 0.
100        loop {
101            if let Some(v) = self.batch.next() {
102                return Some(Ok(v));
103            }
104            if self.cursor == 0 {
105                return None;
106            }
107            match self.fetch_page().await {
108                Ok(()) => {}
109                Err(e) => {
110                    // Never retry a failed page.
111                    self.cursor = 0;
112                    return Some(Err(e));
113                }
114            }
115        }
116    }
117
118    /// Fetch the page at the current cursor.
119    async fn fetch_page(&mut self) -> ValkeyResult<()> {
120        let reply = self
121            .con
122            .glide_dispatch_command(self.spec.to_cmd(self.cursor))
123            .await?;
124        let (cursor, batch): (u64, Vec<RV>) = FromValkeyValue::from_owned_valkey_value(reply)?;
125        self.cursor = cursor;
126        self.batch = batch.into_iter();
127        Ok(())
128    }
129}
130
131/// An in-progress blocking scan iteration.
132///
133/// ```rust,no_run
134/// # use glide::Commands;
135/// # use glide::sync::SyncGlideClient;
136/// # fn demo(client: SyncGlideClient) -> glide::ValkeyResult<()> {
137/// for key in client.scan_match::<_, String>("prefix:*")? {
138///     println!("{}", key?);
139/// }
140/// # Ok(()) }
141/// ```
142#[cfg(feature = "sync")]
143pub struct SyncScanIter<'a, C: ?Sized, RV> {
144    con: &'a C,
145    spec: PageSpec,
146    cursor: u64,
147    batch: std::vec::IntoIter<RV>,
148}
149
150#[cfg(feature = "sync")]
151impl<'a, C: SyncCommandDispatch, RV: FromValkeyValue> SyncScanIter<'a, C, RV> {
152    /// Returns an iterator for the scan, or
153    /// an error if fetching the first page fails.
154    pub(crate) fn new(
155        con: &'a C,
156        prefix: Vec<Vec<u8>>,
157        suffix: Vec<Vec<u8>>,
158    ) -> ValkeyResult<SyncScanIter<'a, C, RV>> {
159        let spec = PageSpec { prefix, suffix };
160
161        // Fetch first page immediately.
162        let (cursor, batch): (u64, Vec<RV>) =
163            FromValkeyValue::from_owned_valkey_value(con.glide_dispatch_command(spec.to_cmd(0))?)?;
164        Ok(SyncScanIter {
165            con,
166            spec,
167            cursor,
168            batch: batch.into_iter(),
169        })
170    }
171
172    /// Fetch the page at the current cursor.
173    fn fetch_page(&mut self) -> ValkeyResult<()> {
174        let reply = self
175            .con
176            .glide_dispatch_command(self.spec.to_cmd(self.cursor))?;
177        let (cursor, batch): (u64, Vec<RV>) = FromValkeyValue::from_owned_valkey_value(reply)?;
178        self.cursor = cursor;
179        self.batch = batch.into_iter();
180        Ok(())
181    }
182}
183
184#[cfg(feature = "sync")]
185impl<C: SyncCommandDispatch, RV: FromValkeyValue> Iterator for SyncScanIter<'_, C, RV> {
186    type Item = ValkeyResult<RV>;
187
188    /// The next element, an error if fetching a page fails, or `None` if the
189    /// scan completed. An error ends the iteration and subsequent calls return
190    /// `None`.
191    fn next(&mut self) -> Option<ValkeyResult<RV>> {
192        // Page may be empty, so keep fetching until an
193        // item is produced or the cursor wraps to 0.
194        loop {
195            if let Some(v) = self.batch.next() {
196                return Some(Ok(v));
197            }
198            if self.cursor == 0 {
199                return None;
200            }
201            match self.fetch_page() {
202                Ok(()) => {}
203                Err(e) => {
204                    // Never retry a failed page.
205                    self.cursor = 0;
206                    return Some(Err(e));
207                }
208            }
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::error::GlideError;
217    use crate::value::ValkeyValue;
218    use crate::{ValkeyFuture, ValkeyResult};
219    use bytes::Bytes;
220    use std::collections::VecDeque;
221    use std::sync::Mutex;
222
223    /// A mock connection that returns queued replies in order.
224    struct MockConnection {
225        replies: Mutex<VecDeque<ValkeyResult<ValkeyValue>>>,
226    }
227
228    impl MockConnection {
229        fn new(replies: Vec<ValkeyResult<ValkeyValue>>) -> Self {
230            MockConnection {
231                replies: Mutex::new(replies.into()),
232            }
233        }
234
235        /// Pops and returns a queued reply.
236        /// Panics if there are none remaining.
237        fn pop(&self) -> ValkeyResult<ValkeyValue> {
238            self.replies
239                .lock()
240                .unwrap()
241                .pop_front()
242                .expect("scan iterator requested more pages than the test queued")
243        }
244
245        /// The number of queued replies remaining.
246        fn remaining(&self) -> usize {
247            self.replies.lock().unwrap().len()
248        }
249    }
250
251    #[sealed::sealed]
252    impl crate::commands::core::CommandDispatch for MockConnection {
253        fn glide_dispatch_command<'a>(&'a self, _cmd: Cmd) -> ValkeyFuture<'a, ValkeyValue> {
254            Box::pin(async move { self.pop() })
255        }
256    }
257
258    #[cfg(feature = "sync")]
259    #[sealed::sealed]
260    impl crate::commands::core::SyncCommandDispatch for MockConnection {
261        fn glide_dispatch_command(&self, _cmd: Cmd) -> ValkeyResult<ValkeyValue> {
262            self.pop()
263        }
264    }
265
266    /// Creates a scan page reply.
267    fn page(cursor: &str, items: &[&str]) -> ValkeyValue {
268        ValkeyValue::Array(vec![
269            ValkeyValue::BulkString(Bytes::copy_from_slice(cursor.as_bytes())),
270            ValkeyValue::Array(
271                items
272                    .iter()
273                    .map(|s| ValkeyValue::BulkString(Bytes::copy_from_slice(s.as_bytes())))
274                    .collect(),
275            ),
276        ])
277    }
278
279    /// Creates a scan error.
280    fn error() -> GlideError {
281        GlideError::Request("simulated scan failure".into())
282    }
283
284    // ---- async (`ScanIter::next_item`) ------------------------------------------
285
286    /// Creates an async scan iterator over the mock connection.
287    async fn scan_iter(con: &MockConnection) -> ValkeyResult<ScanIter<'_, MockConnection, String>> {
288        ScanIter::new(con, vec![b"SCAN".to_vec()], Vec::new()).await
289    }
290
291    /// Returns all the items from an async scan iterator.
292    async fn get_items(iter: &mut ScanIter<'_, MockConnection, String>) -> Vec<String> {
293        let mut items = Vec::new();
294        while let Some(item) = iter.next_item().await {
295            items.push(item.expect("unexpected error during scan"));
296        }
297        items
298    }
299
300    #[tokio::test]
301    async fn async_success_empty_page() {
302        let con = MockConnection::new(vec![Ok(page("4", &[])), Ok(page("0", &[]))]);
303        let mut iter = scan_iter(&con).await.unwrap();
304        assert_eq!(get_items(&mut iter).await, Vec::<String>::new());
305        assert_eq!(con.remaining(), 0);
306    }
307
308    #[tokio::test]
309    async fn async_success_one_page() {
310        let con = MockConnection::new(vec![Ok(page("0", &["a", "b"]))]);
311        let mut iter = scan_iter(&con).await.unwrap();
312        assert_eq!(get_items(&mut iter).await, ["a", "b"]);
313        assert_eq!(con.remaining(), 0);
314    }
315
316    #[tokio::test]
317    async fn async_success_multi_page() {
318        let con = MockConnection::new(vec![Ok(page("6", &["a", "b"])), Ok(page("0", &["c"]))]);
319        let mut iter = scan_iter(&con).await.unwrap();
320        assert_eq!(get_items(&mut iter).await, ["a", "b", "c"]);
321        assert_eq!(con.remaining(), 0);
322    }
323
324    #[tokio::test]
325    async fn async_error_new() {
326        let con = MockConnection::new(vec![Err(error())]);
327        assert!(scan_iter(&con).await.is_err());
328    }
329
330    #[tokio::test]
331    async fn async_error_scan() {
332        let con = MockConnection::new(vec![Ok(page("5", &["a"])), Err(error())]);
333        let mut iter = scan_iter(&con).await.unwrap();
334        assert_eq!(iter.next_item().await.unwrap().unwrap(), "a");
335        assert!(iter.next_item().await.expect("expected an error").is_err());
336        assert!(iter.next_item().await.is_none());
337    }
338
339    // ---- blocking (`SyncScanIter` / `Iterator`) ---------------------------------
340
341    /// Creates a sync scan iterator over the mock connection.
342    #[cfg(feature = "sync")]
343    fn sync_scan_iter(
344        con: &MockConnection,
345    ) -> ValkeyResult<SyncScanIter<'_, MockConnection, String>> {
346        SyncScanIter::new(con, vec![b"SCAN".to_vec()], Vec::new())
347    }
348
349    /// Returns all the items from a sync scan iterator.
350    #[cfg(feature = "sync")]
351    fn sync_get_items(iter: SyncScanIter<'_, MockConnection, String>) -> Vec<String> {
352        iter.collect::<ValkeyResult<Vec<String>>>()
353            .expect("unexpected error during scan")
354    }
355
356    #[cfg(feature = "sync")]
357    #[test]
358    fn sync_success_empty_page() {
359        let con = MockConnection::new(vec![Ok(page("4", &[])), Ok(page("0", &[]))]);
360        let items = sync_get_items(sync_scan_iter(&con).unwrap());
361        assert_eq!(items, Vec::<String>::new());
362        assert_eq!(con.remaining(), 0);
363    }
364
365    #[cfg(feature = "sync")]
366    #[test]
367    fn sync_success_one_page() {
368        let con = MockConnection::new(vec![Ok(page("0", &["a", "b"]))]);
369        let items = sync_get_items(sync_scan_iter(&con).unwrap());
370        assert_eq!(items, ["a", "b"]);
371        assert_eq!(con.remaining(), 0);
372    }
373
374    #[cfg(feature = "sync")]
375    #[test]
376    fn sync_success_multi_page() {
377        let con = MockConnection::new(vec![Ok(page("6", &["a", "b"])), Ok(page("0", &["c"]))]);
378        let items = sync_get_items(sync_scan_iter(&con).unwrap());
379        assert_eq!(items, ["a", "b", "c"]);
380        assert_eq!(con.remaining(), 0);
381    }
382
383    #[cfg(feature = "sync")]
384    #[test]
385    fn sync_error_new() {
386        let con = MockConnection::new(vec![Err(error())]);
387        assert!(sync_scan_iter(&con).is_err());
388    }
389
390    #[cfg(feature = "sync")]
391    #[test]
392    fn sync_error_scan() {
393        let con = MockConnection::new(vec![Ok(page("5", &["a"])), Err(error())]);
394        let mut iter = sync_scan_iter(&con).unwrap();
395        assert_eq!(iter.next().unwrap().unwrap(), "a");
396        assert!(iter.next().expect("expected an error").is_err());
397        assert!(iter.next().is_none());
398    }
399}