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