Skip to main content

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}