Skip to main content

drive/query/drive_document_sum_query/
execute_range_sum.rs

1//! Range execution paths for the sum query. Parallels count's
2//! `execute_range_count.rs`.
3//!
4//! - [`DriveDocumentSumQuery::execute_range_sum_no_proof`] — Rust-side
5//!   walk via `query_aggregate_sum` (or per-In fan-out for compound
6//!   shapes), returning a single `Aggregate` entry or per-(in_key, key)
7//!   distinct entries without a proof.
8//! - [`DriveDocumentSumQuery::execute_aggregate_sum_with_proof`] —
9//!   grovedb `AggregateSumOnRange` proof, returning a single i64
10//!   verified out of the proof.
11//! - [`DriveDocumentSumQuery::execute_distinct_sum_with_proof`] —
12//!   regular range proof against the `ProvableSumTree`, returning
13//!   per-key `KVSum` ops bound to the merk root.
14
15use super::{DriveDocumentSumQuery, RangeSumOptions, RangeSumWalkMode, SumEntry};
16use crate::drive::Drive;
17use crate::error::drive::DriveError;
18use crate::error::query::QuerySyntaxError;
19use crate::error::Error;
20use crate::query::{
21    aggregate_or_zero_when_absent, index_keeps_empty_groups, is_absent_path, WhereClause,
22    WhereOperator,
23};
24use dpp::data_contract::document_type::methods::DocumentTypeV0Methods;
25use dpp::version::PlatformVersion;
26use grovedb::query_result_type::QueryResultType;
27use grovedb::TransactionArg;
28use grovedb_costs::CostContext;
29
30impl DriveDocumentSumQuery<'_> {
31    /// Range-aware sum walk against a `rangeSummable: true` index.
32    ///
33    /// Mirror of count's `execute_range_count_no_proof`. Routing:
34    /// - **Flat summed** (no `In`, distinct=false): single
35    ///   `query_aggregate_sum` call against the merk-level
36    ///   `AggregateSumOnRange` primitive. O(log n).
37    /// - **Compound summed** (`In` on prefix, distinct=false): per-In
38    ///   fan-out — one `query_aggregate_sum` call per matched In
39    ///   branch, summed in Rust.
40    /// - **Distinct mode** (`distinct=true`): walks the unified
41    ///   `distinct_sum_path_query` and emits one entry per matched
42    ///   `(in_key, key)` pair, leaving out the groups summing to zero
43    ///   except over an index that can hold empty groups
44    ///   (`index_keeps_empty_groups`, see the walk).
45    pub fn execute_range_sum_no_proof(
46        &self,
47        drive: &Drive,
48        options: &RangeSumOptions,
49        transaction: TransactionArg,
50        platform_version: &PlatformVersion,
51    ) -> Result<Vec<SumEntry>, Error> {
52        let drive_version = &platform_version.drive;
53        let has_in_on_prefix = self
54            .where_clauses
55            .iter()
56            .any(|wc| wc.operator == WhereOperator::In);
57
58        if matches!(options.walk_mode, RangeSumWalkMode::Aggregate) {
59            // An absent value reads zero as the proof of the same total
60            // verifies it (`aggregate_or_zero_when_absent`).
61            let range_total_verifier = platform_version
62                .drive
63                .methods
64                .verify
65                .document_sum
66                .verify_aggregate_sum_proof;
67            if has_in_on_prefix {
68                // Enforce exactly one `In` clause. Without this, a request
69                // with multiple In filters would silently use only the
70                // first and drop the rest, producing an over-broad total.
71                let in_clauses: Vec<&WhereClause> = self
72                    .where_clauses
73                    .iter()
74                    .filter(|wc| wc.operator == WhereOperator::In)
75                    .collect();
76                if in_clauses.len() != 1 {
77                    return Err(Error::Query(
78                        QuerySyntaxError::InvalidWhereClauseComponents(
79                            "compound summed range sum path requires exactly one `in` clause",
80                        ),
81                    ));
82                }
83                let in_clause = in_clauses[0];
84                let in_values = in_clause.in_values().into_data_with_error()??;
85                let other_clauses: Vec<WhereClause> = self
86                    .where_clauses
87                    .iter()
88                    .filter(|wc| wc.operator != WhereOperator::In)
89                    .cloned()
90                    .collect();
91
92                let mut total: i64 = 0;
93                let mut seen_keys: std::collections::BTreeSet<Vec<u8>> =
94                    std::collections::BTreeSet::new();
95                for value in in_values.iter() {
96                    let key_bytes = self.document_type.serialize_value_for_key(
97                        in_clause.field.as_str(),
98                        value,
99                        platform_version,
100                    )?;
101                    if !seen_keys.insert(key_bytes) {
102                        continue;
103                    }
104
105                    let mut clauses_for_value = other_clauses.clone();
106                    clauses_for_value.push(WhereClause {
107                        field: in_clause.field.clone(),
108                        operator: WhereOperator::Equal,
109                        value: value.clone(),
110                    });
111                    let per_value_query = DriveDocumentSumQuery {
112                        document_type: self.document_type,
113                        contract_id: self.contract_id,
114                        document_type_name: self.document_type_name.clone(),
115                        index: self.index,
116                        where_clauses: clauses_for_value,
117                        sum_property: self.sum_property.clone(),
118                    };
119                    let path_query = per_value_query.aggregate_sum_path_query(platform_version)?;
120                    let CostContext { value, cost: _ } = drive.grove.query_aggregate_sum(
121                        &path_query,
122                        transaction,
123                        &drive_version.grove_version,
124                    );
125                    let sum = aggregate_or_zero_when_absent(
126                        drive,
127                        &path_query.path,
128                        value,
129                        range_total_verifier,
130                        transaction,
131                        platform_version,
132                    )?;
133                    // Use `checked_add` rather than `saturating_add` so an
134                    // overflowed aggregate fails deterministically instead
135                    // of silently clamping at i64::MAX. The proof-side
136                    // verifier sees the same overflow at the same point
137                    // (the grovedb primitive itself returns i64), so
138                    // refusing here keeps prover and verifier in sync
139                    // on the rejection rather than letting the no-proof
140                    // path return a value the proof path would reject.
141                    total = total.checked_add(sum).ok_or_else(|| {
142                        Error::Query(QuerySyntaxError::Unsupported(
143                            "compound In-on-prefix range-sum overflowed i64 when summing \
144                             per-In aggregates. Narrow the query (smaller In set, narrower \
145                             range) or use multiple queries and combine client-side."
146                                .to_string(),
147                        ))
148                    })?;
149                }
150                return Ok(vec![SumEntry {
151                    in_key: None,
152                    key: Vec::new(),
153                    sum: Some(total),
154                }]);
155            }
156            // Flat summed (no In on prefix): single aggregate read.
157            let path_query = self.aggregate_sum_path_query(platform_version)?;
158            let CostContext { value, cost: _ } = drive.grove.query_aggregate_sum(
159                &path_query,
160                transaction,
161                &drive_version.grove_version,
162            );
163            let sum = aggregate_or_zero_when_absent(
164                drive,
165                &path_query.path,
166                value,
167                range_total_verifier,
168                transaction,
169                platform_version,
170            )?;
171            return Ok(vec![SumEntry {
172                in_key: None,
173                key: Vec::new(),
174                sum: Some(sum),
175            }]);
176        }
177
178        let RangeSumWalkMode::Distinct(distinct_limit) = options.walk_mode else {
179            return Err(Error::Drive(DriveError::CorruptedCodeExecution(
180                "aggregate range sums must return before the distinct storage walk",
181            )));
182        };
183        let path_query = self.distinct_sum_path_query(
184            Some(distinct_limit),
185            options.left_to_right,
186            platform_version,
187        )?;
188        let base_path_len = path_query.path.len();
189
190        let mut drive_operations = vec![];
191        let result = drive.grove_get_raw_path_query(
192            &path_query,
193            transaction,
194            QueryResultType::QueryPathKeyElementTrioResultType,
195            &mut drive_operations,
196            drive_version,
197        );
198        let elements = match result {
199            Ok((elements, _)) => elements,
200            Err(error) if is_absent_path(&error) => {
201                return Ok(Vec::new());
202            }
203            Err(e) => return Err(e),
204        };
205
206        let keeps_empty_groups = index_keeps_empty_groups(self.document_type, self.index);
207        let mut entries: Vec<SumEntry> = Vec::new();
208        for triple in elements.to_path_key_elements() {
209            let (path, key, element) = triple;
210            let sum = element.sum_value_or_default();
211            // Zero groups are kept where an index can hold empty groups
212            // (see the helper), as the distinct proof keeps them: the walk's
213            // limit counted them, so leaving them out would shorten the page.
214            // Every other index leaves out its zero sums, as before.
215            if sum == 0 && !keeps_empty_groups {
216                continue;
217            }
218            let in_key = if has_in_on_prefix && path.len() > base_path_len {
219                Some(path[base_path_len].clone())
220            } else {
221                None
222            };
223            entries.push(SumEntry {
224                in_key,
225                key,
226                sum: Some(sum),
227            });
228        }
229
230        Ok(entries)
231    }
232
233    /// Generates a grovedb `AggregateSumOnRange` proof for a range-sum
234    /// query against a `rangeSummable` index. Returned proof bytes
235    /// verify via `GroveDb::verify_aggregate_sum_query` yielding
236    /// `(root_hash, i64 sum)`.
237    pub fn execute_aggregate_sum_with_proof(
238        &self,
239        drive: &Drive,
240        transaction: TransactionArg,
241        platform_version: &PlatformVersion,
242    ) -> Result<Vec<u8>, Error> {
243        let drive_version = &platform_version.drive;
244        let path_query = self.aggregate_sum_path_query(platform_version)?;
245        let CostContext { value, cost: _ } = drive.grove.get_proved_path_query(
246            &path_query,
247            None,
248            transaction,
249            &drive_version.grove_version,
250        );
251        let proof = value.map_err(|e| Error::GroveDB(Box::new(e)))?;
252        Ok(proof)
253    }
254
255    /// Per-distinct-key range-sum proof against this query's
256    /// `rangeSummable` index. Mirror of count's
257    /// `execute_distinct_count_with_proof`, through
258    /// `distinct_sum_path_query`.
259    pub fn execute_distinct_sum_with_proof(
260        &self,
261        drive: &Drive,
262        limit: u16,
263        left_to_right: bool,
264        transaction: TransactionArg,
265        platform_version: &PlatformVersion,
266    ) -> Result<Vec<u8>, Error> {
267        let drive_version = &platform_version.drive;
268        let path_query =
269            self.distinct_sum_path_query(Some(limit), left_to_right, platform_version)?;
270        let CostContext { value, cost: _ } = drive.grove.get_proved_path_query(
271            &path_query,
272            None,
273            transaction,
274            &drive_version.grove_version,
275        );
276        let proof = value.map_err(|e| Error::GroveDB(Box::new(e)))?;
277        Ok(proof)
278    }
279
280    /// Generates a grovedb leaf-PCPS `AggregateCountAndSumOnRange`
281    /// proof for a combined count + sum range query against an index
282    /// that declares BOTH `rangeCountable: true` AND `rangeSummable:
283    /// true`. Returned proof bytes verify via
284    /// `GroveDb::verify_aggregate_count_and_sum_query` yielding
285    /// `(root_hash, u64 count, i64 sum)` — the load-bearing primitive
286    /// for the [average-index-examples chapter]
287    /// (../../../../book/src/drive/average-index-examples.md)'s
288    /// Query 5 ("Class Trend"). PCPS-only: the terminator's value tree
289    /// MUST be a `ProvableCountProvableSumTree`; lighter
290    /// (CountSumTree / ProvableCountSumTree / ProvableSumTree)
291    /// terminators are rejected at the grovedb merk-gate.
292    ///
293    /// Leaf analog of
294    /// [`Self::execute_carrier_aggregate_count_and_sum_with_proof`]:
295    /// same primitive, no outer `In` fan-out — single
296    /// `(count, sum)` per proof rather than per-In-key `(count, sum)`
297    /// triples.
298    pub fn execute_aggregate_count_and_sum_with_proof(
299        &self,
300        drive: &Drive,
301        transaction: TransactionArg,
302        platform_version: &PlatformVersion,
303    ) -> Result<Vec<u8>, Error> {
304        let drive_version = &platform_version.drive;
305        let path_query = self.aggregate_count_and_sum_path_query(platform_version)?;
306        let CostContext { value, cost: _ } = drive.grove.get_proved_path_query(
307            &path_query,
308            None,
309            transaction,
310            &drive_version.grove_version,
311        );
312        let proof = value.map_err(|e| Error::GroveDB(Box::new(e)))?;
313        Ok(proof)
314    }
315
316    /// Generates a grovedb **carrier** `AggregateSumOnRange` proof
317    /// for `In + range` queries with `group_by = [in_field]` (and the
318    /// `RangeAggregateCarrierProof` mode in general). Sum analog of
319    /// count's
320    /// [`crate::query::drive_document_count_query::DriveDocumentCountQuery::execute_carrier_aggregate_count_with_proof`].
321    ///
322    /// Builds the carrier `PathQuery` via
323    /// [`Self::carrier_aggregate_sum_path_query`] and asks grovedb
324    /// for a proof. The proof commits one aggregate sum per resolved
325    /// In branch; verified client-side via
326    /// `GroveDb::verify_aggregate_sum_query_per_key` (grovedb PR #670
327    /// head `e98bab5f`), which returns `(RootHash, Vec<(Vec<u8>, i64)>)`.
328    ///
329    /// `left_to_right` and `limit` are byte-load-bearing — they are
330    /// part of the `PathQuery` bytes the verifier rebuilds. See count's
331    /// analog for the rationale.
332    pub fn execute_carrier_aggregate_sum_with_proof(
333        &self,
334        drive: &Drive,
335        limit: Option<u16>,
336        left_to_right: bool,
337        transaction: TransactionArg,
338        platform_version: &PlatformVersion,
339    ) -> Result<Vec<u8>, Error> {
340        let drive_version = &platform_version.drive;
341        let path_query =
342            self.carrier_aggregate_sum_path_query(limit, left_to_right, platform_version)?;
343        let CostContext { value, cost: _ } = drive.grove.get_proved_path_query(
344            &path_query,
345            None,
346            transaction,
347            &drive_version.grove_version,
348        );
349        let proof = value.map_err(|e| Error::GroveDB(Box::new(e)))?;
350        Ok(proof)
351    }
352
353    /// Combined PCPS carrier proof:
354    /// `AggregateCountAndSumOnRange`-on-carrier. Sum-and-count analog
355    /// of [`Self::execute_carrier_aggregate_sum_with_proof`]. Requires
356    /// the chosen index to declare BOTH `rangeCountable: true` AND
357    /// `rangeSummable: true` so the terminator's value tree is a
358    /// `ProvableCountProvableSumTree`.
359    ///
360    /// Returns proof bytes the verifier maps to
361    /// `Vec<(Vec<u8>, u64, i64)>` via
362    /// `GroveDb::verify_aggregate_count_and_sum_query_per_key` (grovedb
363    /// PR #670 head `e98bab5f`) — one `(in_key, count, sum)` triple
364    /// per resolved In branch.
365    pub fn execute_carrier_aggregate_count_and_sum_with_proof(
366        &self,
367        drive: &Drive,
368        limit: Option<u16>,
369        left_to_right: bool,
370        transaction: TransactionArg,
371        platform_version: &PlatformVersion,
372    ) -> Result<Vec<u8>, Error> {
373        let drive_version = &platform_version.drive;
374        let path_query = self.carrier_aggregate_count_and_sum_path_query(
375            limit,
376            left_to_right,
377            platform_version,
378        )?;
379        let CostContext { value, cost: _ } = drive.grove.get_proved_path_query(
380            &path_query,
381            None,
382            transaction,
383            &drive_version.grove_version,
384        );
385        let proof = value.map_err(|e| Error::GroveDB(Box::new(e)))?;
386        Ok(proof)
387    }
388}