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}