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    key_array_element_clauses, validate_and_canonicalize_where_clauses,
24    validate_resolved_time_range_clause_shapes, 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        // A typed array's elements are keyed first: several only when the
69        // request answers per element (see [`key_array_element_clauses`]).
70        request.where_clauses = key_array_element_clauses(
71            std::mem::take(&mut request.where_clauses),
72            request.document_type,
73            matches!(request.mode, SumMode::GroupByIn),
74            platform_version,
75        )?;
76        request.where_clauses =
77            validate_and_canonicalize_where_clauses(request.where_clauses, platform_version)?;
78        // Same provenance-vs-shape contract as the count and joint
79        // dispatchers, anchored before mode detection just as there.
80        validate_resolved_time_range_clause_shapes(
81            &request.where_clauses,
82            &request.resolved_time_ranges,
83        )?;
84        let resolved_mode = super::mode_detection::detect_sum_mode(&request, platform_version)?;
85
86        let contract_id = request.contract.id().to_buffer();
87        let document_type_name = request.document_type.name().to_string();
88        let where_clauses = request.where_clauses;
89        let resolved_time_ranges = request.resolved_time_ranges;
90        let sum_property = request.sum_property;
91        // Default direction is ascending; the first order clause's
92        // direction (if any) wins. Mirrors count's analog.
93        let order_by_ascending = request
94            .order_clauses
95            .first()
96            .map(|c| c.ascending)
97            .unwrap_or(true);
98
99        match resolved_mode {
100            DocumentSumMode::Total => {
101                let entries = self.execute_document_sum_total_no_proof(
102                    contract_id,
103                    request.document_type,
104                    document_type_name,
105                    where_clauses,
106                    &resolved_time_ranges,
107                    sum_property,
108                    transaction,
109                    platform_version,
110                )?;
111                let total = entries.first().and_then(|e| e.sum).unwrap_or(0);
112                Ok(DocumentSumResponse::Aggregate(total))
113            }
114            DocumentSumMode::PerInValue => {
115                let options = RangeSumOptions {
116                    walk_mode: RangeSumWalkMode::Aggregate,
117                    carrier_outer_limit: None,
118                    left_to_right: order_by_ascending,
119                };
120                Ok(DocumentSumResponse::Entries(
121                    self.execute_document_sum_per_in_value_no_proof(
122                        contract_id,
123                        request.document_type,
124                        document_type_name,
125                        where_clauses,
126                        &resolved_time_ranges,
127                        sum_property,
128                        options,
129                        transaction,
130                        platform_version,
131                    )?,
132                ))
133            }
134            DocumentSumMode::RangeNoProof => {
135                let return_distinct = matches!(
136                    request.mode,
137                    SumMode::GroupByRange | SumMode::GroupByCompound
138                );
139                let walk_mode = if return_distinct {
140                    RangeSumWalkMode::Distinct(effective_no_proof_distinct_limit(
141                        request.limit,
142                        request.drive_config,
143                    )?)
144                } else {
145                    RangeSumWalkMode::Aggregate
146                };
147                let options = RangeSumOptions {
148                    walk_mode,
149                    carrier_outer_limit: None,
150                    left_to_right: order_by_ascending,
151                };
152                let entries = self.execute_document_sum_range_no_proof(
153                    contract_id,
154                    request.document_type,
155                    document_type_name,
156                    where_clauses,
157                    &resolved_time_ranges,
158                    sum_property,
159                    options,
160                    transaction,
161                    platform_version,
162                )?;
163                if matches!(request.mode, SumMode::Aggregate) {
164                    let total = entries.first().and_then(|e| e.sum).unwrap_or(0);
165                    Ok(DocumentSumResponse::Aggregate(total))
166                } else {
167                    Ok(DocumentSumResponse::Entries(entries))
168                }
169            }
170            DocumentSumMode::RangeProof => Ok(DocumentSumResponse::Proof(
171                self.execute_document_sum_range_proof(
172                    contract_id,
173                    request.document_type,
174                    document_type_name,
175                    where_clauses,
176                    &resolved_time_ranges,
177                    sum_property,
178                    transaction,
179                    platform_version,
180                )?,
181            )),
182            DocumentSumMode::RangeDistinctProof => {
183                // Validate-don't-clamp limit policy on the prove path:
184                // client-side proof reconstruction needs the EXACT
185                // limit value the server applied to the path query
186                // (the SDK rebuilds the same `SizedQuery::limit` for
187                // merk-root recomputation). Silent clamping or a
188                // tuned `default_query_limit` would byte-differ the
189                // reconstructed path query and break verification.
190                //
191                // Limit fallback uses [`crate::config::DEFAULT_QUERY_LIMIT`]
192                // (compile-time constant), NOT
193                // `drive_config.default_query_limit` (operator-tunable
194                // runtime value). `max_query_limit` still gates the
195                // request as a DoS-protection knob — proofs never
196                // cross the operator-set ceiling, but the ceiling
197                // itself doesn't shape proof bytes; it only decides
198                // whether the request gets served.
199                //
200                // Mirrors count's policy at
201                // `drive_document_count_query::drive_dispatcher`
202                // `DocumentCountMode::RangeDistinctProof`.
203                let effective_limit = request
204                    .limit
205                    .unwrap_or(crate::config::DEFAULT_QUERY_LIMIT as u32);
206                if effective_limit > request.drive_config.max_query_limit as u32 {
207                    return Err(Error::Query(
208                        crate::error::query::QuerySyntaxError::InvalidLimit(format!(
209                            "limit {} exceeds max_query_limit {} on the prove + \
210                             distinct-walk path (GROUP BY a range field, SUM); \
211                             reduce the requested limit or use prove = false",
212                            effective_limit, request.drive_config.max_query_limit
213                        )),
214                    ));
215                }
216                let limit_u16 = u16::try_from(effective_limit).map_err(|_| {
217                    Error::Query(crate::error::query::QuerySyntaxError::Unsupported(format!(
218                        "limit {} exceeds u16::MAX for range-distinct sum proof",
219                        effective_limit
220                    )))
221                })?;
222                Ok(DocumentSumResponse::Proof(
223                    self.execute_document_sum_range_distinct_proof(
224                        contract_id,
225                        request.document_type,
226                        document_type_name,
227                        where_clauses,
228                        &resolved_time_ranges,
229                        sum_property,
230                        limit_u16,
231                        order_by_ascending,
232                        transaction,
233                        platform_version,
234                    )?,
235                ))
236            }
237            DocumentSumMode::PointLookupProof => Ok(DocumentSumResponse::Proof(
238                self.execute_document_sum_point_lookup_proof(
239                    contract_id,
240                    request.document_type,
241                    document_type_name,
242                    where_clauses,
243                    &resolved_time_ranges,
244                    sum_property,
245                    transaction,
246                    platform_version,
247                )?,
248            )),
249            DocumentSumMode::RangeAggregateCarrierProof => {
250                // Validate-don't-clamp limit policy on the prove path
251                // — same contract as RangeDistinctProof above. The
252                // carrier proof's outer-walk cap is `SizedQuery::limit`
253                // bytes-of-proof material; a silent clamp would
254                // byte-differ the SDK's reconstruction and break
255                // verification. Unlike the distinct arm, the carrier
256                // arm passes `Option<u16>` (None = unbounded outer
257                // walk), so the request's `None` stays `None` instead
258                // of falling back to a default.
259                let limit_u16 = request
260                    .limit
261                    .map(|l| {
262                        if l > request.drive_config.max_query_limit as u32 {
263                            return Err(Error::Query(
264                                crate::error::query::QuerySyntaxError::InvalidLimit(format!(
265                                    "limit {} exceeds max_query_limit {} on the prove + \
266                                     carrier-aggregate path (GROUP BY In + range, SUM); \
267                                     reduce the requested limit or use prove = false",
268                                    l, request.drive_config.max_query_limit
269                                )),
270                            ));
271                        }
272                        u16::try_from(l).map_err(|_| {
273                            Error::Query(crate::error::query::QuerySyntaxError::Unsupported(
274                                format!(
275                                    "limit {} exceeds u16::MAX for carrier-aggregate sum proof",
276                                    l
277                                ),
278                            ))
279                        })
280                    })
281                    .transpose()?;
282                Ok(DocumentSumResponse::Proof(
283                    self.execute_document_sum_range_aggregate_carrier_proof(
284                        contract_id,
285                        request.document_type,
286                        document_type_name,
287                        where_clauses,
288                        &resolved_time_ranges,
289                        sum_property,
290                        limit_u16,
291                        order_by_ascending,
292                        transaction,
293                        platform_version,
294                    )?,
295                ))
296            }
297        }
298    }
299}
300
301// `detect_sum_mode` lives in the versioned
302// [`mode_detection`](super::mode_detection) module — the routing
303// table is consensus-relevant on the query surface and protocol
304// versions that change it must do so behind a method-version bump.
305
306/// Parse the wire-CBOR `Value::Array` shape into structured
307/// `Vec<WhereClause>`. Delegates to count's parser.
308pub fn where_clauses_from_value(
309    value: &Value,
310    platform_version: &PlatformVersion,
311) -> Result<Vec<WhereClause>, Error> {
312    count_dispatcher::where_clauses_from_value(value, platform_version)
313}
314
315/// Parse the wire-CBOR `Value::Array` shape into structured
316/// `Vec<OrderClause>`. Delegates to count's parser.
317pub fn order_clauses_from_value(value: &Value) -> Result<Vec<OrderClause>, Error> {
318    crate::query::drive_document_count_query::drive_dispatcher::order_clauses_from_value(value)
319}
320
321#[cfg(test)]
322mod tests {
323    use super::effective_no_proof_distinct_limit;
324    use crate::config::DriveConfig;
325
326    #[test]
327    fn no_proof_distinct_limit_uses_the_default_and_clamps_to_the_maximum() {
328        let config = DriveConfig {
329            default_query_limit: 25,
330            max_query_limit: 100,
331            ..DriveConfig::default()
332        };
333
334        assert_eq!(
335            effective_no_proof_distinct_limit(None, &config).unwrap(),
336            25
337        );
338        assert_eq!(
339            effective_no_proof_distinct_limit(Some(7), &config).unwrap(),
340            7
341        );
342        assert_eq!(
343            effective_no_proof_distinct_limit(Some(10_000), &config).unwrap(),
344            100
345        );
346
347        let disabled = DriveConfig {
348            max_query_limit: 0,
349            ..config
350        };
351        assert!(effective_no_proof_distinct_limit(None, &disabled).is_err());
352    }
353}