drive/query/drive_document_count_query/execute_range_count.rs
1//! Range execution paths for the count query.
2//!
3//! Four executors, each keyed on a `range_countable: true` index or a
4//! `summableOffCountIndex` index (see below):
5//! - [`DriveDocumentCountQuery::execute_range_count_no_proof`] — Rust-
6//! side walk of the property-name `ProvableCountTree`'s children,
7//! returning per-(in_key, key) entries (or a single sum) without a
8//! proof.
9//! - [`DriveDocumentCountQuery::execute_aggregate_count_with_proof`] —
10//! grovedb `AggregateCountOnRange` proof, returning a single u64.
11//! - [`DriveDocumentCountQuery::execute_distinct_count_with_proof`] —
12//! regular range proof against the `ProvableCountTree`, returning
13//! per-key `KVCount` ops bound to the merk root.
14//! - [`DriveDocumentCountQuery::execute_carrier_aggregate_count_with_proof`]
15//! — one `AggregateCountOnRange` per `In` branch (or outer range key),
16//! proved together.
17//!
18//! Over a `summableOffCountIndex` index each executor reads the index's
19//! range sums instead, through the sum surface's counterpart
20//! ([`DriveDocumentCountQuery::counter_sums_query`]): its count trees count
21//! its counters, one per group, while its sums are its document counts.
22//!
23//! Point-lookup execution (Equal/In with no range) lives in
24//! [`super::execute_point_lookup`](super::execute_point_lookup).
25//!
26//! Whole module is gated `feature = "server"` via the parent's
27//! `pub mod execute_range_count;` declaration.
28
29use super::super::conditions::{WhereClause, WhereOperator};
30use super::super::drive_document_sum_query::{RangeSumOptions, RangeSumWalkMode};
31use super::{counter_sum_entry_as_count_entry, DriveDocumentCountQuery, SplitCountEntry};
32use crate::drive::Drive;
33use crate::error::query::QuerySyntaxError;
34use crate::error::Error;
35use crate::query::{aggregate_or_zero_when_absent, index_keeps_empty_groups, is_absent_path};
36use dpp::data_contract::document_type::methods::DocumentTypeV0Methods;
37use dpp::version::PlatformVersion;
38use grovedb::query_result_type::QueryResultType;
39use grovedb::TransactionArg;
40use grovedb_costs::CostContext;
41
42/// Pagination + ordering knobs for `execute_range_count_no_proof`.
43///
44/// Mirrors the protobuf request fields on
45/// `GetDocumentsCountRequestV0` so the drive-abci handler can pass them
46/// through unmodified. `distinct = false` collapses the range walk to a
47/// single summed entry; `distinct = true` returns one entry per distinct
48/// property value within the range.
49#[derive(Debug, Clone, Default)]
50pub struct RangeCountOptions {
51 /// When `true`, return one [`SplitCountEntry`] per distinct property
52 /// value within the range. When `false`, return a single entry
53 /// (empty `key`) summing all per-value counts.
54 pub distinct: bool,
55 /// Maximum number of entries to return. Only meaningful when
56 /// `distinct = true`. `None` means no limit, except over a
57 /// `summableOffCountIndex` index, read through the sum surface, whose
58 /// walk is always bounded: it stops at `u16::MAX` entries. A limit over
59 /// `u16::MAX` reads `u16::MAX` entries.
60 ///
61 /// To paginate, callers narrow the range itself (`color >
62 /// <last-key-from-previous-page>`). There's no cursor field
63 /// because a single-`bytes` cursor would be ambiguous for
64 /// compound (`In + range + distinct`) queries whose natural sort
65 /// is `(in_key, key)`, and range narrowing has the same
66 /// expressivity for the simple cases.
67 pub limit: Option<u32>,
68 /// Sort order for distinct entries. `true` (default) is ascending by
69 /// serialized key bytes. Ignored when `distinct = false`.
70 pub order_by_ascending: bool,
71}
72
73impl DriveDocumentCountQuery<'_> {
74 /// Executes a range-aware count query against a `range_countable`
75 /// index (or a `summableOffCountIndex` index, read through its range sums;
76 /// see the module docs). Path layout is `[contract_doc, doctype, prefix...,
77 /// range_prop_name]`, whose children are the per-value
78 /// `CountTree` leaves keyed by the range property's serialized
79 /// value.
80 ///
81 /// The caller picks the index via
82 /// [`Self::find_range_countable_index_for_where_clauses`]; this
83 /// method assumes:
84 /// - `self.index.range_countable == true`, or the index is a
85 /// `summableOffCountIndex` index
86 /// - All `Equal` / `In` where clauses cover the index prefix
87 /// - Exactly one range-operator where clause hits the index's last
88 /// property
89 ///
90 /// ## Execution strategies by mode
91 ///
92 /// - **Flat summed** (no `In`, `distinct = false`): single
93 /// `query_aggregate_count` call against the merk-level
94 /// `AggregateCountOnRange` primitive. O(log n).
95 /// - **Compound summed** (`In` on prefix, `distinct = false`):
96 /// per-In-value fan-out — one `query_aggregate_count` call per
97 /// matched In branch, summed in Rust. Bounded by the In
98 /// array's 100-element cap (enforced by
99 /// [`WhereClause::in_values`]) times O(log n), so worst-case
100 /// work is 100 × O(log n) regardless of how many documents
101 /// the range actually matches. Closes the request-amplification
102 /// surface a pre-fix walk-and-sum implementation had: that
103 /// path materialized every matched `(in_key, key)` element
104 /// even though the response was still a single aggregate
105 /// `u64`.
106 /// - **Distinct mode** (`distinct = true`, with or without
107 /// `In` on prefix): walks the unified
108 /// [`Self::distinct_count_path_query`] and emits one entry per
109 /// matched `(in_key, key)` pair. The path query carries
110 /// `options.limit` (clamped to `max_query_limit` upstream by
111 /// the dispatcher) and `options.order_by_ascending`, so
112 /// per-query work is O(limit × log n). Cross-fork aggregation
113 /// is intentionally NOT performed server-side; callers reduce
114 /// by `key` client-side if they want a flat histogram. See the
115 /// book chapter ("No-Merge Compound Semantics") for the rationale.
116 ///
117 /// ## Returned entry shape
118 ///
119 /// When `options.distinct = false`, returns a single entry with
120 /// `in_key = None`, empty `key`, and `count` equal to the sum of
121 /// all matched per-value counts. When `options.distinct = true`,
122 /// returns one entry per emitted `(in_key, key)` pair, after
123 /// applying `order_by_ascending` and `limit` over the
124 /// lexicographic `(in_key, key)` tuple.
125 pub fn execute_range_count_no_proof(
126 &self,
127 drive: &Drive,
128 options: &RangeCountOptions,
129 transaction: TransactionArg,
130 platform_version: &PlatformVersion,
131 ) -> Result<Vec<SplitCountEntry>, Error> {
132 // A `summableOffCountIndex` index's documents are its range sums.
133 if let Some(sums) = self.counter_sums_query() {
134 let walk_mode = if options.distinct {
135 RangeSumWalkMode::Distinct(distinct_walk_limit(options.limit).unwrap_or(u16::MAX))
136 } else {
137 RangeSumWalkMode::Aggregate
138 };
139 // The walk keeps a preallocated counter at zero, a group its
140 // limit counted, as the proof does, so a flat page ends only at
141 // the limit (across an `IN`, grovedb also charges a value whose
142 // range holds nothing, so there a short page may not be the end).
143 let entries = sums.execute_range_sum_no_proof(
144 drive,
145 &RangeSumOptions {
146 walk_mode,
147 carrier_outer_limit: None,
148 left_to_right: options.order_by_ascending,
149 },
150 transaction,
151 platform_version,
152 )?;
153 return Ok(entries
154 .into_iter()
155 .map(counter_sum_entry_as_count_entry)
156 .collect());
157 }
158 let drive_version = &platform_version.drive;
159 let has_in_on_prefix = self
160 .where_clauses
161 .iter()
162 .any(|wc| wc.operator == WhereOperator::In);
163
164 // Summed mode (both flat and compound `In + range`) goes
165 // through grovedb's `AggregateCountOnRange` primitive
166 // (`query_aggregate_count`), bounding per-query work to
167 // O(log n) per merk-tree fan-out. Compound mode loops over
168 // the In values (≤100 per the `in_values()` validator cap
169 // in `WhereClause::in_values()`) and issues one aggregate
170 // call per value, then sums the results — total bound is
171 // O(|In| × log n), independent of how many documents
172 // actually match the range.
173 //
174 // The pre-fix walk-and-sum path materialized every matched
175 // `(in_key, key)` element via `query_raw` to sum them in
176 // Rust. With one broad range × 100 In values that scans
177 // potentially millions of CountTree elements even though
178 // the response is still a single aggregate `u64` — a
179 // classic request-amplification surface on a public DAPI
180 // endpoint. The per-In fan-out closes that surface.
181 if !options.distinct {
182 // An absent value reads zero as the proof of the same total
183 // verifies it (`aggregate_or_zero_when_absent`).
184 let range_total_verifier = platform_version
185 .drive
186 .methods
187 .verify
188 .document_count
189 .verify_aggregate_count_proof;
190 if has_in_on_prefix {
191 let in_clause = self
192 .where_clauses
193 .iter()
194 .find(|wc| wc.operator == WhereOperator::In)
195 .ok_or_else(|| {
196 Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
197 "compound summed range count path requires an `in` clause; \
198 dispatcher bug if reached without one",
199 ))
200 })?;
201 // `in_values()` enforces non-empty, ≤100, no-duplicates
202 // — same defensive cap every other In consumer in
203 // drive uses. Without it a single 64 MiB gRPC request
204 // could schedule arbitrarily many backend aggregate
205 // reads.
206 let in_values = in_clause.in_values().into_data_with_error()??;
207 let other_clauses: Vec<WhereClause> = self
208 .where_clauses
209 .iter()
210 .filter(|wc| wc.operator != WhereOperator::In)
211 .cloned()
212 .collect();
213
214 let mut total: u64 = 0;
215 let mut seen_keys: std::collections::BTreeSet<Vec<u8>> =
216 std::collections::BTreeSet::new();
217 for value in in_values.iter() {
218 // Dedupe by serialized canonical key, not by raw
219 // Value, so that distinct DPP values that
220 // collapse to the same indexed bytes don't get
221 // double-counted. `in_values()` already rejects
222 // raw-Value duplicates, but this is defense-in-
223 // depth against future Value variants that
224 // serialize identically (e.g. integer vs
225 // float-with-zero-fraction).
226 let key_bytes = self.document_type.serialize_value_for_key(
227 in_clause.field.as_str(),
228 value,
229 platform_version,
230 )?;
231 if !seen_keys.insert(key_bytes) {
232 continue;
233 }
234
235 // Per-In-value query: replace the In clause with
236 // an Equal on the specific value. The resulting
237 // shape is flat (no In, Equal-prefix + range
238 // terminator), so `aggregate_count_path_query`
239 // accepts it and `query_aggregate_count` walks
240 // boundary nodes in O(log n).
241 let mut clauses_for_value = other_clauses.clone();
242 clauses_for_value.push(WhereClause {
243 field: in_clause.field.clone(),
244 operator: WhereOperator::Equal,
245 value: value.clone(),
246 });
247 let per_value_query = DriveDocumentCountQuery {
248 document_type: self.document_type,
249 contract_id: self.contract_id,
250 document_type_name: self.document_type_name.clone(),
251 index: self.index,
252 where_clauses: clauses_for_value,
253 };
254 let path_query =
255 per_value_query.aggregate_count_path_query(platform_version)?;
256 // Destructure the `CostContext` explicitly rather than
257 // calling `.unwrap()` on it: `CostContext::unwrap` is
258 // infallible (it just drops the cost field), but the
259 // visual pattern collides with `Option/Result::unwrap`
260 // and makes review noisier. Cost is discarded here
261 // because the per-mode dispatcher in `drive_dispatcher`
262 // wraps these executors with its own fee accounting —
263 // see the module-level docstring.
264 let CostContext { value, cost: _ } = drive.grove.query_aggregate_count(
265 &path_query,
266 transaction,
267 &drive_version.grove_version,
268 );
269 let count = aggregate_or_zero_when_absent(
270 drive,
271 &path_query.path,
272 value,
273 range_total_verifier,
274 transaction,
275 platform_version,
276 )?;
277 total = total.saturating_add(count);
278 }
279 return Ok(vec![SplitCountEntry {
280 in_key: None,
281 key: Vec::new(),
282 // Range-summed total derived from the executor's
283 // per-In aggregate fan-out — verified count.
284 count: Some(total),
285 }]);
286 }
287 // Flat summed (no In on prefix): single aggregate read.
288 let path_query = self.aggregate_count_path_query(platform_version)?;
289 // See In-fan-out branch above for the destructure rationale.
290 let CostContext { value, cost: _ } = drive.grove.query_aggregate_count(
291 &path_query,
292 transaction,
293 &drive_version.grove_version,
294 );
295 let count = aggregate_or_zero_when_absent(
296 drive,
297 &path_query.path,
298 value,
299 range_total_verifier,
300 transaction,
301 platform_version,
302 )?;
303 return Ok(vec![SplitCountEntry {
304 in_key: None,
305 key: Vec::new(),
306 // Single `AggregateCountOnRange` read — explicit
307 // verified count.
308 count: Some(count),
309 }]);
310 }
311
312 // Distinct mode (with or without In on prefix): walk and
313 // emit per-`(in_key, key)` entries. Bounded by the request's
314 // `limit` clause — the dispatcher already clamped that to
315 // `max_query_limit`, so this walk is O(limit × log n) and
316 // can't blow past the operator's DoS budget.
317 //
318 // Builds a single path query via the unified
319 // `distinct_count_path_query` builder. For an Equal-only
320 // prefix this collapses to a flat range-only query at the
321 // terminator's property-name subtree; for an In-on-prefix
322 // it becomes a compound query with one outer `Key` per In
323 // value (sorted lex-ascending by the builder) plus a
324 // `subquery_path`/`subquery` descending to the terminator's
325 // range item. The builder pushes the caller's `limit` and
326 // `order_by_ascending` directly into grovedb so the walk
327 // stops at `limit` elements in the requested direction —
328 // no Rust-side sort/reverse/truncate needed.
329 let (path_query_limit, left_to_right) = (
330 distinct_walk_limit(options.limit),
331 options.order_by_ascending,
332 );
333 let path_query =
334 self.distinct_count_path_query(path_query_limit, left_to_right, platform_version)?;
335 let base_path_len = path_query.path.len();
336
337 let mut drive_operations = vec![];
338 let result = drive.grove_get_raw_path_query(
339 &path_query,
340 transaction,
341 // PathKeyElementTrio so we can recover the In value from
342 // the emitted element's full path (for compound queries
343 // the In value sits at `path[base_path_len]` — the first
344 // segment beyond the path query's `path`).
345 QueryResultType::QueryPathKeyElementTrioResultType,
346 &mut drive_operations,
347 drive_version,
348 );
349 let elements = match result {
350 Ok((elements, _)) => elements,
351 Err(error) if is_absent_path(&error) => {
352 // No matching prefix path — distinct mode returns
353 // an empty entry list. (Summed modes returned earlier
354 // via the aggregate fast path, so the empty case is
355 // distinct-only here.)
356 return Ok(Vec::new());
357 }
358 Err(e) => return Err(e),
359 };
360
361 // Walk emitted `(path, key, element)` triples and build the
362 // unmerged entry list. For compound (In-on-prefix) queries
363 // the In value sits at `path[base_path_len]`; for flat
364 // queries `path.len() == base_path_len` so `in_key` is
365 // `None`. We DO NOT collapse multiple emitted entries with
366 // the same `key` into one — that's the whole point of the
367 // no-merge contract.
368 let keeps_empty_groups = index_keeps_empty_groups(self.document_type, self.index);
369 let mut entries: Vec<SplitCountEntry> = Vec::new();
370 for triple in elements.to_path_key_elements() {
371 let (path, key, element) = triple;
372 let count = element.count_value_or_default();
373 // An empty group a preallocation or an outliving index left
374 // stays, as a count of zero: the walk's limit counted it (see
375 // the helper).
376 if count == 0 && !keeps_empty_groups {
377 continue;
378 }
379 let in_key = if has_in_on_prefix && path.len() > base_path_len {
380 Some(path[base_path_len].clone())
381 } else {
382 None
383 };
384 // Distinct-walk emits one entry per distinct value
385 // with its verified count; always `Some(_)`.
386 entries.push(SplitCountEntry {
387 in_key,
388 key,
389 count: Some(count),
390 });
391 }
392
393 // Distinct mode: grovedb already emitted entries in the
394 // requested direction (controlled by `left_to_right`) and
395 // truncated to the path-query limit, so we return the entry
396 // list as-is. The In keys are lex-sorted by the builder
397 // (see `distinct_count_path_query`), so the natural emit
398 // order is `(in_key_lex_asc, key_lex_asc)` for ascending
399 // and `(in_key_lex_desc, key_lex_desc)` for descending —
400 // the documented order contract holds by construction.
401 //
402 // For pagination, callers narrow the range bound itself
403 // (`color > <last-key>` for the next page) rather than
404 // passing a cursor — see `RangeCountOptions::limit` doc.
405 Ok(entries)
406 }
407
408 /// Generates a grovedb `AggregateCountOnRange` proof for a
409 /// range-count query against a `range_countable` index (a
410 /// `summableOffCountIndex` index proves its range sum instead). The returned
411 /// proof bytes can be verified client-side via
412 /// `GroveDb::verify_aggregate_count_query`, which yields
413 /// `(root_hash, count)` — replacing the materialize-and-count proof
414 /// path that capped at `u16::MAX` documents.
415 ///
416 /// Limitations vs. [`Self::execute_range_count_no_proof`]:
417 /// - Returns ONLY the total count (a single number, no
418 /// per-distinct-value entries) — `AggregateCountOnRange` is a
419 /// single-aggregate primitive at the merk layer.
420 /// - Requires the prefix to resolve to exactly one path. `In` on
421 /// prefix properties is not supported because grovedb's aggregate
422 /// primitive only lifts a single inner range.
423 pub fn execute_aggregate_count_with_proof(
424 &self,
425 drive: &Drive,
426 transaction: TransactionArg,
427 platform_version: &PlatformVersion,
428 ) -> Result<Vec<u8>, Error> {
429 // A `summableOffCountIndex` index's documents are its range sums.
430 if let Some(sums) = self.counter_sums_query() {
431 return sums.execute_aggregate_sum_with_proof(drive, transaction, platform_version);
432 }
433 let drive_version = &platform_version.drive;
434 let path_query = self.aggregate_count_path_query(platform_version)?;
435 // Destructure rather than `.unwrap()` — see the In fan-out branch
436 // in `execute_range_count_no_proof` for rationale.
437 let CostContext { value, cost: _ } = drive.grove.get_proved_path_query(
438 &path_query,
439 None,
440 transaction,
441 &drive_version.grove_version,
442 );
443 let proof = value.map_err(|e| Error::GroveDB(Box::new(e)))?;
444 Ok(proof)
445 }
446
447 /// Generates a regular grovedb range proof against this count
448 /// query's `range_countable` index (a `summableOffCountIndex` index
449 /// proves its per-value range sums instead) — the distinct-counts-with-
450 /// proof companion to [`Self::execute_aggregate_count_with_proof`].
451 ///
452 /// No new prover code: the leaf is a `ProvableCountTree` and
453 /// merk's existing `prove_query` already emits `KVCount(key,
454 /// value, count)` per matched in-range key (via
455 /// `to_kv_count_node`). Each `count` is hash-bound to the merk
456 /// root via `node_hash_with_count`, so the per-key correctness
457 /// guarantee comes for free with the standard hash-chain check —
458 /// the SDK-side
459 /// [`drive_proof_verifier::verify_distinct_count_proof`] just
460 /// pulls the counts out of the proof's op stream after the
461 /// integrity check passes.
462 ///
463 /// Trade-off vs. the aggregate prove path:
464 /// - Returns per-distinct-value counts (one `(key, count)` per
465 /// matched lot value), not just a single sum.
466 /// - Proof size is O(distinct values matched), not O(log n) — so
467 /// ~1 `KVCount` op per matched key instead of subtree collapse
468 /// via `HashWithCount`. Still strictly smaller than
469 /// materialize-and-count, which would emit each underlying doc.
470 pub fn execute_distinct_count_with_proof(
471 &self,
472 drive: &Drive,
473 limit: u16,
474 left_to_right: bool,
475 transaction: TransactionArg,
476 platform_version: &PlatformVersion,
477 ) -> Result<Vec<u8>, Error> {
478 // A `summableOffCountIndex` index's documents are its range sums.
479 if let Some(sums) = self.counter_sums_query() {
480 return sums.execute_distinct_sum_with_proof(
481 drive,
482 limit,
483 left_to_right,
484 transaction,
485 platform_version,
486 );
487 }
488 let drive_version = &platform_version.drive;
489 let path_query =
490 self.distinct_count_path_query(Some(limit), left_to_right, platform_version)?;
491 // Destructure rather than `.unwrap()` — see the In fan-out branch
492 // in `execute_range_count_no_proof` for rationale.
493 let CostContext { value, cost: _ } = drive.grove.get_proved_path_query(
494 &path_query,
495 None,
496 transaction,
497 &drive_version.grove_version,
498 );
499 let proof = value.map_err(|e| Error::GroveDB(Box::new(e)))?;
500 Ok(proof)
501 }
502
503 /// Generates a grovedb **carrier** `AggregateCountOnRange` proof
504 /// for `In + range` queries with `group_by = [in_field]`. The
505 /// proof commits one aggregate count per resolved In branch
506 /// via grovedb's carrier-subquery composition
507 /// ([PR #663](https://github.com/dashpay/grovedb/pull/663)).
508 ///
509 /// Path query: see
510 /// [`Self::carrier_aggregate_count_path_query`].
511 ///
512 /// Trade-off vs. the alternative
513 /// [`Self::execute_distinct_count_with_proof`]
514 /// (`GroupByCompound` shape):
515 /// - **This** (carrier-ACOR): O(|In| · (log B + log C')) proof
516 /// bytes. One commit per merk-tree boundary node per In
517 /// branch — preserves the per-branch aggregate granularity
518 /// that `group_by = [in_field, range_field]` can't express
519 /// (the compound shape commits per-distinct-value-pair
520 /// entries).
521 /// - **Alternative** (distinct compound): O(|In| · R · log C')
522 /// where R is distinct in-range values per branch. Carries
523 /// strictly more information (one `(in_key, range_key)`
524 /// pair per resolved doc) at substantially larger bytes.
525 ///
526 /// Verified client-side via
527 /// [`grovedb::GroveDb::verify_aggregate_count_query_per_key`],
528 /// which returns `(RootHash, Vec<(Vec<u8>, u64)>)`.
529 ///
530 /// # Arguments
531 /// * `left_to_right` — proof-shaping bit. Threaded into the
532 /// outer `Query` via `Query::new_with_direction(left_to_right)`
533 /// on the inner carrier path query (see
534 /// [`Self::carrier_aggregate_count_path_query`]). `true` walks
535 /// the outer range ascending and emits the per-branch `u64`s
536 /// in lex-ascending key order; `false` walks descending and
537 /// emits them in lex-descending order. The serialized
538 /// `PathQuery` bytes differ between the two — the verifier
539 /// rebuilds the path query from `(query, limit, left_to_right)`
540 /// on its side, so the value passed here must match what the
541 /// caller will pass to
542 /// [`Self::verify_carrier_aggregate_count_proof`] or the
543 /// tenderdash root check fails.
544 pub fn execute_carrier_aggregate_count_with_proof(
545 &self,
546 drive: &Drive,
547 limit: Option<u16>,
548 left_to_right: bool,
549 transaction: TransactionArg,
550 platform_version: &PlatformVersion,
551 ) -> Result<Vec<u8>, Error> {
552 // A `summableOffCountIndex` index's documents are its range sums.
553 if let Some(sums) = self.counter_sums_query() {
554 return sums.execute_carrier_aggregate_sum_with_proof(
555 drive,
556 limit,
557 left_to_right,
558 transaction,
559 platform_version,
560 );
561 }
562 let drive_version = &platform_version.drive;
563 let path_query =
564 self.carrier_aggregate_count_path_query(limit, left_to_right, platform_version)?;
565 // Same destructure pattern as the sibling aggregate / distinct
566 // executors. `get_proved_path_query` returns `CostContext<Result>`;
567 // ignoring the cost field is the same pattern those use today.
568 let CostContext { value, cost: _ } = drive.grove.get_proved_path_query(
569 &path_query,
570 None,
571 transaction,
572 &drive_version.grove_version,
573 );
574 let proof = value.map_err(|e| Error::GroveDB(Box::new(e)))?;
575 Ok(proof)
576 }
577}
578
579/// The distinct walk's storage limit for a request's `limit`: a limit over
580/// `u16::MAX` saturates there. Shared by the regular walk and a
581/// `summableOffCountIndex` index's walk through the sum surface, so both
582/// read the same number of entries for one limit.
583fn distinct_walk_limit(limit: Option<u32>) -> Option<u16> {
584 limit.map(|limit| u16::try_from(limit).unwrap_or(u16::MAX))
585}