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;
24
25// Re-exports for the submodules and the `tests` module's
26// `use super::*;`. `WhereOperator` is used by every submodule that
27// builds path queries or executes; `QuerySyntaxError` is the canonical
28// error variant the mode detector and dispatchers surface.
29#[cfg(any(feature = "server", feature = "verify"))]
30pub use super::conditions::WhereOperator;
31#[cfg(any(feature = "server", feature = "verify"))]
32pub use crate::error::query::QuerySyntaxError;
33
34pub mod mode_detection;
35// Index pickers + path-query builders are reachable from both the
36// server prove path and the SDK proof verifier; their submodule cfgs
37// match.
38pub mod index_picker;
39pub mod path_query;
40
41// Server-side execution paths.
42#[cfg(feature = "server")]
43pub mod drive_dispatcher;
44#[cfg(feature = "server")]
45pub mod execute_point_lookup;
46#[cfg(feature = "server")]
47pub mod execute_range_count;
48#[cfg(feature = "server")]
49pub mod executors;
50
51#[cfg(feature = "server")]
52pub use drive_dispatcher::{DocumentCountRequest, DocumentCountResponse};
53#[cfg(feature = "server")]
54pub use execute_range_count::RangeCountOptions;
55
56/// Hard cap on entries the count fan-out arms ask the executor
57/// to return.
58///
59/// Count fan-out (`PerInValue`, and the Aggregate + range
60/// sub-case of `RangeNoProof`) emits at most one entry per `In`
61/// value, and `In` is structurally capped at 100 by
62/// [`super::conditions::WhereClause::in_values`]. This cap sits
63/// well above the real bound. Two reasons to pin it explicitly
64/// instead of leaning on the operator-tunable
65/// `default_query_limit`:
66///
67/// 1. `default_query_limit` is a documents-fetch knob — applying
68///    it to count fan-out can truncate aggregate sums below |In|
69///    under tighter operator tuning, silently producing wrong
70///    totals.
71/// 2. Pinning a number here keeps the dispatcher's correctness
72///    independent of operator configuration.
73///
74/// `1024` is high enough that the cap never fires under the
75/// current `WhereClause::in_values` policy. If a future code
76/// change makes it reachable, treat that as a signal to revisit
77/// the bound before raising the constant.
78///
79/// # Pattern: failsafe cap for structurally-bounded ops
80///
81/// This is the prototype of a small project convention: when an
82/// executor-level operation has a structural upper bound enforced
83/// upstream (here, `WhereClause::in_values()`'s 100-cap on the In
84/// array), pin a failsafe cap at the executor boundary that sits
85/// well above the upstream bound rather than reusing an unrelated
86/// operator-tunable limit. The failsafe never fires under the
87/// upstream constraint — it exists to (a) keep behavior
88/// independent of operator config, and (b) localize the blast
89/// radius if the upstream constraint ever loosens. Constants
90/// added under this pattern should follow the
91/// `MAX_<OPERATION>_AS_FAILSAFE` naming so the role is visible
92/// at the use site.
93#[cfg(feature = "server")]
94pub const MAX_LIMIT_AS_FAILSAFE: u32 = 1024;
95
96/// Platform-wide **maximum** outer-walk cap for carrier-aggregate
97/// range-outer proofs (chapter 30 G8: `outer_range_field > X AND
98/// inner_acor_field > Y` with `group_by = [outer_range_field]` and
99/// `prove = true`).
100///
101/// The cap bounds the proof size: bytes grow linearly with the
102/// number of outer matches (~1 700 B per outer key in this
103/// chapter's widget fixture; `10 × 1 700 B ≈ 17 KB` worst case).
104/// 10 keeps the worst-case proof comfortably inside Tier-1 of the
105/// visualizer's shareable-link guidance (< 20 KB).
106///
107/// **Caller semantics:**
108/// - `request.limit = None` → server uses `MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`
109///   (the default).
110/// - `request.limit = Some(n)` with `n ≤ MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`
111///   → accepted; the dispatcher passes `n` through to
112///   `SizedQuery::limit` so the prover walks exactly `n` outer matches.
113/// - `request.limit = Some(n)` with `n > MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`
114///   → rejected with `InvalidLimit`. The cap is a hard ceiling: callers
115///   that want more results must call repeatedly with disjoint
116///   outer-range windows.
117///
118/// Why the ceiling is a hardcoded compile-time constant rather
119/// than `drive_config.max_query_limit` (the operator-tunable
120/// runtime value): on the prove path, `SizedQuery::limit` is
121/// part of the serialized `PathQuery` and feeds the merk-root
122/// reconstruction. Anchoring the ceiling to a compile-time
123/// constant guarantees prover and verifier agree on what the
124/// "default when None" value is, regardless of operator config
125/// (same rationale as `RangeDistinctProof`'s use of
126/// `crate::config::DEFAULT_QUERY_LIMIT`).
127pub const MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT: u16 = 10;
128
129#[cfg(feature = "server")]
130#[cfg(test)]
131mod tests;
132
133/// A query to count documents using CountTree elements in the index path.
134///
135/// This struct encapsulates all the information needed to perform a count
136/// query on a document type's countable index.
137#[derive(Debug, Clone)]
138pub struct DriveDocumentCountQuery<'a> {
139    /// The document type to count
140    pub document_type: DocumentTypeRef<'a>,
141    /// The contract id (32 bytes)
142    pub contract_id: [u8; 32],
143    /// The document type name
144    pub document_type_name: String,
145    /// The countable index to use
146    pub index: &'a Index,
147    /// The equality where clauses that match index prefix properties
148    pub where_clauses: Vec<WhereClause>,
149}
150
151/// Turns the `(path, key, element)` triples a point-lookup count path
152/// query yields (see `point_lookup_count_path_query`) into one entry per
153/// count tree. For compound (`In`) shapes the `In` value sits at
154/// `path[base_path_len]` when the walk descended past the base path (the
155/// `In` + trailing `Equal`s shape) and IS the key otherwise (the
156/// `In`-on-terminator shape); `Equal`-only shapes have no per-key
157/// dimension. The element's own count is the per-branch document count
158/// (every countable terminator value tree is a CountTree); an absent
159/// element becomes `count: None`. ONE decoder for every reader of that
160/// layout — the proof verifier, the no-proof executor and composite
161/// queries — so the layout has one owner.
162pub fn point_lookup_count_entries(
163    base_path_len: usize,
164    has_in_clause: bool,
165    elements: impl IntoIterator<Item = (Vec<Vec<u8>>, Vec<u8>, Option<grovedb::Element>)>,
166) -> Vec<SplitCountEntry> {
167    elements
168        .into_iter()
169        .map(|(path, grove_key, element)| {
170            let key = if has_in_clause {
171                if path.len() > base_path_len {
172                    path[base_path_len].clone()
173                } else {
174                    grove_key
175                }
176            } else {
177                Vec::new()
178            };
179            SplitCountEntry {
180                in_key: None,
181                key,
182                count: element.map(|element| element.count_value_or_default()),
183            }
184        })
185        .collect()
186}
187
188/// An entry in a split count result, containing the serialized
189/// key(s) and the count of documents matching them.
190///
191/// For flat queries (per-`In`-value mode without a range, or
192/// per-distinct-value-in-range mode without an `In` on prefix) only
193/// `key` is meaningful and `in_key` is `None`.
194///
195/// For compound range-distinct queries (an `In` clause on a prefix
196/// property plus a range on the terminator) BOTH keys are carried:
197/// `in_key` is the In-fork's prefix value and `key` is the
198/// terminator value. Cross-fork aggregation is intentionally NOT
199/// done server-side — emitting the unmerged per-(in_key, key) shape
200/// lets `limit` push directly into grovedb (no pre-merge issue),
201/// keeps proof verification straightforward (no absence-proof
202/// gymnastics for omitted In branches), and gives callers strictly
203/// more information than a flat histogram. Callers reduce
204/// client-side when they want the sum.
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct SplitCountEntry {
207    /// The serialized prefix key for compound queries (the `In`
208    /// value for this fork). `None` for flat queries.
209    pub in_key: Option<Vec<u8>>,
210    /// The serialized terminator/value key for this entry.
211    pub key: Vec<u8>,
212    /// The count of documents matching this `(in_key, key)` tuple
213    /// (or just `key` for flat queries).
214    ///
215    /// Three-valued by design:
216    /// - `Some(n)` with `n > 0` — verified count for an entry the
217    ///   underlying data path materialized.
218    /// - `Some(0)` — caller queried this branch and the executor
219    ///   confirmed zero matching documents. Emitted by the no-proof
220    ///   point-lookup path's aggregated total wrapper (a single
221    ///   summed entry whose value can be 0) and by the no-proof range
222    ///   executors when their walk returns nothing. Not emitted
223    ///   per-In-branch under the current shape — see `None` below.
224    /// - `None` — reserved for a future absence-proof variant. The
225    ///   current `point_lookup_count_path_query` doesn't set
226    ///   `absence_proofs_for_non_existing_searched_keys: true`, so
227    ///   absent In branches are **omitted from the verified entry
228    ///   list entirely** (grovedb's `verify_query` doesn't surface
229    ///   `(path, key, None)` triples for them). Callers that need to
230    ///   distinguish "queried but absent" diff the request's In array
231    ///   against the returned entries by key. The variant exists in
232    ///   the type signature so a future path-query change that flips
233    ///   the flag surfaces absences via `count: None` without a
234    ///   breaking struct change — distinguishable from `Some(0)`
235    ///   (which a zero-count CountTree could never produce on its own
236    ///   since zero-count CountTrees aren't materialized in merk).
237    pub count: Option<u64>,
238}
239
240/// SQL-shaped count-query mode — names the response shape the
241/// caller asked for via `(select, group_by)` on the wire.
242///
243/// **Two count-mode enums coexist in this module.** This one names
244/// the *output shape* the request produces (single aggregate vs
245/// per-group entries). [`DocumentCountMode`] below names the
246/// *executor strategy* (which proof primitive / which walk path
247/// Drive uses to compute that shape). `CountMode` lives on
248/// [`DocumentCountRequest`] as the caller-supplied contract;
249/// `DocumentCountMode` is derived from `(CountMode, where_clauses,
250/// prove)` by [`DriveDocumentCountQuery::detect_mode`] just before
251/// dispatch.
252///
253/// **Result shape vs. executor strategy.** Each variant names a
254/// result shape — the per-variant docstring lists the
255/// where-clause shapes that route to that result shape and
256/// notes which executor strategy
257/// [`DriveDocumentCountQuery::detect_mode`] picks for each.
258/// `(in_field, range_field)` combinations on the same request
259/// are accepted on multiple `CountMode` variants — the executor
260/// strategy distinguishes them. Upstream routing
261/// (drive-abci's `validate_and_route`) picks the `CountMode`
262/// from the caller's `group_by`; downstream `detect_mode`
263/// converts the `(CountMode, where_clauses, prove)` triple into
264/// the resolved [`DocumentCountMode`].
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum CountMode {
267    /// `select=COUNT, group_by=[]`. Single u64 result.
268    ///
269    /// Where-clause shapes accepted:
270    /// - empty (relies on a `documentsCountable: true` doctype),
271    /// - Equal-only (fully covered by a `countable: true` index),
272    /// - one `In` (per-In fan-out, summed server-side),
273    /// - one range (uses `AggregateCountOnRange` for prove,
274    ///   `RangeNoProof` for no-proof),
275    /// - one `In` + one range on the no-proof path (per-In fan-out
276    ///   each doing a range walk; prove is rejected).
277    ///
278    /// `limit` is structurally meaningless (aggregate is one row)
279    /// and is rejected upstream when set.
280    Aggregate,
281
282    /// `select=COUNT, group_by=[in_field]`. One entry per `In` value.
283    ///
284    /// Where-clause shapes accepted:
285    /// - one `In` clause on `group_by[0]` (no range clause): the
286    ///   canonical shape — routes to `PointLookupProof` on the
287    ///   prove path, `PerInValue` on the no-proof path.
288    /// - one `In` on `group_by[0]` AND a range clause on a
289    ///   different field: routes to
290    ///   `RangeAggregateCarrierProof` on the prove path
291    ///   (grovedb #663 carrier-ACOR — one verified `u64` per
292    ///   In branch, range collapsed) and `RangeNoProof` on the
293    ///   no-prove path (per-In-branch range walk). Both produce
294    ///   entries that line up with the caller's GROUP BY shape.
295    ///
296    /// `limit` is rejected upstream when set. The In array is
297    /// already capped at 100 entries by `WhereClause::in_values()`,
298    /// so the result size is bounded by construction; a separate
299    /// `limit` would either be redundant (≤ 100) or would silently
300    /// truncate the proof to fewer In branches than the caller
301    /// asked for (because the PointLookupProof path can't represent
302    /// a partial-In-array selection in its `SizedQuery`). Callers
303    /// that want fewer branches narrow the In array directly.
304    GroupByIn,
305
306    /// `select=COUNT, group_by=[range_field]`. One entry per distinct
307    /// value within the range.
308    ///
309    /// Where-clause shapes accepted:
310    /// - one range clause on `group_by[0]` (no `In` clause):
311    ///   canonical RangeDistinctProof / RangeNoProof distinct.
312    /// - one range on `group_by[0]` AND an `In` clause on a
313    ///   different field: prove path keeps `RangeDistinctProof`
314    ///   with In-fanout via grovedb subquery; no-prove path uses
315    ///   `RangeNoProof` distinct on the merged result. Per-
316    ///   distinct-value entries cover both branches of the In.
317    /// - two range clauses on different fields, the second
318    ///   being `group_by[0]`: routes to
319    ///   `RangeAggregateCarrierProof` (outer range + inner-ACOR
320    ///   carrier per grovedb #664 outer-range cap). See
321    ///   `outer_range_plus_inner_range_with_prove_and_group_by_range_routes_to_carrier_proof`
322    ///   for the regression test pinning this shape.
323    ///
324    /// `limit` caps the number of distinct values; on the prove
325    /// path it's validated-not-clamped (oversized values rejected
326    /// with `InvalidLimit`).
327    GroupByRange,
328
329    /// `select=COUNT, group_by=[in_field, range_field]`. One entry
330    /// per `(in_key, range_key)` pair.
331    ///
332    /// Where-clause invariants: an `In` clause on `group_by[0]`
333    /// AND a range clause on `group_by[1]` (match-any over
334    /// the where-clauses list — clause ordering on the wire
335    /// doesn't affect routing).
336    /// `limit` is a **global cap on the emitted `(in_key, key)` lex
337    /// stream**, not per-In-branch. The executor pushes a single
338    /// `SizedQuery::limit` over the compound walk, so a request
339    /// with `|In| = 3` and `limit = 5` returns at most 5 entries
340    /// total across all In branches (ordered by `(in_key, key)`,
341    /// direction from the first `order_by` clause). On the prove
342    /// path it's validated-not-clamped (oversized values rejected
343    /// with `InvalidLimit`).
344    GroupByCompound,
345}
346
347impl CountMode {
348    /// `true` for [`Self::Aggregate`] (single-row response);
349    /// `false` for the three grouped variants. See each variant's
350    /// docstring for the per-shape semantics.
351    pub fn is_aggregate(self) -> bool {
352        matches!(self, Self::Aggregate)
353    }
354
355    /// `true` for [`Self::GroupByRange`] and [`Self::GroupByCompound`]
356    /// — the two variants whose proof shape requires per-distinct-
357    /// value `KVCount` ops. See each variant's docstring for the
358    /// per-shape proof routing.
359    pub fn requires_distinct_walk(self) -> bool {
360        matches!(self, Self::GroupByRange | Self::GroupByCompound)
361    }
362
363    /// `true` for [`Self::GroupByRange`] and [`Self::GroupByCompound`]
364    /// — the two variants whose result size isn't structurally
365    /// bounded. [`Self::Aggregate`] and [`Self::GroupByIn`] reject
366    /// `limit` upstream; see each variant's docstring for the
367    /// per-shape reasoning.
368    pub fn accepts_limit(self) -> bool {
369        matches!(self, Self::GroupByRange | Self::GroupByCompound)
370    }
371}
372
373/// Classification of a count query's shape, used to dispatch to the
374/// right executor. Returned by
375/// [`DriveDocumentCountQuery::detect_mode`].
376///
377/// The discriminator is purely a function of the where-clause
378/// operators + the caller's [`CountMode`] + `prove`; it does not
379/// depend on the contract's index set. Picking a covering index for
380/// the chosen mode is a separate step that requires the document
381/// type's `BTreeMap<String, Index>`.
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
383pub enum DocumentCountMode {
384    /// No range, no `In` — single summed entry with empty key. Reads
385    /// the `CountTree` count directly at the indexed path.
386    Total,
387    /// Exactly one `In` clause, no range — one entry per (deduped)
388    /// `In` value, each computed as the count at that single value.
389    /// The `In` doubles as the per-value split signal.
390    PerInValue,
391    /// Exactly one range clause, no proof — walks the property-name
392    /// `ProvableCountTree`'s children inside the range. Returns either
393    /// a single summed entry or per-distinct-value entries depending
394    /// on whether the caller's [`CountMode`] requires a distinct walk
395    /// ([`CountMode::GroupByRange`] / [`CountMode::GroupByCompound`])
396    /// or not ([`CountMode::Aggregate`]).
397    RangeNoProof,
398    /// Exactly one range clause + `prove = true` +
399    /// [`CountMode::Aggregate`] — produces a grovedb
400    /// `AggregateCountOnRange` proof that verifies to a single u64.
401    /// The merk-level primitive returns one aggregate; per-distinct-
402    /// value entries with proof go through [`Self::RangeDistinctProof`]
403    /// instead.
404    RangeProof,
405    /// Exactly one range clause + `prove = true` +
406    /// [`CountMode::GroupByRange`] or [`CountMode::GroupByCompound`]
407    /// — produces a regular range proof against the property-name
408    /// `ProvableCountTree`. The
409    /// proof's `KVCount(key, value, count)` ops carry per-distinct-
410    /// value counts, each cryptographically committed via
411    /// `node_hash_with_count` to the merk root. The verifier walks the
412    /// proof op stream and emits a per-key count map, no opt-in
413    /// aggregate-collapse wrapper. Proof size is O(distinct values
414    /// matched) rather than the O(log n) of [`Self::RangeProof`], but
415    /// still much smaller than materialize-and-count.
416    RangeDistinctProof,
417    /// No range clause + `prove = true` — produces a per-branch
418    /// `Element::CountTree` proof. Either an unfiltered total
419    /// (`documents_countable: true` fast path, proving the
420    /// doctype's primary-key CountTree directly) or a covered
421    /// Equal/`In` lookup against a `countable: true` index (proving
422    /// one CountTree element per matched branch via
423    /// [`DriveDocumentCountQuery::point_lookup_count_path_query`]).
424    /// Proof size is O(k × log n) where k is the number of covered
425    /// branches (1 for the empty-where fast path and Equal-only
426    /// fully-covered case; ≤ |In values| for In-on-prefix). No
427    /// document materialization, no `u16::MAX` matching-docs cap —
428    /// the merk-level `count_value` IS the result, the SDK
429    /// extracts it via `verify_point_lookup_count_proof`.
430    PointLookupProof,
431    /// Exactly one `In` clause + one range clause + `prove = true`
432    /// + [`CountMode::GroupByIn`] — produces a grovedb carrier
433    /// `AggregateCountOnRange` proof: one outer-key descent per
434    /// `In` value, each terminating in an ACOR boundary walk over
435    /// the per-branch range subtree. Returns one `(in_key, u64)`
436    /// pair per resolved In branch — same per-key aggregate
437    /// semantics as the no-proof per-In fan-out, just verifiable.
438    ///
439    /// Proof size is `O(|In values| · (log B + log C'))` where `B`
440    /// is the In-property's distinct-value count and `C'` is the
441    /// terminator subtree's distinct-value count. Smaller than the
442    /// alternative [`Self::RangeDistinctProof`] (which scales with
443    /// the number of distinct in-range terminator values per
444    /// branch, not per-branch log-bound boundary nodes) and
445    /// preserves per-In aggregate granularity that GROUP BY
446    /// `[in_field, range_field]` can't express.
447    ///
448    /// Path-query shape (see
449    /// [`DriveDocumentCountQuery::carrier_aggregate_count_path_query`]):
450    /// outer Keys = serialized In values; subquery_path = ranged
451    /// property name; subquery = ACOR(range). Verified via
452    /// [`grovedb::GroveDb::verify_aggregate_count_query_per_key`]
453    /// (returns `Vec<(Vec<u8>, u64)>`).
454    ///
455    /// Enabled by grovedb PR #663 ("allow AggregateCountOnRange as
456    /// carrier subquery"). Before that PR this shape was rejected
457    /// in [`Self::detect_mode`] with the message "range count
458    /// queries with an `in` clause are not supported on the
459    /// aggregate prove path".
460    RangeAggregateCarrierProof,
461}