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 validate_and_canonicalize_where_clauses, validate_resolved_time_range_clause_shapes,
61};
62use dpp::data_contract::accessors::v0::DataContractV0Getters;
63use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters};
64use dpp::version::PlatformVersion;
65use grovedb::TransactionArg;
66
67#[cfg(feature = "server")]
68impl Drive {
69 pub fn execute_document_average_request(
78 &self,
79 mut request: DocumentAverageRequest,
80 transaction: TransactionArg,
81 platform_version: &PlatformVersion,
82 ) -> Result<DocumentAverageResponse, Error> {
83 validate_resolved_time_range_clause_shapes(
92 &request.where_clauses,
93 &request.resolved_time_ranges,
94 )?;
95 if request.prove {
96 request.where_clauses =
102 validate_and_canonicalize_where_clauses(request.where_clauses, platform_version)?;
103 return self.execute_document_average_prove(request, transaction, platform_version);
104 }
105 self.execute_document_count_and_sum_request(request, transaction, platform_version)
106 }
107
108 fn execute_document_average_prove(
141 &self,
142 request: DocumentAverageRequest,
143 transaction: TransactionArg,
144 platform_version: &PlatformVersion,
145 ) -> Result<DocumentAverageResponse, Error> {
146 let contract_id = request.contract.id().to_buffer();
147 let document_type_name = request.document_type.name().to_string();
148 let has_range = request
149 .where_clauses
150 .iter()
151 .any(|wc| is_range_operator(wc.operator));
152 let order_by_ascending = request
153 .order_clauses
154 .first()
155 .map(|c| c.ascending)
156 .unwrap_or(true);
157
158 if matches!(request.mode, AverageMode::Aggregate)
165 && request.where_clauses.is_empty()
166 && request.document_type.documents_countable()
167 && request
168 .document_type
169 .documents_summable()
170 .map(|p| p == request.sum_property)
171 .unwrap_or(false)
172 {
173 let path_query =
174 DriveDocumentSumQuery::primary_key_sum_path_query(contract_id, &document_type_name);
175 let proof = self
176 .grove
177 .get_proved_path_query(
178 &path_query,
179 None,
180 transaction,
181 &platform_version.drive.grove_version,
182 )
183 .unwrap()
184 .map_err(|e| Error::GroveDB(Box::new(e)))?;
185 return Ok(DocumentAverageResponse::Proof(proof));
186 }
187
188 if has_range
192 && matches!(
193 request.mode,
194 AverageMode::Aggregate | AverageMode::GroupByIn
195 )
196 {
197 let index = find_range_summable_index_with_counts_for_where_clauses(
198 request.document_type.indexes(),
199 &request.where_clauses,
200 &request.sum_property,
201 &request.resolved_time_ranges,
202 )
203 .ok_or_else(|| {
204 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
205 "prove AVG requires an index that declares BOTH `rangeCountable: \
206 true` AND `rangeSummable: true` (a `rangeAverageable: true` \
207 index is the shorthand) whose last property matches the range \
208 field and whose summable property matches the request's \
209 `sum_property`"
210 .to_string(),
211 ))
212 })?;
213 let sum_query = DriveDocumentSumQuery {
214 document_type: request.document_type,
215 contract_id,
216 document_type_name,
217 index,
218 where_clauses: request.where_clauses.clone(),
219 sum_property: request.sum_property.clone(),
220 };
221
222 let proof = match request.mode {
223 AverageMode::Aggregate => sum_query.execute_aggregate_count_and_sum_with_proof(
224 self,
225 transaction,
226 platform_version,
227 )?,
228 AverageMode::GroupByIn => {
229 let limit_u16 = request
237 .limit
238 .map(|l| {
239 if l > request.drive_config.max_query_limit as u32 {
240 return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!(
241 "limit {} exceeds max_query_limit {} on the prove + \
242 carrier-aggregate path (GROUP BY In + range, AVG); \
243 reduce the requested limit or use prove = false",
244 l, request.drive_config.max_query_limit
245 ))));
246 }
247 u16::try_from(l).map_err(|_| {
248 Error::Query(QuerySyntaxError::Unsupported(format!(
249 "limit {} exceeds u16::MAX for carrier-aggregate \
250 count+sum (AVG) proof",
251 l
252 )))
253 })
254 })
255 .transpose()?;
256 sum_query.execute_carrier_aggregate_count_and_sum_with_proof(
257 self,
258 limit_u16,
259 order_by_ascending,
260 transaction,
261 platform_version,
262 )?
263 }
264 _ => unreachable!("outer matches! gate filters out non-Aggregate/GroupByIn"),
265 };
266 return Ok(DocumentAverageResponse::Proof(proof));
267 }
268
269 if has_range
278 && matches!(
279 request.mode,
280 AverageMode::GroupByRange | AverageMode::GroupByCompound
281 )
282 {
283 let index = find_range_summable_index_with_counts_for_where_clauses(
284 request.document_type.indexes(),
285 &request.where_clauses,
286 &request.sum_property,
287 &request.resolved_time_ranges,
288 )
289 .ok_or_else(|| {
290 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
291 "prove distinct AVG requires an index that declares BOTH \
292 `rangeCountable: true` AND `rangeSummable: true` (a \
293 `rangeAverageable: true` index is the shorthand) whose last \
294 property matches the range field and whose summable property \
295 matches the request's `sum_property`"
296 .to_string(),
297 ))
298 })?;
299 let effective_limit = request
307 .limit
308 .unwrap_or(crate::config::DEFAULT_QUERY_LIMIT as u32);
309 if effective_limit > request.drive_config.max_query_limit as u32 {
310 return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!(
311 "limit {} exceeds max_query_limit {} on the prove + distinct-walk \
312 path (GROUP BY a range field, AVG); reduce the requested limit \
313 or use prove = false",
314 effective_limit, request.drive_config.max_query_limit
315 ))));
316 }
317 let limit_u16 = u16::try_from(effective_limit).map_err(|_| {
318 Error::Query(QuerySyntaxError::Unsupported(format!(
319 "limit {} exceeds u16::MAX for distinct AVG proof",
320 effective_limit
321 )))
322 })?;
323 let sum_query = DriveDocumentSumQuery {
324 document_type: request.document_type,
325 contract_id,
326 document_type_name,
327 index,
328 where_clauses: request.where_clauses.clone(),
329 sum_property: request.sum_property.clone(),
330 };
331 let proof = sum_query.execute_distinct_sum_with_proof(
332 self,
333 limit_u16,
334 order_by_ascending,
335 transaction,
336 platform_version,
337 )?;
338 return Ok(DocumentAverageResponse::Proof(proof));
339 }
340
341 if !has_range
366 && matches!(
367 request.mode,
368 AverageMode::Aggregate | AverageMode::GroupByIn
369 )
370 {
371 let index = find_summable_index_with_counts_for_where_clauses(
372 request.document_type.indexes(),
373 &request.where_clauses,
374 &request.sum_property,
375 &request.resolved_time_ranges,
376 )
377 .ok_or_else(|| {
378 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
379 "prove point-lookup AVG requires an index that declares BOTH \
380 `summable: \"<prop>\"` AND a countable terminator (`countable: \
381 \"countable\"` or `\"countableAllowingOffset\"`) whose properties \
382 exactly match the where clause fields"
383 .to_string(),
384 ))
385 })?;
386 let sum_query = DriveDocumentSumQuery {
387 document_type: request.document_type,
388 contract_id,
389 document_type_name,
390 index,
391 where_clauses: request.where_clauses.clone(),
392 sum_property: request.sum_property.clone(),
393 };
394 let proof = sum_query.execute_point_lookup_sum_with_proof(
395 self,
396 transaction,
397 platform_version,
398 )?;
399 return Ok(DocumentAverageResponse::Proof(proof));
400 }
401
402 Err(Error::Query(QuerySyntaxError::Unsupported(format!(
407 "execute_document_average_request prove=true: the (mode = {:?}, has_range \
408 = {}) combination is not yet supported on the prove path. \
409 This is likely a new AverageMode variant that hasn't been wired \
410 into the prove dispatcher.",
411 request.mode, has_range,
412 ))))
413 }
414}
415
416#[cfg(all(test, feature = "server"))]
417mod tests {
418 use super::*;
419 use crate::query::ResolvedTimeRange;
420
421 use crate::config::{DriveConfig, DEFAULT_QUERY_LIMIT};
444 use crate::drive::Drive;
445 use crate::error::query::QuerySyntaxError;
446 use crate::query::drive_document_average_query::{
447 AverageMode, DocumentAverageRequest, DocumentAverageResponse,
448 };
449 use crate::query::{WhereClause, WhereOperator};
450 use crate::util::object_size_info::DocumentInfo::DocumentRefInfo;
451 use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo};
452 use crate::util::storage_flags::StorageFlags;
453 use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure;
454 use dpp::block::block_info::BlockInfo;
455 use dpp::data_contract::accessors::v0::DataContractV0Getters;
456 use dpp::data_contract::DataContractFactory;
457 use dpp::document::{Document, DocumentV0};
458 use dpp::identifier::Identifier;
459 use dpp::platform_value::{platform_value, Value};
460 use grovedb::GroveDb;
461 use std::borrow::Cow;
462 use std::collections::BTreeMap as StdBTreeMap;
463
464 const PROTOCOL_VERSION_V12: u32 = 12;
465
466 fn build_widget_contract_pcps() -> dpp::data_contract::DataContract {
471 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
472 let document_schema = platform_value!({
473 "type": "object",
474 "properties": {
475 "color": {"type": "string", "position": 0, "maxLength": 32},
476 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
477 },
478 "required": ["color", "amount"],
479 "indices": [{
480 "name": "byColor",
481 "properties": [{"color": "asc"}],
482 "summable": "amount",
487 "rangeSummable": true,
488 "countable": "countable",
489 "rangeCountable": true,
490 }],
491 "additionalProperties": false,
492 });
493 let schemas = platform_value!({ "widget": document_schema });
494 factory
495 .create_with_value_config(
496 dpp::tests::utils::generate_random_identifier_struct(),
497 0,
498 schemas,
499 None,
500 None,
501 )
502 .expect("create data contract")
503 .data_contract_owned()
504 }
505
506 fn insert_widget(
507 drive: &Drive,
508 contract: &dpp::data_contract::DataContract,
509 i: usize,
510 color: &str,
511 amount: u64,
512 ) {
513 let platform_version = PlatformVersion::latest();
514 let document_type = contract
515 .document_type_for_name("widget")
516 .expect("widget type exists");
517 let mut properties = StdBTreeMap::new();
518 properties.insert("color".to_string(), Value::Text(color.to_string()));
519 properties.insert("amount".to_string(), Value::U64(amount));
520 let document: Document = DocumentV0 {
521 contract_version: None,
522 id: Identifier::from([(i + 1) as u8; 32]),
523 owner_id: Identifier::from([0u8; 32]),
524 properties,
525 revision: None,
526 created_at: None,
527 updated_at: None,
528 transferred_at: None,
529 created_at_block_height: None,
530 updated_at_block_height: None,
531 transferred_at_block_height: None,
532 created_at_core_block_height: None,
533 updated_at_core_block_height: None,
534 transferred_at_core_block_height: None,
535 creator_id: None,
536 moderated_at: None,
537 moderated_by: None,
538 }
539 .into();
540 let storage_flags = Some(Cow::Owned(StorageFlags::SingleEpoch(0)));
541 drive
542 .add_document_for_contract(
543 DocumentAndContractInfo {
544 owned_document_info: OwnedDocumentInfo {
545 document_info: DocumentRefInfo((&document, storage_flags)),
546 owner_id: None,
547 },
548 contract,
549 document_type,
550 },
551 false,
552 BlockInfo::default(),
553 true,
554 None,
555 platform_version,
556 None,
557 )
558 .expect("insert widget");
559 }
560
561 #[test]
570 fn range_distinct_avg_proof_uses_compile_time_default_query_limit_not_operator_config() {
571 const OPERATOR_TUNED_LIMIT: u16 = 1;
572 assert_ne!(
573 DEFAULT_QUERY_LIMIT, OPERATOR_TUNED_LIMIT,
574 "test invariant: OPERATOR_TUNED_LIMIT must differ from DEFAULT_QUERY_LIMIT"
575 );
576
577 let drive = setup_drive_with_initial_state_structure(None);
578 let platform_version = PlatformVersion::latest();
579 let data_contract = build_widget_contract_pcps();
580 drive
581 .apply_contract(
582 &data_contract,
583 BlockInfo::default(),
584 true,
585 StorageFlags::optional_default_as_cow(),
586 None,
587 platform_version,
588 )
589 .expect("apply contract");
590
591 let docs = [
592 ("red", 5u64),
593 ("red", 5),
594 ("green", 7),
595 ("green", 7),
596 ("green", 7),
597 ("blue", 2),
598 ];
599 for (i, (color, amount)) in docs.iter().enumerate() {
600 insert_widget(&drive, &data_contract, i, color, *amount);
601 }
602
603 let document_type = data_contract
604 .document_type_for_name("widget")
605 .expect("widget");
606
607 let drive_config = DriveConfig {
608 default_query_limit: OPERATOR_TUNED_LIMIT,
609 ..Default::default()
610 };
611
612 let color_gt_blue = WhereClause {
613 field: "color".to_string(),
614 operator: WhereOperator::GreaterThan,
615 value: Value::Text("blue".to_string()),
616 };
617 let request = DocumentAverageRequest {
618 contract: &data_contract,
619 document_type,
620 sum_property: "amount".to_string(),
621 where_clauses: vec![color_gt_blue.clone()],
622 order_clauses: Vec::new(),
623 mode: AverageMode::GroupByRange,
624 limit: None,
625 prove: true,
626 drive_config: &drive_config,
627 resolved_time_ranges: vec![],
628 };
629
630 let response = drive
631 .execute_document_average_request(request, None, platform_version)
632 .expect("dispatcher should succeed on distinct AVG path");
633 let proof_bytes = match response {
634 DocumentAverageResponse::Proof(p) => p,
635 other => panic!("expected Proof response, got {:?}", other),
636 };
637 assert!(!proof_bytes.is_empty(), "non-empty proof bytes expected");
638
639 let index = find_range_summable_index_with_counts_for_where_clauses(
642 document_type.indexes(),
643 std::slice::from_ref(&color_gt_blue),
644 "amount",
645 &[],
646 )
647 .expect("byColor rangeAverageable index covers `color > blue`");
648 let sum_query = DriveDocumentSumQuery {
649 document_type,
650 contract_id: data_contract.id().to_buffer(),
651 document_type_name: "widget".to_string(),
652 index,
653 where_clauses: vec![color_gt_blue],
654 sum_property: "amount".to_string(),
655 };
656 let verifier_path_query = sum_query
657 .distinct_sum_path_query(Some(DEFAULT_QUERY_LIMIT), true, platform_version)
658 .expect("path query builder accepts the same shape the prover used");
659
660 let (_root_hash, _elements) = GroveDb::verify_query(
668 &proof_bytes,
669 &verifier_path_query,
670 &platform_version.drive.grove_version,
671 )
672 .expect(
673 "expected proof to verify against a path query rebuilt with DEFAULT_QUERY_LIMIT; \
674 a failure here means the dispatcher signed the AVG proof with the \
675 operator-tunable default_query_limit — a consensus-adjacent silent-verify \
676 regression",
677 );
678 }
679
680 #[test]
684 fn range_distinct_avg_proof_rejects_limit_over_max() {
685 let drive = setup_drive_with_initial_state_structure(None);
686 let platform_version = PlatformVersion::latest();
687 let data_contract = build_widget_contract_pcps();
688 drive
689 .apply_contract(
690 &data_contract,
691 BlockInfo::default(),
692 true,
693 StorageFlags::optional_default_as_cow(),
694 None,
695 platform_version,
696 )
697 .expect("apply contract");
698
699 insert_widget(&drive, &data_contract, 0, "red", 5);
700
701 let document_type = data_contract
702 .document_type_for_name("widget")
703 .expect("widget");
704 let drive_config = DriveConfig::default();
705 let over_max = drive_config.max_query_limit as u32 + 1;
706
707 let color_gt_blue = WhereClause {
708 field: "color".to_string(),
709 operator: WhereOperator::GreaterThan,
710 value: Value::Text("blue".to_string()),
711 };
712 let request = DocumentAverageRequest {
713 contract: &data_contract,
714 document_type,
715 sum_property: "amount".to_string(),
716 where_clauses: vec![color_gt_blue],
717 order_clauses: Vec::new(),
718 mode: AverageMode::GroupByRange,
719 limit: Some(over_max),
720 prove: true,
721 drive_config: &drive_config,
722 resolved_time_ranges: vec![],
723 };
724
725 let err = drive
726 .execute_document_average_request(request, None, platform_version)
727 .expect_err("limit > max_query_limit must reject, not clamp");
728
729 assert!(
730 matches!(err, Error::Query(QuerySyntaxError::InvalidLimit(_))),
731 "expected QuerySyntaxError::InvalidLimit, got {err:?}"
732 );
733 let msg = err.to_string();
734 assert!(
735 msg.contains("exceeds max_query_limit"),
736 "error must name the rejected limit; got: {msg}"
737 );
738 }
739
740 #[test]
756 fn no_range_group_by_in_avg_prove_routes_to_point_lookup() {
757 use grovedb::operations::proof::GroveDBProof;
758
759 let drive = setup_drive_with_initial_state_structure(None);
760 let platform_version = PlatformVersion::latest();
761
762 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
768 let document_schema = platform_value!({
769 "type": "object",
770 "properties": {
771 "color": {"type": "string", "position": 0, "maxLength": 32},
772 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
773 },
774 "required": ["color", "amount"],
775 "indices": [{
776 "name": "byColor",
777 "properties": [{"color": "asc"}],
778 "summable": "amount",
779 "countable": "countable",
780 }],
781 "additionalProperties": false,
782 });
783 let schemas = platform_value!({ "widget": document_schema });
784 let data_contract = factory
785 .create_with_value_config(
786 dpp::tests::utils::generate_random_identifier_struct(),
787 0,
788 schemas,
789 None,
790 None,
791 )
792 .expect("create data contract")
793 .data_contract_owned();
794
795 drive
796 .apply_contract(
797 &data_contract,
798 BlockInfo::default(),
799 true,
800 StorageFlags::optional_default_as_cow(),
801 None,
802 platform_version,
803 )
804 .expect("apply contract");
805
806 insert_widget(&drive, &data_contract, 0, "red", 5);
807 insert_widget(&drive, &data_contract, 1, "red", 7);
808 insert_widget(&drive, &data_contract, 2, "green", 3);
809
810 let document_type = data_contract
811 .document_type_for_name("widget")
812 .expect("widget");
813 let drive_config = DriveConfig::default();
814
815 let color_in = WhereClause {
820 field: "color".to_string(),
821 operator: WhereOperator::In,
822 value: Value::Array(vec![
823 Value::Text("red".to_string()),
824 Value::Text("green".to_string()),
825 ]),
826 };
827 let request = DocumentAverageRequest {
828 contract: &data_contract,
829 document_type,
830 sum_property: "amount".to_string(),
831 where_clauses: vec![color_in],
832 order_clauses: Vec::new(),
833 mode: AverageMode::GroupByIn,
834 limit: None,
835 prove: true,
836 drive_config: &drive_config,
837 resolved_time_ranges: vec![],
838 };
839
840 let response = drive
841 .execute_document_average_request(request, None, platform_version)
842 .expect(
843 "no-range GroupByIn AVG + prove must hit the point-lookup arm \
844 (router resolves this shape to DocumentSumMode::PointLookupProof); \
845 a failure here means execute_document_average_prove regressed to \
846 the pre-fix gap that rejected this combination with Unsupported",
847 );
848 let proof_bytes = match response {
849 DocumentAverageResponse::Proof(p) => p,
850 other => panic!("expected Proof response, got {:?}", other),
851 };
852 assert!(
853 !proof_bytes.is_empty(),
854 "non-empty proof bytes expected from point-lookup AVG path"
855 );
856
857 let bincode_config = bincode::config::standard()
863 .with_big_endian()
864 .with_no_limit();
865 let _: (GroveDBProof, _) = bincode::decode_from_slice(&proof_bytes, bincode_config)
866 .expect("proof bytes must bincode-decode as a GroveDBProof");
867 }
868
869 use crate::query::drive_document_average_query::AverageEntry;
886 use crate::query::drive_document_count_query::{
887 CountMode, DocumentCountRequest, DocumentCountResponse,
888 };
889 use crate::query::drive_document_sum_query::{
890 DocumentSumRequest, DocumentSumResponse, SumMode,
891 };
892
893 fn independent_count_sum_aggregate(
898 drive: &Drive,
899 contract: &dpp::data_contract::DataContract,
900 document_type: dpp::data_contract::document_type::DocumentTypeRef,
901 sum_property: &str,
902 where_clauses: Vec<WhereClause>,
903 drive_config: &DriveConfig,
904 platform_version: &PlatformVersion,
905 ) -> (u64, i64) {
906 let count_request = DocumentCountRequest {
907 contract,
908 document_type,
909 where_clauses: where_clauses.clone(),
910 order_clauses: Vec::new(),
911 mode: CountMode::Aggregate,
912 limit: None,
913 prove: false,
914 drive_config,
915 resolved_time_ranges: vec![],
916 };
917 let sum_request = DocumentSumRequest {
918 contract,
919 document_type,
920 sum_property: sum_property.to_string(),
921 where_clauses,
922 order_clauses: Vec::new(),
923 mode: SumMode::Aggregate,
924 limit: None,
925 prove: false,
926 drive_config,
927 resolved_time_ranges: vec![],
928 };
929 let count_resp = drive
930 .execute_document_count_request(count_request, None, platform_version)
931 .expect("independent count");
932 let sum_resp = drive
933 .execute_document_sum_request(sum_request, None, platform_version)
934 .expect("independent sum");
935 let count = match count_resp {
936 DocumentCountResponse::Aggregate(c) => c,
937 other => panic!("expected count Aggregate, got {:?}", other),
938 };
939 let sum = match sum_resp {
940 DocumentSumResponse::Aggregate(s) => s,
941 other => panic!("expected sum Aggregate, got {:?}", other),
942 };
943 (count, sum)
944 }
945
946 #[test]
950 fn joint_total_executor_matches_independent_count_plus_sum() {
951 let drive = setup_drive_with_initial_state_structure(None);
952 let platform_version = PlatformVersion::latest();
953
954 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
963 let document_schema = platform_value!({
964 "type": "object",
965 "properties": {
966 "color": {"type": "string", "position": 0, "maxLength": 32},
967 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
968 },
969 "required": ["color", "amount"],
970 "indices": [{
971 "name": "byColor",
972 "properties": [{"color": "asc"}],
973 "summable": "amount",
974 "countable": "countable",
975 }],
976 "additionalProperties": false,
977 });
978 let schemas = platform_value!({ "widget": document_schema });
979 let data_contract = factory
980 .create_with_value_config(
981 dpp::tests::utils::generate_random_identifier_struct(),
982 0,
983 schemas,
984 None,
985 None,
986 )
987 .expect("create data contract")
988 .data_contract_owned();
989 drive
990 .apply_contract(
991 &data_contract,
992 BlockInfo::default(),
993 true,
994 StorageFlags::optional_default_as_cow(),
995 None,
996 platform_version,
997 )
998 .expect("apply contract");
999
1000 let docs = [
1001 ("red", 5u64),
1002 ("red", 5),
1003 ("red", 7),
1004 ("green", 3),
1005 ("green", 4),
1006 ("blue", 1),
1007 ];
1008 for (i, (color, amount)) in docs.iter().enumerate() {
1009 insert_widget(&drive, &data_contract, i, color, *amount);
1010 }
1011
1012 let document_type = data_contract
1013 .document_type_for_name("widget")
1014 .expect("widget");
1015 let drive_config = DriveConfig::default();
1016
1017 let where_clauses = vec![WhereClause {
1027 field: "color".to_string(),
1028 operator: WhereOperator::Equal,
1029 value: Value::Text("red".to_string()),
1030 }];
1031
1032 let request = DocumentAverageRequest {
1033 contract: &data_contract,
1034 document_type,
1035 sum_property: "amount".to_string(),
1036 where_clauses: where_clauses.clone(),
1037 order_clauses: Vec::new(),
1038 mode: AverageMode::Aggregate,
1039 limit: None,
1040 prove: false,
1041 drive_config: &drive_config,
1042 resolved_time_ranges: vec![],
1043 };
1044
1045 let joint_response = drive
1046 .execute_document_average_request(request, None, platform_version)
1047 .expect("joint total dispatch");
1048 let (joint_count, joint_sum) = match joint_response {
1049 DocumentAverageResponse::Aggregate { count, sum } => (count, sum),
1050 other => panic!("expected Aggregate, got {:?}", other),
1051 };
1052
1053 let (indep_count, indep_sum) = independent_count_sum_aggregate(
1054 &drive,
1055 &data_contract,
1056 document_type,
1057 "amount",
1058 where_clauses,
1059 &drive_config,
1060 platform_version,
1061 );
1062
1063 assert_eq!(
1064 (joint_count, joint_sum),
1065 (indep_count, indep_sum),
1066 "joint total executor must produce the same (count, sum) as \
1067 independent count + sum dispatch (red == 3 docs / sum 17)"
1068 );
1069 assert_eq!((joint_count, joint_sum), (3, 17));
1071 }
1072
1073 #[test]
1076 fn joint_per_in_value_executor_matches_independent_count_plus_sum() {
1077 let drive = setup_drive_with_initial_state_structure(None);
1078 let platform_version = PlatformVersion::latest();
1079
1080 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
1081 let document_schema = platform_value!({
1082 "type": "object",
1083 "properties": {
1084 "color": {"type": "string", "position": 0, "maxLength": 32},
1085 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
1086 },
1087 "required": ["color", "amount"],
1088 "indices": [{
1089 "name": "byColor",
1090 "properties": [{"color": "asc"}],
1091 "summable": "amount",
1092 "countable": "countable",
1093 }],
1094 "additionalProperties": false,
1095 });
1096 let schemas = platform_value!({ "widget": document_schema });
1097 let data_contract = factory
1098 .create_with_value_config(
1099 dpp::tests::utils::generate_random_identifier_struct(),
1100 0,
1101 schemas,
1102 None,
1103 None,
1104 )
1105 .expect("create data contract")
1106 .data_contract_owned();
1107 drive
1108 .apply_contract(
1109 &data_contract,
1110 BlockInfo::default(),
1111 true,
1112 StorageFlags::optional_default_as_cow(),
1113 None,
1114 platform_version,
1115 )
1116 .expect("apply contract");
1117
1118 let docs = [
1119 ("red", 5u64),
1120 ("red", 7),
1121 ("green", 3),
1122 ("green", 4),
1123 ("blue", 1),
1124 ("blue", 2),
1125 ];
1126 for (i, (color, amount)) in docs.iter().enumerate() {
1127 insert_widget(&drive, &data_contract, i, color, *amount);
1128 }
1129
1130 let document_type = data_contract
1131 .document_type_for_name("widget")
1132 .expect("widget");
1133 let drive_config = DriveConfig::default();
1134
1135 let color_in = WhereClause {
1136 field: "color".to_string(),
1137 operator: WhereOperator::In,
1138 value: Value::Array(vec![
1139 Value::Text("red".to_string()),
1140 Value::Text("green".to_string()),
1141 ]),
1142 };
1143
1144 let request = DocumentAverageRequest {
1145 contract: &data_contract,
1146 document_type,
1147 sum_property: "amount".to_string(),
1148 where_clauses: vec![color_in.clone()],
1149 order_clauses: Vec::new(),
1150 mode: AverageMode::GroupByIn,
1151 limit: None,
1152 prove: false,
1153 drive_config: &drive_config,
1154 resolved_time_ranges: vec![],
1155 };
1156
1157 let joint_response = drive
1158 .execute_document_average_request(request, None, platform_version)
1159 .expect("joint per-in-value dispatch");
1160 let joint_entries = match joint_response {
1161 DocumentAverageResponse::Entries(e) => e,
1162 other => panic!("expected Entries, got {:?}", other),
1163 };
1164
1165 let count_request = DocumentCountRequest {
1167 contract: &data_contract,
1168 document_type,
1169 where_clauses: vec![color_in.clone()],
1170 order_clauses: Vec::new(),
1171 mode: CountMode::GroupByIn,
1172 limit: None,
1173 prove: false,
1174 drive_config: &drive_config,
1175 resolved_time_ranges: vec![],
1176 };
1177 let sum_request = DocumentSumRequest {
1178 contract: &data_contract,
1179 document_type,
1180 sum_property: "amount".to_string(),
1181 where_clauses: vec![color_in],
1182 order_clauses: Vec::new(),
1183 mode: SumMode::GroupByIn,
1184 limit: None,
1185 prove: false,
1186 drive_config: &drive_config,
1187 resolved_time_ranges: vec![],
1188 };
1189 let count_resp = drive
1190 .execute_document_count_request(count_request, None, platform_version)
1191 .expect("independent count");
1192 let sum_resp = drive
1193 .execute_document_sum_request(sum_request, None, platform_version)
1194 .expect("independent sum");
1195 let count_entries = match count_resp {
1196 DocumentCountResponse::Entries(e) => e,
1197 other => panic!("expected count Entries, got {:?}", other),
1198 };
1199 let sum_entries = match sum_resp {
1200 DocumentSumResponse::Entries(e) => e,
1201 other => panic!("expected sum Entries, got {:?}", other),
1202 };
1203
1204 assert_eq!(joint_entries.len(), count_entries.len());
1206 assert_eq!(joint_entries.len(), sum_entries.len());
1207 for ((joint, count), sum) in joint_entries
1208 .iter()
1209 .zip(count_entries.iter())
1210 .zip(sum_entries.iter())
1211 {
1212 assert_eq!(joint.key, count.key);
1213 assert_eq!(joint.key, sum.key);
1214 assert_eq!(joint.count, count.count);
1215 assert_eq!(joint.sum, sum.sum);
1216 }
1217 assert_eq!(joint_entries.len(), 2);
1219 let mut by_key: Vec<&AverageEntry> = joint_entries.iter().collect();
1224 by_key.sort_by(|a, b| a.key.cmp(&b.key));
1225 let red_entry = by_key
1226 .iter()
1227 .find(|e| e.key.windows(3).any(|w| w == b"red"))
1228 .expect("red entry");
1229 let green_entry = by_key
1230 .iter()
1231 .find(|e| e.key.windows(5).any(|w| w == b"green"))
1232 .expect("green entry");
1233 assert_eq!(red_entry.count, Some(2));
1234 assert_eq!(red_entry.sum, Some(12));
1235 assert_eq!(green_entry.count, Some(2));
1236 assert_eq!(green_entry.sum, Some(7));
1237 }
1238
1239 #[test]
1242 fn joint_range_no_proof_executor_matches_independent_count_plus_sum() {
1243 let drive = setup_drive_with_initial_state_structure(None);
1244 let platform_version = PlatformVersion::latest();
1245 let data_contract = build_widget_contract_pcps();
1246 drive
1247 .apply_contract(
1248 &data_contract,
1249 BlockInfo::default(),
1250 true,
1251 StorageFlags::optional_default_as_cow(),
1252 None,
1253 platform_version,
1254 )
1255 .expect("apply contract");
1256
1257 let docs = [
1258 ("red", 5u64),
1259 ("red", 7),
1260 ("green", 3),
1261 ("green", 4),
1262 ("green", 6),
1263 ("blue", 2),
1264 ];
1265 for (i, (color, amount)) in docs.iter().enumerate() {
1266 insert_widget(&drive, &data_contract, i, color, *amount);
1267 }
1268
1269 let document_type = data_contract
1270 .document_type_for_name("widget")
1271 .expect("widget");
1272 let drive_config = DriveConfig::default();
1273
1274 let color_gt_blue = WhereClause {
1276 field: "color".to_string(),
1277 operator: WhereOperator::GreaterThan,
1278 value: Value::Text("blue".to_string()),
1279 };
1280
1281 let request = DocumentAverageRequest {
1282 contract: &data_contract,
1283 document_type,
1284 sum_property: "amount".to_string(),
1285 where_clauses: vec![color_gt_blue.clone()],
1286 order_clauses: Vec::new(),
1287 mode: AverageMode::GroupByRange,
1288 limit: None,
1289 prove: false,
1290 drive_config: &drive_config,
1291 resolved_time_ranges: vec![],
1292 };
1293
1294 let joint_response = drive
1295 .execute_document_average_request(request, None, platform_version)
1296 .expect("joint range distinct dispatch");
1297 let joint_entries = match joint_response {
1298 DocumentAverageResponse::Entries(e) => e,
1299 other => panic!("expected Entries, got {:?}", other),
1300 };
1301
1302 let count_request = DocumentCountRequest {
1304 contract: &data_contract,
1305 document_type,
1306 where_clauses: vec![color_gt_blue.clone()],
1307 order_clauses: Vec::new(),
1308 mode: CountMode::GroupByRange,
1309 limit: None,
1310 prove: false,
1311 drive_config: &drive_config,
1312 resolved_time_ranges: vec![],
1313 };
1314 let sum_request = DocumentSumRequest {
1315 contract: &data_contract,
1316 document_type,
1317 sum_property: "amount".to_string(),
1318 where_clauses: vec![color_gt_blue],
1319 order_clauses: Vec::new(),
1320 mode: SumMode::GroupByRange,
1321 limit: None,
1322 prove: false,
1323 drive_config: &drive_config,
1324 resolved_time_ranges: vec![],
1325 };
1326 let count_resp = drive
1327 .execute_document_count_request(count_request, None, platform_version)
1328 .expect("independent count");
1329 let sum_resp = drive
1330 .execute_document_sum_request(sum_request, None, platform_version)
1331 .expect("independent sum");
1332 let count_entries = match count_resp {
1333 DocumentCountResponse::Entries(e) => e,
1334 other => panic!("expected count Entries, got {:?}", other),
1335 };
1336 let sum_entries = match sum_resp {
1337 DocumentSumResponse::Entries(e) => e,
1338 other => panic!("expected sum Entries, got {:?}", other),
1339 };
1340
1341 assert_eq!(joint_entries.len(), count_entries.len());
1345 assert_eq!(joint_entries.len(), sum_entries.len());
1346 for ((joint, count), sum) in joint_entries
1347 .iter()
1348 .zip(count_entries.iter())
1349 .zip(sum_entries.iter())
1350 {
1351 assert_eq!(joint.key, count.key);
1352 assert_eq!(joint.key, sum.key);
1353 assert_eq!(joint.count, count.count);
1354 assert_eq!(joint.sum, sum.sum);
1355 }
1356 assert_eq!(joint_entries.len(), 2);
1359 }
1360
1361 #[test]
1369 fn joint_range_aggregate_executor_matches_independent_count_plus_sum() {
1370 let drive = setup_drive_with_initial_state_structure(None);
1371 let platform_version = PlatformVersion::latest();
1372 let data_contract = build_widget_contract_pcps();
1373 drive
1374 .apply_contract(
1375 &data_contract,
1376 BlockInfo::default(),
1377 true,
1378 StorageFlags::optional_default_as_cow(),
1379 None,
1380 platform_version,
1381 )
1382 .expect("apply contract");
1383
1384 let docs = [
1385 ("red", 5u64),
1386 ("red", 7),
1387 ("green", 3),
1388 ("green", 4),
1389 ("green", 6),
1390 ("blue", 2),
1391 ];
1392 for (i, (color, amount)) in docs.iter().enumerate() {
1393 insert_widget(&drive, &data_contract, i, color, *amount);
1394 }
1395
1396 let document_type = data_contract
1397 .document_type_for_name("widget")
1398 .expect("widget");
1399 let drive_config = DriveConfig::default();
1400
1401 let color_gt_blue = WhereClause {
1402 field: "color".to_string(),
1403 operator: WhereOperator::GreaterThan,
1404 value: Value::Text("blue".to_string()),
1405 };
1406
1407 let request = DocumentAverageRequest {
1408 contract: &data_contract,
1409 document_type,
1410 sum_property: "amount".to_string(),
1411 where_clauses: vec![color_gt_blue.clone()],
1412 order_clauses: Vec::new(),
1413 mode: AverageMode::Aggregate,
1414 limit: None,
1415 prove: false,
1416 drive_config: &drive_config,
1417 resolved_time_ranges: vec![],
1418 };
1419
1420 let joint_response = drive
1421 .execute_document_average_request(request, None, platform_version)
1422 .expect("joint range aggregate dispatch");
1423 let (joint_count, joint_sum) = match joint_response {
1424 DocumentAverageResponse::Aggregate { count, sum } => (count, sum),
1425 other => panic!("expected Aggregate, got {:?}", other),
1426 };
1427
1428 let (indep_count, indep_sum) = independent_count_sum_aggregate(
1429 &drive,
1430 &data_contract,
1431 document_type,
1432 "amount",
1433 vec![color_gt_blue],
1434 &drive_config,
1435 platform_version,
1436 );
1437
1438 assert_eq!(
1439 (joint_count, joint_sum),
1440 (indep_count, indep_sum),
1441 "joint range-aggregate executor must produce the same (count, sum) \
1442 as independent count + sum range dispatch"
1443 );
1444 assert_eq!((joint_count, joint_sum), (5, 25));
1447 }
1448
1449 #[test]
1461 fn joint_range_group_by_in_executor_matches_independent_count_plus_sum() {
1462 let drive = setup_drive_with_initial_state_structure(None);
1463 let platform_version = PlatformVersion::latest();
1464
1465 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
1468 let document_schema = platform_value!({
1469 "type": "object",
1470 "properties": {
1471 "color": {"type": "string", "position": 0, "maxLength": 32},
1472 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
1473 },
1474 "required": ["color", "amount"],
1475 "indices": [{
1476 "name": "byColorAmount",
1477 "properties": [{"color": "asc"}, {"amount": "asc"}],
1478 "summable": "amount",
1479 "rangeSummable": true,
1480 "countable": "countable",
1481 "rangeCountable": true,
1482 }],
1483 "additionalProperties": false,
1484 });
1485 let schemas = platform_value!({ "widget": document_schema });
1486 let data_contract = factory
1487 .create_with_value_config(
1488 dpp::tests::utils::generate_random_identifier_struct(),
1489 0,
1490 schemas,
1491 None,
1492 None,
1493 )
1494 .expect("create data contract")
1495 .data_contract_owned();
1496 drive
1497 .apply_contract(
1498 &data_contract,
1499 BlockInfo::default(),
1500 true,
1501 StorageFlags::optional_default_as_cow(),
1502 None,
1503 platform_version,
1504 )
1505 .expect("apply contract");
1506
1507 let docs = [
1508 ("red", 5u64),
1509 ("red", 7),
1510 ("red", 9),
1511 ("green", 3),
1512 ("green", 4),
1513 ("blue", 8),
1514 ("blue", 9),
1515 ];
1516 for (i, (color, amount)) in docs.iter().enumerate() {
1517 insert_widget(&drive, &data_contract, i, color, *amount);
1518 }
1519
1520 let document_type = data_contract
1521 .document_type_for_name("widget")
1522 .expect("widget");
1523 let drive_config = DriveConfig::default();
1524
1525 let color_in = WhereClause {
1527 field: "color".to_string(),
1528 operator: WhereOperator::In,
1529 value: Value::Array(vec![
1530 Value::Text("red".to_string()),
1531 Value::Text("green".to_string()),
1532 ]),
1533 };
1534 let amount_ge_4 = WhereClause {
1535 field: "amount".to_string(),
1536 operator: WhereOperator::GreaterThanOrEquals,
1537 value: Value::U64(4),
1538 };
1539
1540 let request = DocumentAverageRequest {
1541 contract: &data_contract,
1542 document_type,
1543 sum_property: "amount".to_string(),
1544 where_clauses: vec![color_in.clone(), amount_ge_4.clone()],
1545 order_clauses: Vec::new(),
1546 mode: AverageMode::GroupByIn,
1547 limit: None,
1548 prove: false,
1549 drive_config: &drive_config,
1550 resolved_time_ranges: vec![],
1551 };
1552
1553 let joint_response = drive
1554 .execute_document_average_request(request, None, platform_version)
1555 .expect("joint range GroupByIn dispatch");
1556 let joint_entries = match joint_response {
1557 DocumentAverageResponse::Entries(e) => e,
1558 other => panic!("expected Entries, got {:?}", other),
1559 };
1560
1561 let count_request = DocumentCountRequest {
1563 contract: &data_contract,
1564 document_type,
1565 where_clauses: vec![color_in.clone(), amount_ge_4.clone()],
1566 order_clauses: Vec::new(),
1567 mode: CountMode::GroupByIn,
1568 limit: None,
1569 prove: false,
1570 drive_config: &drive_config,
1571 resolved_time_ranges: vec![],
1572 };
1573 let sum_request = DocumentSumRequest {
1574 contract: &data_contract,
1575 document_type,
1576 sum_property: "amount".to_string(),
1577 where_clauses: vec![color_in, amount_ge_4],
1578 order_clauses: Vec::new(),
1579 mode: SumMode::GroupByIn,
1580 limit: None,
1581 prove: false,
1582 drive_config: &drive_config,
1583 resolved_time_ranges: vec![],
1584 };
1585 let count_resp = drive
1586 .execute_document_count_request(count_request, None, platform_version)
1587 .expect("independent count");
1588 let sum_resp = drive
1589 .execute_document_sum_request(sum_request, None, platform_version)
1590 .expect("independent sum");
1591 let count_entries = match count_resp {
1592 DocumentCountResponse::Entries(e) => e,
1593 other => panic!("expected count Entries, got {:?}", other),
1594 };
1595 let sum_entries = match sum_resp {
1596 DocumentSumResponse::Entries(e) => e,
1597 other => panic!("expected sum Entries, got {:?}", other),
1598 };
1599
1600 use std::collections::BTreeMap;
1606 let count_by_key: BTreeMap<Vec<u8>, Option<u64>> = count_entries
1607 .iter()
1608 .map(|e| (e.key.clone(), e.count))
1609 .collect();
1610 let sum_by_key: BTreeMap<Vec<u8>, Option<i64>> =
1611 sum_entries.iter().map(|e| (e.key.clone(), e.sum)).collect();
1612 let joint_by_key: BTreeMap<Vec<u8>, (Option<u64>, Option<i64>)> = joint_entries
1613 .iter()
1614 .map(|e| (e.key.clone(), (e.count, e.sum)))
1615 .collect();
1616
1617 assert_eq!(
1618 count_by_key.keys().collect::<Vec<_>>(),
1619 joint_by_key.keys().collect::<Vec<_>>(),
1620 "joint executor must emit the same In-branch keys as independent count"
1621 );
1622 for (key, (joint_count, joint_sum)) in joint_by_key.iter() {
1623 assert_eq!(joint_count, count_by_key.get(key).unwrap());
1624 assert_eq!(joint_sum, sum_by_key.get(key).unwrap());
1625 }
1626 }
1627
1628 #[test]
1636 fn distinct_avg_no_proof_honors_explicit_limit() {
1637 let drive = setup_drive_with_initial_state_structure(None);
1638 let platform_version = PlatformVersion::latest();
1639 let data_contract = build_widget_contract_pcps();
1640 drive
1641 .apply_contract(
1642 &data_contract,
1643 BlockInfo::default(),
1644 true,
1645 StorageFlags::optional_default_as_cow(),
1646 None,
1647 platform_version,
1648 )
1649 .expect("apply contract");
1650
1651 let docs = [
1655 ("red", 5u64),
1656 ("green", 7),
1657 ("blue", 2),
1658 ("yellow", 4),
1659 ("purple", 9),
1660 ];
1661 for (i, (color, amount)) in docs.iter().enumerate() {
1662 insert_widget(&drive, &data_contract, i, color, *amount);
1663 }
1664
1665 let document_type = data_contract
1666 .document_type_for_name("widget")
1667 .expect("widget");
1668 let drive_config = DriveConfig::default();
1669
1670 let color_ge_a = WhereClause {
1671 field: "color".to_string(),
1672 operator: WhereOperator::GreaterThanOrEquals,
1673 value: Value::Text("a".to_string()),
1674 };
1675
1676 let request = DocumentAverageRequest {
1677 contract: &data_contract,
1678 document_type,
1679 sum_property: "amount".to_string(),
1680 where_clauses: vec![color_ge_a],
1681 order_clauses: Vec::new(),
1682 mode: AverageMode::GroupByRange,
1683 limit: Some(2),
1684 prove: false,
1685 drive_config: &drive_config,
1686 resolved_time_ranges: vec![],
1687 };
1688
1689 let response = drive
1690 .execute_document_average_request(request, None, platform_version)
1691 .expect("dispatcher should succeed");
1692 let entries = match response {
1693 DocumentAverageResponse::Entries(e) => e,
1694 other => panic!("expected Entries, got {:?}", other),
1695 };
1696 assert_eq!(
1697 entries.len(),
1698 2,
1699 "distinct AVG no-proof must apply the request's `limit = 2` and \
1700 return exactly 2 entries; got {entries:?}"
1701 );
1702 }
1703
1704 #[test]
1709 fn distinct_avg_no_proof_defaults_limit_to_operator_default_query_limit() {
1710 let drive = setup_drive_with_initial_state_structure(None);
1711 let platform_version = PlatformVersion::latest();
1712 let data_contract = build_widget_contract_pcps();
1713 drive
1714 .apply_contract(
1715 &data_contract,
1716 BlockInfo::default(),
1717 true,
1718 StorageFlags::optional_default_as_cow(),
1719 None,
1720 platform_version,
1721 )
1722 .expect("apply contract");
1723
1724 let docs = [
1732 ("red", 5u64),
1733 ("green", 7),
1734 ("blue", 2),
1735 ("yellow", 4),
1736 ("purple", 9),
1737 ];
1738 for (i, (color, amount)) in docs.iter().enumerate() {
1739 insert_widget(&drive, &data_contract, i, color, *amount);
1740 }
1741
1742 let document_type = data_contract
1743 .document_type_for_name("widget")
1744 .expect("widget");
1745 let drive_config = DriveConfig {
1746 default_query_limit: 3,
1747 ..Default::default()
1748 };
1749
1750 let color_ge_a = WhereClause {
1751 field: "color".to_string(),
1752 operator: WhereOperator::GreaterThanOrEquals,
1753 value: Value::Text("a".to_string()),
1754 };
1755 let request = DocumentAverageRequest {
1756 contract: &data_contract,
1757 document_type,
1758 sum_property: "amount".to_string(),
1759 where_clauses: vec![color_ge_a],
1760 order_clauses: Vec::new(),
1761 mode: AverageMode::GroupByRange,
1762 limit: None,
1763 prove: false,
1764 drive_config: &drive_config,
1765 resolved_time_ranges: vec![],
1766 };
1767
1768 let response = drive
1769 .execute_document_average_request(request, None, platform_version)
1770 .expect("dispatcher should succeed");
1771 let entries = match response {
1772 DocumentAverageResponse::Entries(e) => e,
1773 other => panic!("expected Entries, got {:?}", other),
1774 };
1775 assert_eq!(
1776 entries.len(),
1777 3,
1778 "distinct AVG no-proof with `limit = None` must default to \
1779 `drive_config.default_query_limit` (= 3 here) rather than \
1780 enumerating all 5 distinct keys; got {entries:?}"
1781 );
1782 }
1783
1784 #[test]
1789 fn distinct_avg_no_proof_clamps_limit_to_max_query_limit() {
1790 let drive = setup_drive_with_initial_state_structure(None);
1791 let platform_version = PlatformVersion::latest();
1792 let data_contract = build_widget_contract_pcps();
1793 drive
1794 .apply_contract(
1795 &data_contract,
1796 BlockInfo::default(),
1797 true,
1798 StorageFlags::optional_default_as_cow(),
1799 None,
1800 platform_version,
1801 )
1802 .expect("apply contract");
1803
1804 let docs = [
1805 ("red", 5u64),
1806 ("green", 7),
1807 ("blue", 2),
1808 ("yellow", 4),
1809 ("purple", 9),
1810 ];
1811 for (i, (color, amount)) in docs.iter().enumerate() {
1812 insert_widget(&drive, &data_contract, i, color, *amount);
1813 }
1814
1815 let document_type = data_contract
1816 .document_type_for_name("widget")
1817 .expect("widget");
1818 let drive_config = DriveConfig {
1824 default_query_limit: 100,
1825 max_query_limit: 2,
1826 ..Default::default()
1827 };
1828
1829 let color_ge_a = WhereClause {
1830 field: "color".to_string(),
1831 operator: WhereOperator::GreaterThanOrEquals,
1832 value: Value::Text("a".to_string()),
1833 };
1834 let request = DocumentAverageRequest {
1835 contract: &data_contract,
1836 document_type,
1837 sum_property: "amount".to_string(),
1838 where_clauses: vec![color_ge_a],
1839 order_clauses: Vec::new(),
1840 mode: AverageMode::GroupByRange,
1841 limit: Some(4),
1842 prove: false,
1843 drive_config: &drive_config,
1844 resolved_time_ranges: vec![],
1845 };
1846
1847 let response = drive
1848 .execute_document_average_request(request, None, platform_version)
1849 .expect("dispatcher should succeed (no-proof clamps, never errors)");
1850 let entries = match response {
1851 DocumentAverageResponse::Entries(e) => e,
1852 other => panic!("expected Entries, got {:?}", other),
1853 };
1854 assert_eq!(
1855 entries.len(),
1856 2,
1857 "distinct AVG no-proof must clamp `limit = 4` to \
1858 `max_query_limit = 2`; got {entries:?}"
1859 );
1860 }
1861
1862 #[test]
1870 fn joint_dispatcher_rejects_prove_true_request() {
1871 let drive = setup_drive_with_initial_state_structure(None);
1872 let platform_version = PlatformVersion::latest();
1873 let data_contract = build_widget_contract_pcps();
1874 drive
1875 .apply_contract(
1876 &data_contract,
1877 BlockInfo::default(),
1878 true,
1879 StorageFlags::optional_default_as_cow(),
1880 None,
1881 platform_version,
1882 )
1883 .expect("apply contract");
1884
1885 let document_type = data_contract
1886 .document_type_for_name("widget")
1887 .expect("widget");
1888 let drive_config = DriveConfig::default();
1889
1890 let request = DocumentAverageRequest {
1891 contract: &data_contract,
1892 document_type,
1893 sum_property: "amount".to_string(),
1894 where_clauses: Vec::new(),
1895 order_clauses: Vec::new(),
1896 mode: AverageMode::Aggregate,
1897 limit: None,
1898 prove: true,
1899 drive_config: &drive_config,
1900 resolved_time_ranges: vec![],
1901 };
1902
1903 let err = drive
1904 .execute_document_count_and_sum_request(request, None, platform_version)
1905 .expect_err("prove=true direct call must reject");
1906 let msg = format!("{err:?}");
1907 assert!(
1908 msg.contains("no-prove"),
1909 "expected the prove=true guard to fire; got: {msg}"
1910 );
1911 }
1912
1913 #[test]
1921 fn joint_dispatcher_runs_validate_and_canonicalize_where_clauses() {
1922 let drive = setup_drive_with_initial_state_structure(None);
1923 let platform_version = PlatformVersion::latest();
1924 let data_contract = build_widget_contract_pcps();
1925 drive
1926 .apply_contract(
1927 &data_contract,
1928 BlockInfo::default(),
1929 true,
1930 StorageFlags::optional_default_as_cow(),
1931 None,
1932 platform_version,
1933 )
1934 .expect("apply contract");
1935
1936 let document_type = data_contract
1937 .document_type_for_name("widget")
1938 .expect("widget");
1939 let drive_config = DriveConfig::default();
1940
1941 let dup_color_a = WhereClause {
1944 field: "color".to_string(),
1945 operator: WhereOperator::Equal,
1946 value: Value::Text("red".to_string()),
1947 };
1948 let dup_color_b = WhereClause {
1949 field: "color".to_string(),
1950 operator: WhereOperator::Equal,
1951 value: Value::Text("green".to_string()),
1952 };
1953 let request = DocumentAverageRequest {
1954 contract: &data_contract,
1955 document_type,
1956 sum_property: "amount".to_string(),
1957 where_clauses: vec![dup_color_a, dup_color_b],
1958 order_clauses: Vec::new(),
1959 mode: AverageMode::Aggregate,
1960 limit: None,
1961 prove: false,
1962 drive_config: &drive_config,
1963 resolved_time_ranges: vec![],
1964 };
1965
1966 let err = drive
1967 .execute_document_average_request(request, None, platform_version)
1968 .expect_err(
1969 "AVG no-proof must reject duplicate Equal on the same field via \
1970 validate_and_canonicalize_where_clauses",
1971 );
1972 let msg = format!("{err:?}");
1976 assert!(
1977 !msg.contains("WhereClauseOnNonIndexedProperty"),
1978 "validator should reject before the index picker would: {msg}"
1979 );
1980 }
1981
1982 #[test]
1989 fn avg_prove_path_rejects_resolved_time_range_provenance_on_an_in_clause() {
1990 let drive = setup_drive_with_initial_state_structure(None);
1991 let platform_version = PlatformVersion::latest();
1992 let data_contract = build_widget_contract_pcps();
1993 drive
1994 .apply_contract(
1995 &data_contract,
1996 BlockInfo::default(),
1997 true,
1998 StorageFlags::optional_default_as_cow(),
1999 None,
2000 platform_version,
2001 )
2002 .expect("apply contract");
2003
2004 let document_type = data_contract
2005 .document_type_for_name("widget")
2006 .expect("widget");
2007 let drive_config = DriveConfig::default();
2008
2009 let in_on_resolved_field = WhereClause {
2010 field: "$createdAt".to_string(),
2011 operator: WhereOperator::In,
2012 value: Value::Array(vec![Value::U64(0), Value::U64(7_200_000)]),
2013 };
2014 let request = DocumentAverageRequest {
2015 contract: &data_contract,
2016 document_type,
2017 sum_property: "amount".to_string(),
2018 where_clauses: vec![in_on_resolved_field],
2019 order_clauses: Vec::new(),
2020 mode: AverageMode::Aggregate,
2021 limit: None,
2022 prove: true,
2023 drive_config: &drive_config,
2024 resolved_time_ranges: vec![ResolvedTimeRange {
2025 transform: dpp::data_contract::document_type::TimeRangeTransform {
2026 source: "$createdAt".to_string(),
2027 range_seconds: 21_600,
2028 step_seconds: 7_200,
2029 phase_seconds: 0,
2030 ttl_seconds: None,
2031 }
2032 .into(),
2033 }],
2034 };
2035
2036 let err = drive
2037 .execute_document_average_request(request, None, platform_version)
2038 .expect_err("AVG prove must reject provenance attached to an In clause");
2039 assert!(
2040 format!("{err:?}").contains("InvalidWhereClauseComponents"),
2041 "expected the provenance shape guard, got: {err:?}"
2042 );
2043 }
2044
2045 #[test]
2050 fn per_in_value_avg_no_proof_honors_explicit_limit() {
2051 let drive = setup_drive_with_initial_state_structure(None);
2052 let platform_version = PlatformVersion::latest();
2053
2054 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
2058 let document_schema = platform_value!({
2059 "type": "object",
2060 "properties": {
2061 "color": {"type": "string", "position": 0, "maxLength": 32},
2062 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
2063 },
2064 "required": ["color", "amount"],
2065 "indices": [{
2066 "name": "byColor",
2067 "properties": [{"color": "asc"}],
2068 "summable": "amount",
2069 "countable": "countable",
2070 }],
2071 "additionalProperties": false,
2072 });
2073 let schemas = platform_value!({ "widget": document_schema });
2074 let data_contract = factory
2075 .create_with_value_config(
2076 dpp::tests::utils::generate_random_identifier_struct(),
2077 0,
2078 schemas,
2079 None,
2080 None,
2081 )
2082 .expect("create data contract")
2083 .data_contract_owned();
2084 drive
2085 .apply_contract(
2086 &data_contract,
2087 BlockInfo::default(),
2088 true,
2089 StorageFlags::optional_default_as_cow(),
2090 None,
2091 platform_version,
2092 )
2093 .expect("apply contract");
2094
2095 for (i, (color, amount)) in [("red", 5u64), ("green", 7), ("blue", 2), ("yellow", 4)]
2096 .iter()
2097 .enumerate()
2098 {
2099 insert_widget(&drive, &data_contract, i, color, *amount);
2100 }
2101
2102 let document_type = data_contract
2103 .document_type_for_name("widget")
2104 .expect("widget");
2105 let drive_config = DriveConfig::default();
2106
2107 let color_in = WhereClause {
2110 field: "color".to_string(),
2111 operator: WhereOperator::In,
2112 value: Value::Array(vec![
2113 Value::Text("red".to_string()),
2114 Value::Text("green".to_string()),
2115 Value::Text("blue".to_string()),
2116 Value::Text("yellow".to_string()),
2117 ]),
2118 };
2119 let request = DocumentAverageRequest {
2120 contract: &data_contract,
2121 document_type,
2122 sum_property: "amount".to_string(),
2123 where_clauses: vec![color_in],
2124 order_clauses: Vec::new(),
2125 mode: AverageMode::GroupByIn,
2126 limit: Some(2),
2127 prove: false,
2128 drive_config: &drive_config,
2129 resolved_time_ranges: vec![],
2130 };
2131
2132 let response = drive
2133 .execute_document_average_request(request, None, platform_version)
2134 .expect("dispatcher should succeed");
2135 let entries = match response {
2136 DocumentAverageResponse::Entries(e) => e,
2137 other => panic!("expected Entries, got {:?}", other),
2138 };
2139 assert_eq!(
2140 entries.len(),
2141 2,
2142 "PerInValue AVG no-proof must apply request.limit = 2 to the per-In \
2143 entry list (caller asked for 4 In values, dispatcher must truncate); \
2144 got {entries:?}"
2145 );
2146 }
2147
2148 #[test]
2159 fn empty_where_total_executor_uses_primary_key_count_sum_tree_fast_path() {
2160 let drive = setup_drive_with_initial_state_structure(None);
2161 let platform_version = PlatformVersion::latest();
2162
2163 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
2170 let document_schema = platform_value!({
2171 "type": "object",
2172 "properties": {
2173 "amount": {"type": "integer", "position": 0, "minimum": 0, "maximum": 1000},
2174 },
2175 "required": ["amount"],
2176 "documentsAverageable": "amount",
2177 "additionalProperties": false,
2178 });
2179 let schemas = platform_value!({ "score": document_schema });
2180 let data_contract = factory
2181 .create_with_value_config(
2182 dpp::tests::utils::generate_random_identifier_struct(),
2183 0,
2184 schemas,
2185 None,
2186 None,
2187 )
2188 .expect("create data contract")
2189 .data_contract_owned();
2190 drive
2191 .apply_contract(
2192 &data_contract,
2193 BlockInfo::default(),
2194 true,
2195 StorageFlags::optional_default_as_cow(),
2196 None,
2197 platform_version,
2198 )
2199 .expect("apply contract");
2200
2201 let document_type = data_contract
2204 .document_type_for_name("score")
2205 .expect("score type");
2206 for (i, amount) in [10u64, 20, 30, 40].iter().enumerate() {
2207 let mut properties = std::collections::BTreeMap::new();
2208 properties.insert("amount".to_string(), Value::U64(*amount));
2209 let document: Document = DocumentV0 {
2210 contract_version: None,
2211 id: Identifier::from([(i + 1) as u8; 32]),
2212 owner_id: Identifier::from([0u8; 32]),
2213 properties,
2214 revision: None,
2215 created_at: None,
2216 updated_at: None,
2217 transferred_at: None,
2218 created_at_block_height: None,
2219 updated_at_block_height: None,
2220 transferred_at_block_height: None,
2221 created_at_core_block_height: None,
2222 updated_at_core_block_height: None,
2223 transferred_at_core_block_height: None,
2224 creator_id: None,
2225 moderated_at: None,
2226 moderated_by: None,
2227 }
2228 .into();
2229 let storage_flags = Some(std::borrow::Cow::Owned(StorageFlags::SingleEpoch(0)));
2230 drive
2231 .add_document_for_contract(
2232 DocumentAndContractInfo {
2233 owned_document_info: OwnedDocumentInfo {
2234 document_info: DocumentRefInfo((&document, storage_flags)),
2235 owner_id: None,
2236 },
2237 contract: &data_contract,
2238 document_type,
2239 },
2240 false,
2241 BlockInfo::default(),
2242 true,
2243 None,
2244 platform_version,
2245 None,
2246 )
2247 .expect("insert score");
2248 }
2249
2250 let drive_config = DriveConfig::default();
2251 let request = DocumentAverageRequest {
2252 contract: &data_contract,
2253 document_type,
2254 sum_property: "amount".to_string(),
2255 where_clauses: Vec::new(),
2256 order_clauses: Vec::new(),
2257 mode: AverageMode::Aggregate,
2258 limit: None,
2259 prove: false,
2260 drive_config: &drive_config,
2261 resolved_time_ranges: vec![],
2262 };
2263
2264 let response = drive
2265 .execute_document_average_request(request, None, platform_version)
2266 .expect("empty-where AVG no-proof must succeed via the primary-key fast path");
2267 match response {
2268 DocumentAverageResponse::Aggregate { count, sum } => {
2269 assert_eq!(
2270 (count, sum),
2271 (4, 100),
2272 "primary-key count-sum tree fast path must return (4 docs, sum 10+20+30+40 = 100)"
2273 );
2274 }
2275 other => panic!("expected Aggregate, got {:?}", other),
2276 }
2277 }
2278
2279 #[test]
2286 fn per_in_value_avg_no_proof_defaults_limit_to_operator_default_query_limit() {
2287 let drive = setup_drive_with_initial_state_structure(None);
2288 let platform_version = PlatformVersion::latest();
2289
2290 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
2296 let document_schema = platform_value!({
2297 "type": "object",
2298 "properties": {
2299 "color": {"type": "string", "position": 0, "maxLength": 32},
2300 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
2301 },
2302 "required": ["color", "amount"],
2303 "indices": [{
2304 "name": "byColor",
2305 "properties": [{"color": "asc"}],
2306 "summable": "amount",
2307 "countable": "countable",
2308 }],
2309 "additionalProperties": false,
2310 });
2311 let schemas = platform_value!({ "widget": document_schema });
2312 let data_contract = factory
2313 .create_with_value_config(
2314 dpp::tests::utils::generate_random_identifier_struct(),
2315 0,
2316 schemas,
2317 None,
2318 None,
2319 )
2320 .expect("create data contract")
2321 .data_contract_owned();
2322 drive
2323 .apply_contract(
2324 &data_contract,
2325 BlockInfo::default(),
2326 true,
2327 StorageFlags::optional_default_as_cow(),
2328 None,
2329 platform_version,
2330 )
2331 .expect("apply contract");
2332
2333 for (i, (color, amount)) in [("red", 5u64), ("green", 7), ("blue", 2), ("yellow", 4)]
2334 .iter()
2335 .enumerate()
2336 {
2337 insert_widget(&drive, &data_contract, i, color, *amount);
2338 }
2339
2340 let document_type = data_contract
2341 .document_type_for_name("widget")
2342 .expect("widget");
2343 let drive_config = DriveConfig {
2344 default_query_limit: 2,
2345 ..Default::default()
2346 };
2347
2348 let color_in = WhereClause {
2349 field: "color".to_string(),
2350 operator: WhereOperator::In,
2351 value: Value::Array(vec![
2352 Value::Text("red".to_string()),
2353 Value::Text("green".to_string()),
2354 Value::Text("blue".to_string()),
2355 Value::Text("yellow".to_string()),
2356 ]),
2357 };
2358 let request = DocumentAverageRequest {
2359 contract: &data_contract,
2360 document_type,
2361 sum_property: "amount".to_string(),
2362 where_clauses: vec![color_in],
2363 order_clauses: Vec::new(),
2364 mode: AverageMode::GroupByIn,
2365 limit: None,
2366 prove: false,
2367 drive_config: &drive_config,
2368 resolved_time_ranges: vec![],
2369 };
2370
2371 let response = drive
2372 .execute_document_average_request(request, None, platform_version)
2373 .expect("dispatcher should succeed");
2374 let entries = match response {
2375 DocumentAverageResponse::Entries(e) => e,
2376 other => panic!("expected Entries, got {:?}", other),
2377 };
2378 assert_eq!(
2379 entries.len(),
2380 2,
2381 "PerInValue AVG no-proof with `limit = None` must default to \
2382 `drive_config.default_query_limit` (= 2 here) and truncate the \
2383 per-In entry list; got {entries:?}"
2384 );
2385 }
2386}