drive/query/drive_document_sum_query/mod.rs
1//! `DriveDocumentSumQuery` — Drive's sum-query surface.
2//!
3//! Parallels [`crate::query::drive_document_count_query`] for the sum-tree
4//! family added in v3 (alongside grovedb PR 670's
5//! `Element::ProvableCountSumTree`). The high-level shape mirrors count's
6//! exactly:
7//!
8//! - [`DocumentSumRequest`] carries the request (contract + document_type
9//! + sum_property + where/order/mode/limit/prove).
10//! - [`DocumentSumResponse`] carries one of three response shapes
11//! (`Aggregate(i64)` / `Entries(Vec<SumEntry>)` / `Proof(Vec<u8>)`),
12//! picked by the dispatcher from query shape + flags.
13//! - [`SumMode`] selects which executor handles the query
14//! (Aggregate / GroupByIn / GroupByRange / GroupByCompound).
15//! - [`DriveDocumentSumQuery`] is the compiled query object passed to
16//! path-query builders + verifier wrappers; shared by prover and
17//! verifier as the single source of truth on the path-query shape
18//! (same pattern count uses).
19//!
20//! The sum-specific wrinkle vs count: every sum request carries a
21//! `sum_property` field naming the integer property to aggregate. The
22//! dispatcher validates that the chosen covering index `summable: "<x>"`
23//! matches the request's `sum_property`, and that the doctype-level
24//! `documents_summable: "<x>"` (if set) also matches. See
25//! `book/src/drive/document-sum-trees.md` for the design rationale.
26//!
27//! The bench at
28//! [`packages/rs-drive/benches/document_sum_worst_case.rs`](../../../../../benches/document_sum_worst_case.rs)
29//! targets these public types and the dispatcher entry — Q1–Q9 from
30//! the chapter all roundtrip on the real Drive.
31
32#[cfg(feature = "server")]
33pub mod drive_dispatcher;
34
35#[cfg(any(feature = "server", feature = "verify"))]
36pub mod index_picker;
37
38#[cfg(any(feature = "server", feature = "verify"))]
39pub mod mode_detection;
40
41#[cfg(any(feature = "server", feature = "verify"))]
42pub mod path_query;
43
44#[cfg(feature = "server")]
45pub mod execute_point_lookup;
46
47#[cfg(feature = "server")]
48pub mod execute_range_sum;
49
50#[cfg(feature = "server")]
51pub mod executors;
52
53#[cfg(test)]
54mod tests;
55
56#[cfg(feature = "server")]
57use crate::query::ResolvedTimeRange;
58#[cfg(any(feature = "server", feature = "verify"))]
59use crate::query::{WhereClause, WhereOperator};
60
61#[cfg(any(feature = "server", feature = "verify"))]
62use dpp::data_contract::document_type::{DocumentTypeRef, Index};
63
64#[cfg(feature = "server")]
65use crate::config::DriveConfig;
66#[cfg(feature = "server")]
67use crate::query::OrderClause;
68#[cfg(feature = "server")]
69use dpp::data_contract::DataContract;
70
71/// Failsafe cap on per-`In`-value fan-out, mirroring
72/// [`crate::query::drive_document_count_query::MAX_LIMIT_AS_FAILSAFE`].
73/// `WhereClause::in_values()` already caps each `In` clause at 100
74/// values, so this 1024 ceiling exists only as a defensive guard against
75/// pathological input that slipped past the upstream validator.
76pub const MAX_LIMIT_AS_FAILSAFE: u32 = 1024;
77
78/// Platform-wide cap on the outer walk of a carrier-aggregate
79/// range-outer sum proof, mirroring count's
80/// `MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`. The carrier-aggregate
81/// shape is the sum analog of count's G8 — single proof carrying
82/// per-bucket aggregated sums for an outer range × inner range query.
83/// Bounded so a single proof's outer enumeration can't be made
84/// pathological by a caller.
85pub const MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT: u32 = 10;
86
87/// What kind of sum-query the dispatcher should run. Parallels
88/// [`crate::query::drive_document_count_query::CountMode`].
89///
90/// The four variants correspond to the four response shapes:
91/// - `Aggregate` → `DocumentSumResponse::Aggregate(i64)` (one sum)
92/// - `GroupByIn` → `DocumentSumResponse::Entries(Vec<SumEntry>)`
93/// (one entry per `In` value)
94/// - `GroupByRange` → `Entries` with one entry per distinct
95/// in-range value
96/// - `GroupByCompound` → `Entries` with one entry per `(in_key, key)`
97/// pair (compound `In + range`)
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub enum SumMode {
100 /// One sum across all matched documents.
101 Aggregate,
102 /// One sum per `In` value (cartesian fan-out at the `In`'s position).
103 GroupByIn,
104 /// One sum per distinct value in a range.
105 GroupByRange,
106 /// One sum per `(In-value, range-value)` pair.
107 GroupByCompound,
108}
109
110/// The lower-level routing decision the dispatcher reaches after
111/// detecting the query's shape. Parallels count's `DocumentCountMode`.
112///
113/// Where `SumMode` is *what the caller asked for* (an externally-facing
114/// classification), `DocumentSumMode` is *which executor will run*
115/// (an internally-facing classification that maps onto a specific
116/// grovedb primitive). The dispatcher's job is to translate from the
117/// former to the latter.
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum DocumentSumMode {
120 /// `Aggregate` + no `where` → primary-key SumTree fast path.
121 Total,
122 /// `Aggregate` or `GroupByIn` + Equal/In `where` clauses fully
123 /// covering a `summable: "<prop>"` index.
124 PerInValue,
125 /// `Aggregate` + range `where` → `AggregateSumOnRange` no-prove
126 /// path (or its proven counterpart on the prove path).
127 RangeNoProof,
128 /// `Aggregate` + range `where` + `prove = true` → grovedb
129 /// `AggregateSumOnRange` proof primitive.
130 RangeProof,
131 /// `GroupByRange` (or `GroupByCompound` distinct mode) → per-key
132 /// `KVSum` walk with proof.
133 RangeDistinctProof,
134 /// Point-lookup with proof.
135 PointLookupProof,
136 /// Carrier-aggregate: outer In/range with inner range, sum
137 /// committed per outer bucket. Sum analog of count's
138 /// `RangeAggregateCarrierProof`.
139 RangeAggregateCarrierProof,
140}
141
142/// A single per-key sum entry, parallels count's `SplitCountEntry`.
143///
144/// - `in_key` carries the In value for compound `(In, range)` queries;
145/// `None` for flat queries.
146/// - `key` carries the terminator value (the range-key or the In
147/// single value, depending on shape).
148/// - `sum` carries the aggregated property value; `Some(n)` for a
149/// matched key, `None` for a key explicitly proven absent (mirrors
150/// count's three-valued `count`).
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct SumEntry {
153 /// In-prefix value when the query is compound (`In` on a prefix
154 /// property + range on the terminator). `None` for flat queries.
155 pub in_key: Option<Vec<u8>>,
156 /// The terminator key value (the value of the index's last covered
157 /// property within the query).
158 pub key: Vec<u8>,
159 /// The aggregated `sum_property` value at that key. `Some(n)` for
160 /// matched keys; `None` for keys proven absent (the dispatcher
161 /// emits `None`-sum entries when
162 /// `absence_proofs_for_non_existing_searched_keys` is configured).
163 pub sum: Option<i64>,
164}
165
166/// Server-side request input for the sum dispatcher. Mirrors
167/// [`crate::query::drive_document_count_query::DocumentCountRequest`]
168/// with the addition of the `sum_property` field.
169#[cfg(feature = "server")]
170#[derive(Clone, Debug)]
171pub struct DocumentSumRequest<'a> {
172 /// The data contract this document type belongs to.
173 pub contract: &'a DataContract,
174 /// The document type whose summable indexes will be picked from.
175 pub document_type: DocumentTypeRef<'a>,
176 /// The integer property to sum. Must match the doctype-level
177 /// `documents_summable` (when set) and every covering index's
178 /// `summable: "<x>"` declaration; the dispatcher rejects mismatches
179 /// at parse time.
180 pub sum_property: String,
181 /// Structured where-clauses (parsed via
182 /// [`drive_dispatcher::where_clauses_from_value`] from the
183 /// wire-CBOR shape).
184 pub where_clauses: Vec<WhereClause>,
185 /// The fields among `where_clauses` whose equality clause was produced by
186 /// `IN_TIME_RANGE` resolution rather than written by the caller. Same
187 /// contract and same purpose as
188 /// [`crate::query::DriveDocumentQuery::resolved_time_ranges`]:
189 /// it is what gates which indexes the sum pickers may select.
190 pub resolved_time_ranges: Vec<ResolvedTimeRange>,
191 /// Structured order-clauses (parsed via
192 /// [`drive_dispatcher::order_clauses_from_value`]).
193 pub order_clauses: Vec<OrderClause>,
194 /// The sum mode requested.
195 pub mode: SumMode,
196 /// Optional cap on the number of entries returned in `Entries`-mode
197 /// responses.
198 ///
199 /// **Fallback differs between the no-proof and prove paths**:
200 ///
201 /// - **No-proof path**: unset `limit` falls back to
202 /// [`crate::config::DriveConfig::default_query_limit`] (the
203 /// operator-tunable runtime value); explicit `limit >
204 /// max_query_limit` is clamped to `max_query_limit`. There's
205 /// no consensus-verification step on no-proof responses, so
206 /// operator-tunable defaults are safe here.
207 /// - **Prove path**: unset `limit` falls back to
208 /// [`crate::config::DEFAULT_QUERY_LIMIT`] (the compile-time
209 /// constant the SDK verifier also reads), explicitly NOT
210 /// `drive_config.default_query_limit`. An explicit `limit >
211 /// max_query_limit` is **rejected** with
212 /// [`crate::error::query::QuerySyntaxError::InvalidLimit`]
213 /// rather than clamped, so a tuned operator default or an
214 /// over-max request can't byte-differ the
215 /// `SizedQuery::limit` the SDK reconstructs for merk-root
216 /// verification. See the
217 /// [`drive_dispatcher`]'s `RangeDistinctProof` /
218 /// `RangeAggregateCarrierProof` arms for the
219 /// validate-don't-clamp policy, mirrored from count's
220 /// prove-path arms.
221 pub limit: Option<u32>,
222 /// Whether to return a `Proof(Vec<u8>)` instead of materializing
223 /// the aggregate/entries server-side.
224 pub prove: bool,
225 /// Pointer to the drive config, used for limit defaults.
226 pub drive_config: &'a DriveConfig,
227}
228
229/// Server-side response from the sum dispatcher. Parallels count's
230/// `DocumentCountResponse`.
231#[cfg(feature = "server")]
232#[derive(Clone, Debug)]
233pub enum DocumentSumResponse {
234 /// A single aggregated sum across all matched documents.
235 Aggregate(i64),
236 /// One entry per `In`-value or per distinct in-range value.
237 Entries(Vec<SumEntry>),
238 /// Serialized grovedb proof bytes the client verifies with
239 /// `GroveDb::verify_query` (point-lookup proofs) or
240 /// `GroveDb::verify_aggregate_sum_query` (range-aggregate proofs).
241 Proof(Vec<u8>),
242}
243
244/// Compiled sum-query object. Shared by prover and verifier — both
245/// build the same `PathQuery` via the path-query helpers on this
246/// struct, so the prover and the verifier can't drift on shape.
247/// Parallels count's `DriveDocumentCountQuery`.
248#[cfg(any(feature = "server", feature = "verify"))]
249#[derive(Clone, Debug)]
250pub struct DriveDocumentSumQuery<'a> {
251 /// The document type whose sum tree we're querying.
252 pub document_type: DocumentTypeRef<'a>,
253 /// The data contract id (separated from `document_type` so the
254 /// verifier-side construction doesn't need the full contract).
255 pub contract_id: [u8; 32],
256 /// The document type name (used to construct the index path).
257 pub document_type_name: String,
258 /// The covering index. Either the index whose `summable` flag
259 /// matches the request's `sum_property` (point lookup / aggregate
260 /// case), or the index whose `range_summable` matches (range
261 /// case). The doctype-primary-key fast path stores this as a
262 /// sentinel — see `path_query.rs`'s `primary_key_sum_path_query`.
263 pub index: &'a Index,
264 /// The structured where clauses.
265 pub where_clauses: Vec<WhereClause>,
266 /// The sum target property. Validated against the index's
267 /// `summable` and the doctype's `documents_summable` at dispatch
268 /// time.
269 pub sum_property: String,
270}
271
272/// Storage-walk shape for a server-side range sum.
273#[cfg(feature = "server")]
274#[derive(Clone, Copy, Debug, Default)]
275pub enum RangeSumWalkMode {
276 /// Return one aggregate sum for the range.
277 #[default]
278 Aggregate,
279 /// Return distinct sums, bounded by the supplied storage-walk limit.
280 Distinct(u16),
281}
282
283/// Server-side range-sum executor options, parallels
284/// [`crate::query::drive_document_count_query::RangeCountOptions`].
285#[cfg(feature = "server")]
286#[derive(Clone, Debug, Default)]
287pub struct RangeSumOptions {
288 /// Select aggregate execution or a compile-time bounded distinct walk.
289 pub walk_mode: RangeSumWalkMode,
290 /// `Some(n)` caps the carrier walk for compound `(In, range)`
291 /// shapes at n entries. `None` accepts the platform-wide
292 /// `MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`.
293 pub carrier_outer_limit: Option<u32>,
294 /// Whether the carrier walk iterates ascending (`true`) or
295 /// descending (`false`); flows into grovedb's `Query.left_to_right`.
296 pub left_to_right: bool,
297}
298
299/// Helper used by the verifier-side path-query rebuild to match the
300/// shape the prover used. Same role as count's analog helper — we
301/// don't want the prover and verifier to drift on which operator
302/// classification triggers which executor.
303#[cfg(any(feature = "server", feature = "verify"))]
304pub fn is_range_operator(op: WhereOperator) -> bool {
305 matches!(
306 op,
307 WhereOperator::GreaterThan
308 | WhereOperator::GreaterThanOrEquals
309 | WhereOperator::LessThan
310 | WhereOperator::LessThanOrEquals
311 | WhereOperator::Between
312 | WhereOperator::BetweenExcludeBounds
313 | WhereOperator::BetweenExcludeLeft
314 | WhereOperator::BetweenExcludeRight
315 | WhereOperator::StartsWith
316 )
317}
318
319/// True if the `WhereOperator` is supported on a summable index for
320/// the executor pickers. Parallels count's `is_indexable_for_count`.
321#[cfg(any(feature = "server", feature = "verify"))]
322pub fn is_indexable_for_sum(op: WhereOperator) -> bool {
323 is_range_operator(op) || matches!(op, WhereOperator::Equal | WhereOperator::In)
324}