Skip to main content

drive/query/drive_document_sum_query/
drive_dispatcher.rs

1//! Sum-query dispatcher entry point.
2//!
3//! Parallels [`crate::query::drive_document_count_query::drive_dispatcher`]
4//! for the sum surface. Routes a parsed [`DocumentSumRequest`] to one of
5//! the per-mode executors based on the (where × mode × prove) triple,
6//! exactly the way count's dispatcher does.
7//!
8//! `where_clauses_from_value` / `order_clauses_from_value` are wire-shape
9//! adapters that the bench and the gRPC handler both use to convert the
10//! CBOR-decoded `Value::Array` input into structured `Vec<WhereClause>` /
11//! `Vec<OrderClause>`. Identical input contract to count.
12
13use crate::config::DriveConfig;
14use crate::drive::Drive;
15use crate::error::query::QuerySyntaxError;
16use crate::error::Error;
17use crate::query::drive_document_count_query::drive_dispatcher as count_dispatcher;
18use crate::query::drive_document_sum_query::{
19    DocumentSumMode, DocumentSumRequest, DocumentSumResponse, RangeSumOptions, RangeSumWalkMode,
20    SumMode,
21};
22use crate::query::{
23    validate_and_canonicalize_where_clauses, validate_resolved_time_range_clause_shapes,
24    OrderClause, WhereClause,
25};
26use dpp::data_contract::accessors::v0::DataContractV0Getters;
27use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
28use dpp::platform_value::Value;
29use dpp::version::PlatformVersion;
30use grovedb::TransactionArg;
31
32fn effective_no_proof_distinct_limit(
33    requested_limit: Option<u32>,
34    drive_config: &DriveConfig,
35) -> Result<u16, Error> {
36    let effective_limit = requested_limit
37        .unwrap_or(drive_config.default_query_limit as u32)
38        .min(drive_config.max_query_limit as u32);
39
40    if effective_limit == 0 {
41        return Err(Error::Query(QuerySyntaxError::InvalidLimit(
42            "effective distinct SUM limit must be greater than zero".to_string(),
43        )));
44    }
45
46    // Both configuration limits are u16, and the `min` above bounds every
47    // caller-supplied value to `max_query_limit` before this conversion.
48    Ok(effective_limit as u16)
49}
50
51#[cfg(feature = "server")]
52impl Drive {
53    /// Server-side entry point for the sum surface. Routes a
54    /// [`DocumentSumRequest`] to the appropriate executor based on the
55    /// where-shape, requested mode, and `prove` flag.
56    ///
57    /// Mirrors [`Drive::execute_document_count_request`].
58    pub fn execute_document_sum_request(
59        &self,
60        mut request: DocumentSumRequest,
61        transaction: TransactionArg,
62        platform_version: &PlatformVersion,
63    ) -> Result<DocumentSumResponse, Error> {
64        // Canonicalize exactly as the count and joint dispatchers do (the
65        // shared step in [`crate::query::canonicalize`]), so callers can
66        // pass either the bounded pair form (`[f > A, f < B]`) or the
67        // pre-merged `between*` form and get equivalent mode detection.
68        request.where_clauses =
69            validate_and_canonicalize_where_clauses(request.where_clauses, platform_version)?;
70        // Same provenance-vs-shape contract as the count and joint
71        // dispatchers, anchored before mode detection just as there.
72        validate_resolved_time_range_clause_shapes(
73            &request.where_clauses,
74            &request.resolved_time_ranges,
75        )?;
76        let resolved_mode = super::mode_detection::detect_sum_mode(&request, platform_version)?;
77
78        let contract_id = request.contract.id().to_buffer();
79        let document_type_name = request.document_type.name().to_string();
80        let where_clauses = request.where_clauses;
81        let resolved_time_ranges = request.resolved_time_ranges;
82        let sum_property = request.sum_property;
83        // Default direction is ascending; the first order clause's
84        // direction (if any) wins. Mirrors count's analog.
85        let order_by_ascending = request
86            .order_clauses
87            .first()
88            .map(|c| c.ascending)
89            .unwrap_or(true);
90
91        match resolved_mode {
92            DocumentSumMode::Total => {
93                let entries = self.execute_document_sum_total_no_proof(
94                    contract_id,
95                    request.document_type,
96                    document_type_name,
97                    where_clauses,
98                    &resolved_time_ranges,
99                    sum_property,
100                    transaction,
101                    platform_version,
102                )?;
103                let total = entries.first().and_then(|e| e.sum).unwrap_or(0);
104                Ok(DocumentSumResponse::Aggregate(total))
105            }
106            DocumentSumMode::PerInValue => {
107                let options = RangeSumOptions {
108                    walk_mode: RangeSumWalkMode::Aggregate,
109                    carrier_outer_limit: None,
110                    left_to_right: order_by_ascending,
111                };
112                Ok(DocumentSumResponse::Entries(
113                    self.execute_document_sum_per_in_value_no_proof(
114                        contract_id,
115                        request.document_type,
116                        document_type_name,
117                        where_clauses,
118                        &resolved_time_ranges,
119                        sum_property,
120                        options,
121                        transaction,
122                        platform_version,
123                    )?,
124                ))
125            }
126            DocumentSumMode::RangeNoProof => {
127                let return_distinct = matches!(
128                    request.mode,
129                    SumMode::GroupByRange | SumMode::GroupByCompound
130                );
131                let walk_mode = if return_distinct {
132                    RangeSumWalkMode::Distinct(effective_no_proof_distinct_limit(
133                        request.limit,
134                        request.drive_config,
135                    )?)
136                } else {
137                    RangeSumWalkMode::Aggregate
138                };
139                let options = RangeSumOptions {
140                    walk_mode,
141                    carrier_outer_limit: None,
142                    left_to_right: order_by_ascending,
143                };
144                let entries = self.execute_document_sum_range_no_proof(
145                    contract_id,
146                    request.document_type,
147                    document_type_name,
148                    where_clauses,
149                    &resolved_time_ranges,
150                    sum_property,
151                    options,
152                    transaction,
153                    platform_version,
154                )?;
155                if matches!(request.mode, SumMode::Aggregate) {
156                    let total = entries.first().and_then(|e| e.sum).unwrap_or(0);
157                    Ok(DocumentSumResponse::Aggregate(total))
158                } else {
159                    Ok(DocumentSumResponse::Entries(entries))
160                }
161            }
162            DocumentSumMode::RangeProof => Ok(DocumentSumResponse::Proof(
163                self.execute_document_sum_range_proof(
164                    contract_id,
165                    request.document_type,
166                    document_type_name,
167                    where_clauses,
168                    &resolved_time_ranges,
169                    sum_property,
170                    transaction,
171                    platform_version,
172                )?,
173            )),
174            DocumentSumMode::RangeDistinctProof => {
175                // Validate-don't-clamp limit policy on the prove path:
176                // client-side proof reconstruction needs the EXACT
177                // limit value the server applied to the path query
178                // (the SDK rebuilds the same `SizedQuery::limit` for
179                // merk-root recomputation). Silent clamping or a
180                // tuned `default_query_limit` would byte-differ the
181                // reconstructed path query and break verification.
182                //
183                // Limit fallback uses [`crate::config::DEFAULT_QUERY_LIMIT`]
184                // (compile-time constant), NOT
185                // `drive_config.default_query_limit` (operator-tunable
186                // runtime value). `max_query_limit` still gates the
187                // request as a DoS-protection knob — proofs never
188                // cross the operator-set ceiling, but the ceiling
189                // itself doesn't shape proof bytes; it only decides
190                // whether the request gets served.
191                //
192                // Mirrors count's policy at
193                // `drive_document_count_query::drive_dispatcher`
194                // `DocumentCountMode::RangeDistinctProof`.
195                let effective_limit = request
196                    .limit
197                    .unwrap_or(crate::config::DEFAULT_QUERY_LIMIT as u32);
198                if effective_limit > request.drive_config.max_query_limit as u32 {
199                    return Err(Error::Query(
200                        crate::error::query::QuerySyntaxError::InvalidLimit(format!(
201                            "limit {} exceeds max_query_limit {} on the prove + \
202                             distinct-walk path (GROUP BY a range field, SUM); \
203                             reduce the requested limit or use prove = false",
204                            effective_limit, request.drive_config.max_query_limit
205                        )),
206                    ));
207                }
208                let limit_u16 = u16::try_from(effective_limit).map_err(|_| {
209                    Error::Query(crate::error::query::QuerySyntaxError::Unsupported(format!(
210                        "limit {} exceeds u16::MAX for range-distinct sum proof",
211                        effective_limit
212                    )))
213                })?;
214                Ok(DocumentSumResponse::Proof(
215                    self.execute_document_sum_range_distinct_proof(
216                        contract_id,
217                        request.document_type,
218                        document_type_name,
219                        where_clauses,
220                        &resolved_time_ranges,
221                        sum_property,
222                        limit_u16,
223                        order_by_ascending,
224                        transaction,
225                        platform_version,
226                    )?,
227                ))
228            }
229            DocumentSumMode::PointLookupProof => Ok(DocumentSumResponse::Proof(
230                self.execute_document_sum_point_lookup_proof(
231                    contract_id,
232                    request.document_type,
233                    document_type_name,
234                    where_clauses,
235                    &resolved_time_ranges,
236                    sum_property,
237                    transaction,
238                    platform_version,
239                )?,
240            )),
241            DocumentSumMode::RangeAggregateCarrierProof => {
242                // Validate-don't-clamp limit policy on the prove path
243                // — same contract as RangeDistinctProof above. The
244                // carrier proof's outer-walk cap is `SizedQuery::limit`
245                // bytes-of-proof material; a silent clamp would
246                // byte-differ the SDK's reconstruction and break
247                // verification. Unlike the distinct arm, the carrier
248                // arm passes `Option<u16>` (None = unbounded outer
249                // walk), so the request's `None` stays `None` instead
250                // of falling back to a default.
251                let limit_u16 = request
252                    .limit
253                    .map(|l| {
254                        if l > request.drive_config.max_query_limit as u32 {
255                            return Err(Error::Query(
256                                crate::error::query::QuerySyntaxError::InvalidLimit(format!(
257                                    "limit {} exceeds max_query_limit {} on the prove + \
258                                     carrier-aggregate path (GROUP BY In + range, SUM); \
259                                     reduce the requested limit or use prove = false",
260                                    l, request.drive_config.max_query_limit
261                                )),
262                            ));
263                        }
264                        u16::try_from(l).map_err(|_| {
265                            Error::Query(crate::error::query::QuerySyntaxError::Unsupported(
266                                format!(
267                                    "limit {} exceeds u16::MAX for carrier-aggregate sum proof",
268                                    l
269                                ),
270                            ))
271                        })
272                    })
273                    .transpose()?;
274                Ok(DocumentSumResponse::Proof(
275                    self.execute_document_sum_range_aggregate_carrier_proof(
276                        contract_id,
277                        request.document_type,
278                        document_type_name,
279                        where_clauses,
280                        &resolved_time_ranges,
281                        sum_property,
282                        limit_u16,
283                        order_by_ascending,
284                        transaction,
285                        platform_version,
286                    )?,
287                ))
288            }
289        }
290    }
291}
292
293// `detect_sum_mode` lives in the versioned
294// [`mode_detection`](super::mode_detection) module — the routing
295// table is consensus-relevant on the query surface and protocol
296// versions that change it must do so behind a method-version bump.
297
298/// Parse the wire-CBOR `Value::Array` shape into structured
299/// `Vec<WhereClause>`. Delegates to count's parser.
300pub fn where_clauses_from_value(
301    value: &Value,
302    platform_version: &PlatformVersion,
303) -> Result<Vec<WhereClause>, Error> {
304    count_dispatcher::where_clauses_from_value(value, platform_version)
305}
306
307/// Parse the wire-CBOR `Value::Array` shape into structured
308/// `Vec<OrderClause>`. Delegates to count's parser.
309pub fn order_clauses_from_value(value: &Value) -> Result<Vec<OrderClause>, Error> {
310    crate::query::drive_document_count_query::drive_dispatcher::order_clauses_from_value(value)
311}
312
313#[cfg(test)]
314mod tests {
315    use super::effective_no_proof_distinct_limit;
316    use crate::config::DriveConfig;
317
318    #[test]
319    fn no_proof_distinct_limit_uses_the_default_and_clamps_to_the_maximum() {
320        let config = DriveConfig {
321            default_query_limit: 25,
322            max_query_limit: 100,
323            ..DriveConfig::default()
324        };
325
326        assert_eq!(
327            effective_no_proof_distinct_limit(None, &config).unwrap(),
328            25
329        );
330        assert_eq!(
331            effective_no_proof_distinct_limit(Some(7), &config).unwrap(),
332            7
333        );
334        assert_eq!(
335            effective_no_proof_distinct_limit(Some(10_000), &config).unwrap(),
336            100
337        );
338
339        let disabled = DriveConfig {
340            max_query_limit: 0,
341            ..config
342        };
343        assert!(effective_no_proof_distinct_limit(None, &disabled).is_err());
344    }
345}