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