Skip to main content

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}