Skip to main content

kevy_embedded/
ops_scan.rs

1//! Cursor-based and iterator-based scanning: `scan` / `hscan` /
2//! `zscan` plus the `keys_iter` / `hash_iter` / `zset_iter` adapters.
3//!
4//! Two API shapes:
5//!
6//! - **Cursor-based** (Redis-shaped): `scan(cursor, pattern, count) ->
7//!   (next_cursor, batch)`. A `cursor` of `0` starts a fresh walk; a
8//!   returned `next_cursor` of `0` means the walk completed.
9//! - **Iterator-based** (Rust-shaped): `keys_iter(pattern) -> impl
10//!   Iterator<Item = Vec<u8>>`, etc.
11//!
12//! The keyspace walk has the server's `SCAN` contract: a key present for
13//! the whole walk is returned at least once, and a page holds what it
14//! walked rather than a snapshot of the keyspace. `hscan` / `zscan` read
15//! the one collection they name and slice it by cursor.
16
17use crate::KevyResult;
18
19use crate::store::{Store, store_err};
20use crate::store_glue::lock_read;
21
22/// Bits of a keyspace cursor that carry the position within a shard; the
23/// shard index sits above them (the server's layout).
24const SCAN_POS_BITS: u32 = 54;
25const SCAN_POS_MASK: u64 = (1 << SCAN_POS_BITS) - 1;
26/// Keys a [`KeysIter`] asks for per page.
27const ITER_PAGE: usize = 256;
28
29/// The iterator [`Store::keys_iter`] returns: the keys matching its
30/// pattern, a page at a time.
31///
32/// ```
33/// # use kevy_embedded::{Config, Store};
34/// let s = Store::open(Config::default())?;
35/// s.set(b"user:1", b"x")?;
36/// s.set(b"order:1", b"y")?;
37/// let users: Vec<Vec<u8>> = s.keys_iter(Some(b"user:*")).collect();
38/// assert_eq!(users, [b"user:1".to_vec()]);
39/// # Ok::<(), kevy_embedded::KevyError>(())
40/// ```
41pub struct KeysIter<'a> {
42    store: &'a Store,
43    pattern: Option<Vec<u8>>,
44    cursor: u64,
45    page: std::vec::IntoIter<Vec<u8>>,
46    done: bool,
47}
48
49impl std::fmt::Debug for KeysIter<'_> {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.debug_struct("KeysIter")
52            .field("pattern", &self.pattern)
53            .field("cursor", &self.cursor)
54            .field("done", &self.done)
55            .finish_non_exhaustive()
56    }
57}
58
59impl Iterator for KeysIter<'_> {
60    type Item = Vec<u8>;
61
62    fn next(&mut self) -> Option<Vec<u8>> {
63        loop {
64            if let Some(k) = self.page.next() {
65                return Some(k);
66            }
67            if self.done {
68                return None;
69            }
70            let (next, keys) = self.store.scan(self.cursor, self.pattern.as_deref(), ITER_PAGE);
71            (self.cursor, self.done) = (next, next == 0);
72            self.page = keys.into_iter();
73        }
74    }
75}
76
77/// One `HSCAN`/`ZSCAN`-style page: `(next_cursor, items)` where each
78/// item is a `(member, value-or-score)` pair.
79type PairPage = (u64, Vec<(Vec<u8>, Vec<u8>)>);
80
81/// One `ZSCAN` page: `(next_cursor, (member, score) pairs)`.
82type ScorePage = (u64, Vec<(Vec<u8>, f64)>);
83
84impl Store {
85    // ---- keyspace scan ----------------------------------------------
86
87    /// `SCAN cursor [MATCH pattern] [COUNT n]` — keys and the next cursor.
88    /// `cursor = 0` starts the walk; `next_cursor = 0` means it completed.
89    ///
90    /// As in Redis, `count` is how much of the keyspace a page walks, not
91    /// a promise of how many keys it holds: a page returns at least
92    /// `count` keys unless the walk ends first, and may return more. A key
93    /// present for the whole walk is returned at least once; one written
94    /// meanwhile may come back twice. `usize::MAX` drains in one call. The
95    /// cursor names a shard and a position in it, so a page costs what it
96    /// walks, not the whole keyspace.
97    ///
98    /// ```
99    /// # use kevy_embedded::{Config, Store};
100    /// let s = Store::open(Config::default())?;
101    /// for i in 0..50 {
102    ///     s.set(format!("k{i}").as_bytes(), b"v")?;
103    /// }
104    /// let (mut cursor, mut seen) = (0, std::collections::BTreeSet::new());
105    /// loop {
106    ///     let (next, keys) = s.scan(cursor, Some(b"k*"), 10);
107    ///     seen.extend(keys);
108    ///     if next == 0 {
109    ///         break;
110    ///     }
111    ///     cursor = next;
112    /// }
113    /// assert_eq!(seen.len(), 50);
114    /// # Ok::<(), kevy_embedded::KevyError>(())
115    /// ```
116    pub fn scan(&self, cursor: u64, pattern: Option<&[u8]>, count: usize) -> (u64, Vec<Vec<u8>>) {
117        if count == 0 {
118            return (0, Vec::new());
119        }
120        let (mut shard, mut pos) = ((cursor >> SCAN_POS_BITS) as usize, cursor & SCAN_POS_MASK);
121        let mut keys = Vec::new();
122        while shard < self.shards.len() {
123            let want = count.saturating_sub(keys.len());
124            let g = lock_read(&self.shards[shard]);
125            let (next, mut page, _) = g.store.scan_page(pos, want, pattern, None);
126            drop(g);
127            keys.append(&mut page);
128            if next == 0 {
129                (shard, pos) = (shard + 1, 0);
130            } else {
131                pos = next;
132            }
133            if keys.len() >= count && shard < self.shards.len() {
134                return ((shard as u64) << SCAN_POS_BITS | pos, keys);
135            }
136        }
137        (0, keys)
138    }
139
140    /// Every key matching `pattern`, walked a page at a time: the iterator
141    /// holds one page, not the keyspace. Same contract as [`Self::scan`] —
142    /// a key present throughout is yielded at least once.
143    ///
144    /// ```
145    /// # use kevy_embedded::{Config, Store};
146    /// let s = Store::open(Config::default())?;
147    /// s.set(b"a", b"1")?;
148    /// s.set(b"b", b"2")?;
149    /// let mut keys: Vec<Vec<u8>> = s.keys_iter(None).collect();
150    /// keys.sort();
151    /// assert_eq!(keys, [b"a".to_vec(), b"b".to_vec()]);
152    /// # Ok::<(), kevy_embedded::KevyError>(())
153    /// ```
154    pub fn keys_iter(&self, pattern: Option<&[u8]>) -> KeysIter<'_> {
155        KeysIter {
156            store: self,
157            pattern: pattern.map(<[u8]>::to_vec),
158            cursor: 0,
159            page: Vec::new().into_iter(),
160            done: false,
161        }
162    }
163
164    // ---- hash scan --------------------------------------------------
165
166    /// `HSCAN key cursor [COUNT n]` — return up to `count` `(field,
167    /// value)` pairs from the hash at `key`, plus the next cursor.
168    /// `cursor = 0` starts; `next_cursor = 0` means complete.
169    pub fn hscan(&self, key: &[u8], cursor: u64, count: usize) -> KevyResult<PairPage> {
170        let pairs = self.hgetall(key)?;
171        Ok(page_into(pairs, cursor, count))
172    }
173
174    /// Iterator wrapper around [`Self::hscan`].
175    pub fn hash_iter(&self, key: &[u8]) -> KevyResult<std::vec::IntoIter<(Vec<u8>, Vec<u8>)>> {
176        Ok(self.hgetall(key)?.into_iter())
177    }
178
179    // ---- zset scan --------------------------------------------------
180
181    /// `ZSCAN key cursor [COUNT n]` — return up to `count` `(member,
182    /// score)` pairs from the sorted set at `key`, in ascending score
183    /// order, plus the next cursor.
184    pub fn zscan(&self, key: &[u8], cursor: u64, count: usize) -> KevyResult<ScorePage> {
185        let pairs = self.wshard(key).store.zrange(key, 0, -1).map_err(store_err)?;
186        Ok(page_into(pairs, cursor, count))
187    }
188
189    /// Iterator wrapper around [`Self::zscan`].
190    pub fn zset_iter(&self, key: &[u8]) -> KevyResult<std::vec::IntoIter<(Vec<u8>, f64)>> {
191        let pairs = self.wshard(key).store.zrange(key, 0, -1).map_err(store_err)?;
192        Ok(pairs.into_iter())
193    }
194}
195
196/// Slice `data[cursor..cursor+count]` and report the next cursor
197/// (`0` when the walk completed).
198fn page_into<T>(data: Vec<T>, cursor: u64, count: usize) -> (u64, Vec<T>) {
199    let total = data.len();
200    let start = (cursor as usize).min(total);
201    let end = start.saturating_add(count).min(total);
202    let batch = data.into_iter().skip(start).take(end - start).collect();
203    let next_cursor = if end >= total { 0 } else { end as u64 };
204    (next_cursor, batch)
205}