kevy_store/scan.rs
1//! `SCAN`'s per-shard page: a bounded-work, rehash-tolerant walk over
2//! this store's keyspace. Cursor arithmetic lives in
3//! [`kevy_map::KevyMap::scan_step`]; this layer only applies kevy's key
4//! semantics (expiry, `MATCH` glob, `TYPE` filter) and the `COUNT`
5//! work bound.
6
7#[cfg(not(feature = "std"))]
8use crate::nostd_prelude::*;
9use crate::{Store, glob_match, now_ns};
10
11/// Buckets visited per [`kevy_map::KevyMap::scan_step`] call (one home
12/// bucket-group). Mirrors kevy-map's group width; only used for the
13/// COUNT work-bound accounting.
14const BUCKETS_PER_STEP: usize = 16;
15
16impl Store {
17 /// One `SCAN` page over this shard: walk from `cursor`, visiting
18 /// roughly `count` buckets (COUNT is a work bound, not a result-size
19 /// promise — Redis semantics), collecting live keys that pass the
20 /// optional `MATCH` glob and `TYPE` filter.
21 ///
22 /// Returns `(next_cursor, keys, buckets_visited)`. `next_cursor == 0`
23 /// means this shard's sweep is complete. Expired-but-unreaped keys
24 /// are treated as absent (no removal — same as [`Store::collect_keys`]).
25 /// `type_filter` compares case-insensitively against the value's
26 /// [`type name`](crate::Value::type_name); an unknown name simply
27 /// never matches (Redis behaviour).
28 pub fn scan_page(
29 &self,
30 cursor: u64,
31 count: usize,
32 pattern: Option<&[u8]>,
33 type_filter: Option<&[u8]>,
34 ) -> (u64, Vec<Vec<u8>>, usize) {
35 if self.map.capacity() == 0 {
36 // Never-allocated table: no buckets exist, no work was done.
37 // (Distinct from "allocated but empty", which honestly costs
38 // one group visit per step.)
39 return (0, Vec::new(), 0);
40 }
41 let now = now_ns();
42 let mut keys = Vec::new();
43 let mut cursor = cursor;
44 let mut visited = 0usize;
45 let budget = count.max(1);
46 loop {
47 cursor = self.map.scan_step(cursor, |k, e| {
48 if e.is_expired_at(now) {
49 return;
50 }
51 if let Some(t) = type_filter
52 && !t.eq_ignore_ascii_case(e.value.type_name().as_bytes())
53 {
54 return;
55 }
56 if let Some(p) = pattern
57 && !glob_match(p, k.as_slice())
58 {
59 return;
60 }
61 keys.push(k.to_vec());
62 });
63 visited += BUCKETS_PER_STEP;
64 if cursor == 0 || visited >= budget {
65 return (cursor, keys, visited);
66 }
67 }
68 }
69}