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}