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 dpp::data_contract::accessors::v0::DataContractV0Getters;
59use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters};
60use dpp::version::PlatformVersion;
61use grovedb::TransactionArg;
62
63#[cfg(feature = "server")]
64impl Drive {
65 pub fn execute_document_average_request(
74 &self,
75 request: DocumentAverageRequest,
76 transaction: TransactionArg,
77 platform_version: &PlatformVersion,
78 ) -> Result<DocumentAverageResponse, Error> {
79 if request.prove {
80 return self.execute_document_average_prove(request, transaction, platform_version);
81 }
82 self.execute_document_count_and_sum_request(request, transaction, platform_version)
83 }
84
85 fn execute_document_average_prove(
118 &self,
119 request: DocumentAverageRequest,
120 transaction: TransactionArg,
121 platform_version: &PlatformVersion,
122 ) -> Result<DocumentAverageResponse, Error> {
123 let contract_id = request.contract.id().to_buffer();
124 let document_type_name = request.document_type.name().to_string();
125 let has_range = request
126 .where_clauses
127 .iter()
128 .any(|wc| is_range_operator(wc.operator));
129 let order_by_ascending = request
130 .order_clauses
131 .first()
132 .map(|c| c.ascending)
133 .unwrap_or(true);
134
135 if matches!(request.mode, AverageMode::Aggregate)
142 && request.where_clauses.is_empty()
143 && request.document_type.documents_countable()
144 && request
145 .document_type
146 .documents_summable()
147 .map(|p| p == request.sum_property)
148 .unwrap_or(false)
149 {
150 let path_query =
151 DriveDocumentSumQuery::primary_key_sum_path_query(contract_id, &document_type_name);
152 let proof = self
153 .grove
154 .get_proved_path_query(
155 &path_query,
156 None,
157 transaction,
158 &platform_version.drive.grove_version,
159 )
160 .unwrap()
161 .map_err(|e| Error::GroveDB(Box::new(e)))?;
162 return Ok(DocumentAverageResponse::Proof(proof));
163 }
164
165 if has_range
170 && matches!(
171 request.mode,
172 AverageMode::Aggregate | AverageMode::GroupByIn
173 )
174 {
175 let index = find_range_summable_index_for_where_clauses(
176 request.document_type.indexes(),
177 &request.where_clauses,
178 &request.sum_property,
179 )
180 .filter(|idx| idx.range_countable)
181 .ok_or_else(|| {
182 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
183 "prove AVG requires an index that declares BOTH `rangeCountable: \
184 true` AND `rangeSummable: true` (a `rangeAverageable: true` \
185 index is the shorthand) whose last property matches the range \
186 field and whose summable property matches the request's \
187 `sum_property`"
188 .to_string(),
189 ))
190 })?;
191 let sum_query = DriveDocumentSumQuery {
192 document_type: request.document_type,
193 contract_id,
194 document_type_name,
195 index,
196 where_clauses: request.where_clauses.clone(),
197 sum_property: request.sum_property.clone(),
198 };
199
200 let proof = match request.mode {
201 AverageMode::Aggregate => sum_query.execute_aggregate_count_and_sum_with_proof(
202 self,
203 transaction,
204 platform_version,
205 )?,
206 AverageMode::GroupByIn => {
207 let limit_u16 = request
215 .limit
216 .map(|l| {
217 if l > request.drive_config.max_query_limit as u32 {
218 return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!(
219 "limit {} exceeds max_query_limit {} on the prove + \
220 carrier-aggregate path (GROUP BY In + range, AVG); \
221 reduce the requested limit or use prove = false",
222 l, request.drive_config.max_query_limit
223 ))));
224 }
225 u16::try_from(l).map_err(|_| {
226 Error::Query(QuerySyntaxError::Unsupported(format!(
227 "limit {} exceeds u16::MAX for carrier-aggregate \
228 count+sum (AVG) proof",
229 l
230 )))
231 })
232 })
233 .transpose()?;
234 sum_query.execute_carrier_aggregate_count_and_sum_with_proof(
235 self,
236 limit_u16,
237 order_by_ascending,
238 transaction,
239 platform_version,
240 )?
241 }
242 _ => unreachable!("outer matches! gate filters out non-Aggregate/GroupByIn"),
243 };
244 return Ok(DocumentAverageResponse::Proof(proof));
245 }
246
247 if has_range
256 && matches!(
257 request.mode,
258 AverageMode::GroupByRange | AverageMode::GroupByCompound
259 )
260 {
261 let index = find_range_summable_index_for_where_clauses(
262 request.document_type.indexes(),
263 &request.where_clauses,
264 &request.sum_property,
265 )
266 .filter(|idx| idx.range_countable)
267 .ok_or_else(|| {
268 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
269 "prove distinct AVG requires an index that declares BOTH \
270 `rangeCountable: true` AND `rangeSummable: true` (a \
271 `rangeAverageable: true` index is the shorthand) whose last \
272 property matches the range field and whose summable property \
273 matches the request's `sum_property`"
274 .to_string(),
275 ))
276 })?;
277 let effective_limit = request
285 .limit
286 .unwrap_or(crate::config::DEFAULT_QUERY_LIMIT as u32);
287 if effective_limit > request.drive_config.max_query_limit as u32 {
288 return Err(Error::Query(QuerySyntaxError::InvalidLimit(format!(
289 "limit {} exceeds max_query_limit {} on the prove + distinct-walk \
290 path (GROUP BY a range field, AVG); reduce the requested limit \
291 or use prove = false",
292 effective_limit, request.drive_config.max_query_limit
293 ))));
294 }
295 let limit_u16 = u16::try_from(effective_limit).map_err(|_| {
296 Error::Query(QuerySyntaxError::Unsupported(format!(
297 "limit {} exceeds u16::MAX for distinct AVG proof",
298 effective_limit
299 )))
300 })?;
301 let sum_query = DriveDocumentSumQuery {
302 document_type: request.document_type,
303 contract_id,
304 document_type_name,
305 index,
306 where_clauses: request.where_clauses.clone(),
307 sum_property: request.sum_property.clone(),
308 };
309 let proof = sum_query.execute_distinct_sum_with_proof(
310 self,
311 limit_u16,
312 order_by_ascending,
313 transaction,
314 platform_version,
315 )?;
316 return Ok(DocumentAverageResponse::Proof(proof));
317 }
318
319 if !has_range
344 && matches!(
345 request.mode,
346 AverageMode::Aggregate | AverageMode::GroupByIn
347 )
348 {
349 let index = find_summable_index_for_where_clauses(
350 request.document_type.indexes(),
351 &request.where_clauses,
352 &request.sum_property,
353 )
354 .filter(|idx| idx.countable.is_countable())
355 .ok_or_else(|| {
356 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
357 "prove point-lookup AVG requires an index that declares BOTH \
358 `summable: \"<prop>\"` AND a countable terminator (`countable: \
359 \"countable\"` or `\"countableAllowingOffset\"`) whose properties \
360 exactly match the where clause fields"
361 .to_string(),
362 ))
363 })?;
364 let sum_query = DriveDocumentSumQuery {
365 document_type: request.document_type,
366 contract_id,
367 document_type_name,
368 index,
369 where_clauses: request.where_clauses.clone(),
370 sum_property: request.sum_property.clone(),
371 };
372 let proof = sum_query.execute_point_lookup_sum_with_proof(
373 self,
374 transaction,
375 platform_version,
376 )?;
377 return Ok(DocumentAverageResponse::Proof(proof));
378 }
379
380 Err(Error::Query(QuerySyntaxError::Unsupported(format!(
385 "execute_document_average_request prove=true: the (mode = {:?}, has_range \
386 = {}) combination is not yet supported on the prove path. \
387 This is likely a new AverageMode variant that hasn't been wired \
388 into the prove dispatcher.",
389 request.mode, has_range,
390 ))))
391 }
392}
393
394#[cfg(all(test, feature = "server"))]
395mod tests {
396 use super::*;
397
398 use crate::config::{DriveConfig, DEFAULT_QUERY_LIMIT};
421 use crate::drive::Drive;
422 use crate::error::query::QuerySyntaxError;
423 use crate::query::drive_document_average_query::{
424 AverageMode, DocumentAverageRequest, DocumentAverageResponse,
425 };
426 use crate::query::{WhereClause, WhereOperator};
427 use crate::util::object_size_info::DocumentInfo::DocumentRefInfo;
428 use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo};
429 use crate::util::storage_flags::StorageFlags;
430 use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure;
431 use dpp::block::block_info::BlockInfo;
432 use dpp::data_contract::accessors::v0::DataContractV0Getters;
433 use dpp::data_contract::DataContractFactory;
434 use dpp::document::{Document, DocumentV0};
435 use dpp::identifier::Identifier;
436 use dpp::platform_value::{platform_value, Value};
437 use grovedb::GroveDb;
438 use std::borrow::Cow;
439 use std::collections::BTreeMap as StdBTreeMap;
440
441 const PROTOCOL_VERSION_V12: u32 = 12;
442
443 fn build_widget_contract_pcps() -> dpp::data_contract::DataContract {
448 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
449 let document_schema = platform_value!({
450 "type": "object",
451 "properties": {
452 "color": {"type": "string", "position": 0, "maxLength": 32},
453 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
454 },
455 "required": ["color", "amount"],
456 "indices": [{
457 "name": "byColor",
458 "properties": [{"color": "asc"}],
459 "summable": "amount",
464 "rangeSummable": true,
465 "countable": "countable",
466 "rangeCountable": true,
467 }],
468 "additionalProperties": false,
469 });
470 let schemas = platform_value!({ "widget": document_schema });
471 factory
472 .create_with_value_config(
473 dpp::tests::utils::generate_random_identifier_struct(),
474 0,
475 schemas,
476 None,
477 None,
478 )
479 .expect("create data contract")
480 .data_contract_owned()
481 }
482
483 fn insert_widget(
484 drive: &Drive,
485 contract: &dpp::data_contract::DataContract,
486 i: usize,
487 color: &str,
488 amount: u64,
489 ) {
490 let platform_version = PlatformVersion::latest();
491 let document_type = contract
492 .document_type_for_name("widget")
493 .expect("widget type exists");
494 let mut properties = StdBTreeMap::new();
495 properties.insert("color".to_string(), Value::Text(color.to_string()));
496 properties.insert("amount".to_string(), Value::U64(amount));
497 let document: Document = DocumentV0 {
498 id: Identifier::from([(i + 1) as u8; 32]),
499 owner_id: Identifier::from([0u8; 32]),
500 properties,
501 revision: None,
502 created_at: None,
503 updated_at: None,
504 transferred_at: None,
505 created_at_block_height: None,
506 updated_at_block_height: None,
507 transferred_at_block_height: None,
508 created_at_core_block_height: None,
509 updated_at_core_block_height: None,
510 transferred_at_core_block_height: None,
511 creator_id: None,
512 }
513 .into();
514 let storage_flags = Some(Cow::Owned(StorageFlags::SingleEpoch(0)));
515 drive
516 .add_document_for_contract(
517 DocumentAndContractInfo {
518 owned_document_info: OwnedDocumentInfo {
519 document_info: DocumentRefInfo((&document, storage_flags)),
520 owner_id: None,
521 },
522 contract,
523 document_type,
524 },
525 false,
526 BlockInfo::default(),
527 true,
528 None,
529 platform_version,
530 None,
531 )
532 .expect("insert widget");
533 }
534
535 #[test]
544 fn range_distinct_avg_proof_uses_compile_time_default_query_limit_not_operator_config() {
545 const OPERATOR_TUNED_LIMIT: u16 = 1;
546 assert_ne!(
547 DEFAULT_QUERY_LIMIT, OPERATOR_TUNED_LIMIT,
548 "test invariant: OPERATOR_TUNED_LIMIT must differ from DEFAULT_QUERY_LIMIT"
549 );
550
551 let drive = setup_drive_with_initial_state_structure(None);
552 let platform_version = PlatformVersion::latest();
553 let data_contract = build_widget_contract_pcps();
554 drive
555 .apply_contract(
556 &data_contract,
557 BlockInfo::default(),
558 true,
559 StorageFlags::optional_default_as_cow(),
560 None,
561 platform_version,
562 )
563 .expect("apply contract");
564
565 let docs = [
566 ("red", 5u64),
567 ("red", 5),
568 ("green", 7),
569 ("green", 7),
570 ("green", 7),
571 ("blue", 2),
572 ];
573 for (i, (color, amount)) in docs.iter().enumerate() {
574 insert_widget(&drive, &data_contract, i, color, *amount);
575 }
576
577 let document_type = data_contract
578 .document_type_for_name("widget")
579 .expect("widget");
580
581 let drive_config = DriveConfig {
582 default_query_limit: OPERATOR_TUNED_LIMIT,
583 ..Default::default()
584 };
585
586 let color_gt_blue = WhereClause {
587 field: "color".to_string(),
588 operator: WhereOperator::GreaterThan,
589 value: Value::Text("blue".to_string()),
590 };
591 let request = DocumentAverageRequest {
592 contract: &data_contract,
593 document_type,
594 sum_property: "amount".to_string(),
595 where_clauses: vec![color_gt_blue.clone()],
596 order_clauses: Vec::new(),
597 mode: AverageMode::GroupByRange,
598 limit: None,
599 prove: true,
600 drive_config: &drive_config,
601 };
602
603 let response = drive
604 .execute_document_average_request(request, None, platform_version)
605 .expect("dispatcher should succeed on distinct AVG path");
606 let proof_bytes = match response {
607 DocumentAverageResponse::Proof(p) => p,
608 other => panic!("expected Proof response, got {:?}", other),
609 };
610 assert!(!proof_bytes.is_empty(), "non-empty proof bytes expected");
611
612 let index = find_range_summable_index_for_where_clauses(
615 document_type.indexes(),
616 std::slice::from_ref(&color_gt_blue),
617 "amount",
618 )
619 .filter(|idx| idx.range_countable)
620 .expect("byColor rangeAverageable index covers `color > blue`");
621 let sum_query = DriveDocumentSumQuery {
622 document_type,
623 contract_id: data_contract.id().to_buffer(),
624 document_type_name: "widget".to_string(),
625 index,
626 where_clauses: vec![color_gt_blue],
627 sum_property: "amount".to_string(),
628 };
629 let verifier_path_query = sum_query
630 .distinct_sum_path_query(Some(DEFAULT_QUERY_LIMIT), true, platform_version)
631 .expect("path query builder accepts the same shape the prover used");
632
633 let (_root_hash, _elements) = GroveDb::verify_query(
641 &proof_bytes,
642 &verifier_path_query,
643 &platform_version.drive.grove_version,
644 )
645 .expect(
646 "expected proof to verify against a path query rebuilt with DEFAULT_QUERY_LIMIT; \
647 a failure here means the dispatcher signed the AVG proof with the \
648 operator-tunable default_query_limit — a consensus-adjacent silent-verify \
649 regression",
650 );
651 }
652
653 #[test]
657 fn range_distinct_avg_proof_rejects_limit_over_max() {
658 let drive = setup_drive_with_initial_state_structure(None);
659 let platform_version = PlatformVersion::latest();
660 let data_contract = build_widget_contract_pcps();
661 drive
662 .apply_contract(
663 &data_contract,
664 BlockInfo::default(),
665 true,
666 StorageFlags::optional_default_as_cow(),
667 None,
668 platform_version,
669 )
670 .expect("apply contract");
671
672 insert_widget(&drive, &data_contract, 0, "red", 5);
673
674 let document_type = data_contract
675 .document_type_for_name("widget")
676 .expect("widget");
677 let drive_config = DriveConfig::default();
678 let over_max = drive_config.max_query_limit as u32 + 1;
679
680 let color_gt_blue = WhereClause {
681 field: "color".to_string(),
682 operator: WhereOperator::GreaterThan,
683 value: Value::Text("blue".to_string()),
684 };
685 let request = DocumentAverageRequest {
686 contract: &data_contract,
687 document_type,
688 sum_property: "amount".to_string(),
689 where_clauses: vec![color_gt_blue],
690 order_clauses: Vec::new(),
691 mode: AverageMode::GroupByRange,
692 limit: Some(over_max),
693 prove: true,
694 drive_config: &drive_config,
695 };
696
697 let err = drive
698 .execute_document_average_request(request, None, platform_version)
699 .expect_err("limit > max_query_limit must reject, not clamp");
700
701 assert!(
702 matches!(err, Error::Query(QuerySyntaxError::InvalidLimit(_))),
703 "expected QuerySyntaxError::InvalidLimit, got {err:?}"
704 );
705 let msg = err.to_string();
706 assert!(
707 msg.contains("exceeds max_query_limit"),
708 "error must name the rejected limit; got: {msg}"
709 );
710 }
711
712 #[test]
728 fn no_range_group_by_in_avg_prove_routes_to_point_lookup() {
729 use grovedb::operations::proof::GroveDBProof;
730
731 let drive = setup_drive_with_initial_state_structure(None);
732 let platform_version = PlatformVersion::latest();
733
734 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
740 let document_schema = platform_value!({
741 "type": "object",
742 "properties": {
743 "color": {"type": "string", "position": 0, "maxLength": 32},
744 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
745 },
746 "required": ["color", "amount"],
747 "indices": [{
748 "name": "byColor",
749 "properties": [{"color": "asc"}],
750 "summable": "amount",
751 "countable": "countable",
752 }],
753 "additionalProperties": false,
754 });
755 let schemas = platform_value!({ "widget": document_schema });
756 let data_contract = factory
757 .create_with_value_config(
758 dpp::tests::utils::generate_random_identifier_struct(),
759 0,
760 schemas,
761 None,
762 None,
763 )
764 .expect("create data contract")
765 .data_contract_owned();
766
767 drive
768 .apply_contract(
769 &data_contract,
770 BlockInfo::default(),
771 true,
772 StorageFlags::optional_default_as_cow(),
773 None,
774 platform_version,
775 )
776 .expect("apply contract");
777
778 insert_widget(&drive, &data_contract, 0, "red", 5);
779 insert_widget(&drive, &data_contract, 1, "red", 7);
780 insert_widget(&drive, &data_contract, 2, "green", 3);
781
782 let document_type = data_contract
783 .document_type_for_name("widget")
784 .expect("widget");
785 let drive_config = DriveConfig::default();
786
787 let color_in = WhereClause {
792 field: "color".to_string(),
793 operator: WhereOperator::In,
794 value: Value::Array(vec![
795 Value::Text("red".to_string()),
796 Value::Text("green".to_string()),
797 ]),
798 };
799 let request = DocumentAverageRequest {
800 contract: &data_contract,
801 document_type,
802 sum_property: "amount".to_string(),
803 where_clauses: vec![color_in],
804 order_clauses: Vec::new(),
805 mode: AverageMode::GroupByIn,
806 limit: None,
807 prove: true,
808 drive_config: &drive_config,
809 };
810
811 let response = drive
812 .execute_document_average_request(request, None, platform_version)
813 .expect(
814 "no-range GroupByIn AVG + prove must hit the point-lookup arm \
815 (router resolves this shape to DocumentSumMode::PointLookupProof); \
816 a failure here means execute_document_average_prove regressed to \
817 the pre-fix gap that rejected this combination with Unsupported",
818 );
819 let proof_bytes = match response {
820 DocumentAverageResponse::Proof(p) => p,
821 other => panic!("expected Proof response, got {:?}", other),
822 };
823 assert!(
824 !proof_bytes.is_empty(),
825 "non-empty proof bytes expected from point-lookup AVG path"
826 );
827
828 let bincode_config = bincode::config::standard()
834 .with_big_endian()
835 .with_no_limit();
836 let _: (GroveDBProof, _) = bincode::decode_from_slice(&proof_bytes, bincode_config)
837 .expect("proof bytes must bincode-decode as a GroveDBProof");
838 }
839
840 use crate::query::drive_document_average_query::AverageEntry;
857 use crate::query::drive_document_count_query::{
858 CountMode, DocumentCountRequest, DocumentCountResponse,
859 };
860 use crate::query::drive_document_sum_query::{
861 DocumentSumRequest, DocumentSumResponse, SumMode,
862 };
863
864 fn independent_count_sum_aggregate(
869 drive: &Drive,
870 contract: &dpp::data_contract::DataContract,
871 document_type: dpp::data_contract::document_type::DocumentTypeRef,
872 sum_property: &str,
873 where_clauses: Vec<WhereClause>,
874 drive_config: &DriveConfig,
875 platform_version: &PlatformVersion,
876 ) -> (u64, i64) {
877 let count_request = DocumentCountRequest {
878 contract,
879 document_type,
880 where_clauses: where_clauses.clone(),
881 order_clauses: Vec::new(),
882 mode: CountMode::Aggregate,
883 limit: None,
884 prove: false,
885 drive_config,
886 };
887 let sum_request = DocumentSumRequest {
888 contract,
889 document_type,
890 sum_property: sum_property.to_string(),
891 where_clauses,
892 order_clauses: Vec::new(),
893 mode: SumMode::Aggregate,
894 limit: None,
895 prove: false,
896 drive_config,
897 };
898 let count_resp = drive
899 .execute_document_count_request(count_request, None, platform_version)
900 .expect("independent count");
901 let sum_resp = drive
902 .execute_document_sum_request(sum_request, None, platform_version)
903 .expect("independent sum");
904 let count = match count_resp {
905 DocumentCountResponse::Aggregate(c) => c,
906 other => panic!("expected count Aggregate, got {:?}", other),
907 };
908 let sum = match sum_resp {
909 DocumentSumResponse::Aggregate(s) => s,
910 other => panic!("expected sum Aggregate, got {:?}", other),
911 };
912 (count, sum)
913 }
914
915 #[test]
919 fn joint_total_executor_matches_independent_count_plus_sum() {
920 let drive = setup_drive_with_initial_state_structure(None);
921 let platform_version = PlatformVersion::latest();
922
923 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
932 let document_schema = platform_value!({
933 "type": "object",
934 "properties": {
935 "color": {"type": "string", "position": 0, "maxLength": 32},
936 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
937 },
938 "required": ["color", "amount"],
939 "indices": [{
940 "name": "byColor",
941 "properties": [{"color": "asc"}],
942 "summable": "amount",
943 "countable": "countable",
944 }],
945 "additionalProperties": false,
946 });
947 let schemas = platform_value!({ "widget": document_schema });
948 let data_contract = factory
949 .create_with_value_config(
950 dpp::tests::utils::generate_random_identifier_struct(),
951 0,
952 schemas,
953 None,
954 None,
955 )
956 .expect("create data contract")
957 .data_contract_owned();
958 drive
959 .apply_contract(
960 &data_contract,
961 BlockInfo::default(),
962 true,
963 StorageFlags::optional_default_as_cow(),
964 None,
965 platform_version,
966 )
967 .expect("apply contract");
968
969 let docs = [
970 ("red", 5u64),
971 ("red", 5),
972 ("red", 7),
973 ("green", 3),
974 ("green", 4),
975 ("blue", 1),
976 ];
977 for (i, (color, amount)) in docs.iter().enumerate() {
978 insert_widget(&drive, &data_contract, i, color, *amount);
979 }
980
981 let document_type = data_contract
982 .document_type_for_name("widget")
983 .expect("widget");
984 let drive_config = DriveConfig::default();
985
986 let where_clauses = vec![WhereClause {
996 field: "color".to_string(),
997 operator: WhereOperator::Equal,
998 value: Value::Text("red".to_string()),
999 }];
1000
1001 let request = DocumentAverageRequest {
1002 contract: &data_contract,
1003 document_type,
1004 sum_property: "amount".to_string(),
1005 where_clauses: where_clauses.clone(),
1006 order_clauses: Vec::new(),
1007 mode: AverageMode::Aggregate,
1008 limit: None,
1009 prove: false,
1010 drive_config: &drive_config,
1011 };
1012
1013 let joint_response = drive
1014 .execute_document_average_request(request, None, platform_version)
1015 .expect("joint total dispatch");
1016 let (joint_count, joint_sum) = match joint_response {
1017 DocumentAverageResponse::Aggregate { count, sum } => (count, sum),
1018 other => panic!("expected Aggregate, got {:?}", other),
1019 };
1020
1021 let (indep_count, indep_sum) = independent_count_sum_aggregate(
1022 &drive,
1023 &data_contract,
1024 document_type,
1025 "amount",
1026 where_clauses,
1027 &drive_config,
1028 platform_version,
1029 );
1030
1031 assert_eq!(
1032 (joint_count, joint_sum),
1033 (indep_count, indep_sum),
1034 "joint total executor must produce the same (count, sum) as \
1035 independent count + sum dispatch (red == 3 docs / sum 17)"
1036 );
1037 assert_eq!((joint_count, joint_sum), (3, 17));
1039 }
1040
1041 #[test]
1044 fn joint_per_in_value_executor_matches_independent_count_plus_sum() {
1045 let drive = setup_drive_with_initial_state_structure(None);
1046 let platform_version = PlatformVersion::latest();
1047
1048 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
1049 let document_schema = platform_value!({
1050 "type": "object",
1051 "properties": {
1052 "color": {"type": "string", "position": 0, "maxLength": 32},
1053 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
1054 },
1055 "required": ["color", "amount"],
1056 "indices": [{
1057 "name": "byColor",
1058 "properties": [{"color": "asc"}],
1059 "summable": "amount",
1060 "countable": "countable",
1061 }],
1062 "additionalProperties": false,
1063 });
1064 let schemas = platform_value!({ "widget": document_schema });
1065 let data_contract = factory
1066 .create_with_value_config(
1067 dpp::tests::utils::generate_random_identifier_struct(),
1068 0,
1069 schemas,
1070 None,
1071 None,
1072 )
1073 .expect("create data contract")
1074 .data_contract_owned();
1075 drive
1076 .apply_contract(
1077 &data_contract,
1078 BlockInfo::default(),
1079 true,
1080 StorageFlags::optional_default_as_cow(),
1081 None,
1082 platform_version,
1083 )
1084 .expect("apply contract");
1085
1086 let docs = [
1087 ("red", 5u64),
1088 ("red", 7),
1089 ("green", 3),
1090 ("green", 4),
1091 ("blue", 1),
1092 ("blue", 2),
1093 ];
1094 for (i, (color, amount)) in docs.iter().enumerate() {
1095 insert_widget(&drive, &data_contract, i, color, *amount);
1096 }
1097
1098 let document_type = data_contract
1099 .document_type_for_name("widget")
1100 .expect("widget");
1101 let drive_config = DriveConfig::default();
1102
1103 let color_in = WhereClause {
1104 field: "color".to_string(),
1105 operator: WhereOperator::In,
1106 value: Value::Array(vec![
1107 Value::Text("red".to_string()),
1108 Value::Text("green".to_string()),
1109 ]),
1110 };
1111
1112 let request = DocumentAverageRequest {
1113 contract: &data_contract,
1114 document_type,
1115 sum_property: "amount".to_string(),
1116 where_clauses: vec![color_in.clone()],
1117 order_clauses: Vec::new(),
1118 mode: AverageMode::GroupByIn,
1119 limit: None,
1120 prove: false,
1121 drive_config: &drive_config,
1122 };
1123
1124 let joint_response = drive
1125 .execute_document_average_request(request, None, platform_version)
1126 .expect("joint per-in-value dispatch");
1127 let joint_entries = match joint_response {
1128 DocumentAverageResponse::Entries(e) => e,
1129 other => panic!("expected Entries, got {:?}", other),
1130 };
1131
1132 let count_request = DocumentCountRequest {
1134 contract: &data_contract,
1135 document_type,
1136 where_clauses: vec![color_in.clone()],
1137 order_clauses: Vec::new(),
1138 mode: CountMode::GroupByIn,
1139 limit: None,
1140 prove: false,
1141 drive_config: &drive_config,
1142 };
1143 let sum_request = DocumentSumRequest {
1144 contract: &data_contract,
1145 document_type,
1146 sum_property: "amount".to_string(),
1147 where_clauses: vec![color_in],
1148 order_clauses: Vec::new(),
1149 mode: SumMode::GroupByIn,
1150 limit: None,
1151 prove: false,
1152 drive_config: &drive_config,
1153 };
1154 let count_resp = drive
1155 .execute_document_count_request(count_request, None, platform_version)
1156 .expect("independent count");
1157 let sum_resp = drive
1158 .execute_document_sum_request(sum_request, None, platform_version)
1159 .expect("independent sum");
1160 let count_entries = match count_resp {
1161 DocumentCountResponse::Entries(e) => e,
1162 other => panic!("expected count Entries, got {:?}", other),
1163 };
1164 let sum_entries = match sum_resp {
1165 DocumentSumResponse::Entries(e) => e,
1166 other => panic!("expected sum Entries, got {:?}", other),
1167 };
1168
1169 assert_eq!(joint_entries.len(), count_entries.len());
1171 assert_eq!(joint_entries.len(), sum_entries.len());
1172 for ((joint, count), sum) in joint_entries
1173 .iter()
1174 .zip(count_entries.iter())
1175 .zip(sum_entries.iter())
1176 {
1177 assert_eq!(joint.key, count.key);
1178 assert_eq!(joint.key, sum.key);
1179 assert_eq!(joint.count, count.count);
1180 assert_eq!(joint.sum, sum.sum);
1181 }
1182 assert_eq!(joint_entries.len(), 2);
1184 let mut by_key: Vec<&AverageEntry> = joint_entries.iter().collect();
1189 by_key.sort_by(|a, b| a.key.cmp(&b.key));
1190 let red_entry = by_key
1191 .iter()
1192 .find(|e| e.key.windows(3).any(|w| w == b"red"))
1193 .expect("red entry");
1194 let green_entry = by_key
1195 .iter()
1196 .find(|e| e.key.windows(5).any(|w| w == b"green"))
1197 .expect("green entry");
1198 assert_eq!(red_entry.count, Some(2));
1199 assert_eq!(red_entry.sum, Some(12));
1200 assert_eq!(green_entry.count, Some(2));
1201 assert_eq!(green_entry.sum, Some(7));
1202 }
1203
1204 #[test]
1207 fn joint_range_no_proof_executor_matches_independent_count_plus_sum() {
1208 let drive = setup_drive_with_initial_state_structure(None);
1209 let platform_version = PlatformVersion::latest();
1210 let data_contract = build_widget_contract_pcps();
1211 drive
1212 .apply_contract(
1213 &data_contract,
1214 BlockInfo::default(),
1215 true,
1216 StorageFlags::optional_default_as_cow(),
1217 None,
1218 platform_version,
1219 )
1220 .expect("apply contract");
1221
1222 let docs = [
1223 ("red", 5u64),
1224 ("red", 7),
1225 ("green", 3),
1226 ("green", 4),
1227 ("green", 6),
1228 ("blue", 2),
1229 ];
1230 for (i, (color, amount)) in docs.iter().enumerate() {
1231 insert_widget(&drive, &data_contract, i, color, *amount);
1232 }
1233
1234 let document_type = data_contract
1235 .document_type_for_name("widget")
1236 .expect("widget");
1237 let drive_config = DriveConfig::default();
1238
1239 let color_gt_blue = WhereClause {
1241 field: "color".to_string(),
1242 operator: WhereOperator::GreaterThan,
1243 value: Value::Text("blue".to_string()),
1244 };
1245
1246 let request = DocumentAverageRequest {
1247 contract: &data_contract,
1248 document_type,
1249 sum_property: "amount".to_string(),
1250 where_clauses: vec![color_gt_blue.clone()],
1251 order_clauses: Vec::new(),
1252 mode: AverageMode::GroupByRange,
1253 limit: None,
1254 prove: false,
1255 drive_config: &drive_config,
1256 };
1257
1258 let joint_response = drive
1259 .execute_document_average_request(request, None, platform_version)
1260 .expect("joint range distinct dispatch");
1261 let joint_entries = match joint_response {
1262 DocumentAverageResponse::Entries(e) => e,
1263 other => panic!("expected Entries, got {:?}", other),
1264 };
1265
1266 let count_request = DocumentCountRequest {
1268 contract: &data_contract,
1269 document_type,
1270 where_clauses: vec![color_gt_blue.clone()],
1271 order_clauses: Vec::new(),
1272 mode: CountMode::GroupByRange,
1273 limit: None,
1274 prove: false,
1275 drive_config: &drive_config,
1276 };
1277 let sum_request = DocumentSumRequest {
1278 contract: &data_contract,
1279 document_type,
1280 sum_property: "amount".to_string(),
1281 where_clauses: vec![color_gt_blue],
1282 order_clauses: Vec::new(),
1283 mode: SumMode::GroupByRange,
1284 limit: None,
1285 prove: false,
1286 drive_config: &drive_config,
1287 };
1288 let count_resp = drive
1289 .execute_document_count_request(count_request, None, platform_version)
1290 .expect("independent count");
1291 let sum_resp = drive
1292 .execute_document_sum_request(sum_request, None, platform_version)
1293 .expect("independent sum");
1294 let count_entries = match count_resp {
1295 DocumentCountResponse::Entries(e) => e,
1296 other => panic!("expected count Entries, got {:?}", other),
1297 };
1298 let sum_entries = match sum_resp {
1299 DocumentSumResponse::Entries(e) => e,
1300 other => panic!("expected sum Entries, got {:?}", other),
1301 };
1302
1303 assert_eq!(joint_entries.len(), count_entries.len());
1307 assert_eq!(joint_entries.len(), sum_entries.len());
1308 for ((joint, count), sum) in joint_entries
1309 .iter()
1310 .zip(count_entries.iter())
1311 .zip(sum_entries.iter())
1312 {
1313 assert_eq!(joint.key, count.key);
1314 assert_eq!(joint.key, sum.key);
1315 assert_eq!(joint.count, count.count);
1316 assert_eq!(joint.sum, sum.sum);
1317 }
1318 assert_eq!(joint_entries.len(), 2);
1321 }
1322
1323 #[test]
1331 fn joint_range_aggregate_executor_matches_independent_count_plus_sum() {
1332 let drive = setup_drive_with_initial_state_structure(None);
1333 let platform_version = PlatformVersion::latest();
1334 let data_contract = build_widget_contract_pcps();
1335 drive
1336 .apply_contract(
1337 &data_contract,
1338 BlockInfo::default(),
1339 true,
1340 StorageFlags::optional_default_as_cow(),
1341 None,
1342 platform_version,
1343 )
1344 .expect("apply contract");
1345
1346 let docs = [
1347 ("red", 5u64),
1348 ("red", 7),
1349 ("green", 3),
1350 ("green", 4),
1351 ("green", 6),
1352 ("blue", 2),
1353 ];
1354 for (i, (color, amount)) in docs.iter().enumerate() {
1355 insert_widget(&drive, &data_contract, i, color, *amount);
1356 }
1357
1358 let document_type = data_contract
1359 .document_type_for_name("widget")
1360 .expect("widget");
1361 let drive_config = DriveConfig::default();
1362
1363 let color_gt_blue = WhereClause {
1364 field: "color".to_string(),
1365 operator: WhereOperator::GreaterThan,
1366 value: Value::Text("blue".to_string()),
1367 };
1368
1369 let request = DocumentAverageRequest {
1370 contract: &data_contract,
1371 document_type,
1372 sum_property: "amount".to_string(),
1373 where_clauses: vec![color_gt_blue.clone()],
1374 order_clauses: Vec::new(),
1375 mode: AverageMode::Aggregate,
1376 limit: None,
1377 prove: false,
1378 drive_config: &drive_config,
1379 };
1380
1381 let joint_response = drive
1382 .execute_document_average_request(request, None, platform_version)
1383 .expect("joint range aggregate dispatch");
1384 let (joint_count, joint_sum) = match joint_response {
1385 DocumentAverageResponse::Aggregate { count, sum } => (count, sum),
1386 other => panic!("expected Aggregate, got {:?}", other),
1387 };
1388
1389 let (indep_count, indep_sum) = independent_count_sum_aggregate(
1390 &drive,
1391 &data_contract,
1392 document_type,
1393 "amount",
1394 vec![color_gt_blue],
1395 &drive_config,
1396 platform_version,
1397 );
1398
1399 assert_eq!(
1400 (joint_count, joint_sum),
1401 (indep_count, indep_sum),
1402 "joint range-aggregate executor must produce the same (count, sum) \
1403 as independent count + sum range dispatch"
1404 );
1405 assert_eq!((joint_count, joint_sum), (5, 25));
1408 }
1409
1410 #[test]
1422 fn joint_range_group_by_in_executor_matches_independent_count_plus_sum() {
1423 let drive = setup_drive_with_initial_state_structure(None);
1424 let platform_version = PlatformVersion::latest();
1425
1426 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
1429 let document_schema = platform_value!({
1430 "type": "object",
1431 "properties": {
1432 "color": {"type": "string", "position": 0, "maxLength": 32},
1433 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
1434 },
1435 "required": ["color", "amount"],
1436 "indices": [{
1437 "name": "byColorAmount",
1438 "properties": [{"color": "asc"}, {"amount": "asc"}],
1439 "summable": "amount",
1440 "rangeSummable": true,
1441 "countable": "countable",
1442 "rangeCountable": true,
1443 }],
1444 "additionalProperties": false,
1445 });
1446 let schemas = platform_value!({ "widget": document_schema });
1447 let data_contract = factory
1448 .create_with_value_config(
1449 dpp::tests::utils::generate_random_identifier_struct(),
1450 0,
1451 schemas,
1452 None,
1453 None,
1454 )
1455 .expect("create data contract")
1456 .data_contract_owned();
1457 drive
1458 .apply_contract(
1459 &data_contract,
1460 BlockInfo::default(),
1461 true,
1462 StorageFlags::optional_default_as_cow(),
1463 None,
1464 platform_version,
1465 )
1466 .expect("apply contract");
1467
1468 let docs = [
1469 ("red", 5u64),
1470 ("red", 7),
1471 ("red", 9),
1472 ("green", 3),
1473 ("green", 4),
1474 ("blue", 8),
1475 ("blue", 9),
1476 ];
1477 for (i, (color, amount)) in docs.iter().enumerate() {
1478 insert_widget(&drive, &data_contract, i, color, *amount);
1479 }
1480
1481 let document_type = data_contract
1482 .document_type_for_name("widget")
1483 .expect("widget");
1484 let drive_config = DriveConfig::default();
1485
1486 let color_in = WhereClause {
1488 field: "color".to_string(),
1489 operator: WhereOperator::In,
1490 value: Value::Array(vec![
1491 Value::Text("red".to_string()),
1492 Value::Text("green".to_string()),
1493 ]),
1494 };
1495 let amount_ge_4 = WhereClause {
1496 field: "amount".to_string(),
1497 operator: WhereOperator::GreaterThanOrEquals,
1498 value: Value::U64(4),
1499 };
1500
1501 let request = DocumentAverageRequest {
1502 contract: &data_contract,
1503 document_type,
1504 sum_property: "amount".to_string(),
1505 where_clauses: vec![color_in.clone(), amount_ge_4.clone()],
1506 order_clauses: Vec::new(),
1507 mode: AverageMode::GroupByIn,
1508 limit: None,
1509 prove: false,
1510 drive_config: &drive_config,
1511 };
1512
1513 let joint_response = drive
1514 .execute_document_average_request(request, None, platform_version)
1515 .expect("joint range GroupByIn dispatch");
1516 let joint_entries = match joint_response {
1517 DocumentAverageResponse::Entries(e) => e,
1518 other => panic!("expected Entries, got {:?}", other),
1519 };
1520
1521 let count_request = DocumentCountRequest {
1523 contract: &data_contract,
1524 document_type,
1525 where_clauses: vec![color_in.clone(), amount_ge_4.clone()],
1526 order_clauses: Vec::new(),
1527 mode: CountMode::GroupByIn,
1528 limit: None,
1529 prove: false,
1530 drive_config: &drive_config,
1531 };
1532 let sum_request = DocumentSumRequest {
1533 contract: &data_contract,
1534 document_type,
1535 sum_property: "amount".to_string(),
1536 where_clauses: vec![color_in, amount_ge_4],
1537 order_clauses: Vec::new(),
1538 mode: SumMode::GroupByIn,
1539 limit: None,
1540 prove: false,
1541 drive_config: &drive_config,
1542 };
1543 let count_resp = drive
1544 .execute_document_count_request(count_request, None, platform_version)
1545 .expect("independent count");
1546 let sum_resp = drive
1547 .execute_document_sum_request(sum_request, None, platform_version)
1548 .expect("independent sum");
1549 let count_entries = match count_resp {
1550 DocumentCountResponse::Entries(e) => e,
1551 other => panic!("expected count Entries, got {:?}", other),
1552 };
1553 let sum_entries = match sum_resp {
1554 DocumentSumResponse::Entries(e) => e,
1555 other => panic!("expected sum Entries, got {:?}", other),
1556 };
1557
1558 use std::collections::BTreeMap;
1564 let count_by_key: BTreeMap<Vec<u8>, Option<u64>> = count_entries
1565 .iter()
1566 .map(|e| (e.key.clone(), e.count))
1567 .collect();
1568 let sum_by_key: BTreeMap<Vec<u8>, Option<i64>> =
1569 sum_entries.iter().map(|e| (e.key.clone(), e.sum)).collect();
1570 let joint_by_key: BTreeMap<Vec<u8>, (Option<u64>, Option<i64>)> = joint_entries
1571 .iter()
1572 .map(|e| (e.key.clone(), (e.count, e.sum)))
1573 .collect();
1574
1575 assert_eq!(
1576 count_by_key.keys().collect::<Vec<_>>(),
1577 joint_by_key.keys().collect::<Vec<_>>(),
1578 "joint executor must emit the same In-branch keys as independent count"
1579 );
1580 for (key, (joint_count, joint_sum)) in joint_by_key.iter() {
1581 assert_eq!(joint_count, count_by_key.get(key).unwrap());
1582 assert_eq!(joint_sum, sum_by_key.get(key).unwrap());
1583 }
1584 }
1585
1586 #[test]
1594 fn distinct_avg_no_proof_honors_explicit_limit() {
1595 let drive = setup_drive_with_initial_state_structure(None);
1596 let platform_version = PlatformVersion::latest();
1597 let data_contract = build_widget_contract_pcps();
1598 drive
1599 .apply_contract(
1600 &data_contract,
1601 BlockInfo::default(),
1602 true,
1603 StorageFlags::optional_default_as_cow(),
1604 None,
1605 platform_version,
1606 )
1607 .expect("apply contract");
1608
1609 let docs = [
1613 ("red", 5u64),
1614 ("green", 7),
1615 ("blue", 2),
1616 ("yellow", 4),
1617 ("purple", 9),
1618 ];
1619 for (i, (color, amount)) in docs.iter().enumerate() {
1620 insert_widget(&drive, &data_contract, i, color, *amount);
1621 }
1622
1623 let document_type = data_contract
1624 .document_type_for_name("widget")
1625 .expect("widget");
1626 let drive_config = DriveConfig::default();
1627
1628 let color_ge_a = WhereClause {
1629 field: "color".to_string(),
1630 operator: WhereOperator::GreaterThanOrEquals,
1631 value: Value::Text("a".to_string()),
1632 };
1633
1634 let request = DocumentAverageRequest {
1635 contract: &data_contract,
1636 document_type,
1637 sum_property: "amount".to_string(),
1638 where_clauses: vec![color_ge_a],
1639 order_clauses: Vec::new(),
1640 mode: AverageMode::GroupByRange,
1641 limit: Some(2),
1642 prove: false,
1643 drive_config: &drive_config,
1644 };
1645
1646 let response = drive
1647 .execute_document_average_request(request, None, platform_version)
1648 .expect("dispatcher should succeed");
1649 let entries = match response {
1650 DocumentAverageResponse::Entries(e) => e,
1651 other => panic!("expected Entries, got {:?}", other),
1652 };
1653 assert_eq!(
1654 entries.len(),
1655 2,
1656 "distinct AVG no-proof must apply the request's `limit = 2` and \
1657 return exactly 2 entries; got {entries:?}"
1658 );
1659 }
1660
1661 #[test]
1666 fn distinct_avg_no_proof_defaults_limit_to_operator_default_query_limit() {
1667 let drive = setup_drive_with_initial_state_structure(None);
1668 let platform_version = PlatformVersion::latest();
1669 let data_contract = build_widget_contract_pcps();
1670 drive
1671 .apply_contract(
1672 &data_contract,
1673 BlockInfo::default(),
1674 true,
1675 StorageFlags::optional_default_as_cow(),
1676 None,
1677 platform_version,
1678 )
1679 .expect("apply contract");
1680
1681 let docs = [
1689 ("red", 5u64),
1690 ("green", 7),
1691 ("blue", 2),
1692 ("yellow", 4),
1693 ("purple", 9),
1694 ];
1695 for (i, (color, amount)) in docs.iter().enumerate() {
1696 insert_widget(&drive, &data_contract, i, color, *amount);
1697 }
1698
1699 let document_type = data_contract
1700 .document_type_for_name("widget")
1701 .expect("widget");
1702 let drive_config = DriveConfig {
1703 default_query_limit: 3,
1704 ..Default::default()
1705 };
1706
1707 let color_ge_a = WhereClause {
1708 field: "color".to_string(),
1709 operator: WhereOperator::GreaterThanOrEquals,
1710 value: Value::Text("a".to_string()),
1711 };
1712 let request = DocumentAverageRequest {
1713 contract: &data_contract,
1714 document_type,
1715 sum_property: "amount".to_string(),
1716 where_clauses: vec![color_ge_a],
1717 order_clauses: Vec::new(),
1718 mode: AverageMode::GroupByRange,
1719 limit: None,
1720 prove: false,
1721 drive_config: &drive_config,
1722 };
1723
1724 let response = drive
1725 .execute_document_average_request(request, None, platform_version)
1726 .expect("dispatcher should succeed");
1727 let entries = match response {
1728 DocumentAverageResponse::Entries(e) => e,
1729 other => panic!("expected Entries, got {:?}", other),
1730 };
1731 assert_eq!(
1732 entries.len(),
1733 3,
1734 "distinct AVG no-proof with `limit = None` must default to \
1735 `drive_config.default_query_limit` (= 3 here) rather than \
1736 enumerating all 5 distinct keys; got {entries:?}"
1737 );
1738 }
1739
1740 #[test]
1745 fn distinct_avg_no_proof_clamps_limit_to_max_query_limit() {
1746 let drive = setup_drive_with_initial_state_structure(None);
1747 let platform_version = PlatformVersion::latest();
1748 let data_contract = build_widget_contract_pcps();
1749 drive
1750 .apply_contract(
1751 &data_contract,
1752 BlockInfo::default(),
1753 true,
1754 StorageFlags::optional_default_as_cow(),
1755 None,
1756 platform_version,
1757 )
1758 .expect("apply contract");
1759
1760 let docs = [
1761 ("red", 5u64),
1762 ("green", 7),
1763 ("blue", 2),
1764 ("yellow", 4),
1765 ("purple", 9),
1766 ];
1767 for (i, (color, amount)) in docs.iter().enumerate() {
1768 insert_widget(&drive, &data_contract, i, color, *amount);
1769 }
1770
1771 let document_type = data_contract
1772 .document_type_for_name("widget")
1773 .expect("widget");
1774 let drive_config = DriveConfig {
1780 default_query_limit: 100,
1781 max_query_limit: 2,
1782 ..Default::default()
1783 };
1784
1785 let color_ge_a = WhereClause {
1786 field: "color".to_string(),
1787 operator: WhereOperator::GreaterThanOrEquals,
1788 value: Value::Text("a".to_string()),
1789 };
1790 let request = DocumentAverageRequest {
1791 contract: &data_contract,
1792 document_type,
1793 sum_property: "amount".to_string(),
1794 where_clauses: vec![color_ge_a],
1795 order_clauses: Vec::new(),
1796 mode: AverageMode::GroupByRange,
1797 limit: Some(4),
1798 prove: false,
1799 drive_config: &drive_config,
1800 };
1801
1802 let response = drive
1803 .execute_document_average_request(request, None, platform_version)
1804 .expect("dispatcher should succeed (no-proof clamps, never errors)");
1805 let entries = match response {
1806 DocumentAverageResponse::Entries(e) => e,
1807 other => panic!("expected Entries, got {:?}", other),
1808 };
1809 assert_eq!(
1810 entries.len(),
1811 2,
1812 "distinct AVG no-proof must clamp `limit = 4` to \
1813 `max_query_limit = 2`; got {entries:?}"
1814 );
1815 }
1816
1817 #[test]
1825 fn joint_dispatcher_rejects_prove_true_request() {
1826 let drive = setup_drive_with_initial_state_structure(None);
1827 let platform_version = PlatformVersion::latest();
1828 let data_contract = build_widget_contract_pcps();
1829 drive
1830 .apply_contract(
1831 &data_contract,
1832 BlockInfo::default(),
1833 true,
1834 StorageFlags::optional_default_as_cow(),
1835 None,
1836 platform_version,
1837 )
1838 .expect("apply contract");
1839
1840 let document_type = data_contract
1841 .document_type_for_name("widget")
1842 .expect("widget");
1843 let drive_config = DriveConfig::default();
1844
1845 let request = DocumentAverageRequest {
1846 contract: &data_contract,
1847 document_type,
1848 sum_property: "amount".to_string(),
1849 where_clauses: Vec::new(),
1850 order_clauses: Vec::new(),
1851 mode: AverageMode::Aggregate,
1852 limit: None,
1853 prove: true,
1854 drive_config: &drive_config,
1855 };
1856
1857 let err = drive
1858 .execute_document_count_and_sum_request(request, None, platform_version)
1859 .expect_err("prove=true direct call must reject");
1860 let msg = format!("{err:?}");
1861 assert!(
1862 msg.contains("no-prove"),
1863 "expected the prove=true guard to fire; got: {msg}"
1864 );
1865 }
1866
1867 #[test]
1875 fn joint_dispatcher_runs_validate_and_canonicalize_where_clauses() {
1876 let drive = setup_drive_with_initial_state_structure(None);
1877 let platform_version = PlatformVersion::latest();
1878 let data_contract = build_widget_contract_pcps();
1879 drive
1880 .apply_contract(
1881 &data_contract,
1882 BlockInfo::default(),
1883 true,
1884 StorageFlags::optional_default_as_cow(),
1885 None,
1886 platform_version,
1887 )
1888 .expect("apply contract");
1889
1890 let document_type = data_contract
1891 .document_type_for_name("widget")
1892 .expect("widget");
1893 let drive_config = DriveConfig::default();
1894
1895 let dup_color_a = WhereClause {
1898 field: "color".to_string(),
1899 operator: WhereOperator::Equal,
1900 value: Value::Text("red".to_string()),
1901 };
1902 let dup_color_b = WhereClause {
1903 field: "color".to_string(),
1904 operator: WhereOperator::Equal,
1905 value: Value::Text("green".to_string()),
1906 };
1907 let request = DocumentAverageRequest {
1908 contract: &data_contract,
1909 document_type,
1910 sum_property: "amount".to_string(),
1911 where_clauses: vec![dup_color_a, dup_color_b],
1912 order_clauses: Vec::new(),
1913 mode: AverageMode::Aggregate,
1914 limit: None,
1915 prove: false,
1916 drive_config: &drive_config,
1917 };
1918
1919 let err = drive
1920 .execute_document_average_request(request, None, platform_version)
1921 .expect_err(
1922 "AVG no-proof must reject duplicate Equal on the same field via \
1923 validate_and_canonicalize_where_clauses",
1924 );
1925 let msg = format!("{err:?}");
1929 assert!(
1930 !msg.contains("WhereClauseOnNonIndexedProperty"),
1931 "validator should reject before the index picker would: {msg}"
1932 );
1933 }
1934
1935 #[test]
1940 fn per_in_value_avg_no_proof_honors_explicit_limit() {
1941 let drive = setup_drive_with_initial_state_structure(None);
1942 let platform_version = PlatformVersion::latest();
1943
1944 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
1948 let document_schema = platform_value!({
1949 "type": "object",
1950 "properties": {
1951 "color": {"type": "string", "position": 0, "maxLength": 32},
1952 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
1953 },
1954 "required": ["color", "amount"],
1955 "indices": [{
1956 "name": "byColor",
1957 "properties": [{"color": "asc"}],
1958 "summable": "amount",
1959 "countable": "countable",
1960 }],
1961 "additionalProperties": false,
1962 });
1963 let schemas = platform_value!({ "widget": document_schema });
1964 let data_contract = factory
1965 .create_with_value_config(
1966 dpp::tests::utils::generate_random_identifier_struct(),
1967 0,
1968 schemas,
1969 None,
1970 None,
1971 )
1972 .expect("create data contract")
1973 .data_contract_owned();
1974 drive
1975 .apply_contract(
1976 &data_contract,
1977 BlockInfo::default(),
1978 true,
1979 StorageFlags::optional_default_as_cow(),
1980 None,
1981 platform_version,
1982 )
1983 .expect("apply contract");
1984
1985 for (i, (color, amount)) in [("red", 5u64), ("green", 7), ("blue", 2), ("yellow", 4)]
1986 .iter()
1987 .enumerate()
1988 {
1989 insert_widget(&drive, &data_contract, i, color, *amount);
1990 }
1991
1992 let document_type = data_contract
1993 .document_type_for_name("widget")
1994 .expect("widget");
1995 let drive_config = DriveConfig::default();
1996
1997 let color_in = WhereClause {
2000 field: "color".to_string(),
2001 operator: WhereOperator::In,
2002 value: Value::Array(vec![
2003 Value::Text("red".to_string()),
2004 Value::Text("green".to_string()),
2005 Value::Text("blue".to_string()),
2006 Value::Text("yellow".to_string()),
2007 ]),
2008 };
2009 let request = DocumentAverageRequest {
2010 contract: &data_contract,
2011 document_type,
2012 sum_property: "amount".to_string(),
2013 where_clauses: vec![color_in],
2014 order_clauses: Vec::new(),
2015 mode: AverageMode::GroupByIn,
2016 limit: Some(2),
2017 prove: false,
2018 drive_config: &drive_config,
2019 };
2020
2021 let response = drive
2022 .execute_document_average_request(request, None, platform_version)
2023 .expect("dispatcher should succeed");
2024 let entries = match response {
2025 DocumentAverageResponse::Entries(e) => e,
2026 other => panic!("expected Entries, got {:?}", other),
2027 };
2028 assert_eq!(
2029 entries.len(),
2030 2,
2031 "PerInValue AVG no-proof must apply request.limit = 2 to the per-In \
2032 entry list (caller asked for 4 In values, dispatcher must truncate); \
2033 got {entries:?}"
2034 );
2035 }
2036
2037 #[test]
2048 fn empty_where_total_executor_uses_primary_key_count_sum_tree_fast_path() {
2049 let drive = setup_drive_with_initial_state_structure(None);
2050 let platform_version = PlatformVersion::latest();
2051
2052 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
2059 let document_schema = platform_value!({
2060 "type": "object",
2061 "properties": {
2062 "amount": {"type": "integer", "position": 0, "minimum": 0, "maximum": 1000},
2063 },
2064 "required": ["amount"],
2065 "documentsAverageable": "amount",
2066 "additionalProperties": false,
2067 });
2068 let schemas = platform_value!({ "score": document_schema });
2069 let data_contract = factory
2070 .create_with_value_config(
2071 dpp::tests::utils::generate_random_identifier_struct(),
2072 0,
2073 schemas,
2074 None,
2075 None,
2076 )
2077 .expect("create data contract")
2078 .data_contract_owned();
2079 drive
2080 .apply_contract(
2081 &data_contract,
2082 BlockInfo::default(),
2083 true,
2084 StorageFlags::optional_default_as_cow(),
2085 None,
2086 platform_version,
2087 )
2088 .expect("apply contract");
2089
2090 let document_type = data_contract
2093 .document_type_for_name("score")
2094 .expect("score type");
2095 for (i, amount) in [10u64, 20, 30, 40].iter().enumerate() {
2096 let mut properties = std::collections::BTreeMap::new();
2097 properties.insert("amount".to_string(), Value::U64(*amount));
2098 let document: Document = DocumentV0 {
2099 id: Identifier::from([(i + 1) as u8; 32]),
2100 owner_id: Identifier::from([0u8; 32]),
2101 properties,
2102 revision: None,
2103 created_at: None,
2104 updated_at: None,
2105 transferred_at: None,
2106 created_at_block_height: None,
2107 updated_at_block_height: None,
2108 transferred_at_block_height: None,
2109 created_at_core_block_height: None,
2110 updated_at_core_block_height: None,
2111 transferred_at_core_block_height: None,
2112 creator_id: None,
2113 }
2114 .into();
2115 let storage_flags = Some(std::borrow::Cow::Owned(StorageFlags::SingleEpoch(0)));
2116 drive
2117 .add_document_for_contract(
2118 DocumentAndContractInfo {
2119 owned_document_info: OwnedDocumentInfo {
2120 document_info: DocumentRefInfo((&document, storage_flags)),
2121 owner_id: None,
2122 },
2123 contract: &data_contract,
2124 document_type,
2125 },
2126 false,
2127 BlockInfo::default(),
2128 true,
2129 None,
2130 platform_version,
2131 None,
2132 )
2133 .expect("insert score");
2134 }
2135
2136 let drive_config = DriveConfig::default();
2137 let request = DocumentAverageRequest {
2138 contract: &data_contract,
2139 document_type,
2140 sum_property: "amount".to_string(),
2141 where_clauses: Vec::new(),
2142 order_clauses: Vec::new(),
2143 mode: AverageMode::Aggregate,
2144 limit: None,
2145 prove: false,
2146 drive_config: &drive_config,
2147 };
2148
2149 let response = drive
2150 .execute_document_average_request(request, None, platform_version)
2151 .expect("empty-where AVG no-proof must succeed via the primary-key fast path");
2152 match response {
2153 DocumentAverageResponse::Aggregate { count, sum } => {
2154 assert_eq!(
2155 (count, sum),
2156 (4, 100),
2157 "primary-key count-sum tree fast path must return (4 docs, sum 10+20+30+40 = 100)"
2158 );
2159 }
2160 other => panic!("expected Aggregate, got {:?}", other),
2161 }
2162 }
2163
2164 #[test]
2171 fn per_in_value_avg_no_proof_defaults_limit_to_operator_default_query_limit() {
2172 let drive = setup_drive_with_initial_state_structure(None);
2173 let platform_version = PlatformVersion::latest();
2174
2175 let factory = DataContractFactory::new(PROTOCOL_VERSION_V12).expect("create factory");
2181 let document_schema = platform_value!({
2182 "type": "object",
2183 "properties": {
2184 "color": {"type": "string", "position": 0, "maxLength": 32},
2185 "amount": {"type": "integer", "position": 1, "minimum": 0, "maximum": 1000},
2186 },
2187 "required": ["color", "amount"],
2188 "indices": [{
2189 "name": "byColor",
2190 "properties": [{"color": "asc"}],
2191 "summable": "amount",
2192 "countable": "countable",
2193 }],
2194 "additionalProperties": false,
2195 });
2196 let schemas = platform_value!({ "widget": document_schema });
2197 let data_contract = factory
2198 .create_with_value_config(
2199 dpp::tests::utils::generate_random_identifier_struct(),
2200 0,
2201 schemas,
2202 None,
2203 None,
2204 )
2205 .expect("create data contract")
2206 .data_contract_owned();
2207 drive
2208 .apply_contract(
2209 &data_contract,
2210 BlockInfo::default(),
2211 true,
2212 StorageFlags::optional_default_as_cow(),
2213 None,
2214 platform_version,
2215 )
2216 .expect("apply contract");
2217
2218 for (i, (color, amount)) in [("red", 5u64), ("green", 7), ("blue", 2), ("yellow", 4)]
2219 .iter()
2220 .enumerate()
2221 {
2222 insert_widget(&drive, &data_contract, i, color, *amount);
2223 }
2224
2225 let document_type = data_contract
2226 .document_type_for_name("widget")
2227 .expect("widget");
2228 let drive_config = DriveConfig {
2229 default_query_limit: 2,
2230 ..Default::default()
2231 };
2232
2233 let color_in = WhereClause {
2234 field: "color".to_string(),
2235 operator: WhereOperator::In,
2236 value: Value::Array(vec![
2237 Value::Text("red".to_string()),
2238 Value::Text("green".to_string()),
2239 Value::Text("blue".to_string()),
2240 Value::Text("yellow".to_string()),
2241 ]),
2242 };
2243 let request = DocumentAverageRequest {
2244 contract: &data_contract,
2245 document_type,
2246 sum_property: "amount".to_string(),
2247 where_clauses: vec![color_in],
2248 order_clauses: Vec::new(),
2249 mode: AverageMode::GroupByIn,
2250 limit: None,
2251 prove: false,
2252 drive_config: &drive_config,
2253 };
2254
2255 let response = drive
2256 .execute_document_average_request(request, None, platform_version)
2257 .expect("dispatcher should succeed");
2258 let entries = match response {
2259 DocumentAverageResponse::Entries(e) => e,
2260 other => panic!("expected Entries, got {:?}", other),
2261 };
2262 assert_eq!(
2263 entries.len(),
2264 2,
2265 "PerInValue AVG no-proof with `limit = None` must default to \
2266 `drive_config.default_query_limit` (= 2 here) and truncate the \
2267 per-In entry list; got {entries:?}"
2268 );
2269 }
2270}