1use 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 pub fn execute_document_average_request(
79 &self,
80 mut request: DocumentAverageRequest,
81 transaction: TransactionArg,
82 platform_version: &PlatformVersion,
83 ) -> Result<DocumentAverageResponse, Error> {
84 validate_resolved_time_range_clause_shapes(
93 &request.where_clauses,
94 &request.resolved_time_ranges,
95 )?;
96 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 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 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 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 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 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 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 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 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 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 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 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 "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 #[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 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 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 #[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 #[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 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 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 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 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 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 #[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 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 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 assert_eq!((joint_count, joint_sum), (3, 17));
1081 }
1082
1083 #[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 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 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 assert_eq!(joint_entries.len(), 2);
1229 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 #[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 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 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 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 assert_eq!(joint_entries.len(), 2);
1369 }
1370
1371 #[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 assert_eq!((joint_count, joint_sum), (5, 25));
1457 }
1458
1459 #[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 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 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 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 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 #[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 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 #[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 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 #[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 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 #[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 #[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 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 let msg = format!("{err:?}");
1986 assert!(
1987 !msg.contains("WhereClauseOnNonIndexedProperty"),
1988 "validator should reject before the index picker would: {msg}"
1989 );
1990 }
1991
1992 #[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 #[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 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 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 #[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 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 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 #[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 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}