Skip to main content

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}