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