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