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