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}