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