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