Skip to main content

drive/query/drive_document_average_query/
drive_dispatcher.rs

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