Skip to main content

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}