Skip to main content

drive/query/drive_document_average_query/
drive_dispatcher.rs

1//! Average-query dispatcher entry point.
2//!
3//! Routes a [`DocumentAverageRequest`] to one of two backends:
4//! - **No-prove path** → delegates to the joint count-and-sum
5//!   dispatcher
6//!   [`Drive::execute_document_count_and_sum_request`], which walks
7//!   grovedb ONCE and reads both metrics from each visited
8//!   count-sum-bearing element via
9//!   [`grovedb::Element::count_sum_value_or_default`]. See its module
10//!   docstring for the routing / atomicity contract.
11//! - **Prove path** → dispatched to
12//!   [`Drive::execute_document_average_prove`] (defined below), which
13//!   routes to one of the PCPS / direct-read prove executors based on
14//!   `(mode, where_clauses)`. The prove path's per-shape rules are
15//!   unchanged.
16//!
17//! ## Joint dispatch
18//!
19//! The no-prove dispatcher at
20//! [`crate::query::drive_document_count_and_sum_query`] reads
21//! `(count, sum)` together — via grovedb's combined
22//! `query_aggregate_count_and_sum` accumulator on the aggregate range
23//! branch, and via a single PCPS walk on the distinct-grouped branch.
24//! Routing reuses sum's versioned mode-detection table so the
25//! `(where_clauses × mode)` → executor decision has a single source
26//! of truth shared with the count and sum surfaces.
27//!
28//! ## Prove path shapes (unchanged)
29//!
30//! The prove-path routing table at
31//! [`Self::execute_document_average_prove`] picks one of:
32//!     - empty-where + `documentsCountable + documentsSummable`
33//!       doctype → primary-key count-sum tree direct read
34//!     - range AVG on a `rangeAverageable` index → PCPS
35//!       `AggregateCountAndSumOnRange` proof
36//!     - In + range AVG on a `rangeAverageable` index → carrier-PCPS
37//!       proof
38//!     - GroupByRange / GroupByCompound + range on a
39//!       `rangeAverageable` index → per-distinct-key
40//!       count-and-sum proof (walks `ProvableCountProvableSumTree`
41//!       terminators)
42//!     - Equal/In + no range on a summable + countable index →
43//!       point-lookup count-and-sum proof (walks count-sum-bearing
44//!       terminator elements)
45//!   The client verifies with the matching
46//!   `verify_*_count_and_sum_proof` helpers in `drive-proof-verifier`.
47
48use crate::drive::Drive;
49use crate::error::query::QuerySyntaxError;
50use crate::error::Error;
51use crate::query::drive_document_average_query::{
52    AverageMode, DocumentAverageRequest, DocumentAverageResponse,
53};
54use crate::query::drive_document_sum_query::index_picker::{
55    find_range_summable_index_with_counts_for_where_clauses,
56    find_summable_index_with_counts_for_where_clauses,
57};
58use crate::query::drive_document_sum_query::{is_range_operator, DriveDocumentSumQuery};
59use crate::query::{
60    validate_and_canonicalize_where_clauses, validate_resolved_time_range_clause_shapes,
61};
62use dpp::data_contract::accessors::v0::DataContractV0Getters;
63use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters};
64use dpp::version::PlatformVersion;
65use grovedb::TransactionArg;
66
67#[cfg(feature = "server")]
68impl Drive {
69    /// Server-side entry point for the average surface.
70    ///
71    /// Splits prove vs. no-prove at the top level:
72    /// - `prove = true` → routes to
73    ///   [`Self::execute_document_average_prove`].
74    /// - `prove = false` → routes to
75    ///   [`Self::execute_document_count_and_sum_request`], the joint
76    ///   dispatcher that reads `(count, sum)` together.
77    pub fn execute_document_average_request(
78        &self,
79        mut request: DocumentAverageRequest,
80        transaction: TransactionArg,
81        platform_version: &PlatformVersion,
82    ) -> Result<DocumentAverageResponse, Error> {
83        // Provenance-vs-shape contract, BEFORE the prove/no-prove split: the
84        // no-prove path re-checks inside the joint dispatcher, but the prove
85        // path would otherwise reach its executors unguarded — a direct
86        // caller marking an `In`/range clause as time-range-resolved could
87        // have the pickers admit the bucketed index and prove an aggregate
88        // that counts a document once per overlapping bucket. The guard only
89        // inspects Equal clauses, so running it before range-pair
90        // canonicalization is equivalent to running it after.
91        validate_resolved_time_range_clause_shapes(
92            &request.where_clauses,
93            &request.resolved_time_ranges,
94        )?;
95        if request.prove {
96            // The no-prove path canonicalizes inside the joint dispatcher;
97            // run the identical shared step (see
98            // [`crate::query::canonicalize`]) on the prove path so both
99            // accept the bounded pair form (`[f > A, f < B]`) as well as
100            // the pre-merged `between*` form.
101            request.where_clauses =
102                validate_and_canonicalize_where_clauses(request.where_clauses, platform_version)?;
103            return self.execute_document_average_prove(request, transaction, platform_version);
104        }
105        self.execute_document_count_and_sum_request(request, transaction, platform_version)
106    }
107
108    /// Prove path of [`Self::execute_document_average_request`].
109    ///
110    /// Routes the `(where_clauses × mode)` pair to one of the
111    /// available PCPS / direct-read prove executors and returns
112    /// proof bytes the client verifies with the matching
113    /// `verify_*_count_and_sum_proof` helper.
114    ///
115    /// Supported prove shapes:
116    /// - `Aggregate` + empty where + doctype's primary key tree is a
117    ///   count-sum-bearing variant (`CountSumTree` /
118    ///   `ProvableCountSumTree` /
119    ///   `ProvableCountProvableSumTree`) — proves the primary-key
120    ///   element directly via `primary_key_sum_path_query`. Client
121    ///   verifies with `verify_primary_key_count_sum_tree_proof`.
122    /// - `Aggregate` + range clause on a PCPS-eligible index
123    ///   (`rangeCountable: true` AND `rangeSummable: true`) — proves
124    ///   via `execute_aggregate_count_and_sum_with_proof`. Client
125    ///   verifies with `verify_aggregate_count_and_sum_proof`.
126    /// - `Aggregate` + Equal/In, no range, on a count+sum index
127    ///   (or doctype's count-sum primary key) — proves via
128    ///   `execute_point_lookup_sum_with_proof`. Client verifies
129    ///   with `verify_point_lookup_count_and_sum_proof`.
130    /// - `GroupByIn` + In + range on a PCPS-eligible index — proves
131    ///   via `execute_carrier_aggregate_count_and_sum_with_proof`.
132    ///   Client verifies with
133    ///   `verify_carrier_aggregate_count_and_sum_proof`.
134    /// - `GroupByRange` / `GroupByCompound` + range on a PCPS-
135    ///   eligible index — proves via
136    ///   `execute_distinct_sum_with_proof` against a path query
137    ///   whose terminator value trees are
138    ///   `ProvableCountProvableSumTree`. Client verifies with
139    ///   `verify_distinct_count_and_sum_proof`.
140    fn execute_document_average_prove(
141        &self,
142        request: DocumentAverageRequest,
143        transaction: TransactionArg,
144        platform_version: &PlatformVersion,
145    ) -> Result<DocumentAverageResponse, Error> {
146        let contract_id = request.contract.id().to_buffer();
147        let document_type_name = request.document_type.name().to_string();
148        let has_range = request
149            .where_clauses
150            .iter()
151            .any(|wc| is_range_operator(wc.operator));
152        let order_by_ascending = request
153            .order_clauses
154            .first()
155            .map(|c| c.ascending)
156            .unwrap_or(true);
157
158        // Empty-where AVG fast path: prove the primary-key
159        // count-sum-bearing element directly when the doctype
160        // declares both `documents_countable: true` (implied by
161        // having a CountSumTree primary key) and a matching
162        // `documents_summable`. The verifier extracts `(count,
163        // sum)` from one element.
164        if matches!(request.mode, AverageMode::Aggregate)
165            && request.where_clauses.is_empty()
166            && request.document_type.documents_countable()
167            && request
168                .document_type
169                .documents_summable()
170                .map(|p| p == request.sum_property)
171                .unwrap_or(false)
172        {
173            let path_query =
174                DriveDocumentSumQuery::primary_key_sum_path_query(contract_id, &document_type_name);
175            let proof = self
176                .grove
177                .get_proved_path_query(
178                    &path_query,
179                    None,
180                    transaction,
181                    &platform_version.drive.grove_version,
182                )
183                .unwrap()
184                .map_err(|e| Error::GroveDB(Box::new(e)))?;
185            return Ok(DocumentAverageResponse::Proof(proof));
186        }
187
188        // Range AVG: pick a PCPS-eligible index (range_countable
189        // AND range_summable) covering the where clauses. Sum's range
190        // picker restricted to `range_countable` indexes.
191        if has_range
192            && matches!(
193                request.mode,
194                AverageMode::Aggregate | AverageMode::GroupByIn
195            )
196        {
197            let index = find_range_summable_index_with_counts_for_where_clauses(
198                request.document_type.indexes(),
199                &request.where_clauses,
200                &request.sum_property,
201                &request.resolved_time_ranges,
202            )
203            .ok_or_else(|| {
204                Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
205                    "prove AVG requires an index that declares BOTH `rangeCountable: \
206                     true` AND `rangeSummable: true` (a `rangeAverageable: true` \
207                     index is the shorthand) whose last property matches the range \
208                     field and whose summable property matches the request's \
209                     `sum_property`"
210                        .to_string(),
211                ))
212            })?;
213            let sum_query = DriveDocumentSumQuery {
214                document_type: request.document_type,
215                contract_id,
216                document_type_name,
217                index,
218                where_clauses: request.where_clauses.clone(),
219                sum_property: request.sum_property.clone(),
220            };
221
222            let proof = match request.mode {
223                AverageMode::Aggregate => sum_query.execute_aggregate_count_and_sum_with_proof(
224                    self,
225                    transaction,
226                    platform_version,
227                )?,
228                AverageMode::GroupByIn => {
229                    // Carrier-PCPS: one (count, sum) per In branch.
230                    // Validate-don't-clamp limit policy on the prove
231                    // path — `SizedQuery::limit` is bytes-of-proof
232                    // material; silent clamping would byte-differ the
233                    // SDK's reconstruction and break verification.
234                    // Same contract as sum's `RangeAggregateCarrierProof`
235                    // arm. `None` stays `None` (unbounded outer walk).
236                    let limit_u16 = request
237                        .limit
238                        .map(|l| {
239                            if l > request.drive_config.max_query_limit as u32 {
240                                return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!(
241                                    "limit {} exceeds max_query_limit {} on the prove + \
242                                         carrier-aggregate path (GROUP BY In + range, AVG); \
243                                         reduce the requested limit or use prove = false",
244                                    l, request.drive_config.max_query_limit
245                                ))));
246                            }
247                            u16::try_from(l).map_err(|_| {
248                                Error::Query(QuerySyntaxError::Unsupported(format!(
249                                    "limit {} exceeds u16::MAX for carrier-aggregate \
250                                     count+sum (AVG) proof",
251                                    l
252                                )))
253                            })
254                        })
255                        .transpose()?;
256                    sum_query.execute_carrier_aggregate_count_and_sum_with_proof(
257                        self,
258                        limit_u16,
259                        order_by_ascending,
260                        transaction,
261                        platform_version,
262                    )?
263                }
264                _ => unreachable!("outer matches! gate filters out non-Aggregate/GroupByIn"),
265            };
266            return Ok(DocumentAverageResponse::Proof(proof));
267        }
268
269        // Distinct AVG (GroupByRange / GroupByCompound + range) —
270        // per-distinct-key (count, sum) proof against a PCPS-
271        // eligible index (rangeCountable + rangeSummable, i.e. a
272        // `rangeAverageable: true` index). The prover uses sum's
273        // `execute_distinct_sum_with_proof` against a path query
274        // whose terminators are `ProvableCountProvableSumTree`; the
275        // verifier extracts `count_sum_value_or_default()` from
276        // each emitted element.
277        if has_range
278            && matches!(
279                request.mode,
280                AverageMode::GroupByRange | AverageMode::GroupByCompound
281            )
282        {
283            let index = find_range_summable_index_with_counts_for_where_clauses(
284                request.document_type.indexes(),
285                &request.where_clauses,
286                &request.sum_property,
287                &request.resolved_time_ranges,
288            )
289            .ok_or_else(|| {
290                Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
291                    "prove distinct AVG requires an index that declares BOTH \
292                     `rangeCountable: true` AND `rangeSummable: true` (a \
293                     `rangeAverageable: true` index is the shorthand) whose last \
294                     property matches the range field and whose summable property \
295                     matches the request's `sum_property`"
296                        .to_string(),
297                ))
298            })?;
299            // Validate-don't-clamp limit policy on the prove path —
300            // see sum's `RangeDistinctProof` arm for the full
301            // rationale. Limit fallback uses
302            // [`crate::config::DEFAULT_QUERY_LIMIT`] (compile-time
303            // constant) so the SDK's reconstruction lands on the same
304            // `SizedQuery::limit` value; `max_query_limit` still
305            // gates as a DoS ceiling.
306            let effective_limit = request
307                .limit
308                .unwrap_or(crate::config::DEFAULT_QUERY_LIMIT as u32);
309            if effective_limit > request.drive_config.max_query_limit as u32 {
310                return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!(
311                    "limit {} exceeds max_query_limit {} on the prove + distinct-walk \
312                     path (GROUP BY a range field, AVG); reduce the requested limit \
313                     or use prove = false",
314                    effective_limit, request.drive_config.max_query_limit
315                ))));
316            }
317            let limit_u16 = u16::try_from(effective_limit).map_err(|_| {
318                Error::Query(QuerySyntaxError::Unsupported(format!(
319                    "limit {} exceeds u16::MAX for distinct AVG proof",
320                    effective_limit
321                )))
322            })?;
323            let sum_query = DriveDocumentSumQuery {
324                document_type: request.document_type,
325                contract_id,
326                document_type_name,
327                index,
328                where_clauses: request.where_clauses.clone(),
329                sum_property: request.sum_property.clone(),
330            };
331            let proof = sum_query.execute_distinct_sum_with_proof(
332                self,
333                limit_u16,
334                order_by_ascending,
335                transaction,
336                platform_version,
337            )?;
338            return Ok(DocumentAverageResponse::Proof(proof));
339        }
340
341        // Point-lookup AVG: Equal/In on a count+sum index (whose
342        // `summable.is_some()` AND `countable.is_countable()`) OR
343        // doctype-level documentsSummable + documentsCountable for
344        // the empty-where case (handled by the fast path above —
345        // this arm handles the non-empty-where Equal/In shape).
346        //
347        // Accepts both `Aggregate` (caller wants one aggregate row
348        // collapsed across all matched In branches — folded
349        // client-side by `DocumentAverage`) and `GroupByIn` (caller
350        // wants per-In-branch entries — `DocumentSplitAverages`
351        // shape). The grovedb-side proof is identical: one walk
352        // through the point-lookup `subquery` per In key emits one
353        // count-sum-bearing element per branch.
354        //
355        // Mirrors the sum router's resolved-mode table
356        // (`mode_detection/v0/mod.rs`) which maps both
357        // `(SumMode::Aggregate, !range, _, true)` and
358        // `(SumMode::GroupByIn, !range, _, true)` to
359        // `DocumentSumMode::PointLookupProof`. Before adding
360        // `GroupByIn` here the SDK could ask drive for a no-range
361        // GroupByIn AVG proof, drive would 500 with `Unsupported`,
362        // and the SDK's `verify_point_lookup_count_and_sum_proof`
363        // arm (gated on the same resolved mode) would never get
364        // proof bytes to verify.
365        if !has_range
366            && matches!(
367                request.mode,
368                AverageMode::Aggregate | AverageMode::GroupByIn
369            )
370        {
371            let index = find_summable_index_with_counts_for_where_clauses(
372                request.document_type.indexes(),
373                &request.where_clauses,
374                &request.sum_property,
375                &request.resolved_time_ranges,
376            )
377            .ok_or_else(|| {
378                Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
379                    "prove point-lookup AVG requires an index that declares BOTH \
380                     `summable: \"<prop>\"` AND a countable terminator (`countable: \
381                     \"countable\"` or `\"countableAllowingOffset\"`) whose properties \
382                     exactly match the where clause fields"
383                        .to_string(),
384                ))
385            })?;
386            let sum_query = DriveDocumentSumQuery {
387                document_type: request.document_type,
388                contract_id,
389                document_type_name,
390                index,
391                where_clauses: request.where_clauses.clone(),
392                sum_property: request.sum_property.clone(),
393            };
394            let proof = sum_query.execute_point_lookup_sum_with_proof(
395                self,
396                transaction,
397                platform_version,
398            )?;
399            return Ok(DocumentAverageResponse::Proof(proof));
400        }
401
402        // Unreachable in practice — the matches!() gates above
403        // cover every (mode × has_range) combination today. Kept as
404        // a typed error in case a future AverageMode variant lands
405        // without a corresponding prove arm.
406        Err(Error::Query(QuerySyntaxError::Unsupported(format!(
407            "execute_document_average_request prove=true: the (mode = {:?}, has_range \
408             = {}) combination is not yet supported on the prove path. \
409             This is likely a new AverageMode variant that hasn't been wired \
410             into the prove dispatcher.",
411            request.mode, has_range,
412        ))))
413    }
414}
415
416#[cfg(all(test, feature = "server"))]
417mod tests {
418    use super::*;
419    use crate::query::ResolvedTimeRange;
420
421    // ── Dispatcher limit-policy regression tests ───────────────────
422    //
423    // AVG-side analogs of count's
424    // `test_range_distinct_proof_uses_compile_time_default_query_limit_not_operator_config`
425    // and the sum-side tests in `drive_document_sum_query/tests.rs`'s
426    // `limit_policy_regression` module. The AVG dispatcher's
427    // `RangeDistinctProof` arm mirrors the same validate-don't-clamp
428    // policy on the prove path; these tests pin that the dispatcher
429    // uses [`crate::config::DEFAULT_QUERY_LIMIT`] (compile-time
430    // constant) rather than the operator-tunable
431    // `drive_config.default_query_limit`, AND that an explicit
432    // `limit > max_query_limit` returns a typed
433    // `QuerySyntaxError::InvalidLimit` instead of silently clamping.
434    //
435    // The AVG distinct path internally calls
436    // `execute_distinct_sum_with_proof` (the same primitive sum's
437    // RangeDistinctProof uses — see `drive_document_average_query/
438    // drive_dispatcher.rs::execute_document_average_prove`); the
439    // distinction is the index requirement (`rangeCountable +
440    // rangeSummable`, i.e. PCPS / `rangeAverageable`) and the
441    // verifier helper (`verify_aggregate_count_and_sum_query`).
442
443    use crate::config::{DriveConfig, DEFAULT_QUERY_LIMIT};
444    use crate::drive::Drive;
445    use crate::error::query::QuerySyntaxError;
446    use crate::query::drive_document_average_query::{
447        AverageMode, DocumentAverageRequest, DocumentAverageResponse,
448    };
449    use crate::query::{WhereClause, WhereOperator};
450    use crate::util::object_size_info::DocumentInfo::DocumentRefInfo;
451    use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo};
452    use crate::util::storage_flags::StorageFlags;
453    use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure;
454    use dpp::block::block_info::BlockInfo;
455    use dpp::data_contract::accessors::v0::DataContractV0Getters;
456    use dpp::data_contract::DataContractFactory;
457    use dpp::document::{Document, DocumentV0};
458    use dpp::identifier::Identifier;
459    use dpp::platform_value::{platform_value, Value};
460    use grovedb::GroveDb;
461    use std::borrow::Cow;
462    use std::collections::BTreeMap as StdBTreeMap;
463
464    const PROTOCOL_VERSION_V12: u32 = 12;
465
466    /// v12 contract with a `widget` doctype carrying a single
467    /// `(color, amount)` `rangeAverageable: true` (= `rangeCountable +
468    /// rangeSummable`) index. The PCPS combined `byColor` index is
469    /// what the AVG `RangeDistinctProof` arm walks.
470    fn build_widget_contract_pcps() -> dpp::data_contract::DataContract {
471        let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
472        let document_schema = platform_value!({
473            "type": "object",
474            "properties": {
475                "color":  {"type": "string",  "position": 0, "maxLength": 32},
476                "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
477            },
478            "required": ["color", "amount"],
479            "indices": [{
480                "name": "byColor",
481                "properties": [{"color": "asc"}],
482                // rangeAverageable is shorthand for rangeCountable +
483                // rangeSummable on the same summable property. The
484                // DPP parser desugars it into both flags; the picker
485                // routes it through the PCPS path.
486                "summable":        "amount",
487                "rangeSummable":   true,
488                "countable":       "countable",
489                "rangeCountable":  true,
490            }],
491            "additionalProperties": false,
492        });
493        let schemas = platform_value!({ "widget": document_schema });
494        factory
495            .create_with_value_config(
496                dpp::tests::utils::generate_random_identifier_struct(),
497                0,
498                schemas,
499                None,
500                None,
501            )
502            .expect("create data contract")
503            .data_contract_owned()
504    }
505
506    fn insert_widget(
507        drive: &Drive,
508        contract: &dpp::data_contract::DataContract,
509        i: usize,
510        color: &str,
511        amount: u64,
512    ) {
513        let platform_version = PlatformVersion::latest();
514        let document_type = contract
515            .document_type_for_name("widget")
516            .expect("widget type exists");
517        let mut properties = StdBTreeMap::new();
518        properties.insert("color".to_string(), Value::Text(color.to_string()));
519        properties.insert("amount".to_string(), Value::U64(amount));
520        let document: Document = DocumentV0 {
521            contract_version: None,
522            id: Identifier::from([(i + 1) as u8; 32]),
523            owner_id: Identifier::from([0u8; 32]),
524            properties,
525            revision: None,
526            created_at: None,
527            updated_at: None,
528            transferred_at: None,
529            created_at_block_height: None,
530            updated_at_block_height: None,
531            transferred_at_block_height: None,
532            created_at_core_block_height: None,
533            updated_at_core_block_height: None,
534            transferred_at_core_block_height: None,
535            creator_id: None,
536            moderated_at: None,
537            moderated_by: None,
538        }
539        .into();
540        let storage_flags = Some(Cow::Owned(StorageFlags::SingleEpoch(0)));
541        drive
542            .add_document_for_contract(
543                DocumentAndContractInfo {
544                    owned_document_info: OwnedDocumentInfo {
545                        document_info: DocumentRefInfo((&document, storage_flags)),
546                        owner_id: None,
547                    },
548                    contract,
549                    document_type,
550                },
551                false,
552                BlockInfo::default(),
553                true,
554                None,
555                platform_version,
556                None,
557            )
558            .expect("insert widget");
559    }
560
561    /// AVG mirror of the SUM/count regression: with
562    /// `drive_config.default_query_limit = 1` and a `limit = None`
563    /// request, the dispatcher must use `DEFAULT_QUERY_LIMIT` (= 100)
564    /// for the prove path's `SizedQuery::limit`. If it regressed to
565    /// using the runtime `default_query_limit`, the reconstructed
566    /// path query would byte-differ and `verify_aggregate_count_and_sum_query`
567    /// would return Err — exactly the silent-verify-failure surface
568    /// this test guards.
569    #[test]
570    fn range_distinct_avg_proof_uses_compile_time_default_query_limit_not_operator_config() {
571        const OPERATOR_TUNED_LIMIT: u16 = 1;
572        assert_ne!(
573            DEFAULT_QUERY_LIMIT, OPERATOR_TUNED_LIMIT,
574            "test invariant: OPERATOR_TUNED_LIMIT must differ from DEFAULT_QUERY_LIMIT"
575        );
576
577        let drive = setup_drive_with_initial_state_structure(None);
578        let platform_version = PlatformVersion::latest();
579        let data_contract = build_widget_contract_pcps();
580        drive
581            .apply_contract(
582                &data_contract,
583                BlockInfo::default(),
584                true,
585                StorageFlags::optional_default_as_cow(),
586                None,
587                platform_version,
588            )
589            .expect("apply contract");
590
591        let docs = [
592            ("red", 5u64),
593            ("red", 5),
594            ("green", 7),
595            ("green", 7),
596            ("green", 7),
597            ("blue", 2),
598        ];
599        for (i, (color, amount)) in docs.iter().enumerate() {
600            insert_widget(&drive, &data_contract, i, color, *amount);
601        }
602
603        let document_type = data_contract
604            .document_type_for_name("widget")
605            .expect("widget");
606
607        let drive_config = DriveConfig {
608            default_query_limit: OPERATOR_TUNED_LIMIT,
609            ..Default::default()
610        };
611
612        let color_gt_blue = WhereClause {
613            field: "color".to_string(),
614            operator: WhereOperator::GreaterThan,
615            value: Value::Text("blue".to_string()),
616        };
617        let request = DocumentAverageRequest {
618            contract: &data_contract,
619            document_type,
620            sum_property: "amount".to_string(),
621            where_clauses: vec![color_gt_blue.clone()],
622            order_clauses: Vec::new(),
623            mode: AverageMode::GroupByRange,
624            limit: None,
625            prove: true,
626            drive_config: &drive_config,
627            resolved_time_ranges: vec![],
628        };
629
630        let response = drive
631            .execute_document_average_request(request, None, platform_version)
632            .expect("dispatcher should succeed on distinct AVG path");
633        let proof_bytes = match response {
634            DocumentAverageResponse::Proof(p) => p,
635            other => panic!("expected Proof response, got {:?}", other),
636        };
637        assert!(!proof_bytes.is_empty(), "non-empty proof bytes expected");
638
639        // Reconstruct the path query the way the SDK verifier does
640        // — anchored to DEFAULT_QUERY_LIMIT.
641        let index = find_range_summable_index_with_counts_for_where_clauses(
642            document_type.indexes(),
643            std::slice::from_ref(&color_gt_blue),
644            "amount",
645            &[],
646        )
647        .expect("byColor rangeAverageable index covers `color > blue`");
648        let sum_query = DriveDocumentSumQuery {
649            document_type,
650            contract_id: data_contract.id().to_buffer(),
651            document_type_name: "widget".to_string(),
652            index,
653            where_clauses: vec![color_gt_blue],
654            sum_property: "amount".to_string(),
655        };
656        let verifier_path_query = sum_query
657            .distinct_sum_path_query(Some(DEFAULT_QUERY_LIMIT), true, platform_version)
658            .expect("path query builder accepts the same shape the prover used");
659
660        // AVG distinct path's proof verifies via the same
661        // `GroveDb::verify_query` shape sum uses — the difference is
662        // the PCPS terminator the proof commits, and the SDK extracts
663        // (count, sum) from each via `count_sum_value_or_default()`.
664        // For this regression test we only need to confirm root-hash
665        // recomputation succeeds against the DEFAULT_QUERY_LIMIT-anchored
666        // path query; any limit mismatch surfaces as Err here.
667        let (_root_hash, _elements) = GroveDb::verify_query(
668            &proof_bytes,
669            &verifier_path_query,
670            &platform_version.drive.grove_version,
671        )
672        .expect(
673            "expected proof to verify against a path query rebuilt with DEFAULT_QUERY_LIMIT; \
674             a failure here means the dispatcher signed the AVG proof with the \
675             operator-tunable default_query_limit — a consensus-adjacent silent-verify \
676             regression",
677        );
678    }
679
680    /// AVG `RangeDistinctProof` over-max rejection: explicit
681    /// `limit > max_query_limit` MUST surface as `InvalidLimit`,
682    /// not a silent clamp.
683    #[test]
684    fn range_distinct_avg_proof_rejects_limit_over_max() {
685        let drive = setup_drive_with_initial_state_structure(None);
686        let platform_version = PlatformVersion::latest();
687        let data_contract = build_widget_contract_pcps();
688        drive
689            .apply_contract(
690                &data_contract,
691                BlockInfo::default(),
692                true,
693                StorageFlags::optional_default_as_cow(),
694                None,
695                platform_version,
696            )
697            .expect("apply contract");
698
699        insert_widget(&drive, &data_contract, 0, "red", 5);
700
701        let document_type = data_contract
702            .document_type_for_name("widget")
703            .expect("widget");
704        let drive_config = DriveConfig::default();
705        let over_max = drive_config.max_query_limit as u32 + 1;
706
707        let color_gt_blue = WhereClause {
708            field: "color".to_string(),
709            operator: WhereOperator::GreaterThan,
710            value: Value::Text("blue".to_string()),
711        };
712        let request = DocumentAverageRequest {
713            contract: &data_contract,
714            document_type,
715            sum_property: "amount".to_string(),
716            where_clauses: vec![color_gt_blue],
717            order_clauses: Vec::new(),
718            mode: AverageMode::GroupByRange,
719            limit: Some(over_max),
720            prove: true,
721            drive_config: &drive_config,
722            resolved_time_ranges: vec![],
723        };
724
725        let err = drive
726            .execute_document_average_request(request, None, platform_version)
727            .expect_err("limit > max_query_limit must reject, not clamp");
728
729        assert!(
730            matches!(err, Error::Query(QuerySyntaxError::InvalidLimit(_))),
731            "expected QuerySyntaxError::InvalidLimit, got {err:?}"
732        );
733        let msg = err.to_string();
734        assert!(
735            msg.contains("exceeds max_query_limit"),
736            "error must name the rejected limit; got: {msg}"
737        );
738    }
739
740    /// AVG no-range `GroupByIn` + prove MUST hit the point-lookup
741    /// arm and emit proof bytes — the sum router resolves this
742    /// shape to `DocumentSumMode::PointLookupProof` and the SDK
743    /// helper at `verify_point_lookup_count_and_sum_proof` is the
744    /// matching verifier. Before the fix this fell through every
745    /// arm in `execute_document_average_prove` and returned
746    /// `QuerySyntaxError::Unsupported`, leaving the SDK unable to
747    /// finish what it had already started: encode + dispatch a
748    /// valid AVG `GroupByIn` request.
749    ///
750    /// This regression test pins both halves of the contract:
751    ///   1. The server returns proof bytes (no fallthrough error).
752    ///   2. The proof bytes are bincode-decodable as a `GroveDBProof`
753    ///      (sanity-check that it's a real point-lookup payload
754    ///      rather than an empty placeholder).
755    #[test]
756    fn no_range_group_by_in_avg_prove_routes_to_point_lookup() {
757        use grovedb::operations::proof::GroveDBProof;
758
759        let drive = setup_drive_with_initial_state_structure(None);
760        let platform_version = PlatformVersion::latest();
761
762        // A `summable + countable` (non-range) index is what the
763        // point-lookup AVG arm walks. Build a `widget` doctype with
764        // `byColor` index: `summable: "amount" + countable:
765        // "countable"`. (No rangeSummable / rangeCountable — those
766        // are for the range arms.)
767        let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
768        let document_schema = platform_value!({
769            "type": "object",
770            "properties": {
771                "color":  {"type": "string",  "position": 0, "maxLength": 32},
772                "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
773            },
774            "required": ["color", "amount"],
775            "indices": [{
776                "name": "byColor",
777                "properties": [{"color": "asc"}],
778                "summable":  "amount",
779                "countable": "countable",
780            }],
781            "additionalProperties": false,
782        });
783        let schemas = platform_value!({ "widget": document_schema });
784        let data_contract = factory
785            .create_with_value_config(
786                dpp::tests::utils::generate_random_identifier_struct(),
787                0,
788                schemas,
789                None,
790                None,
791            )
792            .expect("create data contract")
793            .data_contract_owned();
794
795        drive
796            .apply_contract(
797                &data_contract,
798                BlockInfo::default(),
799                true,
800                StorageFlags::optional_default_as_cow(),
801                None,
802                platform_version,
803            )
804            .expect("apply contract");
805
806        insert_widget(&drive, &data_contract, 0, "red", 5);
807        insert_widget(&drive, &data_contract, 1, "red", 7);
808        insert_widget(&drive, &data_contract, 2, "green", 3);
809
810        let document_type = data_contract
811            .document_type_for_name("widget")
812            .expect("widget");
813        let drive_config = DriveConfig::default();
814
815        // GroupByIn shape: `color IN ["red", "green"]`, no range,
816        // no order. The router maps this to PointLookupProof and
817        // the dispatcher must hand back proof bytes (NOT
818        // QuerySyntaxError::Unsupported).
819        let color_in = WhereClause {
820            field: "color".to_string(),
821            operator: WhereOperator::In,
822            value: Value::Array(vec![
823                Value::Text("red".to_string()),
824                Value::Text("green".to_string()),
825            ]),
826        };
827        let request = DocumentAverageRequest {
828            contract: &data_contract,
829            document_type,
830            sum_property: "amount".to_string(),
831            where_clauses: vec![color_in],
832            order_clauses: Vec::new(),
833            mode: AverageMode::GroupByIn,
834            limit: None,
835            prove: true,
836            drive_config: &drive_config,
837            resolved_time_ranges: vec![],
838        };
839
840        let response = drive
841            .execute_document_average_request(request, None, platform_version)
842            .expect(
843                "no-range GroupByIn AVG + prove must hit the point-lookup arm \
844                 (router resolves this shape to DocumentSumMode::PointLookupProof); \
845                 a failure here means execute_document_average_prove regressed to \
846                 the pre-fix gap that rejected this combination with Unsupported",
847            );
848        let proof_bytes = match response {
849            DocumentAverageResponse::Proof(p) => p,
850            other => panic!("expected Proof response, got {:?}", other),
851        };
852        assert!(
853            !proof_bytes.is_empty(),
854            "non-empty proof bytes expected from point-lookup AVG path"
855        );
856
857        // Decode as a GroveDBProof — sanity-checks that it's a real
858        // payload rather than a placeholder. Verification (root-hash
859        // recomputation) is exercised end-to-end in the SDK
860        // FromProof tests; the dispatcher-level test here just pins
861        // the routing decision.
862        let bincode_config = bincode::config::standard()
863            .with_big_endian()
864            .with_no_limit();
865        let _: (GroveDBProof, _) = bincode::decode_from_slice(&proof_bytes, bincode_config)
866            .expect("proof bytes must bincode-decode as a GroveDBProof");
867    }
868
869    // ── Joint count-and-sum no-prove executor cross-checks ────────
870    //
871    // Acceptance criterion 4 of issue #3687: "one [test] per joint
872    // executor confirming `(count, sum)` match what the current
873    // double-dispatch produces, against the same grades-contract
874    // fixture."
875    //
876    // Strategy: for each joint executor (Total / PerInValue /
877    // RangeNoProof — and RangeNoProof's distinct branch), issue the
878    // AVG no-prove request via `execute_document_average_request`
879    // AND independently issue separate count + sum requests under
880    // the same transaction. Assert the joint executor's
881    // `(count, sum)` matches the zipped pair from the independent
882    // count + sum surfaces — a cross-check the joint and per-surface
883    // dispatchers cannot silently disagree.
884
885    use crate::query::drive_document_average_query::AverageEntry;
886    use crate::query::drive_document_count_query::{
887        CountMode, DocumentCountRequest, DocumentCountResponse,
888    };
889    use crate::query::drive_document_sum_query::{
890        DocumentSumRequest, DocumentSumResponse, SumMode,
891    };
892
893    /// Issue an independent count + sum pair via the per-surface
894    /// dispatchers and return the zipped `(count, sum)` aggregate.
895    /// Used as the source of truth for cross-checking the joint
896    /// executor's output.
897    fn independent_count_sum_aggregate(
898        drive: &Drive,
899        contract: &dpp::data_contract::DataContract,
900        document_type: dpp::data_contract::document_type::DocumentTypeRef,
901        sum_property: &str,
902        where_clauses: Vec<WhereClause>,
903        drive_config: &DriveConfig,
904        platform_version: &PlatformVersion,
905    ) -> (u64, i64) {
906        let count_request = DocumentCountRequest {
907            contract,
908            document_type,
909            where_clauses: where_clauses.clone(),
910            order_clauses: Vec::new(),
911            mode: CountMode::Aggregate,
912            limit: None,
913            prove: false,
914            drive_config,
915            resolved_time_ranges: vec![],
916        };
917        let sum_request = DocumentSumRequest {
918            contract,
919            document_type,
920            sum_property: sum_property.to_string(),
921            where_clauses,
922            order_clauses: Vec::new(),
923            mode: SumMode::Aggregate,
924            limit: None,
925            prove: false,
926            drive_config,
927            resolved_time_ranges: vec![],
928        };
929        let count_resp = drive
930            .execute_document_count_request(count_request, None, platform_version)
931            .expect("independent count");
932        let sum_resp = drive
933            .execute_document_sum_request(sum_request, None, platform_version)
934            .expect("independent sum");
935        let count = match count_resp {
936            DocumentCountResponse::Aggregate(c) => c,
937            other => panic!("expected count Aggregate, got {:?}", other),
938        };
939        let sum = match sum_resp {
940            DocumentSumResponse::Aggregate(s) => s,
941            other => panic!("expected sum Aggregate, got {:?}", other),
942        };
943        (count, sum)
944    }
945
946    /// `execute_document_count_and_sum_total_no_proof` cross-check:
947    /// empty-where total on a doctype with `documents_summable +
948    /// documents_countable`. Goes through the primary-key fast path.
949    #[test]
950    fn joint_total_executor_matches_independent_count_plus_sum() {
951        let drive = setup_drive_with_initial_state_structure(None);
952        let platform_version = PlatformVersion::latest();
953
954        // The empty-where Total path requires the doctype's
955        // documents_summable + documents_countable to be set, but a
956        // covering `summable + countable` byColor index also works
957        // for the Equal-only-fully-covered sub-path. Use the latter
958        // since the test factory above doesn't easily produce
959        // doctype-level summable+countable. The Equal-only branch
960        // of execute_document_count_and_sum_total_no_proof still
961        // routes through `DocumentSumMode::Total` per sum's table.
962        let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
963        let document_schema = platform_value!({
964            "type": "object",
965            "properties": {
966                "color":  {"type": "string",  "position": 0, "maxLength": 32},
967                "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
968            },
969            "required": ["color", "amount"],
970            "indices": [{
971                "name": "byColor",
972                "properties": [{"color": "asc"}],
973                "summable":  "amount",
974                "countable": "countable",
975            }],
976            "additionalProperties": false,
977        });
978        let schemas = platform_value!({ "widget": document_schema });
979        let data_contract = factory
980            .create_with_value_config(
981                dpp::tests::utils::generate_random_identifier_struct(),
982                0,
983                schemas,
984                None,
985                None,
986            )
987            .expect("create data contract")
988            .data_contract_owned();
989        drive
990            .apply_contract(
991                &data_contract,
992                BlockInfo::default(),
993                true,
994                StorageFlags::optional_default_as_cow(),
995                None,
996                platform_version,
997            )
998            .expect("apply contract");
999
1000        let docs = [
1001            ("red", 5u64),
1002            ("red", 5),
1003            ("red", 7),
1004            ("green", 3),
1005            ("green", 4),
1006            ("blue", 1),
1007        ];
1008        for (i, (color, amount)) in docs.iter().enumerate() {
1009            insert_widget(&drive, &data_contract, i, color, *amount);
1010        }
1011
1012        let document_type = data_contract
1013            .document_type_for_name("widget")
1014            .expect("widget");
1015        let drive_config = DriveConfig::default();
1016
1017        // Aggregate, no where → empty-where Total path. The doctype
1018        // doesn't declare documents_summable here so the executor
1019        // fall-through is the picker path on the byColor index. But
1020        // the empty-where branch requires documents_summable; if the
1021        // doctype lacks it, the picker is invoked with empty where,
1022        // which `find_summable_index_for_where_clauses` rejects
1023        // (zero indexable fields). So we test Equal-only-fully-
1024        // covered instead — same `DocumentSumMode::Total`
1025        // resolution.
1026        let where_clauses = vec![WhereClause {
1027            field: "color".to_string(),
1028            operator: WhereOperator::Equal,
1029            value: Value::Text("red".to_string()),
1030        }];
1031
1032        let request = DocumentAverageRequest {
1033            contract: &data_contract,
1034            document_type,
1035            sum_property: "amount".to_string(),
1036            where_clauses: where_clauses.clone(),
1037            order_clauses: Vec::new(),
1038            mode: AverageMode::Aggregate,
1039            limit: None,
1040            prove: false,
1041            drive_config: &drive_config,
1042            resolved_time_ranges: vec![],
1043        };
1044
1045        let joint_response = drive
1046            .execute_document_average_request(request, None, platform_version)
1047            .expect("joint total dispatch");
1048        let (joint_count, joint_sum) = match joint_response {
1049            DocumentAverageResponse::Aggregate { count, sum } => (count, sum),
1050            other => panic!("expected Aggregate, got {:?}", other),
1051        };
1052
1053        let (indep_count, indep_sum) = independent_count_sum_aggregate(
1054            &drive,
1055            &data_contract,
1056            document_type,
1057            "amount",
1058            where_clauses,
1059            &drive_config,
1060            platform_version,
1061        );
1062
1063        assert_eq!(
1064            (joint_count, joint_sum),
1065            (indep_count, indep_sum),
1066            "joint total executor must produce the same (count, sum) as \
1067             independent count + sum dispatch (red == 3 docs / sum 17)"
1068        );
1069        // Sanity check against the fixture: red docs are 5+5+7 = 17 / count 3.
1070        assert_eq!((joint_count, joint_sum), (3, 17));
1071    }
1072
1073    /// `execute_document_count_and_sum_per_in_value_no_proof`
1074    /// cross-check: In on a `summable + countable` index.
1075    #[test]
1076    fn joint_per_in_value_executor_matches_independent_count_plus_sum() {
1077        let drive = setup_drive_with_initial_state_structure(None);
1078        let platform_version = PlatformVersion::latest();
1079
1080        let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
1081        let document_schema = platform_value!({
1082            "type": "object",
1083            "properties": {
1084                "color":  {"type": "string",  "position": 0, "maxLength": 32},
1085                "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
1086            },
1087            "required": ["color", "amount"],
1088            "indices": [{
1089                "name": "byColor",
1090                "properties": [{"color": "asc"}],
1091                "summable":  "amount",
1092                "countable": "countable",
1093            }],
1094            "additionalProperties": false,
1095        });
1096        let schemas = platform_value!({ "widget": document_schema });
1097        let data_contract = factory
1098            .create_with_value_config(
1099                dpp::tests::utils::generate_random_identifier_struct(),
1100                0,
1101                schemas,
1102                None,
1103                None,
1104            )
1105            .expect("create data contract")
1106            .data_contract_owned();
1107        drive
1108            .apply_contract(
1109                &data_contract,
1110                BlockInfo::default(),
1111                true,
1112                StorageFlags::optional_default_as_cow(),
1113                None,
1114                platform_version,
1115            )
1116            .expect("apply contract");
1117
1118        let docs = [
1119            ("red", 5u64),
1120            ("red", 7),
1121            ("green", 3),
1122            ("green", 4),
1123            ("blue", 1),
1124            ("blue", 2),
1125        ];
1126        for (i, (color, amount)) in docs.iter().enumerate() {
1127            insert_widget(&drive, &data_contract, i, color, *amount);
1128        }
1129
1130        let document_type = data_contract
1131            .document_type_for_name("widget")
1132            .expect("widget");
1133        let drive_config = DriveConfig::default();
1134
1135        let color_in = WhereClause {
1136            field: "color".to_string(),
1137            operator: WhereOperator::In,
1138            value: Value::Array(vec![
1139                Value::Text("red".to_string()),
1140                Value::Text("green".to_string()),
1141            ]),
1142        };
1143
1144        let request = DocumentAverageRequest {
1145            contract: &data_contract,
1146            document_type,
1147            sum_property: "amount".to_string(),
1148            where_clauses: vec![color_in.clone()],
1149            order_clauses: Vec::new(),
1150            mode: AverageMode::GroupByIn,
1151            limit: None,
1152            prove: false,
1153            drive_config: &drive_config,
1154            resolved_time_ranges: vec![],
1155        };
1156
1157        let joint_response = drive
1158            .execute_document_average_request(request, None, platform_version)
1159            .expect("joint per-in-value dispatch");
1160        let joint_entries = match joint_response {
1161            DocumentAverageResponse::Entries(e) => e,
1162            other => panic!("expected Entries, got {:?}", other),
1163        };
1164
1165        // Cross-check via independent count + sum per-In dispatch.
1166        let count_request = DocumentCountRequest {
1167            contract: &data_contract,
1168            document_type,
1169            where_clauses: vec![color_in.clone()],
1170            order_clauses: Vec::new(),
1171            mode: CountMode::GroupByIn,
1172            limit: None,
1173            prove: false,
1174            drive_config: &drive_config,
1175            resolved_time_ranges: vec![],
1176        };
1177        let sum_request = DocumentSumRequest {
1178            contract: &data_contract,
1179            document_type,
1180            sum_property: "amount".to_string(),
1181            where_clauses: vec![color_in],
1182            order_clauses: Vec::new(),
1183            mode: SumMode::GroupByIn,
1184            limit: None,
1185            prove: false,
1186            drive_config: &drive_config,
1187            resolved_time_ranges: vec![],
1188        };
1189        let count_resp = drive
1190            .execute_document_count_request(count_request, None, platform_version)
1191            .expect("independent count");
1192        let sum_resp = drive
1193            .execute_document_sum_request(sum_request, None, platform_version)
1194            .expect("independent sum");
1195        let count_entries = match count_resp {
1196            DocumentCountResponse::Entries(e) => e,
1197            other => panic!("expected count Entries, got {:?}", other),
1198        };
1199        let sum_entries = match sum_resp {
1200            DocumentSumResponse::Entries(e) => e,
1201            other => panic!("expected sum Entries, got {:?}", other),
1202        };
1203
1204        // Zip by key and assert joint matches.
1205        assert_eq!(joint_entries.len(), count_entries.len());
1206        assert_eq!(joint_entries.len(), sum_entries.len());
1207        for ((joint, count), sum) in joint_entries
1208            .iter()
1209            .zip(count_entries.iter())
1210            .zip(sum_entries.iter())
1211        {
1212            assert_eq!(joint.key, count.key);
1213            assert_eq!(joint.key, sum.key);
1214            assert_eq!(joint.count, count.count);
1215            assert_eq!(joint.sum, sum.sum);
1216        }
1217        // Two entries — red and green.
1218        assert_eq!(joint_entries.len(), 2);
1219        // red: 2 docs, sum = 12.
1220        // green: 2 docs, sum = 7.
1221        // BTreeMap orders by serialized key bytes (lex on string
1222        // bytes since color is Text). "green" < "red" lex.
1223        let mut by_key: Vec<&AverageEntry> = joint_entries.iter().collect();
1224        by_key.sort_by(|a, b| a.key.cmp(&b.key));
1225        let red_entry = by_key
1226            .iter()
1227            .find(|e| e.key.windows(3).any(|w| w == b"red"))
1228            .expect("red entry");
1229        let green_entry = by_key
1230            .iter()
1231            .find(|e| e.key.windows(5).any(|w| w == b"green"))
1232            .expect("green entry");
1233        assert_eq!(red_entry.count, Some(2));
1234        assert_eq!(red_entry.sum, Some(12));
1235        assert_eq!(green_entry.count, Some(2));
1236        assert_eq!(green_entry.sum, Some(7));
1237    }
1238
1239    /// `execute_document_count_and_sum_range_no_proof` cross-check:
1240    /// distinct GroupByRange on a `rangeAverageable` (PCPS) index.
1241    #[test]
1242    fn joint_range_no_proof_executor_matches_independent_count_plus_sum() {
1243        let drive = setup_drive_with_initial_state_structure(None);
1244        let platform_version = PlatformVersion::latest();
1245        let data_contract = build_widget_contract_pcps();
1246        drive
1247            .apply_contract(
1248                &data_contract,
1249                BlockInfo::default(),
1250                true,
1251                StorageFlags::optional_default_as_cow(),
1252                None,
1253                platform_version,
1254            )
1255            .expect("apply contract");
1256
1257        let docs = [
1258            ("red", 5u64),
1259            ("red", 7),
1260            ("green", 3),
1261            ("green", 4),
1262            ("green", 6),
1263            ("blue", 2),
1264        ];
1265        for (i, (color, amount)) in docs.iter().enumerate() {
1266            insert_widget(&drive, &data_contract, i, color, *amount);
1267        }
1268
1269        let document_type = data_contract
1270            .document_type_for_name("widget")
1271            .expect("widget");
1272        let drive_config = DriveConfig::default();
1273
1274        // `color > "blue"` on the byColor rangeAverageable index.
1275        let color_gt_blue = WhereClause {
1276            field: "color".to_string(),
1277            operator: WhereOperator::GreaterThan,
1278            value: Value::Text("blue".to_string()),
1279        };
1280
1281        let request = DocumentAverageRequest {
1282            contract: &data_contract,
1283            document_type,
1284            sum_property: "amount".to_string(),
1285            where_clauses: vec![color_gt_blue.clone()],
1286            order_clauses: Vec::new(),
1287            mode: AverageMode::GroupByRange,
1288            limit: None,
1289            prove: false,
1290            drive_config: &drive_config,
1291            resolved_time_ranges: vec![],
1292        };
1293
1294        let joint_response = drive
1295            .execute_document_average_request(request, None, platform_version)
1296            .expect("joint range distinct dispatch");
1297        let joint_entries = match joint_response {
1298            DocumentAverageResponse::Entries(e) => e,
1299            other => panic!("expected Entries, got {:?}", other),
1300        };
1301
1302        // Cross-check via independent count + sum distinct dispatch.
1303        let count_request = DocumentCountRequest {
1304            contract: &data_contract,
1305            document_type,
1306            where_clauses: vec![color_gt_blue.clone()],
1307            order_clauses: Vec::new(),
1308            mode: CountMode::GroupByRange,
1309            limit: None,
1310            prove: false,
1311            drive_config: &drive_config,
1312            resolved_time_ranges: vec![],
1313        };
1314        let sum_request = DocumentSumRequest {
1315            contract: &data_contract,
1316            document_type,
1317            sum_property: "amount".to_string(),
1318            where_clauses: vec![color_gt_blue],
1319            order_clauses: Vec::new(),
1320            mode: SumMode::GroupByRange,
1321            limit: None,
1322            prove: false,
1323            drive_config: &drive_config,
1324            resolved_time_ranges: vec![],
1325        };
1326        let count_resp = drive
1327            .execute_document_count_request(count_request, None, platform_version)
1328            .expect("independent count");
1329        let sum_resp = drive
1330            .execute_document_sum_request(sum_request, None, platform_version)
1331            .expect("independent sum");
1332        let count_entries = match count_resp {
1333            DocumentCountResponse::Entries(e) => e,
1334            other => panic!("expected count Entries, got {:?}", other),
1335        };
1336        let sum_entries = match sum_resp {
1337            DocumentSumResponse::Entries(e) => e,
1338            other => panic!("expected sum Entries, got {:?}", other),
1339        };
1340
1341        // Both executors emit per-distinct-key entries in ascending
1342        // serialized-key order; the lengths must match and per-key
1343        // (count, sum) must zip to the same values.
1344        assert_eq!(joint_entries.len(), count_entries.len());
1345        assert_eq!(joint_entries.len(), sum_entries.len());
1346        for ((joint, count), sum) in joint_entries
1347            .iter()
1348            .zip(count_entries.iter())
1349            .zip(sum_entries.iter())
1350        {
1351            assert_eq!(joint.key, count.key);
1352            assert_eq!(joint.key, sum.key);
1353            assert_eq!(joint.count, count.count);
1354            assert_eq!(joint.sum, sum.sum);
1355        }
1356        // Two distinct keys (green, red); blue is filtered out by
1357        // the range. green: 3 docs, sum=13; red: 2 docs, sum=12.
1358        assert_eq!(joint_entries.len(), 2);
1359    }
1360
1361    /// Flat-summed range cross-check: `Aggregate + range` on a PCPS
1362    /// index resolves to `DocumentSumMode::RangeNoProof` with
1363    /// `walk_mode = RangeSumWalkMode::Aggregate`. The joint executor
1364    /// folds visited PCPS elements via `count_sum_value_or_default()`
1365    /// in Rust (no engine-side combined accumulator exists). Pin parity
1366    /// vs. the independent count + sum aggregate dispatch — this is the
1367    /// path where the issue's perf win lands.
1368    #[test]
1369    fn joint_range_aggregate_executor_matches_independent_count_plus_sum() {
1370        let drive = setup_drive_with_initial_state_structure(None);
1371        let platform_version = PlatformVersion::latest();
1372        let data_contract = build_widget_contract_pcps();
1373        drive
1374            .apply_contract(
1375                &data_contract,
1376                BlockInfo::default(),
1377                true,
1378                StorageFlags::optional_default_as_cow(),
1379                None,
1380                platform_version,
1381            )
1382            .expect("apply contract");
1383
1384        let docs = [
1385            ("red", 5u64),
1386            ("red", 7),
1387            ("green", 3),
1388            ("green", 4),
1389            ("green", 6),
1390            ("blue", 2),
1391        ];
1392        for (i, (color, amount)) in docs.iter().enumerate() {
1393            insert_widget(&drive, &data_contract, i, color, *amount);
1394        }
1395
1396        let document_type = data_contract
1397            .document_type_for_name("widget")
1398            .expect("widget");
1399        let drive_config = DriveConfig::default();
1400
1401        let color_gt_blue = WhereClause {
1402            field: "color".to_string(),
1403            operator: WhereOperator::GreaterThan,
1404            value: Value::Text("blue".to_string()),
1405        };
1406
1407        let request = DocumentAverageRequest {
1408            contract: &data_contract,
1409            document_type,
1410            sum_property: "amount".to_string(),
1411            where_clauses: vec![color_gt_blue.clone()],
1412            order_clauses: Vec::new(),
1413            mode: AverageMode::Aggregate,
1414            limit: None,
1415            prove: false,
1416            drive_config: &drive_config,
1417            resolved_time_ranges: vec![],
1418        };
1419
1420        let joint_response = drive
1421            .execute_document_average_request(request, None, platform_version)
1422            .expect("joint range aggregate dispatch");
1423        let (joint_count, joint_sum) = match joint_response {
1424            DocumentAverageResponse::Aggregate { count, sum } => (count, sum),
1425            other => panic!("expected Aggregate, got {:?}", other),
1426        };
1427
1428        let (indep_count, indep_sum) = independent_count_sum_aggregate(
1429            &drive,
1430            &data_contract,
1431            document_type,
1432            "amount",
1433            vec![color_gt_blue],
1434            &drive_config,
1435            platform_version,
1436        );
1437
1438        assert_eq!(
1439            (joint_count, joint_sum),
1440            (indep_count, indep_sum),
1441            "joint range-aggregate executor must produce the same (count, sum) \
1442             as independent count + sum range dispatch"
1443        );
1444        // Sanity check: color > "blue" matches green (3,4,6 = sum 13)
1445        // + red (5,7 = sum 12); total 5 docs / sum 25.
1446        assert_eq!((joint_count, joint_sum), (5, 25));
1447    }
1448
1449    /// Compound-summed range cross-check: `GroupByIn + In + range` on
1450    /// a PCPS index resolves to `DocumentSumMode::RangeNoProof` with
1451    /// `walk_mode = RangeSumWalkMode::Aggregate`. The joint executor's
1452    /// distinct path query expresses the multi-In outer walk as a
1453    /// single grovedb call (atomicity inherent) and folds each
1454    /// In-branch's PCPS elements into one `(count, sum)` pair via
1455    /// `count_sum_value_or_default()`.
1456    ///
1457    /// Pin parity vs. the independent count + sum dispatch. This is
1458    /// the second untested-flat-summed branch the agent's three tests
1459    /// don't cover.
1460    #[test]
1461    fn joint_range_group_by_in_executor_matches_independent_count_plus_sum() {
1462        let drive = setup_drive_with_initial_state_structure(None);
1463        let platform_version = PlatformVersion::latest();
1464
1465        // PCPS index keyed on (color, amount) so In on color + range
1466        // on amount fits the rangeCountable + rangeSummable shape.
1467        let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
1468        let document_schema = platform_value!({
1469            "type": "object",
1470            "properties": {
1471                "color":  {"type": "string",  "position": 0, "maxLength": 32},
1472                "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
1473            },
1474            "required": ["color", "amount"],
1475            "indices": [{
1476                "name": "byColorAmount",
1477                "properties": [{"color": "asc"}, {"amount": "asc"}],
1478                "summable":        "amount",
1479                "rangeSummable":   true,
1480                "countable":       "countable",
1481                "rangeCountable":  true,
1482            }],
1483            "additionalProperties": false,
1484        });
1485        let schemas = platform_value!({ "widget": document_schema });
1486        let data_contract = factory
1487            .create_with_value_config(
1488                dpp::tests::utils::generate_random_identifier_struct(),
1489                0,
1490                schemas,
1491                None,
1492                None,
1493            )
1494            .expect("create data contract")
1495            .data_contract_owned();
1496        drive
1497            .apply_contract(
1498                &data_contract,
1499                BlockInfo::default(),
1500                true,
1501                StorageFlags::optional_default_as_cow(),
1502                None,
1503                platform_version,
1504            )
1505            .expect("apply contract");
1506
1507        let docs = [
1508            ("red", 5u64),
1509            ("red", 7),
1510            ("red", 9),
1511            ("green", 3),
1512            ("green", 4),
1513            ("blue", 8),
1514            ("blue", 9),
1515        ];
1516        for (i, (color, amount)) in docs.iter().enumerate() {
1517            insert_widget(&drive, &data_contract, i, color, *amount);
1518        }
1519
1520        let document_type = data_contract
1521            .document_type_for_name("widget")
1522            .expect("widget");
1523        let drive_config = DriveConfig::default();
1524
1525        // In on color (red, green) + range on amount (≥ 4).
1526        let color_in = WhereClause {
1527            field: "color".to_string(),
1528            operator: WhereOperator::In,
1529            value: Value::Array(vec![
1530                Value::Text("red".to_string()),
1531                Value::Text("green".to_string()),
1532            ]),
1533        };
1534        let amount_ge_4 = WhereClause {
1535            field: "amount".to_string(),
1536            operator: WhereOperator::GreaterThanOrEquals,
1537            value: Value::U64(4),
1538        };
1539
1540        let request = DocumentAverageRequest {
1541            contract: &data_contract,
1542            document_type,
1543            sum_property: "amount".to_string(),
1544            where_clauses: vec![color_in.clone(), amount_ge_4.clone()],
1545            order_clauses: Vec::new(),
1546            mode: AverageMode::GroupByIn,
1547            limit: None,
1548            prove: false,
1549            drive_config: &drive_config,
1550            resolved_time_ranges: vec![],
1551        };
1552
1553        let joint_response = drive
1554            .execute_document_average_request(request, None, platform_version)
1555            .expect("joint range GroupByIn dispatch");
1556        let joint_entries = match joint_response {
1557            DocumentAverageResponse::Entries(e) => e,
1558            other => panic!("expected Entries, got {:?}", other),
1559        };
1560
1561        // Independent count + sum GroupByIn dispatch.
1562        let count_request = DocumentCountRequest {
1563            contract: &data_contract,
1564            document_type,
1565            where_clauses: vec![color_in.clone(), amount_ge_4.clone()],
1566            order_clauses: Vec::new(),
1567            mode: CountMode::GroupByIn,
1568            limit: None,
1569            prove: false,
1570            drive_config: &drive_config,
1571            resolved_time_ranges: vec![],
1572        };
1573        let sum_request = DocumentSumRequest {
1574            contract: &data_contract,
1575            document_type,
1576            sum_property: "amount".to_string(),
1577            where_clauses: vec![color_in, amount_ge_4],
1578            order_clauses: Vec::new(),
1579            mode: SumMode::GroupByIn,
1580            limit: None,
1581            prove: false,
1582            drive_config: &drive_config,
1583            resolved_time_ranges: vec![],
1584        };
1585        let count_resp = drive
1586            .execute_document_count_request(count_request, None, platform_version)
1587            .expect("independent count");
1588        let sum_resp = drive
1589            .execute_document_sum_request(sum_request, None, platform_version)
1590            .expect("independent sum");
1591        let count_entries = match count_resp {
1592            DocumentCountResponse::Entries(e) => e,
1593            other => panic!("expected count Entries, got {:?}", other),
1594        };
1595        let sum_entries = match sum_resp {
1596            DocumentSumResponse::Entries(e) => e,
1597            other => panic!("expected sum Entries, got {:?}", other),
1598        };
1599
1600        // The independent count and sum dispatches both produce entries
1601        // for every In branch (with `count`/`sum` reflecting the In
1602        // branch's value); the joint executor must produce the same
1603        // shape. Build a key-keyed map for each and assert pairwise
1604        // equality on the (count, sum) pair.
1605        use std::collections::BTreeMap;
1606        let count_by_key: BTreeMap<Vec<u8>, Option<u64>> = count_entries
1607            .iter()
1608            .map(|e| (e.key.clone(), e.count))
1609            .collect();
1610        let sum_by_key: BTreeMap<Vec<u8>, Option<i64>> =
1611            sum_entries.iter().map(|e| (e.key.clone(), e.sum)).collect();
1612        let joint_by_key: BTreeMap<Vec<u8>, (Option<u64>, Option<i64>)> = joint_entries
1613            .iter()
1614            .map(|e| (e.key.clone(), (e.count, e.sum)))
1615            .collect();
1616
1617        assert_eq!(
1618            count_by_key.keys().collect::<Vec<_>>(),
1619            joint_by_key.keys().collect::<Vec<_>>(),
1620            "joint executor must emit the same In-branch keys as independent count"
1621        );
1622        for (key, (joint_count, joint_sum)) in joint_by_key.iter() {
1623            assert_eq!(joint_count, count_by_key.get(key).unwrap());
1624            assert_eq!(joint_sum, sum_by_key.get(key).unwrap());
1625        }
1626    }
1627
1628    /// Distinct AVG no-proof MUST honor the request's `limit` —
1629    /// `GroupByRange` over a wide range should truncate to the
1630    /// caller's `limit` rather than enumerate every distinct in-range
1631    /// terminator. Regression test for the joint dispatcher's
1632    /// `RangeNoProof` distinct branch: prior to the P2 fix the
1633    /// dispatcher hard-coded `None` into `distinct_sum_path_query`,
1634    /// silently returning every matching key.
1635    #[test]
1636    fn distinct_avg_no_proof_honors_explicit_limit() {
1637        let drive = setup_drive_with_initial_state_structure(None);
1638        let platform_version = PlatformVersion::latest();
1639        let data_contract = build_widget_contract_pcps();
1640        drive
1641            .apply_contract(
1642                &data_contract,
1643                BlockInfo::default(),
1644                true,
1645                StorageFlags::optional_default_as_cow(),
1646                None,
1647                platform_version,
1648            )
1649            .expect("apply contract");
1650
1651        // Five distinct color buckets so a `limit = 2` request must
1652        // truncate the result set; otherwise the executor would emit
1653        // all five.
1654        let docs = [
1655            ("red", 5u64),
1656            ("green", 7),
1657            ("blue", 2),
1658            ("yellow", 4),
1659            ("purple", 9),
1660        ];
1661        for (i, (color, amount)) in docs.iter().enumerate() {
1662            insert_widget(&drive, &data_contract, i, color, *amount);
1663        }
1664
1665        let document_type = data_contract
1666            .document_type_for_name("widget")
1667            .expect("widget");
1668        let drive_config = DriveConfig::default();
1669
1670        let color_ge_a = WhereClause {
1671            field: "color".to_string(),
1672            operator: WhereOperator::GreaterThanOrEquals,
1673            value: Value::Text("a".to_string()),
1674        };
1675
1676        let request = DocumentAverageRequest {
1677            contract: &data_contract,
1678            document_type,
1679            sum_property: "amount".to_string(),
1680            where_clauses: vec![color_ge_a],
1681            order_clauses: Vec::new(),
1682            mode: AverageMode::GroupByRange,
1683            limit: Some(2),
1684            prove: false,
1685            drive_config: &drive_config,
1686            resolved_time_ranges: vec![],
1687        };
1688
1689        let response = drive
1690            .execute_document_average_request(request, None, platform_version)
1691            .expect("dispatcher should succeed");
1692        let entries = match response {
1693            DocumentAverageResponse::Entries(e) => e,
1694            other => panic!("expected Entries, got {:?}", other),
1695        };
1696        assert_eq!(
1697            entries.len(),
1698            2,
1699            "distinct AVG no-proof must apply the request's `limit = 2` and \
1700             return exactly 2 entries; got {entries:?}"
1701        );
1702    }
1703
1704    /// Distinct AVG no-proof with `limit = None` must default to
1705    /// `drive_config.default_query_limit`, not enumerate every
1706    /// distinct key. Regression test for the same hard-coded `None`
1707    /// the prior implementation passed.
1708    #[test]
1709    fn distinct_avg_no_proof_defaults_limit_to_operator_default_query_limit() {
1710        let drive = setup_drive_with_initial_state_structure(None);
1711        let platform_version = PlatformVersion::latest();
1712        let data_contract = build_widget_contract_pcps();
1713        drive
1714            .apply_contract(
1715                &data_contract,
1716                BlockInfo::default(),
1717                true,
1718                StorageFlags::optional_default_as_cow(),
1719                None,
1720                platform_version,
1721            )
1722            .expect("apply contract");
1723
1724        // Five distinct buckets and an operator-tuned
1725        // `default_query_limit = 3`. The dispatcher must honor the
1726        // operator's runtime default on the no-proof path (this is
1727        // explicitly documented as DIFFERENT from the prove path,
1728        // which uses the compile-time constant for byte-stability of
1729        // proof reconstruction). A regression that leaves limit as
1730        // `None` would emit all 5 entries.
1731        let docs = [
1732            ("red", 5u64),
1733            ("green", 7),
1734            ("blue", 2),
1735            ("yellow", 4),
1736            ("purple", 9),
1737        ];
1738        for (i, (color, amount)) in docs.iter().enumerate() {
1739            insert_widget(&drive, &data_contract, i, color, *amount);
1740        }
1741
1742        let document_type = data_contract
1743            .document_type_for_name("widget")
1744            .expect("widget");
1745        let drive_config = DriveConfig {
1746            default_query_limit: 3,
1747            ..Default::default()
1748        };
1749
1750        let color_ge_a = WhereClause {
1751            field: "color".to_string(),
1752            operator: WhereOperator::GreaterThanOrEquals,
1753            value: Value::Text("a".to_string()),
1754        };
1755        let request = DocumentAverageRequest {
1756            contract: &data_contract,
1757            document_type,
1758            sum_property: "amount".to_string(),
1759            where_clauses: vec![color_ge_a],
1760            order_clauses: Vec::new(),
1761            mode: AverageMode::GroupByRange,
1762            limit: None,
1763            prove: false,
1764            drive_config: &drive_config,
1765            resolved_time_ranges: vec![],
1766        };
1767
1768        let response = drive
1769            .execute_document_average_request(request, None, platform_version)
1770            .expect("dispatcher should succeed");
1771        let entries = match response {
1772            DocumentAverageResponse::Entries(e) => e,
1773            other => panic!("expected Entries, got {:?}", other),
1774        };
1775        assert_eq!(
1776            entries.len(),
1777            3,
1778            "distinct AVG no-proof with `limit = None` must default to \
1779             `drive_config.default_query_limit` (= 3 here) rather than \
1780             enumerating all 5 distinct keys; got {entries:?}"
1781        );
1782    }
1783
1784    /// Distinct AVG no-proof with `limit > max_query_limit` must
1785    /// clamp to `max_query_limit`, not return an error. Mirrors
1786    /// count's no-proof distinct-walk clamp policy (documented in
1787    /// `DocumentAverageRequest::limit`).
1788    #[test]
1789    fn distinct_avg_no_proof_clamps_limit_to_max_query_limit() {
1790        let drive = setup_drive_with_initial_state_structure(None);
1791        let platform_version = PlatformVersion::latest();
1792        let data_contract = build_widget_contract_pcps();
1793        drive
1794            .apply_contract(
1795                &data_contract,
1796                BlockInfo::default(),
1797                true,
1798                StorageFlags::optional_default_as_cow(),
1799                None,
1800                platform_version,
1801            )
1802            .expect("apply contract");
1803
1804        let docs = [
1805            ("red", 5u64),
1806            ("green", 7),
1807            ("blue", 2),
1808            ("yellow", 4),
1809            ("purple", 9),
1810        ];
1811        for (i, (color, amount)) in docs.iter().enumerate() {
1812            insert_widget(&drive, &data_contract, i, color, *amount);
1813        }
1814
1815        let document_type = data_contract
1816            .document_type_for_name("widget")
1817            .expect("widget");
1818        // Operator-tuned `max_query_limit = 2`. An explicit `limit =
1819        // 4` MUST clamp to 2 (no-proof policy; the prove path errors
1820        // on this combination instead — see the
1821        // `range_distinct_avg_proof_rejects_limit_over_max` test
1822        // above for the prove counterpart).
1823        let drive_config = DriveConfig {
1824            default_query_limit: 100,
1825            max_query_limit: 2,
1826            ..Default::default()
1827        };
1828
1829        let color_ge_a = WhereClause {
1830            field: "color".to_string(),
1831            operator: WhereOperator::GreaterThanOrEquals,
1832            value: Value::Text("a".to_string()),
1833        };
1834        let request = DocumentAverageRequest {
1835            contract: &data_contract,
1836            document_type,
1837            sum_property: "amount".to_string(),
1838            where_clauses: vec![color_ge_a],
1839            order_clauses: Vec::new(),
1840            mode: AverageMode::GroupByRange,
1841            limit: Some(4),
1842            prove: false,
1843            drive_config: &drive_config,
1844            resolved_time_ranges: vec![],
1845        };
1846
1847        let response = drive
1848            .execute_document_average_request(request, None, platform_version)
1849            .expect("dispatcher should succeed (no-proof clamps, never errors)");
1850        let entries = match response {
1851            DocumentAverageResponse::Entries(e) => e,
1852            other => panic!("expected Entries, got {:?}", other),
1853        };
1854        assert_eq!(
1855            entries.len(),
1856            2,
1857            "distinct AVG no-proof must clamp `limit = 4` to \
1858             `max_query_limit = 2`; got {entries:?}"
1859        );
1860    }
1861
1862    /// `execute_document_count_and_sum_request` must reject a direct
1863    /// caller passing `prove = true`. The wrapper
1864    /// `execute_document_average_request` is the only legitimate entry
1865    /// that routes prove requests (to the prove-side dispatcher);
1866    /// reaching the joint dispatcher with `prove = true` would
1867    /// otherwise silently produce a no-proof response. Regression for
1868    /// the CodeRabbit "enforce no-prove precondition" finding.
1869    #[test]
1870    fn joint_dispatcher_rejects_prove_true_request() {
1871        let drive = setup_drive_with_initial_state_structure(None);
1872        let platform_version = PlatformVersion::latest();
1873        let data_contract = build_widget_contract_pcps();
1874        drive
1875            .apply_contract(
1876                &data_contract,
1877                BlockInfo::default(),
1878                true,
1879                StorageFlags::optional_default_as_cow(),
1880                None,
1881                platform_version,
1882            )
1883            .expect("apply contract");
1884
1885        let document_type = data_contract
1886            .document_type_for_name("widget")
1887            .expect("widget");
1888        let drive_config = DriveConfig::default();
1889
1890        let request = DocumentAverageRequest {
1891            contract: &data_contract,
1892            document_type,
1893            sum_property: "amount".to_string(),
1894            where_clauses: Vec::new(),
1895            order_clauses: Vec::new(),
1896            mode: AverageMode::Aggregate,
1897            limit: None,
1898            prove: true,
1899            drive_config: &drive_config,
1900            resolved_time_ranges: vec![],
1901        };
1902
1903        let err = drive
1904            .execute_document_count_and_sum_request(request, None, platform_version)
1905            .expect_err("prove=true direct call must reject");
1906        let msg = format!("{err:?}");
1907        assert!(
1908            msg.contains("no-prove"),
1909            "expected the prove=true guard to fire; got: {msg}"
1910        );
1911    }
1912
1913    /// AVG no-proof dispatcher must run
1914    /// `validate_and_canonicalize_where_clauses` so it shares the same
1915    /// accept/reject contract as the count and document-query
1916    /// surfaces. Pin a representative rejection: a duplicate Equal on
1917    /// the same field. Without the validator the executor would
1918    /// either succeed with a silently-collapsed shape or fail
1919    /// downstream with a less precise error.
1920    #[test]
1921    fn joint_dispatcher_runs_validate_and_canonicalize_where_clauses() {
1922        let drive = setup_drive_with_initial_state_structure(None);
1923        let platform_version = PlatformVersion::latest();
1924        let data_contract = build_widget_contract_pcps();
1925        drive
1926            .apply_contract(
1927                &data_contract,
1928                BlockInfo::default(),
1929                true,
1930                StorageFlags::optional_default_as_cow(),
1931                None,
1932                platform_version,
1933            )
1934            .expect("apply contract");
1935
1936        let document_type = data_contract
1937            .document_type_for_name("widget")
1938            .expect("widget");
1939        let drive_config = DriveConfig::default();
1940
1941        // Duplicate Equal on `color` — validator rejects via
1942        // `WhereClause::group_clauses`.
1943        let dup_color_a = WhereClause {
1944            field: "color".to_string(),
1945            operator: WhereOperator::Equal,
1946            value: Value::Text("red".to_string()),
1947        };
1948        let dup_color_b = WhereClause {
1949            field: "color".to_string(),
1950            operator: WhereOperator::Equal,
1951            value: Value::Text("green".to_string()),
1952        };
1953        let request = DocumentAverageRequest {
1954            contract: &data_contract,
1955            document_type,
1956            sum_property: "amount".to_string(),
1957            where_clauses: vec![dup_color_a, dup_color_b],
1958            order_clauses: Vec::new(),
1959            mode: AverageMode::Aggregate,
1960            limit: None,
1961            prove: false,
1962            drive_config: &drive_config,
1963            resolved_time_ranges: vec![],
1964        };
1965
1966        let err = drive
1967            .execute_document_average_request(request, None, platform_version)
1968            .expect_err(
1969                "AVG no-proof must reject duplicate Equal on the same field via \
1970                 validate_and_canonicalize_where_clauses",
1971            );
1972        // The exact error variant comes from `WhereClause::group_clauses` —
1973        // pin only that the call returned `Err` and the error mentions
1974        // the problematic shape rather than a generic index-picker miss.
1975        let msg = format!("{err:?}");
1976        assert!(
1977            !msg.contains("WhereClauseOnNonIndexedProperty"),
1978            "validator should reject before the index picker would: {msg}"
1979        );
1980    }
1981
1982    /// The prove path returns before the joint dispatcher, so the
1983    /// provenance-vs-shape guard must run at the shared entry: without it a
1984    /// direct caller marking an `In` clause as time-range-resolved would
1985    /// reach the prove executors, have the pickers admit a bucketed index,
1986    /// and prove an aggregate that counts a document once per overlapping
1987    /// bucket.
1988    #[test]
1989    fn avg_prove_path_rejects_resolved_time_range_provenance_on_an_in_clause() {
1990        let drive = setup_drive_with_initial_state_structure(None);
1991        let platform_version = PlatformVersion::latest();
1992        let data_contract = build_widget_contract_pcps();
1993        drive
1994            .apply_contract(
1995                &data_contract,
1996                BlockInfo::default(),
1997                true,
1998                StorageFlags::optional_default_as_cow(),
1999                None,
2000                platform_version,
2001            )
2002            .expect("apply contract");
2003
2004        let document_type = data_contract
2005            .document_type_for_name("widget")
2006            .expect("widget");
2007        let drive_config = DriveConfig::default();
2008
2009        let in_on_resolved_field = WhereClause {
2010            field: "$createdAt".to_string(),
2011            operator: WhereOperator::In,
2012            value: Value::Array(vec![Value::U64(0), Value::U64(7_200_000)]),
2013        };
2014        let request = DocumentAverageRequest {
2015            contract: &data_contract,
2016            document_type,
2017            sum_property: "amount".to_string(),
2018            where_clauses: vec![in_on_resolved_field],
2019            order_clauses: Vec::new(),
2020            mode: AverageMode::Aggregate,
2021            limit: None,
2022            prove: true,
2023            drive_config: &drive_config,
2024            resolved_time_ranges: vec![ResolvedTimeRange {
2025                transform: dpp::data_contract::document_type::TimeRangeTransform {
2026                    source: "$createdAt".to_string(),
2027                    range_seconds: 21_600,
2028                    step_seconds: 7_200,
2029                    phase_seconds: 0,
2030                    ttl_seconds: None,
2031                }
2032                .into(),
2033            }],
2034        };
2035
2036        let err = drive
2037            .execute_document_average_request(request, None, platform_version)
2038            .expect_err("AVG prove must reject provenance attached to an In clause");
2039        assert!(
2040            format!("{err:?}").contains("InvalidWhereClauseComponents"),
2041            "expected the provenance shape guard, got: {err:?}"
2042        );
2043    }
2044
2045    /// `PerInValue` no-proof AVG must honor `request.limit` on the
2046    /// returned entry list. Regression for the reviewer's "joint
2047    /// dispatcher drops `request.limit`" finding on the PerInValue
2048    /// arm. Count's per-In executor truncates at this same point.
2049    #[test]
2050    fn per_in_value_avg_no_proof_honors_explicit_limit() {
2051        let drive = setup_drive_with_initial_state_structure(None);
2052        let platform_version = PlatformVersion::latest();
2053
2054        // `byColor` index: `summable: "amount"` + `countable:
2055        // "countable"`. No range flags — this is the no-range
2056        // PerInValue shape.
2057        let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
2058        let document_schema = platform_value!({
2059            "type": "object",
2060            "properties": {
2061                "color":  {"type": "string",  "position": 0, "maxLength": 32},
2062                "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
2063            },
2064            "required": ["color", "amount"],
2065            "indices": [{
2066                "name": "byColor",
2067                "properties": [{"color": "asc"}],
2068                "summable":  "amount",
2069                "countable": "countable",
2070            }],
2071            "additionalProperties": false,
2072        });
2073        let schemas = platform_value!({ "widget": document_schema });
2074        let data_contract = factory
2075            .create_with_value_config(
2076                dpp::tests::utils::generate_random_identifier_struct(),
2077                0,
2078                schemas,
2079                None,
2080                None,
2081            )
2082            .expect("create data contract")
2083            .data_contract_owned();
2084        drive
2085            .apply_contract(
2086                &data_contract,
2087                BlockInfo::default(),
2088                true,
2089                StorageFlags::optional_default_as_cow(),
2090                None,
2091                platform_version,
2092            )
2093            .expect("apply contract");
2094
2095        for (i, (color, amount)) in [("red", 5u64), ("green", 7), ("blue", 2), ("yellow", 4)]
2096            .iter()
2097            .enumerate()
2098        {
2099            insert_widget(&drive, &data_contract, i, color, *amount);
2100        }
2101
2102        let document_type = data_contract
2103            .document_type_for_name("widget")
2104            .expect("widget");
2105        let drive_config = DriveConfig::default();
2106
2107        // `In` over 4 color values, `limit = 2` — dispatcher must
2108        // truncate the per-In entry list to 2.
2109        let color_in = WhereClause {
2110            field: "color".to_string(),
2111            operator: WhereOperator::In,
2112            value: Value::Array(vec![
2113                Value::Text("red".to_string()),
2114                Value::Text("green".to_string()),
2115                Value::Text("blue".to_string()),
2116                Value::Text("yellow".to_string()),
2117            ]),
2118        };
2119        let request = DocumentAverageRequest {
2120            contract: &data_contract,
2121            document_type,
2122            sum_property: "amount".to_string(),
2123            where_clauses: vec![color_in],
2124            order_clauses: Vec::new(),
2125            mode: AverageMode::GroupByIn,
2126            limit: Some(2),
2127            prove: false,
2128            drive_config: &drive_config,
2129            resolved_time_ranges: vec![],
2130        };
2131
2132        let response = drive
2133            .execute_document_average_request(request, None, platform_version)
2134            .expect("dispatcher should succeed");
2135        let entries = match response {
2136            DocumentAverageResponse::Entries(e) => e,
2137            other => panic!("expected Entries, got {:?}", other),
2138        };
2139        assert_eq!(
2140            entries.len(),
2141            2,
2142            "PerInValue AVG no-proof must apply request.limit = 2 to the per-In \
2143             entry list (caller asked for 4 In values, dispatcher must truncate); \
2144             got {entries:?}"
2145        );
2146    }
2147
2148    /// Empty-where `Aggregate` AVG MUST exercise the
2149    /// [`Drive::execute_document_count_and_sum_total_no_proof`]
2150    /// primary-key fast path when the doctype declares
2151    /// `documentsAverageable` (= `documentsCountable: true +
2152    /// documentsSummable: "<prop>"`). The fast path reads
2153    /// `[contract_doc, contract_id, [1], doctype, 0]` — the PCPS
2154    /// primary-key element — and decodes `(count, sum)` from it in one
2155    /// grovedb call without any index. Consensus-critical: a regression
2156    /// here would silently produce wrong `(count, sum)` for the
2157    /// most-trafficked AVG shape (unfiltered total).
2158    #[test]
2159    fn empty_where_total_executor_uses_primary_key_count_sum_tree_fast_path() {
2160        let drive = setup_drive_with_initial_state_structure(None);
2161        let platform_version = PlatformVersion::latest();
2162
2163        // `documentsAverageable: "amount"` desugars to BOTH
2164        // `documentsCountable: true` AND `documentsSummable:
2165        // "amount"`, which is exactly what the empty-where fast path
2166        // requires. No `indices` block — the fast path doesn't use
2167        // an index, it reads the doctype's primary-key
2168        // count-sum-bearing tree directly at `[..., doctype, 0]`.
2169        let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
2170        let document_schema = platform_value!({
2171            "type": "object",
2172            "properties": {
2173                "amount": {"type": "integer", "position": 0, "minimum": 0, "maximum": 1000},
2174            },
2175            "required": ["amount"],
2176            "documentsAverageable": "amount",
2177            "additionalProperties": false,
2178        });
2179        let schemas = platform_value!({ "score": document_schema });
2180        let data_contract = factory
2181            .create_with_value_config(
2182                dpp::tests::utils::generate_random_identifier_struct(),
2183                0,
2184                schemas,
2185                None,
2186                None,
2187            )
2188            .expect("create data contract")
2189            .data_contract_owned();
2190        drive
2191            .apply_contract(
2192                &data_contract,
2193                BlockInfo::default(),
2194                true,
2195                StorageFlags::optional_default_as_cow(),
2196                None,
2197                platform_version,
2198            )
2199            .expect("apply contract");
2200
2201        // Insert documents directly (no need for the widget helper —
2202        // this doctype has no color property).
2203        let document_type = data_contract
2204            .document_type_for_name("score")
2205            .expect("score type");
2206        for (i, amount) in [10u64, 20, 30, 40].iter().enumerate() {
2207            let mut properties = std::collections::BTreeMap::new();
2208            properties.insert("amount".to_string(), Value::U64(*amount));
2209            let document: Document = DocumentV0 {
2210                contract_version: None,
2211                id: Identifier::from([(i + 1) as u8; 32]),
2212                owner_id: Identifier::from([0u8; 32]),
2213                properties,
2214                revision: None,
2215                created_at: None,
2216                updated_at: None,
2217                transferred_at: None,
2218                created_at_block_height: None,
2219                updated_at_block_height: None,
2220                transferred_at_block_height: None,
2221                created_at_core_block_height: None,
2222                updated_at_core_block_height: None,
2223                transferred_at_core_block_height: None,
2224                creator_id: None,
2225                moderated_at: None,
2226                moderated_by: None,
2227            }
2228            .into();
2229            let storage_flags = Some(std::borrow::Cow::Owned(StorageFlags::SingleEpoch(0)));
2230            drive
2231                .add_document_for_contract(
2232                    DocumentAndContractInfo {
2233                        owned_document_info: OwnedDocumentInfo {
2234                            document_info: DocumentRefInfo((&document, storage_flags)),
2235                            owner_id: None,
2236                        },
2237                        contract: &data_contract,
2238                        document_type,
2239                    },
2240                    false,
2241                    BlockInfo::default(),
2242                    true,
2243                    None,
2244                    platform_version,
2245                    None,
2246                )
2247                .expect("insert score");
2248        }
2249
2250        let drive_config = DriveConfig::default();
2251        let request = DocumentAverageRequest {
2252            contract: &data_contract,
2253            document_type,
2254            sum_property: "amount".to_string(),
2255            where_clauses: Vec::new(),
2256            order_clauses: Vec::new(),
2257            mode: AverageMode::Aggregate,
2258            limit: None,
2259            prove: false,
2260            drive_config: &drive_config,
2261            resolved_time_ranges: vec![],
2262        };
2263
2264        let response = drive
2265            .execute_document_average_request(request, None, platform_version)
2266            .expect("empty-where AVG no-proof must succeed via the primary-key fast path");
2267        match response {
2268            DocumentAverageResponse::Aggregate { count, sum } => {
2269                assert_eq!(
2270                    (count, sum),
2271                    (4, 100),
2272                    "primary-key count-sum tree fast path must return (4 docs, sum 10+20+30+40 = 100)"
2273                );
2274            }
2275            other => panic!("expected Aggregate, got {:?}", other),
2276        }
2277    }
2278
2279    /// `PerInValue` no-proof AVG with `limit = None` must default to
2280    /// `drive_config.default_query_limit` per
2281    /// `DocumentAverageRequest::limit`'s documented contract.
2282    /// Regression test paired with the explicit-limit case above; pins
2283    /// the no-proof contract parity reviewers flagged after the
2284    /// initial PerInValue fix landed.
2285    #[test]
2286    fn per_in_value_avg_no_proof_defaults_limit_to_operator_default_query_limit() {
2287        let drive = setup_drive_with_initial_state_structure(None);
2288        let platform_version = PlatformVersion::latest();
2289
2290        // Same `summable + countable` `byColor` index as
2291        // `per_in_value_avg_no_proof_honors_explicit_limit`, but with
2292        // `default_query_limit = 2` and `limit = None` on the request
2293        // — the dispatcher must fall back to the operator's runtime
2294        // default and truncate the per-In entry list to 2.
2295        let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
2296        let document_schema = platform_value!({
2297            "type": "object",
2298            "properties": {
2299                "color":  {"type": "string",  "position": 0, "maxLength": 32},
2300                "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
2301            },
2302            "required": ["color", "amount"],
2303            "indices": [{
2304                "name": "byColor",
2305                "properties": [{"color": "asc"}],
2306                "summable":  "amount",
2307                "countable": "countable",
2308            }],
2309            "additionalProperties": false,
2310        });
2311        let schemas = platform_value!({ "widget": document_schema });
2312        let data_contract = factory
2313            .create_with_value_config(
2314                dpp::tests::utils::generate_random_identifier_struct(),
2315                0,
2316                schemas,
2317                None,
2318                None,
2319            )
2320            .expect("create data contract")
2321            .data_contract_owned();
2322        drive
2323            .apply_contract(
2324                &data_contract,
2325                BlockInfo::default(),
2326                true,
2327                StorageFlags::optional_default_as_cow(),
2328                None,
2329                platform_version,
2330            )
2331            .expect("apply contract");
2332
2333        for (i, (color, amount)) in [("red", 5u64), ("green", 7), ("blue", 2), ("yellow", 4)]
2334            .iter()
2335            .enumerate()
2336        {
2337            insert_widget(&drive, &data_contract, i, color, *amount);
2338        }
2339
2340        let document_type = data_contract
2341            .document_type_for_name("widget")
2342            .expect("widget");
2343        let drive_config = DriveConfig {
2344            default_query_limit: 2,
2345            ..Default::default()
2346        };
2347
2348        let color_in = WhereClause {
2349            field: "color".to_string(),
2350            operator: WhereOperator::In,
2351            value: Value::Array(vec![
2352                Value::Text("red".to_string()),
2353                Value::Text("green".to_string()),
2354                Value::Text("blue".to_string()),
2355                Value::Text("yellow".to_string()),
2356            ]),
2357        };
2358        let request = DocumentAverageRequest {
2359            contract: &data_contract,
2360            document_type,
2361            sum_property: "amount".to_string(),
2362            where_clauses: vec![color_in],
2363            order_clauses: Vec::new(),
2364            mode: AverageMode::GroupByIn,
2365            limit: None,
2366            prove: false,
2367            drive_config: &drive_config,
2368            resolved_time_ranges: vec![],
2369        };
2370
2371        let response = drive
2372            .execute_document_average_request(request, None, platform_version)
2373            .expect("dispatcher should succeed");
2374        let entries = match response {
2375            DocumentAverageResponse::Entries(e) => e,
2376            other => panic!("expected Entries, got {:?}", other),
2377        };
2378        assert_eq!(
2379            entries.len(),
2380            2,
2381            "PerInValue AVG no-proof with `limit = None` must default to \
2382             `drive_config.default_query_limit` (= 2 here) and truncate the \
2383             per-In entry list; got {entries:?}"
2384        );
2385    }
2386}