SCAN: cursor-based iteration with optional key pattern matching and index-accelerated predicate pushdown. If `filter_field` and `filter_value` are provided AND a secondary index exists for that field, the scan uses the index to narrow candidates (O(log n) + O(k) where k = matching keys) instead of full table scan. Returns `(entries, next_cursor_bytes)`. `next_cursor_bytes` is empty when the scan
(&self, params: KvScanParams<'_>)
| 290 | /// when the scan is complete. Each entry is `(key, value)`. |
| 291 | /// `params.surrogate_ceiling` enforces clone snapshot isolation when set. |
| 292 | pub fn scan(&self, params: KvScanParams<'_>) -> ScanResult { |
| 293 | let KvScanParams { |
| 294 | tenant_id, |
| 295 | collection, |
| 296 | cursor, |
| 297 | count, |
| 298 | now_ms, |
| 299 | match_pattern, |
| 300 | filter_field, |
| 301 | filter_value, |
| 302 | surrogate_ceiling, |
| 303 | } = params; |
| 304 | let tkey = table_key(tenant_id, collection); |
| 305 | let table = match self.tables.get(&tkey) { |
| 306 | Some(t) => t, |
| 307 | None => return (Vec::new(), Vec::new()), |
| 308 | }; |
| 309 | |
| 310 | let surrogate_visible = |s: u32| -> bool { |
| 311 | match surrogate_ceiling { |
| 312 | Some(c) => s == 0 || s <= c, |
| 313 | None => true, |
| 314 | } |
| 315 | }; |
| 316 | |
| 317 | // Index-accelerated path: if we have an equality filter and an index, use it. |
| 318 | // Also checks composite indexes for prefix matches. |
| 319 | if let Some(field) = filter_field |
| 320 | && let Some(value) = filter_value |
| 321 | && let Some(idx_set) = self.indexes.get(&tkey) |
| 322 | { |
| 323 | // Try single-field index first. |
| 324 | let candidate_keys = if idx_set.get_index(field).is_some() { |
| 325 | idx_set.lookup_eq(field, value) |
| 326 | } else if let Some(ci) = idx_set.find_composite_with_prefix(field) { |
| 327 | // Composite index prefix match: use leading field. |
| 328 | ci.lookup_prefix(&[value]) |
| 329 | } else { |
| 330 | Vec::new() // No index available — will fall through to full scan. |
| 331 | }; |
| 332 | |
| 333 | if !candidate_keys.is_empty() { |
| 334 | let mut results = Vec::with_capacity(count.min(candidate_keys.len())); |
| 335 | |
| 336 | for pk in candidate_keys { |
| 337 | if results.len() >= count { |
| 338 | break; |
| 339 | } |
| 340 | if let Some((val, surrogate)) = table.get_with_surrogate(pk, now_ms) |
| 341 | && (match_pattern.is_none() |
| 342 | || super::scan::matches_pattern_pub(pk, match_pattern)) |
| 343 | && surrogate_visible(surrogate.as_u32()) |
| 344 | { |
| 345 | results.push((pk.to_vec(), val.to_vec())); |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | return (results, Vec::new()); |
nothing calls this directly
no test coverage detected