drive/query/drive_document_count_query/mod.rs
1//! Types and module structure for the `GetDocumentsCount` query.
2//!
3//! The implementation is split across siblings:
4//! - [`mode_detection`] — operator classification + `detect_mode`.
5//! - [`index_picker`] — covering-index pickers
6//! (`find_countable_index_*`, `find_range_countable_index_*`).
7//! - [`path_query`] — the load-bearing prover/verifier-agreement
8//! path-query builders (`aggregate_count_path_query`,
9//! `distinct_count_path_query`, `range_clause_to_query_item`).
10//! - [`execute_point_lookup`] — Equal/In point-lookup execution
11//! (`execute_no_proof`, `execute_with_proof`).
12//! - [`execute_range_count`] — range-mode execution + `RangeCountOptions`.
13//! - [`drive_dispatcher`] — `impl Drive` per-mode dispatchers +
14//! `DocumentCountRequest` / `DocumentCountResponse` +
15//! `execute_document_count_request`.
16//! - [`tests`] (cfg `server` + `test`) — integration tests.
17//!
18//! This file owns the three public types every other submodule
19//! references and the corresponding `mod` / `pub use` plumbing.
20
21use dpp::data_contract::document_type::{DocumentTypeRef, Index};
22
23use super::conditions::WhereClause;
24use super::drive_document_sum_query::{DriveDocumentSumQuery, SumEntry};
25use crate::error::Error;
26
27// Re-exports for the submodules and the `tests` module's
28// `use super::*;`. `WhereOperator` is used by every submodule that
29// builds path queries or executes; `QuerySyntaxError` is the canonical
30// error variant the mode detector and dispatchers surface.
31#[cfg(any(feature = "server", feature = "verify"))]
32pub use super::conditions::WhereOperator;
33#[cfg(any(feature = "server", feature = "verify"))]
34pub use crate::error::query::QuerySyntaxError;
35
36pub mod mode_detection;
37// Index pickers + path-query builders are reachable from both the
38// server prove path and the SDK proof verifier; their submodule cfgs
39// match.
40pub mod index_picker;
41pub mod path_query;
42
43// Server-side execution paths.
44#[cfg(feature = "server")]
45pub mod drive_dispatcher;
46#[cfg(feature = "server")]
47pub mod execute_point_lookup;
48#[cfg(feature = "server")]
49pub mod execute_range_count;
50#[cfg(feature = "server")]
51pub mod executors;
52
53#[cfg(feature = "server")]
54pub use drive_dispatcher::{DocumentCountRequest, DocumentCountResponse};
55#[cfg(feature = "server")]
56pub use execute_range_count::RangeCountOptions;
57
58/// Hard cap on entries the count fan-out arms ask the executor
59/// to return.
60///
61/// Count fan-out (`PerInValue`, and the Aggregate + range
62/// sub-case of `RangeNoProof`) emits at most one entry per `In`
63/// value, and `In` is structurally capped at 100 by
64/// [`super::conditions::WhereClause::in_values`]. This cap sits
65/// well above the real bound. Two reasons to pin it explicitly
66/// instead of leaning on the operator-tunable
67/// `default_query_limit`:
68///
69/// 1. `default_query_limit` is a documents-fetch knob — applying
70/// it to count fan-out can truncate aggregate sums below |In|
71/// under tighter operator tuning, silently producing wrong
72/// totals.
73/// 2. Pinning a number here keeps the dispatcher's correctness
74/// independent of operator configuration.
75///
76/// `1024` is high enough that the cap never fires under the
77/// current `WhereClause::in_values` policy. If a future code
78/// change makes it reachable, treat that as a signal to revisit
79/// the bound before raising the constant.
80///
81/// # Pattern: failsafe cap for structurally-bounded ops
82///
83/// This is the prototype of a small project convention: when an
84/// executor-level operation has a structural upper bound enforced
85/// upstream (here, `WhereClause::in_values()`'s 100-cap on the In
86/// array), pin a failsafe cap at the executor boundary that sits
87/// well above the upstream bound rather than reusing an unrelated
88/// operator-tunable limit. The failsafe never fires under the
89/// upstream constraint — it exists to (a) keep behavior
90/// independent of operator config, and (b) localize the blast
91/// radius if the upstream constraint ever loosens. Constants
92/// added under this pattern should follow the
93/// `MAX_<OPERATION>_AS_FAILSAFE` naming so the role is visible
94/// at the use site.
95#[cfg(feature = "server")]
96pub const MAX_LIMIT_AS_FAILSAFE: u32 = 1024;
97
98/// Platform-wide **maximum** outer-walk cap for carrier-aggregate
99/// range-outer proofs (chapter 30 G8: `outer_range_field > X AND
100/// inner_acor_field > Y` with `group_by = [outer_range_field]` and
101/// `prove = true`).
102///
103/// The cap bounds the proof size: bytes grow linearly with the
104/// number of outer matches (~1 700 B per outer key in this
105/// chapter's widget fixture; `10 × 1 700 B ≈ 17 KB` worst case).
106/// 10 keeps the worst-case proof comfortably inside Tier-1 of the
107/// visualizer's shareable-link guidance (< 20 KB).
108///
109/// **Caller semantics:**
110/// - `request.limit = None` → server uses `MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`
111/// (the default).
112/// - `request.limit = Some(n)` with `n ≤ MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`
113/// → accepted; the dispatcher passes `n` through to
114/// `SizedQuery::limit` so the prover walks exactly `n` outer matches.
115/// - `request.limit = Some(n)` with `n > MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`
116/// → rejected with `InvalidLimit`. The cap is a hard ceiling: callers
117/// that want more results must call repeatedly with disjoint
118/// outer-range windows.
119///
120/// Why the ceiling is a hardcoded compile-time constant rather
121/// than `drive_config.max_query_limit` (the operator-tunable
122/// runtime value): on the prove path, `SizedQuery::limit` is
123/// part of the serialized `PathQuery` and feeds the merk-root
124/// reconstruction. Anchoring the ceiling to a compile-time
125/// constant guarantees prover and verifier agree on what the
126/// "default when None" value is, regardless of operator config
127/// (same rationale as `RangeDistinctProof`'s use of
128/// `crate::config::DEFAULT_QUERY_LIMIT`).
129pub const MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT: u16 = 10;
130
131impl DriveDocumentCountQuery<'_> {
132 /// The `SizedQuery` limit a carrier-aggregate count proof walks with,
133 /// given the request's `limit`. The server's dispatcher and the SDK's
134 /// verifier both take it from here, so they build the same path query:
135 ///
136 /// - Range-outer carrier (two range clauses): `None` means
137 /// [`MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`]; `Some(n)` is taken for
138 /// `1 <= n <= MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT` and refused
139 /// otherwise.
140 /// - `In`-outer carrier: the `In` array bounds the walk, so the limit is
141 /// `None`, and a request carrying one is refused.
142 pub fn carrier_aggregate_count_limit(
143 where_clauses: &[WhereClause],
144 limit: Option<u32>,
145 ) -> Result<Option<u16>, Error> {
146 let has_outer_range = where_clauses
147 .iter()
148 .filter(|wc| Self::is_range_operator(wc.operator))
149 .count()
150 == 2;
151 if !has_outer_range {
152 return match limit {
153 Some(n) => Err(Error::Query(QuerySyntaxError::InvalidLimit(format!(
154 "carrier-aggregate In-outer queries (e.g. `outer_in_field IN \
155 [...] AND inner_acor_field > Y` with `group_by = \
156 [outer_in_field]`) don't accept `limit` — the In array's \
157 length already bounds the result. Got limit = {n}.",
158 )))),
159 None => Ok(None),
160 };
161 }
162 match limit {
163 None => Ok(Some(MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT)),
164 Some(n) if n > MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT as u32 => {
165 Err(Error::Query(QuerySyntaxError::InvalidLimit(format!(
166 "carrier-aggregate range-outer queries (e.g. \
167 `outer_range_field > X AND inner_acor_field > \
168 Y` with `group_by = [outer_range_field]`) cap \
169 the outer walk at {} entries (compile-time \
170 constant `MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`); \
171 got limit = {}. Pass a value ≤ {} or omit \
172 `limit` to use the default.",
173 MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT,
174 n,
175 MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT,
176 ))))
177 }
178 Some(0) => Err(Error::Query(QuerySyntaxError::InvalidLimit(
179 "carrier-aggregate range-outer queries require limit \
180 ≥ 1; got limit = 0"
181 .to_string(),
182 ))),
183 Some(n) => Ok(Some(n as u16)),
184 }
185 }
186}
187
188#[cfg(feature = "server")]
189#[cfg(test)]
190mod tests;
191
192/// A query to count documents using CountTree elements in the index path.
193///
194/// This struct encapsulates all the information needed to perform a count
195/// query on a document type's countable index.
196#[derive(Debug, Clone)]
197pub struct DriveDocumentCountQuery<'a> {
198 /// The document type to count
199 pub document_type: DocumentTypeRef<'a>,
200 /// The contract id (32 bytes)
201 pub contract_id: [u8; 32],
202 /// The document type name
203 pub document_type_name: String,
204 /// The countable index to use
205 pub index: &'a Index,
206 /// The equality where clauses that match index prefix properties
207 pub where_clauses: Vec<WhereClause>,
208}
209
210/// Turns the `(path, key, element)` triples a point-lookup count path
211/// query yields (see `point_lookup_count_path_query`) into one entry per
212/// count tree. For compound (`In`) shapes the `In` value sits at
213/// `path[base_path_len]` when the walk descended past the base path (the
214/// `In` + trailing `Equal`s shape) and IS the key otherwise (the
215/// `In`-on-terminator shape); `Equal`-only shapes have no per-key
216/// dimension. The element's document count ([`document_count_of_element`])
217/// is the per-branch count; an absent element becomes `count: None`. ONE
218/// decoder for every reader of that layout — the proof verifier, the
219/// no-proof executor and composite queries — so the layout has one owner.
220pub fn point_lookup_count_entries(
221 index: &Index,
222 base_path_len: usize,
223 has_in_clause: bool,
224 elements: impl IntoIterator<Item = (Vec<Vec<u8>>, Vec<u8>, Option<grovedb::Element>)>,
225) -> Vec<SplitCountEntry> {
226 elements
227 .into_iter()
228 .map(|(path, grove_key, element)| {
229 let key = if has_in_clause {
230 if path.len() > base_path_len {
231 path[base_path_len].clone()
232 } else {
233 grove_key
234 }
235 } else {
236 Vec::new()
237 };
238 // A proof returns a tree element as stored, wrapper included,
239 // while the unproven read unwraps it: a prefix-to-last read's
240 // tree sits wrapped to contribute nothing under an aggregating
241 // value tree, so the decode looks through the wrapper. Before
242 // protocol version 14 the only wrapper a count read can reach is
243 // `NotSummed` (a summing index ending at the pinned level, the
244 // read's tree continuing below it), whose count grovedb passes
245 // through, so those decode as before
246 // (`should_count_through_a_wrapped_tree_unchanged_at_protocol_version_13`).
247 SplitCountEntry {
248 in_key: None,
249 key,
250 count: element
251 .map(|element| document_count_of_element(index, element.underlying())),
252 }
253 })
254 .collect()
255}
256
257/// The number of documents an element a count read of `index` reaches
258/// stands for: its count, or, on a `summableOffCountIndex` index, its sum.
259/// Such an index keeps one counter per group, which counts one in its count
260/// trees and adds its group's documents to their sums, and its registration
261/// rules make each counter equal its source group's entries, so its sums are
262/// the document counts.
263///
264/// Unversioned, so every protocol version reaches it: it departs from the
265/// plain count only on a `summableOffCountIndex` index, which only
266/// meta-schema v3 (protocol version 14) admits.
267pub fn document_count_of_element(index: &Index, element: &grovedb::Element) -> u64 {
268 if index.is_summable_off_count_index() {
269 counter_sum_as_document_count(element.sum_value_or_default())
270 } else {
271 element.count_value_or_default()
272 }
273}
274
275/// A `summableOffCountIndex` index's sum read as a document count. A
276/// counter is never negative: it counts entries.
277pub fn counter_sum_as_document_count(sum: i64) -> u64 {
278 u64::try_from(sum).unwrap_or_default()
279}
280
281/// A sum entry read through [`DriveDocumentCountQuery::counter_sums_query`],
282/// as the count entry it stands for.
283pub fn counter_sum_entry_as_count_entry(entry: SumEntry) -> SplitCountEntry {
284 SplitCountEntry {
285 in_key: entry.in_key,
286 key: entry.key,
287 count: entry.sum.map(counter_sum_as_document_count),
288 }
289}
290
291impl<'a> DriveDocumentCountQuery<'a> {
292 /// The sum query a range count over a `summableOffCountIndex` index
293 /// reads in its place. Its counters each count one group in its count
294 /// trees, so a range count over them would count groups, while their
295 /// sums are its document counts: the sum of the source index's entries
296 /// (the summed value the index names) over the same index and clauses.
297 /// The count's range forms (aggregate, per value, per `In` branch) and
298 /// their proofs are then the sum surface's, read back as counts. `None`
299 /// on any other index.
300 ///
301 /// Unversioned, so every protocol version reaches it: it is `Some` only
302 /// on a `summableOffCountIndex` index, which only meta-schema v3
303 /// (protocol version 14) admits.
304 pub fn counter_sums_query(&self) -> Option<DriveDocumentSumQuery<'a>> {
305 let source = self.index.summable_off_count_index.as_ref()?;
306 Some(DriveDocumentSumQuery {
307 document_type: self.document_type,
308 contract_id: self.contract_id,
309 document_type_name: self.document_type_name.clone(),
310 index: self.index,
311 where_clauses: self.where_clauses.clone(),
312 sum_property: source.clone(),
313 })
314 }
315}
316
317/// The position of the shallowest level of `index` whose value trees a
318/// count read may stop at, taking the subtree's document count: the count
319/// chain's ([`Index::shallowest_count_chain_position`]), or on a
320/// `summableOffCountIndex` index the sum chain's, whose sums are its document
321/// counts. `None` without such a chain.
322///
323/// Unversioned, so every protocol version reaches it: it departs from the
324/// plain count chain only on a `summableOffCountIndex` index, which only
325/// meta-schema v3 (protocol version 14) admits.
326pub fn document_count_chain_position(index: &Index) -> Option<usize> {
327 if index.is_summable_off_count_index() {
328 index.shallowest_sum_chain_position()
329 } else {
330 index.shallowest_count_chain_position()
331 }
332}
333
334/// Whether a point count read of `index` yields document counts: a countable
335/// index's count trees count its documents, and so do a
336/// `summableOffCountIndex` index's sums, which the read takes instead
337/// ([`document_count_of_element`]). The picker and the path builder both ask
338/// this, so they agree on the element read.
339///
340/// Unversioned, so every protocol version reaches it: it departs from the
341/// plain `countable` test only on a `summableOffCountIndex` index, which only
342/// meta-schema v3 (protocol version 14) admits.
343pub(crate) fn point_count_reads_documents(index: &Index) -> bool {
344 index.countable.is_countable() || index.is_summable_off_count_index()
345}
346
347/// Whether a count read of `index` may stop at its last property's tree,
348/// reading that tree's own element as the whole prefix's document count: the
349/// tree is count-bearing (`rangeCountable`, which implies `countable`), or
350/// sum-bearing on a `summableOffCountIndex` index, and not ranked. A ranked
351/// axis makes it an INDEXED tree, which grovedb's query dispatch refuses to
352/// return as a result element ("path_queries can not refer to trees"). The
353/// picker and the path builder both ask this, so they agree on the form.
354///
355/// Unversioned, so every protocol version reaches it: it departs from the
356/// plain `rangeCountable` test only on a `summableOffCountIndex` index, which only
357/// meta-schema v3 (protocol version 14) admits.
358pub(crate) fn prefix_to_last_count_reads_documents(index: &Index) -> bool {
359 terminal_reads_documents(index) && !index.ranks_its_last_property()
360}
361
362/// Whether the tree of `index`'s last property reads documents: a
363/// `rangeCountable` index's counts them (it implies `countable`; grovedb's
364/// `AggregateCountOnRange` for a range), and a `summableOffCountIndex`
365/// index's sums do (such an index is always `rangeSummable`; read through
366/// [`DriveDocumentCountQuery::counter_sums_query`] for a range, since its
367/// count trees count groups). The range picker and the prefix-to-last form
368/// both ask this.
369///
370/// Unversioned, so every protocol version reaches it: it departs from the
371/// plain `rangeCountable` test only on a `summableOffCountIndex` index, which
372/// only meta-schema v3 (protocol version 14) admits.
373pub(crate) fn terminal_reads_documents(index: &Index) -> bool {
374 (index.range_countable && index.countable.is_countable()) || index.is_summable_off_count_index()
375}
376
377/// An entry in a split count result, containing the serialized
378/// key(s) and the count of documents matching them.
379///
380/// For flat queries (per-`In`-value mode without a range, or
381/// per-distinct-value-in-range mode without an `In` on prefix) only
382/// `key` is meaningful and `in_key` is `None`.
383///
384/// For compound range-distinct queries (an `In` clause on a prefix
385/// property plus a range on the terminator) BOTH keys are carried:
386/// `in_key` is the In-fork's prefix value and `key` is the
387/// terminator value. Cross-fork aggregation is intentionally NOT
388/// done server-side — emitting the unmerged per-(in_key, key) shape
389/// lets `limit` push directly into grovedb (no pre-merge issue),
390/// keeps proof verification straightforward (no absence-proof
391/// gymnastics for omitted In branches), and gives callers strictly
392/// more information than a flat histogram. Callers reduce
393/// client-side when they want the sum.
394#[derive(Debug, Clone, PartialEq, Eq)]
395pub struct SplitCountEntry {
396 /// The serialized prefix key for compound queries (the `In`
397 /// value for this fork). `None` for flat queries.
398 pub in_key: Option<Vec<u8>>,
399 /// The serialized terminator/value key for this entry.
400 pub key: Vec<u8>,
401 /// The count of documents matching this `(in_key, key)` tuple
402 /// (or just `key` for flat queries).
403 ///
404 /// Three-valued by design:
405 /// - `Some(n)` with `n > 0` — verified count for an entry the
406 /// underlying data path materialized.
407 /// - `Some(0)` — caller queried this branch and the executor
408 /// confirmed zero matching documents. Emitted by the no-proof
409 /// point-lookup path's aggregated total wrapper (a single
410 /// summed entry whose value can be 0), by the no-proof range
411 /// executors when their walk returns nothing, by the per-`In`
412 /// no-proof fan-out for a branch matching nothing, and, on an
413 /// index that can hold an empty group (see
414 /// `index_keeps_empty_groups`), for each empty group a range
415 /// walk or a point lookup reads, proved or not, per `In` branch
416 /// included.
417 /// - `None` — reserved for a future absence-proof variant. The
418 /// current `point_lookup_count_path_query` doesn't set
419 /// `absence_proofs_for_non_existing_searched_keys: true`, so
420 /// absent In branches are **omitted from the verified entry
421 /// list entirely** (grovedb's `verify_query` doesn't surface
422 /// `(path, key, None)` triples for them). Callers that need to
423 /// distinguish "queried but absent" diff the request's In array
424 /// against the returned entries by key. The variant exists in
425 /// the type signature so a future path-query change that flips
426 /// the flag surfaces absences via `count: None` without a
427 /// breaking struct change — distinguishable from `Some(0)`,
428 /// which only a materialized empty group (a preallocation's) or a
429 /// zero total produces.
430 pub count: Option<u64>,
431}
432
433/// SQL-shaped count-query mode — names the response shape the
434/// caller asked for via `(select, group_by)` on the wire.
435///
436/// **Two count-mode enums coexist in this module.** This one names
437/// the *output shape* the request produces (single aggregate vs
438/// per-group entries). [`DocumentCountMode`] below names the
439/// *executor strategy* (which proof primitive / which walk path
440/// Drive uses to compute that shape). `CountMode` lives on
441/// [`DocumentCountRequest`] as the caller-supplied contract;
442/// `DocumentCountMode` is derived from `(CountMode, where_clauses,
443/// prove)` by [`DriveDocumentCountQuery::detect_mode`] just before
444/// dispatch.
445///
446/// **Result shape vs. executor strategy.** Each variant names a
447/// result shape — the per-variant docstring lists the
448/// where-clause shapes that route to that result shape and
449/// notes which executor strategy
450/// [`DriveDocumentCountQuery::detect_mode`] picks for each.
451/// `(in_field, range_field)` combinations on the same request
452/// are accepted on multiple `CountMode` variants — the executor
453/// strategy distinguishes them. Upstream routing
454/// (drive-abci's `validate_and_route`) picks the `CountMode`
455/// from the caller's `group_by`; downstream `detect_mode`
456/// converts the `(CountMode, where_clauses, prove)` triple into
457/// the resolved [`DocumentCountMode`].
458#[derive(Debug, Clone, Copy, PartialEq, Eq)]
459pub enum CountMode {
460 /// `select=COUNT, group_by=[]`. Single u64 result.
461 ///
462 /// Where-clause shapes accepted:
463 /// - empty (relies on a `documentsCountable: true` doctype),
464 /// - Equal-only (fully covered by a `countable: true` index),
465 /// - one `In` (per-In fan-out, summed server-side),
466 /// - one range (uses `AggregateCountOnRange` for prove,
467 /// `RangeNoProof` for no-proof),
468 /// - one `In` + one range on the no-proof path (per-In fan-out
469 /// each doing a range walk; prove is rejected).
470 ///
471 /// `limit` is structurally meaningless (aggregate is one row)
472 /// and is rejected upstream when set.
473 Aggregate,
474
475 /// `select=COUNT, group_by=[in_field]`. One entry per `In` value.
476 ///
477 /// Where-clause shapes accepted:
478 /// - one `In` clause on `group_by[0]` (no range clause): the
479 /// canonical shape — routes to `PointLookupProof` on the
480 /// prove path, `PerInValue` on the no-proof path.
481 /// - one `In` on `group_by[0]` AND a range clause on a
482 /// different field: routes to
483 /// `RangeAggregateCarrierProof` on the prove path
484 /// (grovedb #663 carrier-ACOR — one verified `u64` per
485 /// In branch, range collapsed) and `RangeNoProof` on the
486 /// no-prove path, which runs the per-In-branch fan-out and
487 /// folds the branches into ONE entry (`in_key: None`, the
488 /// total), as the sum and average dispatchers do: only the
489 /// proved answer carries one entry per `In` value.
490 ///
491 /// `limit` is rejected upstream when set. The In array is
492 /// already capped at 100 entries by `WhereClause::in_values()`,
493 /// so the result size is bounded by construction; a separate
494 /// `limit` would either be redundant (≤ 100) or would silently
495 /// truncate the proof to fewer In branches than the caller
496 /// asked for (because the PointLookupProof path can't represent
497 /// a partial-In-array selection in its `SizedQuery`). Callers
498 /// that want fewer branches narrow the In array directly.
499 GroupByIn,
500
501 /// `select=COUNT, group_by=[range_field]`. One entry per distinct
502 /// value within the range.
503 ///
504 /// Where-clause shapes accepted:
505 /// - one range clause on `group_by[0]` (no `In` clause):
506 /// canonical RangeDistinctProof / RangeNoProof distinct.
507 /// - one range on `group_by[0]` AND an `In` clause on a
508 /// different field: prove path keeps `RangeDistinctProof`
509 /// with In-fanout via grovedb subquery; no-prove path uses
510 /// `RangeNoProof` distinct on the merged result. Per-
511 /// distinct-value entries cover both branches of the In.
512 /// - two range clauses on different fields, the second
513 /// being `group_by[0]`: routes to
514 /// `RangeAggregateCarrierProof` (outer range + inner-ACOR
515 /// carrier per grovedb #664 outer-range cap). See
516 /// `outer_range_plus_inner_range_with_prove_and_group_by_range_routes_to_carrier_proof`
517 /// for the regression test pinning this shape.
518 ///
519 /// `limit` caps the number of distinct values; on the prove
520 /// path it's validated-not-clamped (oversized values rejected
521 /// with `InvalidLimit`).
522 GroupByRange,
523
524 /// `select=COUNT, group_by=[in_field, range_field]`. One entry
525 /// per `(in_key, range_key)` pair.
526 ///
527 /// Where-clause invariants: an `In` clause on `group_by[0]`
528 /// AND a range clause on `group_by[1]` (match-any over
529 /// the where-clauses list — clause ordering on the wire
530 /// doesn't affect routing).
531 /// `limit` is a **global cap on the emitted `(in_key, key)` lex
532 /// stream**, not per-In-branch. The executor pushes a single
533 /// `SizedQuery::limit` over the compound walk, so a request
534 /// with `|In| = 3` and `limit = 5` returns at most 5 entries
535 /// total across all In branches (ordered by `(in_key, key)`,
536 /// direction from the first `order_by` clause). On the prove
537 /// path it's validated-not-clamped (oversized values rejected
538 /// with `InvalidLimit`).
539 GroupByCompound,
540}
541
542impl CountMode {
543 /// `true` for [`Self::Aggregate`] (single-row response);
544 /// `false` for the three grouped variants. See each variant's
545 /// docstring for the per-shape semantics.
546 pub fn is_aggregate(self) -> bool {
547 matches!(self, Self::Aggregate)
548 }
549
550 /// `true` for [`Self::GroupByRange`] and [`Self::GroupByCompound`]
551 /// — the two variants whose proof shape requires per-distinct-
552 /// value `KVCount` ops. See each variant's docstring for the
553 /// per-shape proof routing.
554 pub fn requires_distinct_walk(self) -> bool {
555 matches!(self, Self::GroupByRange | Self::GroupByCompound)
556 }
557
558 /// `true` for [`Self::GroupByRange`] and [`Self::GroupByCompound`]
559 /// — the two variants whose result size isn't structurally
560 /// bounded. [`Self::Aggregate`] and [`Self::GroupByIn`] reject
561 /// `limit` upstream; see each variant's docstring for the
562 /// per-shape reasoning.
563 pub fn accepts_limit(self) -> bool {
564 matches!(self, Self::GroupByRange | Self::GroupByCompound)
565 }
566}
567
568/// Classification of a count query's shape, used to dispatch to the
569/// right executor. Returned by
570/// [`DriveDocumentCountQuery::detect_mode`].
571///
572/// The discriminator is purely a function of the where-clause
573/// operators + the caller's [`CountMode`] + `prove`; it does not
574/// depend on the contract's index set. Picking a covering index for
575/// the chosen mode is a separate step that requires the document
576/// type's `BTreeMap<String, Index>`.
577#[derive(Debug, Clone, Copy, PartialEq, Eq)]
578pub enum DocumentCountMode {
579 /// No range, no `In` — single summed entry with empty key. Reads
580 /// the `CountTree` count directly at the indexed path.
581 Total,
582 /// Exactly one `In` clause, no range — one entry per (deduped)
583 /// `In` value, each computed as the count at that single value.
584 /// The `In` doubles as the per-value split signal.
585 PerInValue,
586 /// Exactly one range clause, no proof — walks the property-name
587 /// `ProvableCountTree`'s children inside the range. Returns either
588 /// a single summed entry or per-distinct-value entries depending
589 /// on whether the caller's [`CountMode`] requires a distinct walk
590 /// ([`CountMode::GroupByRange`] / [`CountMode::GroupByCompound`])
591 /// or not ([`CountMode::Aggregate`]).
592 RangeNoProof,
593 /// Exactly one range clause + `prove = true` +
594 /// [`CountMode::Aggregate`] — produces a grovedb
595 /// `AggregateCountOnRange` proof that verifies to a single u64.
596 /// The merk-level primitive returns one aggregate; per-distinct-
597 /// value entries with proof go through [`Self::RangeDistinctProof`]
598 /// instead.
599 RangeProof,
600 /// Exactly one range clause + `prove = true` +
601 /// [`CountMode::GroupByRange`] or [`CountMode::GroupByCompound`]
602 /// — produces a regular range proof against the property-name
603 /// `ProvableCountTree`. The
604 /// proof's `KVCount(key, value, count)` ops carry per-distinct-
605 /// value counts, each cryptographically committed via
606 /// `node_hash_with_count` to the merk root. The verifier walks the
607 /// proof op stream and emits a per-key count map, no opt-in
608 /// aggregate-collapse wrapper. Proof size is O(distinct values
609 /// matched) rather than the O(log n) of [`Self::RangeProof`], but
610 /// still much smaller than materialize-and-count.
611 RangeDistinctProof,
612 /// No range clause + `prove = true` — produces a per-branch
613 /// `Element::CountTree` proof. Either an unfiltered total
614 /// (`documents_countable: true` fast path, proving the
615 /// doctype's primary-key CountTree directly) or a covered
616 /// Equal/`In` lookup against a `countable: true` index (proving
617 /// one CountTree element per matched branch via
618 /// [`DriveDocumentCountQuery::point_lookup_count_path_query`]).
619 /// Proof size is O(k × log n) where k is the number of covered
620 /// branches (1 for the empty-where fast path and Equal-only
621 /// fully-covered case; ≤ |In values| for In-on-prefix). No
622 /// document materialization, no `u16::MAX` matching-docs cap —
623 /// the merk-level `count_value` IS the result, the SDK
624 /// extracts it via `verify_point_lookup_count_proof`.
625 PointLookupProof,
626 /// Exactly one `In` clause + one range clause + `prove = true`
627 /// + [`CountMode::GroupByIn`] — produces a grovedb carrier
628 /// `AggregateCountOnRange` proof: one outer-key descent per
629 /// `In` value, each terminating in an ACOR boundary walk over
630 /// the per-branch range subtree. Returns one `(in_key, u64)`
631 /// pair per resolved In branch — same per-key aggregate
632 /// semantics as the no-proof per-In fan-out, just verifiable.
633 ///
634 /// Proof size is `O(|In values| · (log B + log C'))` where `B`
635 /// is the In-property's distinct-value count and `C'` is the
636 /// terminator subtree's distinct-value count. Smaller than the
637 /// alternative [`Self::RangeDistinctProof`] (which scales with
638 /// the number of distinct in-range terminator values per
639 /// branch, not per-branch log-bound boundary nodes) and
640 /// preserves per-In aggregate granularity that GROUP BY
641 /// `[in_field, range_field]` can't express.
642 ///
643 /// Path-query shape (see
644 /// [`DriveDocumentCountQuery::carrier_aggregate_count_path_query`]):
645 /// outer Keys = serialized In values; subquery_path = ranged
646 /// property name; subquery = ACOR(range). Verified via
647 /// [`grovedb::GroveDb::verify_aggregate_count_query_per_key`]
648 /// (returns `Vec<(Vec<u8>, u64)>`).
649 ///
650 /// Enabled by grovedb PR #663 ("allow AggregateCountOnRange as
651 /// carrier subquery"). Before that PR this shape was rejected
652 /// in [`Self::detect_mode`] with the message "range count
653 /// queries with an `in` clause are not supported on the
654 /// aggregate prove path".
655 RangeAggregateCarrierProof,
656}