drive/query/drive_document_count_query/drive_dispatcher.rs
1//! Top-level dispatcher for the unified `GetDocumentsCount` request.
2//!
3//! Owns the whole pipeline: CBOR-decode → mode detection →
4//! per-mode executor (see [`super::executors`]) → response
5//! wrapping. The drive-abci handler builds a
6//! [`DocumentCountRequest`] and calls
7//! [`Drive::execute_document_count_request`]; everything past
8//! contract lookup lives in drive.
9//!
10//! Both `DocumentCountRequest` and `DocumentCountResponse` are
11//! the ABI for this dispatcher — they're public so drive-abci can
12//! name the input/output types without reaching into the
13//! executor surface.
14//!
15//! Module is gated `feature = "server"` via the parent's
16//! `pub mod drive_dispatcher;` declaration.
17
18use super::super::conditions::WhereClause;
19use super::super::ordering::OrderClause;
20use super::execute_range_count::RangeCountOptions;
21use super::{DocumentCountMode, DriveDocumentCountQuery, SplitCountEntry};
22use crate::drive::Drive;
23use crate::error::query::QuerySyntaxError;
24use crate::error::Error;
25use crate::query::ResolvedTimeRange;
26// Shared with the sum / average / joint dispatchers and the SDK proof
27// verifiers — see `crate::query::canonicalize` for the shape contract.
28use crate::query::{
29 validate_and_canonicalize_where_clauses, validate_resolved_time_range_clause_shapes,
30};
31use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
32use dpp::data_contract::document_type::DocumentTypeRef;
33use dpp::version::PlatformVersion;
34use grovedb::TransactionArg;
35
36// `impl Drive { ... per-mode executors ... }` lives in
37// [`super::executors`] — it's a deliberate physical split between
38// "dispatcher routes" (this file) and "executors execute" (sibling).
39// All per-mode executor methods this file calls
40// (`execute_document_count_total_no_proof` etc.) are reachable via
41// the shared `Drive` type from there.
42
43/// All inputs required for the unified document-count entry point
44/// [`Drive::execute_document_count_request`]. Built by the gRPC
45/// handler from a `GetDocumentsRequestV1` after wire-decoding +
46/// contract lookup; drive owns everything past this point including
47/// mode-detection-from-clauses, index picking, and per-mode dispatch.
48///
49/// `where_clauses` and `order_clauses` arrive already structured —
50/// the v1 ABCI handler converts proto `repeated WhereClause` /
51/// `repeated OrderClause` upstream; benches and tests that want a
52/// `Value`-shape fixture call [`where_clauses_from_value`] /
53/// [`order_clauses_from_value`] to parse before constructing the
54/// request. The dispatcher entry point runs
55/// [`validate_and_canonicalize_where_clauses`] on the input so
56/// shape-validation rejection (duplicate equal, multiple In, …)
57/// and the `> AND <` → `between*` canonicalization happen
58/// regardless of upstream path.
59pub struct DocumentCountRequest<'a> {
60 /// Live contract (already loaded by the handler).
61 pub contract: &'a dpp::data_contract::DataContract,
62 /// Resolved document type within `contract`.
63 pub document_type: DocumentTypeRef<'a>,
64 /// Structured `where` clauses. The dispatcher runs the same
65 /// [`WhereClause::group_clauses`] validator + same-field
66 /// range-pair merge the regular document-query path runs (see
67 /// [`validate_and_canonicalize_where_clauses`]'s docstring for
68 /// the catalog of rejections this enables and the In/range +
69 /// `between*` canonicalization rules) before mode detection.
70 pub where_clauses: Vec<WhereClause>,
71 /// The fields among `where_clauses` whose equality clause was produced by
72 /// `IN_TIME_RANGE` resolution rather than written by the caller. Same
73 /// contract and same purpose as
74 /// [`crate::query::DriveDocumentQuery::resolved_time_ranges`]:
75 /// it is what gates which indexes the count pickers may select.
76 pub resolved_time_ranges: Vec<ResolvedTimeRange>,
77 /// Structured `order_by` clauses. The first clause's direction
78 /// governs split-mode entry ordering (per-`In`-value /
79 /// per-distinct-value-in-range) and, on the
80 /// `RangeDistinctProof` prove path, is part of the path-query
81 /// bytes the SDK reconstructs to verify the proof.
82 /// `PointLookupProof` and the no-proof `Total` / `PerInValue`
83 /// paths don't read order_by. Empty list → ascending default
84 /// for split-mode response ordering.
85 pub order_clauses: Vec<OrderClause>,
86 /// SQL-shaped output mode — the caller's `(select, group_by)`
87 /// contract resolved into one of four shapes (Aggregate,
88 /// GroupByIn, GroupByRange, GroupByCompound). The dispatcher
89 /// uses this to distinguish e.g. "aggregate count with In
90 /// fan-out" (which does NOT accept `limit`) from "per-In-value
91 /// entries" (which does) — they're otherwise indistinguishable
92 /// from the where clauses alone. See [`CountMode`] for the
93 /// per-variant where-clause and `limit` invariants.
94 pub mode: super::CountMode,
95 /// Limit cap from the request. Callers SHOULD pre-clamp against
96 /// their server-side `max_query_limit` policy, but Drive also
97 /// enforces a defense-in-depth clamp before forwarding to the
98 /// distinct-mode walk: an `Option::None` here is normalized to
99 /// `drive_config.default_query_limit` and any `Some(value)` is
100 /// reduced to `drive_config.max_query_limit` if larger. After
101 /// dispatch, the limit forwarded to
102 /// [`RangeCountOptions::limit`] is always `Some(_)` ≤ system cap.
103 pub limit: Option<u32>,
104 /// Whether to produce a proof (vs. raw counts).
105 pub prove: bool,
106 /// Drive-side query config — only consumed by the materialize-and-
107 /// count fallback.
108 pub drive_config: &'a crate::config::DriveConfig,
109}
110
111/// Output shape of [`Drive::execute_document_count_request`]. Three
112/// variants mirror the proto's `CountResults.variant` oneof (for
113/// no-proof responses) plus the outer `Proof` arm:
114///
115/// - `Aggregate(u64)` — total-count modes (`Total` and
116/// `RangeNoProof` under [`super::CountMode::Aggregate`]). The abci
117/// handler maps this to `CountResults.aggregate_count`.
118/// - `Entries(Vec<SplitCountEntry>)` — per-key modes (`PerInValue`
119/// and `RangeNoProof` under [`super::CountMode::GroupByRange`] /
120/// [`super::CountMode::GroupByCompound`]). The abci handler maps
121/// this to `CountResults.entries`.
122/// - `Proof(Vec<u8>)` — grovedb proof bytes the client verifies via
123/// either `verify_aggregate_count_query` (for `RangeProof`),
124/// `verify_distinct_count_proof` (for `RangeDistinctProof`), or
125/// the `DriveDocumentQuery` proof verifier (for
126/// `PointLookupProof`).
127#[derive(Debug, Clone)]
128pub enum DocumentCountResponse {
129 /// Single aggregate count — total across the matching set.
130 Aggregate(u64),
131 /// Per-key entries.
132 Entries(Vec<SplitCountEntry>),
133 /// Grovedb proof bytes.
134 Proof(Vec<u8>),
135}
136
137/// Parse the decoded `where` value into structured [`WhereClause`]s.
138///
139/// Mirrors the per-clause loop the regular `query_documents_v0`
140/// handler delegates to `DriveDocumentQuery::from_decomposed_values`:
141/// the abci layer just CBOR-decodes the wire bytes into a `Value` and
142/// hands the raw value down. Drive owns the parsing so a future
143/// per-clause validation (e.g. forbidding operators in distinct mode)
144/// can live next to the executors instead of being scattered across
145/// abci handlers.
146///
147/// `Value::Null` (empty `where` field) → no clauses. Any other shape
148/// must be an outer array of inner arrays-of-components.
149///
150/// After component parsing, the resulting clause list is run through
151/// [`WhereClause::group_clauses`] — the same validator the regular
152/// document-query path uses — to reject malformed shapes the count
153/// path otherwise silently reduces:
154///
155/// - Duplicate `Equal` clauses on the same field
156/// (`DuplicateNonGroupableClauseSameField`).
157/// - Multiple `In` clauses (`MultipleInClauses`) — rejected here: the
158/// shared grammar accepts them for protocol version 14+ document
159/// queries, but the aggregate surfaces do not.
160/// - Multiple non-groupable range clauses (`MultipleRangeClauses`).
161/// - Equality + `In` on the same field, range + equality/In on the
162/// same field (`DuplicateNonGroupableClauseSameField` /
163/// `InvalidWhereClauseComponents`).
164///
165/// Without this validation, downstream
166/// [`DriveDocumentCountQuery::find_countable_index_for_where_clauses`]
167/// collapses repeated fields into a `BTreeSet` and
168/// [`DriveDocumentCountQuery::point_lookup_count_path_query`]
169/// resolves each index property with a single `.find(...)` — both
170/// of which silently pick the first clause on a duplicated field
171/// and return a count for an arbitrarily reduced query rather than
172/// rejecting the malformed request. `group_clauses` is the single
173/// source of truth for what shapes the query stack as a whole
174/// accepts; running it here aligns the count endpoint with the
175/// regular document-query path's rejection contract.
176///
177/// Only the validation side-effect is consumed — the dispatcher
178/// continues to operate on the parsed `Vec<WhereClause>` directly,
179/// since the count-specific mode detection and index pickers
180/// expect a flat list, not the equal-clauses/in-clause/range-clause
181/// triple that `group_clauses` returns. (The regular query path's
182/// `InternalClauses::extract_from_clauses` uses the triple; the
183/// count path doesn't.)
184pub fn where_clauses_from_value(
185 value: &dpp::platform_value::Value,
186 platform_version: &PlatformVersion,
187) -> Result<Vec<WhereClause>, Error> {
188 let clauses: Vec<WhereClause> = match value {
189 dpp::platform_value::Value::Null => Vec::new(),
190 dpp::platform_value::Value::Array(clauses) => clauses
191 .iter()
192 .map(|wc| match wc {
193 dpp::platform_value::Value::Array(components) => {
194 WhereClause::from_components(components)
195 }
196 _ => Err(Error::Query(QuerySyntaxError::InvalidFormatWhereClause(
197 "where clause must be an array".to_string(),
198 ))),
199 })
200 .collect::<Result<Vec<_>, _>>()?,
201 _ => {
202 return Err(Error::Query(QuerySyntaxError::InvalidFormatWhereClause(
203 "where clause must be an array".to_string(),
204 )));
205 }
206 };
207
208 validate_and_canonicalize_where_clauses(clauses, platform_version)
209}
210
211/// Parse the decoded `order_by` value into structured [`OrderClause`]s.
212///
213/// Same shape as [`where_clauses_from_value`] for `order_by`:
214/// `Value::Null` (empty `order_by` field on the wire) → no clauses;
215/// any other shape must be an outer array of `[field, direction]`
216/// inner arrays. Direction is `"asc"` / `"desc"` per
217/// `OrderClause::from_components`.
218pub fn order_clauses_from_value(
219 value: &dpp::platform_value::Value,
220) -> Result<Vec<OrderClause>, Error> {
221 match value {
222 dpp::platform_value::Value::Null => Ok(Vec::new()),
223 dpp::platform_value::Value::Array(clauses) => clauses
224 .iter()
225 .map(|oc| match oc {
226 dpp::platform_value::Value::Array(components) => {
227 // `OrderClause::from_components` returns
228 // `grovedb::Error`; wrap as drive's query-syntax
229 // error so the dispatcher's error contract stays
230 // uniform with the where-clause parser above.
231 OrderClause::from_components(components).map_err(|_e| {
232 Error::Query(QuerySyntaxError::InvalidFormatWhereClause(
233 "order_by clause must have [field, \"asc\"|\"desc\"] shape".to_string(),
234 ))
235 })
236 }
237 _ => Err(Error::Query(QuerySyntaxError::InvalidFormatWhereClause(
238 "order_by clause must be an array".to_string(),
239 ))),
240 })
241 .collect(),
242 _ => Err(Error::Query(QuerySyntaxError::InvalidFormatWhereClause(
243 "order_by clause must be an array".to_string(),
244 ))),
245 }
246}
247
248impl Drive {
249 /// Single entry point for the unified `GetDocumentsCount` request.
250 ///
251 /// Owns the whole pipeline:
252 /// 1. [`DriveDocumentCountQuery::detect_mode`] classifies the
253 /// query shape from the where clauses + flags.
254 /// 2. The matching `Drive::execute_document_count_*` per-mode
255 /// method picks an index and runs the executor.
256 /// 3. The result is wrapped in [`DocumentCountResponse`] —
257 /// `Counts(...)` for no-proof modes, `Proof(...)` for proof
258 /// modes.
259 ///
260 /// Errors:
261 /// - Mode-detection failures (multiple range clauses, range +
262 /// `In`, distinct on prove path, …) come back as
263 /// `Error::Query(QuerySyntaxError::InvalidWhereClauseComponents)`.
264 /// - "No covering index" failures come back as
265 /// `Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty)`.
266 /// - All other failures (grovedb, cost calculation, …) surface
267 /// as their native `Error` variants.
268 ///
269 /// The handler maps both `Error::Query(...)` cases to its own
270 /// `QueryError::Query(...)` variant uniformly.
271 pub fn execute_document_count_request(
272 &self,
273 request: DocumentCountRequest,
274 transaction: TransactionArg,
275 platform_version: &PlatformVersion,
276 ) -> Result<DocumentCountResponse, Error> {
277 use dpp::data_contract::accessors::v0::DataContractV0Getters;
278
279 // Validate + canonicalize the structured `where_clauses` —
280 // same rejections the regular document-query path runs,
281 // applied here so the count endpoint's shape contract is
282 // independent of whether the caller arrived via the CBOR-
283 // shaped legacy path or the v1 typed-proto path. See
284 // [`validate_and_canonicalize_where_clauses`]'s docstring
285 // for the catalog of rejections / canonicalization rules.
286 let where_clauses =
287 validate_and_canonicalize_where_clauses(request.where_clauses, platform_version)?;
288 let resolved_time_ranges = request.resolved_time_ranges;
289 validate_resolved_time_range_clause_shapes(&where_clauses, &resolved_time_ranges)?;
290 let order_clauses = request.order_clauses;
291
292 // Split-mode entry direction is whatever the first orderBy
293 // clause specifies. Empty orderBy → ascending default. Used
294 // by per-`In`-value, distinct-range no-proof, and
295 // distinct-range prove paths; the `PointLookupProof` and
296 // flat `Total` paths don't read it.
297 let order_by_ascending = order_clauses.first().map(|c| c.ascending).unwrap_or(true);
298
299 let mode = DriveDocumentCountQuery::detect_mode_versioned(
300 &where_clauses,
301 request.mode,
302 request.prove,
303 platform_version,
304 )?;
305
306 let contract_id = request.contract.id_ref().to_buffer();
307 let document_type_name = request.document_type.name().to_string();
308
309 match mode {
310 DocumentCountMode::Total => {
311 // Total mode → single aggregate. The executor returns
312 // at most one entry (with empty key); collapse to
313 // `Aggregate(count)` here so the response is a u64
314 // with no per-key wrapping. Empty result (indexed
315 // path doesn't exist yet) → `Aggregate(0)`.
316 let entries = self.execute_document_count_total_no_proof(
317 contract_id,
318 request.document_type,
319 document_type_name,
320 where_clauses,
321 &resolved_time_ranges,
322 transaction,
323 platform_version,
324 )?;
325 let total = entries.first().and_then(|e| e.count).unwrap_or(0);
326 Ok(DocumentCountResponse::Aggregate(total))
327 }
328 DocumentCountMode::PerInValue => {
329 // |In| ≤ 100 is the structural bound; failsafe cap
330 // keeps behavior independent of `default_query_limit`.
331 // See [`super::MAX_LIMIT_AS_FAILSAFE`].
332 let options = RangeCountOptions {
333 distinct: false, // ignored by PerInValue executor
334 limit: Some(super::MAX_LIMIT_AS_FAILSAFE),
335 order_by_ascending,
336 };
337 Ok(DocumentCountResponse::Entries(
338 self.execute_document_count_per_in_value_no_proof(
339 contract_id,
340 request.document_type,
341 document_type_name,
342 where_clauses,
343 &resolved_time_ranges,
344 options,
345 transaction,
346 platform_version,
347 )?,
348 ))
349 }
350 DocumentCountMode::RangeNoProof => {
351 // Aggregate → failsafe cap (per-In fan-out bounded by
352 // |In| ≤ 100); distinct walk → caller's limit with
353 // `default_query_limit` fallback since range is
354 // genuinely unbounded.
355 let effective_limit = if request.mode.is_aggregate() {
356 super::MAX_LIMIT_AS_FAILSAFE
357 } else {
358 let effective_limit = request
359 .limit
360 .unwrap_or(request.drive_config.default_query_limit as u32)
361 .min(request.drive_config.max_query_limit as u32);
362 // Fail closed instead of walking storage with a
363 // zero bound (grovedb treats `limit: 0` as "return
364 // nothing", which would masquerade as an empty
365 // result set). Mirrors the sum-side
366 // `effective_no_proof_distinct_limit` policy.
367 if effective_limit == 0 {
368 return Err(Error::Query(QuerySyntaxError::InvalidLimit(
369 "effective distinct COUNT limit must be greater than zero".to_string(),
370 )));
371 }
372 effective_limit
373 };
374 let options = RangeCountOptions {
375 distinct: request.mode.requires_distinct_walk(),
376 limit: Some(effective_limit),
377 order_by_ascending,
378 };
379 let entries = self.execute_document_count_range_no_proof(
380 contract_id,
381 request.document_type,
382 document_type_name,
383 where_clauses,
384 &resolved_time_ranges,
385 options,
386 transaction,
387 platform_version,
388 )?;
389 if request.mode.is_aggregate() {
390 // Aggregate mode: executor returns a single
391 // empty-key entry containing the sum (or empty
392 // vec if the path doesn't exist). Collapse to
393 // `Aggregate`.
394 let total = entries.first().and_then(|e| e.count).unwrap_or(0);
395 Ok(DocumentCountResponse::Aggregate(total))
396 } else {
397 Ok(DocumentCountResponse::Entries(entries))
398 }
399 }
400 DocumentCountMode::RangeProof => Ok(DocumentCountResponse::Proof(
401 self.execute_document_count_range_proof(
402 contract_id,
403 request.document_type,
404 document_type_name,
405 where_clauses,
406 &resolved_time_ranges,
407 transaction,
408 platform_version,
409 )?,
410 )),
411 DocumentCountMode::RangeDistinctProof => {
412 // Validate-don't-clamp limit policy on the prove
413 // path: client-side proof reconstruction needs the
414 // exact same limit value the server applied to the
415 // path query (so the merk-root recomputation
416 // matches). Silent clamping would invisibly break
417 // verification on any request with `limit >
418 // max_query_limit`.
419 //
420 // **Limit fallback uses `crate::config::DEFAULT_QUERY_LIMIT`
421 // (the compile-time constant), NOT
422 // `drive_config.default_query_limit` (the
423 // operator-tunable runtime value).** The SDK verifier
424 // can't know an operator's tuned config, so any
425 // operator who tuned `default_query_limit` away from
426 // `DEFAULT_QUERY_LIMIT` would produce proofs whose
427 // `SizedQuery::limit` byte-differs from the
428 // verifier's reconstruction — silent verify failure
429 // on a consensus-adjacent path. Anchoring the
430 // fallback to the shared compile-time constant
431 // removes that operator-tunable degree of freedom
432 // from proof bytes entirely; the runtime
433 // `default_query_limit` continues to govern no-proof
434 // dispatch paths where there's no verifier to match.
435 // `max_query_limit` still gates the request as a
436 // DoS-protection knob (proofs never cross the
437 // operator-set ceiling, but the ceiling itself doesn't
438 // affect proof bytes — it only decides whether the
439 // request gets served).
440 let effective_limit = request
441 .limit
442 .unwrap_or(crate::config::DEFAULT_QUERY_LIMIT as u32);
443 if effective_limit > request.drive_config.max_query_limit as u32 {
444 return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!(
445 "limit {} exceeds max_query_limit {} on the prove + \
446 distinct-walk path (GROUP BY a range field); reduce the \
447 requested limit or use prove = false",
448 effective_limit, request.drive_config.max_query_limit
449 ))));
450 }
451 let limit_u16 = effective_limit as u16;
452 // Default to ascending if the request didn't specify
453 // — matches the no-proof default. The verifier reads
454 // the same field to reconstruct the matching path
455 // query (see SDK's `FromProof<DocumentQuery>` impl
456 // for `DocumentSplitCounts`); both sides MUST land
457 // on the same `left_to_right` value or the merk-root
458 // recomputation fails.
459 let left_to_right = order_by_ascending;
460 Ok(DocumentCountResponse::Proof(
461 self.execute_document_count_range_distinct_proof(
462 contract_id,
463 request.document_type,
464 document_type_name,
465 where_clauses,
466 &resolved_time_ranges,
467 limit_u16,
468 left_to_right,
469 transaction,
470 platform_version,
471 )?,
472 ))
473 }
474 DocumentCountMode::PointLookupProof => Ok(DocumentCountResponse::Proof(
475 self.execute_document_count_point_lookup_proof(
476 contract_id,
477 request.document_type,
478 document_type_name,
479 where_clauses,
480 &resolved_time_ranges,
481 transaction,
482 platform_version,
483 )?,
484 )),
485 DocumentCountMode::RangeAggregateCarrierProof => {
486 // Validate-don't-clamp limit policy on the prove path
487 // (same rationale as `RangeDistinctProof` above): the
488 // verifier reconstructs the SizedQuery's `limit` byte-
489 // identically, so silent clamping would invisibly
490 // break verification.
491 //
492 // Two shape-dependent rules apply here:
493 //
494 // - **In-outer carrier (G7):** the caller's `|In|`
495 // already bounds the result. `SizedQuery::limit`
496 // stays `None`; if the caller passed a non-`None`
497 // `limit`, reject — there's no use case for a sub-
498 // `|In|` limit on this path, and accepting it would
499 // silently change which In-branches appear in the
500 // proof.
501 //
502 // - **Range-outer carrier (G8):** the platform
503 // enforces a max outer-walk cap of
504 // [`super::MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`]
505 // on how many outer-range matches the carrier walks.
506 // Caller may pass a smaller `limit` to truncate the
507 // walk further; passing a larger one is rejected.
508 // If the caller passes `None`, the platform default
509 // (the cap itself) is used.
510 let has_outer_range = where_clauses
511 .iter()
512 .filter(|wc| DriveDocumentCountQuery::is_range_operator(wc.operator))
513 .count()
514 == 2;
515 let effective_limit = if has_outer_range {
516 match request.limit {
517 None => Some(super::MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT),
518 Some(n) => {
519 if n > super::MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT as u32 {
520 return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!(
521 "carrier-aggregate range-outer queries (e.g. \
522 `outer_range_field > X AND inner_acor_field > \
523 Y` with `group_by = [outer_range_field]`) cap \
524 the outer walk at {} entries (compile-time \
525 constant `MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`); \
526 got limit = {}. Pass a value ≤ {} or omit \
527 `limit` to use the default.",
528 super::MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT,
529 n,
530 super::MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT,
531 ))));
532 }
533 if n == 0 {
534 return Err(Error::Query(QuerySyntaxError::InvalidLimit(
535 "carrier-aggregate range-outer queries require limit \
536 ≥ 1; got limit = 0"
537 .to_string(),
538 )));
539 }
540 Some(n as u16)
541 }
542 }
543 } else {
544 if let Some(n) = request.limit {
545 return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!(
546 "carrier-aggregate In-outer queries (e.g. `outer_in_field IN \
547 [...] AND inner_acor_field > Y` with `group_by = \
548 [outer_in_field]`) don't accept `limit` — the In array's \
549 length already bounds the result. Got limit = {n}.",
550 ))));
551 }
552 None
553 };
554 // Outer-walk direction: ascending by default (the
555 // grovedb invariant for serialized-key carriers), or
556 // descending when the caller's `order_by` first
557 // clause is `desc`. Carried byte-identically through
558 // `Query::left_to_right` so the verifier rebuilds the
559 // exact same `PathQuery` — same load-bearing pattern
560 // as the `RangeDistinctProof` arm above.
561 let left_to_right = order_by_ascending;
562 Ok(DocumentCountResponse::Proof(
563 self.execute_document_count_range_aggregate_carrier_proof(
564 contract_id,
565 request.document_type,
566 document_type_name,
567 where_clauses,
568 &resolved_time_ranges,
569 effective_limit,
570 left_to_right,
571 transaction,
572 platform_version,
573 )?,
574 ))
575 }
576 }
577 }
578}