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