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