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//! Each call snapshots the matching set in one shot and slices into
13//! it by cursor. The snapshot is stable across a single walk even
14//! while other writers mutate concurrently, and the memory cost is
15//! bounded by the matching subset.
16
17use std::io;
18
19use crate::store::{Store, store_err};
20
21/// One `HSCAN`/`ZSCAN`-style page: `(next_cursor, items)` where each
22/// item is a `(member, value-or-score)` pair.
23type PairPage = (u64, Vec<(Vec<u8>, Vec<u8>)>);
24
25/// One `ZSCAN` page: `(next_cursor, (member, score) pairs)`.
26type ScorePage = (u64, Vec<(Vec<u8>, f64)>);
27
28impl Store {
29 // ---- keyspace scan ----------------------------------------------
30
31 /// `SCAN cursor [MATCH pattern] [COUNT n]` — return up to `count`
32 /// keys, plus the next cursor. `cursor = 0` starts the walk;
33 /// `next_cursor = 0` means the walk completed.
34 ///
35 /// `count` is the page size; pass `usize::MAX` to drain in one
36 /// call.
37 pub fn scan(
38 &self,
39 cursor: u64,
40 pattern: Option<&[u8]>,
41 count: usize,
42 ) -> (u64, Vec<Vec<u8>>) {
43 let all = self.collect_keys(pattern, None);
44 page_into(all, cursor, count)
45 }
46
47 /// Iterator wrapper around [`Self::scan`] — emits every matching
48 /// key as a `Vec<u8>`. Drains the keyspace in one snapshot at
49 /// construction time; matches Rust idioms.
50 pub fn keys_iter(&self, pattern: Option<&[u8]>) -> std::vec::IntoIter<Vec<u8>> {
51 self.collect_keys(pattern, None).into_iter()
52 }
53
54 // ---- hash scan --------------------------------------------------
55
56 /// `HSCAN key cursor [COUNT n]` — return up to `count` `(field,
57 /// value)` pairs from the hash at `key`, plus the next cursor.
58 /// `cursor = 0` starts; `next_cursor = 0` means complete.
59 pub fn hscan(
60 &self,
61 key: &[u8],
62 cursor: u64,
63 count: usize,
64 ) -> io::Result<PairPage> {
65 let pairs = self.hgetall(key)?;
66 Ok(page_into(pairs, cursor, count))
67 }
68
69 /// Iterator wrapper around [`Self::hscan`].
70 pub fn hash_iter(
71 &self,
72 key: &[u8],
73 ) -> io::Result<std::vec::IntoIter<(Vec<u8>, Vec<u8>)>> {
74 Ok(self.hgetall(key)?.into_iter())
75 }
76
77 // ---- zset scan --------------------------------------------------
78
79 /// `ZSCAN key cursor [COUNT n]` — return up to `count` `(member,
80 /// score)` pairs from the sorted set at `key`, in ascending score
81 /// order, plus the next cursor.
82 pub fn zscan(
83 &self,
84 key: &[u8],
85 cursor: u64,
86 count: usize,
87 ) -> io::Result<ScorePage> {
88 let pairs = self
89 .wshard(key)
90 .store
91 .zrange(key, 0, -1)
92 .map_err(store_err)?;
93 Ok(page_into(pairs, cursor, count))
94 }
95
96 /// Iterator wrapper around [`Self::zscan`].
97 pub fn zset_iter(
98 &self,
99 key: &[u8],
100 ) -> io::Result<std::vec::IntoIter<(Vec<u8>, f64)>> {
101 let pairs = self
102 .wshard(key)
103 .store
104 .zrange(key, 0, -1)
105 .map_err(store_err)?;
106 Ok(pairs.into_iter())
107 }
108}
109
110/// Slice `data[cursor..cursor+count]` and report the next cursor
111/// (`0` when the walk completed).
112fn page_into<T>(data: Vec<T>, cursor: u64, count: usize) -> (u64, Vec<T>) {
113 let total = data.len();
114 let start = (cursor as usize).min(total);
115 let end = start.saturating_add(count).min(total);
116 let batch = data.into_iter().skip(start).take(end - start).collect();
117 let next_cursor = if end >= total { 0 } else { end as u64 };
118 (next_cursor, batch)
119}