Skip to main content

nodedb_lite/nodedb/collection/
kv_scan.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! KV reads over key ranges and expiry sweeps: range scan, cursor scan,
4//! key listing, shape subscription, and eager expiry compaction.
5
6use nodedb_types::Namespace;
7use nodedb_types::error::{NodeDbError, NodeDbResult};
8
9use super::super::{LockExt, NodeDbLite};
10use super::kv::{KV_FLUSH_THRESHOLD, decode_value, is_expired, kv_key, split_kv_key};
11use crate::storage::engine::{StorageEngine, WriteOp};
12
13impl<S: StorageEngine> NodeDbLite<S> {
14    /// KV RANGE SCAN: ordered key scan with optional bounds and limit.
15    ///
16    /// Returns `(key, value)` pairs where `start <= key < end`, ordered by
17    /// key in lexicographic byte order. Expired keys are skipped and lazily
18    /// deleted.
19    ///
20    /// - `start = None` means scan from the beginning of the collection.
21    /// - `end = None` means scan to the end of the collection.
22    /// - `limit = None` means no cap on results.
23    ///
24    /// Flushes the write buffer before scanning so the KV store reflects all pending
25    /// writes.
26    pub async fn kv_range_scan(
27        &self,
28        collection: &str,
29        start: Option<&[u8]>,
30        end: Option<&[u8]>,
31        limit: Option<usize>,
32    ) -> NodeDbResult<Vec<(Vec<u8>, Vec<u8>)>> {
33        self.kv_flush_inner().await?;
34
35        let col_prefix_end = {
36            let mut p = collection.as_bytes().to_vec();
37            p.push(0);
38            p
39        };
40
41        // Build absolute start key (collection\0[user_start]).
42        let start_key: Option<Vec<u8>> = Some(match start {
43            Some(s) => {
44                let mut k = col_prefix_end.clone();
45                k.extend_from_slice(s);
46                k
47            }
48            None => col_prefix_end.clone(),
49        });
50
51        // Build absolute end key (collection\0[user_end]).
52        let end_key: Option<Vec<u8>> = end.map(|e| {
53            let mut k = col_prefix_end.clone();
54            k.extend_from_slice(e);
55            k
56        });
57
58        let entries = self
59            .storage
60            .scan_range_bounded(
61                Namespace::Kv,
62                start_key.as_deref(),
63                end_key.as_deref(),
64                limit.map(|l| l + 32), // over-fetch slightly to account for skipped expired keys
65            )
66            .await
67            .map_err(NodeDbError::storage)?;
68
69        let mut results: Vec<(Vec<u8>, Vec<u8>)> =
70            Vec::with_capacity(limit.unwrap_or(entries.len()).min(entries.len()));
71        let mut expired_keys: Vec<Vec<u8>> = Vec::new();
72
73        for (composite_key, raw_value) in entries {
74            if let Some(limit) = limit
75                && results.len() >= limit
76            {
77                break;
78            }
79            let Some((coll, user_key_bytes)) = split_kv_key(&composite_key) else {
80                continue;
81            };
82            if coll != collection {
83                break;
84            }
85            let Some((deadline, user_bytes)) = decode_value(&raw_value) else {
86                continue;
87            };
88            if is_expired(deadline) {
89                expired_keys.push(kv_key(collection, user_key_bytes));
90                continue;
91            }
92            results.push((user_key_bytes.to_vec(), user_bytes.to_vec()));
93        }
94
95        // Lazy-delete expired keys discovered during scan.
96        if !expired_keys.is_empty() {
97            let should_flush = {
98                let mut buf = self.kv_local.write_buf.lock_or_recover();
99                for rkey in &expired_keys {
100                    buf.overlay.insert(rkey.clone(), None);
101                    buf.ops.push(WriteOp::Delete {
102                        ns: Namespace::Kv,
103                        key: rkey.clone(),
104                    });
105                }
106                buf.ops.len() >= KV_FLUSH_THRESHOLD
107            };
108            // Evict expired keys from the cache.
109            {
110                let mut cache = self.kv_local.cache.lock_or_recover();
111                for rkey in &expired_keys {
112                    cache.pop(rkey);
113                }
114            }
115            if should_flush {
116                self.kv_flush_inner().await?;
117            }
118        }
119
120        Ok(results)
121    }
122
123    /// KV COMPACT EXPIRED: eagerly remove all expired keys in a collection.
124    ///
125    /// Flushes the write buffer, then scans all keys in the collection and
126    /// deletes any whose TTL deadline has passed. Returns the count of keys
127    /// removed.
128    pub async fn kv_compact_expired(&self, collection: &str) -> NodeDbResult<usize> {
129        self.kv_flush_inner().await?;
130
131        let col_prefix = {
132            let mut p = collection.as_bytes().to_vec();
133            p.push(0);
134            p
135        };
136
137        let entries = self
138            .storage
139            .scan_range_bounded(Namespace::Kv, Some(&col_prefix), None, None)
140            .await
141            .map_err(NodeDbError::storage)?;
142
143        let now = crate::runtime::now_millis();
144        let mut delete_ops: Vec<WriteOp> = Vec::new();
145
146        for (composite_key, raw_value) in entries {
147            let Some((coll, _user_key_bytes)) = split_kv_key(&composite_key) else {
148                continue;
149            };
150            if coll != collection {
151                break;
152            }
153            if let Some((deadline, _)) = decode_value(&raw_value)
154                && deadline != 0
155                && now >= deadline
156            {
157                // composite_key is the user-key (namespace byte
158                // already stripped by scan_range_bounded). WriteOp
159                // re-prepends the namespace byte via make_key internally.
160                delete_ops.push(WriteOp::Delete {
161                    ns: Namespace::Kv,
162                    key: composite_key,
163                });
164            }
165        }
166
167        let count = delete_ops.len();
168        if count > 0 {
169            self.kv_local
170                .commit(&*self.storage, delete_ops)
171                .await
172                .map_err(NodeDbError::storage)?;
173        }
174
175        Ok(count)
176    }
177
178    /// KV SCAN: iterate keys in sorted order starting from `cursor`.
179    ///
180    /// Returns up to `count` key-value pairs where key >= cursor (inclusive).
181    /// Pass an empty cursor to start from the beginning of the collection.
182    ///
183    /// Flushes the write buffer first to ensure the KV store has all data, then
184    /// uses the storage's B-tree range scan — O(log N + count).
185    pub async fn kv_scan(
186        &self,
187        collection: &str,
188        cursor: &str,
189        count: usize,
190    ) -> NodeDbResult<Vec<(String, Vec<u8>)>> {
191        // Flush pending writes so storage is up to date.
192        self.kv_flush_inner().await?;
193
194        let start = kv_key(collection, cursor.as_bytes());
195        let entries = self
196            .storage
197            .scan_range(Namespace::Kv, &start, count)
198            .await
199            .map_err(NodeDbError::storage)?;
200
201        let mut results = Vec::with_capacity(entries.len());
202        for (composite_key, raw_value) in entries {
203            let Some((coll, key_bytes)) = split_kv_key(&composite_key) else {
204                continue;
205            };
206            if coll != collection {
207                break;
208            }
209            let Some((deadline, user_bytes)) = decode_value(&raw_value) else {
210                continue;
211            };
212            if is_expired(deadline) {
213                continue;
214            }
215            if let Ok(key_str) = std::str::from_utf8(key_bytes) {
216                results.push((key_str.to_string(), user_bytes.to_vec()));
217            }
218        }
219
220        Ok(results)
221    }
222
223    /// List all keys in a KV collection.
224    pub async fn kv_keys(&self, collection: &str) -> NodeDbResult<Vec<String>> {
225        // Flush pending writes first.
226        self.kv_flush_inner().await?;
227
228        let prefix = kv_key(collection, b"");
229        let entries = self
230            .storage
231            .scan_range(Namespace::Kv, &prefix, usize::MAX)
232            .await
233            .map_err(NodeDbError::storage)?;
234
235        let mut keys = Vec::with_capacity(entries.len());
236        for (composite_key, raw_value) in entries {
237            let Some((coll, key_bytes)) = split_kv_key(&composite_key) else {
238                continue;
239            };
240            if coll != collection {
241                break;
242            }
243            // Skip expired keys.
244            if let Some((deadline, _)) = decode_value(&raw_value) {
245                if is_expired(deadline) {
246                    continue;
247                }
248            } else {
249                continue;
250            }
251            if let Ok(key_str) = std::str::from_utf8(key_bytes) {
252                keys.push(key_str.to_string());
253            }
254        }
255        Ok(keys)
256    }
257
258    /// Subscribe to a subset of KV keys matching a pattern.
259    pub async fn kv_subscribe_shape(
260        &self,
261        collection: &str,
262        key_pattern: &str,
263    ) -> NodeDbResult<Vec<String>> {
264        let all_keys = self.kv_keys(collection).await?;
265        let matched: Vec<String> = all_keys
266            .into_iter()
267            .filter(|k| glob_matches(key_pattern, k))
268            .collect();
269        Ok(matched)
270    }
271}
272
273/// Simple glob matching for shape subscriptions.
274fn glob_matches(pattern: &str, input: &str) -> bool {
275    let pat = pattern.as_bytes();
276    let inp = input.as_bytes();
277    let mut pi = 0;
278    let mut ii = 0;
279    let mut star_pi = usize::MAX;
280    let mut star_ii = 0;
281
282    while ii < inp.len() {
283        if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == inp[ii]) {
284            pi += 1;
285            ii += 1;
286        } else if pi < pat.len() && pat[pi] == b'*' {
287            star_pi = pi;
288            star_ii = ii;
289            pi += 1;
290        } else if star_pi != usize::MAX {
291            pi = star_pi + 1;
292            star_ii += 1;
293            ii = star_ii;
294        } else {
295            return false;
296        }
297    }
298
299    while pi < pat.len() && pat[pi] == b'*' {
300        pi += 1;
301    }
302
303    pi == pat.len()
304}