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}