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}