1use crate::drive::RootTree;
22use crate::error::query::QuerySyntaxError;
23use crate::error::Error;
24use crate::query::drive_document_sum_query::{is_range_operator, DriveDocumentSumQuery};
25use crate::query::{WhereClause, WhereOperator};
26use dpp::data_contract::document_type::methods::DocumentTypeV0Methods;
32use dpp::data_contract::document_type::DocumentTypeRef;
33use dpp::data_contract::DataContract;
34use dpp::version::PlatformVersion;
35use grovedb::{PathQuery, Query, QueryItem, SizedQuery};
36
37const SUM_TREE_KEY: u8 = 0;
41
42#[cfg(any(feature = "server", feature = "verify"))]
43impl<'a> DriveDocumentSumQuery<'a> {
44 pub fn primary_key_sum_path_query(
52 contract_id: [u8; 32],
53 document_type_name: &str,
54 ) -> PathQuery {
55 let path = vec![
56 vec![RootTree::DataContractDocuments as u8],
57 contract_id.to_vec(),
58 vec![1u8],
59 document_type_name.as_bytes().to_vec(),
60 ];
61 let mut query = Query::new();
62 query.insert_key(vec![SUM_TREE_KEY]);
63 PathQuery::new(path, SizedQuery::new(query, None, None))
64 }
65
66 pub fn point_lookup_sum_path_query(
71 &self,
72 platform_version: &PlatformVersion,
73 ) -> Result<PathQuery, Error> {
74 if self.index.properties.is_empty() {
75 return Err(Error::Query(
76 QuerySyntaxError::InvalidWhereClauseComponents(
77 "point_lookup_sum_path_query: index must have at least one property",
78 ),
79 ));
80 }
81
82 let mut base_path: Vec<Vec<u8>> = vec![
83 vec![RootTree::DataContractDocuments as u8],
84 self.contract_id.to_vec(),
85 vec![1u8],
86 self.document_type_name.as_bytes().to_vec(),
87 ];
88
89 let mut in_outer_keys: Option<Vec<Vec<u8>>> = None;
90 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
91
92 for prop in self.index.properties.iter() {
93 let clause = self
94 .where_clauses
95 .iter()
96 .find(|wc| wc.field == prop.name)
97 .ok_or_else(|| {
98 Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
99 "prove sum requires the where clauses to fully cover the \
100 summable index; one or more index properties have no matching \
101 `==` or `in` clause — define a more specific summable index \
102 (with `summable: \"<prop>\"` whose properties exactly equal \
103 the clauses) or use `prove=false`",
104 ))
105 })?;
106
107 match clause.operator {
108 WhereOperator::Equal => {
109 let serialized = self.document_type.serialize_value_for_key(
110 prop.name.as_str(),
111 &clause.value,
112 platform_version,
113 )?;
114 if in_outer_keys.is_some() {
115 subquery_path_extension.push(prop.name.as_bytes().to_vec());
116 subquery_path_extension.push(serialized);
117 } else {
118 base_path.push(prop.name.as_bytes().to_vec());
119 base_path.push(serialized);
120 }
121 }
122 WhereOperator::In => {
123 if in_outer_keys.is_some() {
124 return Err(Error::Query(
125 QuerySyntaxError::InvalidWhereClauseComponents(
126 "prove sum: at most one `in` clause is supported on the \
127 covering summable index",
128 ),
129 ));
130 }
131 base_path.push(prop.name.as_bytes().to_vec());
132 let in_values = clause.in_values().into_data_with_error()??;
133 let mut keys: Vec<Vec<u8>> = in_values
134 .iter()
135 .map(|v| {
136 self.document_type.serialize_value_for_key(
137 prop.name.as_str(),
138 v,
139 platform_version,
140 )
141 })
142 .collect::<Result<_, _>>()?;
143 keys.sort();
144 in_outer_keys = Some(keys);
145 }
146 _ => {
147 return Err(Error::Query(
148 QuerySyntaxError::InvalidWhereClauseComponents(
149 "point_lookup_sum_path_query: index properties must use \
150 `==` or `in`",
151 ),
152 ));
153 }
154 }
155 }
156
157 let sum_tree_terminator = self.index.summable.is_some();
165
166 match in_outer_keys {
167 None => {
168 let mut query = Query::new();
170 if sum_tree_terminator {
171 let last_value = base_path.pop().expect(
176 "Equal-only loop pushes (name, value) per prop; \
177 base_path must hold the terminator's serialized value",
178 );
179 query.insert_key(last_value);
180 } else {
181 query.insert_key(vec![SUM_TREE_KEY]);
182 }
183 Ok(PathQuery::new(
184 base_path,
185 SizedQuery::new(query, None, None),
186 ))
187 }
188 Some(keys) => {
189 let mut outer_query = Query::new();
191 for key in keys {
192 outer_query.insert_key(key);
193 }
194
195 if subquery_path_extension.is_empty() {
196 if sum_tree_terminator {
197 } else {
200 let mut subquery = Query::new();
201 subquery.insert_key(vec![SUM_TREE_KEY]);
202 outer_query.set_subquery(subquery);
203 }
204 } else {
205 let mut subquery = Query::new();
206 if sum_tree_terminator {
207 let termval = subquery_path_extension.pop().expect(
208 "trailing-Equal loop pushes (name, value) pairs; \
209 non-empty extension's tail must be the terminator's \
210 serialized value",
211 );
212 subquery.insert_key(termval);
213 } else {
214 subquery.insert_key(vec![SUM_TREE_KEY]);
215 }
216 outer_query.set_subquery_path(subquery_path_extension);
217 outer_query.set_subquery(subquery);
218 }
219
220 Ok(PathQuery::new(
221 base_path,
222 SizedQuery::new(outer_query, None, None),
223 ))
224 }
225 }
226 }
227
228 pub fn aggregate_sum_path_query(
234 &self,
235 platform_version: &PlatformVersion,
236 ) -> Result<PathQuery, Error> {
237 let terminator_prop_name = &self
242 .index
243 .properties
244 .last()
245 .ok_or(Error::Query(
246 QuerySyntaxError::InvalidWhereClauseComponents(
247 "range_summable index must have at least one property",
248 ),
249 ))?
250 .name;
251 let range_clause = self
252 .where_clauses
253 .iter()
254 .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
255 .ok_or(Error::Query(
256 QuerySyntaxError::InvalidWhereClauseComponents(
257 "aggregate_sum_path_query requires a range where-clause on the index terminator property",
258 ),
259 ))?;
260 let query_item = self.range_clause_to_query_item(range_clause, platform_version)?;
261
262 let mut path = vec![
263 vec![RootTree::DataContractDocuments as u8],
264 self.contract_id.to_vec(),
265 vec![1u8],
266 self.document_type_name.as_bytes().to_vec(),
267 ];
268 let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
269 for prop in prefix_props {
270 let clause = self
271 .where_clauses
272 .iter()
273 .find(|wc| wc.field == prop.name)
274 .ok_or(Error::Query(
275 QuerySyntaxError::InvalidWhereClauseComponents(
276 "aggregate-sum proof: missing where clause for an index prefix property",
277 ),
278 ))?;
279 if clause.operator != WhereOperator::Equal {
280 return Err(Error::Query(
281 QuerySyntaxError::InvalidWhereClauseComponents(
282 "aggregate-sum proof: prefix properties must use `==` (no `in`); use \
283 `group_by = [in_field, range_field]` (carrier-aggregate variant) for \
284 compound In-on-prefix sum queries",
285 ),
286 ));
287 }
288 path.push(prop.name.as_bytes().to_vec());
289 path.push(self.document_type.serialize_value_for_key(
290 prop.name.as_str(),
291 &clause.value,
292 platform_version,
293 )?);
294 }
295 let range_prop_name = &self
296 .index
297 .properties
298 .last()
299 .ok_or(Error::Query(
300 QuerySyntaxError::InvalidWhereClauseComponents(
301 "range_summable index must have at least one property",
302 ),
303 ))?
304 .name;
305 path.push(range_prop_name.as_bytes().to_vec());
306
307 let query = Query::new_aggregate_sum_on_range(query_item);
309 Ok(PathQuery::new(path, SizedQuery::new(query, None, None)))
310 }
311
312 pub fn aggregate_count_and_sum_path_query(
317 &self,
318 platform_version: &PlatformVersion,
319 ) -> Result<PathQuery, Error> {
320 if !self.index.range_countable {
321 return Err(Error::Query(QuerySyntaxError::Unsupported(
322 "aggregate_count_and_sum_path_query: index must declare BOTH \
323 `rangeCountable: true` AND `rangeSummable: true` to produce a PCPS \
324 (ProvableCountProvableSumTree) property-name tree."
325 .to_string(),
326 )));
327 }
328
329 let terminator_prop_name = &self
332 .index
333 .properties
334 .last()
335 .ok_or(Error::Query(
336 QuerySyntaxError::InvalidWhereClauseComponents(
337 "PCPS index must have at least one property",
338 ),
339 ))?
340 .name;
341 let range_clause = self
342 .where_clauses
343 .iter()
344 .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
345 .ok_or(Error::Query(
346 QuerySyntaxError::InvalidWhereClauseComponents(
347 "aggregate_count_and_sum_path_query requires a range where-clause on the index terminator property",
348 ),
349 ))?;
350 let query_item = self.range_clause_to_query_item(range_clause, platform_version)?;
351
352 let mut path = vec![
353 vec![RootTree::DataContractDocuments as u8],
354 self.contract_id.to_vec(),
355 vec![1u8],
356 self.document_type_name.as_bytes().to_vec(),
357 ];
358 let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
359 for prop in prefix_props {
360 let clause = self
361 .where_clauses
362 .iter()
363 .find(|wc| wc.field == prop.name)
364 .ok_or(Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
365 "aggregate-count-and-sum proof: missing where clause for an index prefix property",
366 )))?;
367 if clause.operator != WhereOperator::Equal {
368 return Err(Error::Query(
369 QuerySyntaxError::InvalidWhereClauseComponents(
370 "aggregate-count-and-sum proof: prefix properties must use `==` (no `in`)",
371 ),
372 ));
373 }
374 path.push(prop.name.as_bytes().to_vec());
375 path.push(self.document_type.serialize_value_for_key(
376 prop.name.as_str(),
377 &clause.value,
378 platform_version,
379 )?);
380 }
381 let range_prop_name = &self
382 .index
383 .properties
384 .last()
385 .ok_or(Error::Query(
386 QuerySyntaxError::InvalidWhereClauseComponents(
387 "range_countable + range_summable index must have at least one property",
388 ),
389 ))?
390 .name;
391 path.push(range_prop_name.as_bytes().to_vec());
392
393 let query = grovedb::Query::new_aggregate_count_and_sum_on_range(query_item);
394 Ok(PathQuery::new(
395 path,
396 grovedb::SizedQuery::new(query, None, None),
397 ))
398 }
399
400 fn range_clause_to_query_item(
410 &self,
411 clause: &WhereClause,
412 platform_version: &PlatformVersion,
413 ) -> Result<QueryItem, Error> {
414 let serialize = |v: &dpp::platform_value::Value| -> Result<Vec<u8>, Error> {
415 Ok(self.document_type.serialize_value_for_key(
416 clause.field.as_str(),
417 v,
418 platform_version,
419 )?)
420 };
421 let serialize_pair = || -> Result<(Vec<u8>, Vec<u8>), Error> {
422 let arr = clause.value.as_array().ok_or_else(|| {
423 Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
424 "range bounds value must be a 2-element array",
425 ))
426 })?;
427 if arr.len() != 2 {
428 return Err(Error::Query(
429 QuerySyntaxError::InvalidWhereClauseComponents(
430 "range bounds value must be a 2-element array",
431 ),
432 ));
433 }
434 let a = serialize(&arr[0])?;
435 let b = serialize(&arr[1])?;
436 if a > b {
437 return Err(Error::Query(
438 QuerySyntaxError::InvalidWhereClauseComponents(
439 "range lower bound must be <= upper bound",
440 ),
441 ));
442 }
443 Ok((a, b))
444 };
445
446 Ok(match clause.operator {
447 WhereOperator::GreaterThan => {
448 let v = serialize(&clause.value)?;
449 QueryItem::RangeAfter(v..)
450 }
451 WhereOperator::GreaterThanOrEquals => {
452 let v = serialize(&clause.value)?;
453 QueryItem::RangeFrom(v..)
454 }
455 WhereOperator::LessThan => {
456 let v = serialize(&clause.value)?;
457 QueryItem::RangeTo(..v)
458 }
459 WhereOperator::LessThanOrEquals => {
460 let v = serialize(&clause.value)?;
461 QueryItem::RangeToInclusive(..=v)
462 }
463 WhereOperator::Between => {
464 let (a, b) = serialize_pair()?;
465 QueryItem::RangeInclusive(a..=b)
466 }
467 WhereOperator::BetweenExcludeBounds => {
468 let (a, b) = serialize_pair()?;
469 QueryItem::RangeAfterTo(a..b)
470 }
471 WhereOperator::BetweenExcludeLeft => {
472 let (a, b) = serialize_pair()?;
473 QueryItem::RangeAfterToInclusive(a..=b)
474 }
475 WhereOperator::BetweenExcludeRight => {
476 let (a, b) = serialize_pair()?;
477 QueryItem::Range(a..b)
478 }
479 WhereOperator::StartsWith => {
480 let left_key = serialize(&clause.value)?;
481 let mut right_key = left_key.clone();
482 if right_key.is_empty() {
483 return Err(Error::Query(QuerySyntaxError::InvalidStartsWithClause(
484 "startsWith prefix must have at least one byte",
485 )));
486 }
487 let mut i = right_key.len();
494 while i > 0 && right_key[i - 1] == 0xFF {
495 i -= 1;
496 }
497 if i == 0 {
498 return Err(Error::Query(QuerySyntaxError::InvalidStartsWithClause(
499 "startsWith prefix is all 0xFF bytes; cannot form half-open upper bound",
500 )));
501 }
502 right_key.truncate(i);
503 *right_key
504 .last_mut()
505 .expect("non-empty after truncate to non-zero length") += 1;
506 QueryItem::Range(left_key..right_key)
507 }
508 _ => {
509 return Err(Error::Query(
510 QuerySyntaxError::InvalidWhereClauseComponents(
511 "range_clause_to_query_item called on a non-range operator",
512 ),
513 ));
514 }
515 })
516 }
517
518 pub fn distinct_sum_path_query(
541 &self,
542 limit: Option<u16>,
543 left_to_right: bool,
544 platform_version: &PlatformVersion,
545 ) -> Result<PathQuery, Error> {
546 let range_clause = self
547 .where_clauses
548 .iter()
549 .find(|wc| is_range_operator(wc.operator))
550 .ok_or(Error::Query(
551 QuerySyntaxError::InvalidWhereClauseComponents(
552 "distinct_sum_path_query requires a range where-clause",
553 ),
554 ))?;
555 let range_item = self.range_clause_to_query_item(range_clause, platform_version)?;
556
557 let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
558 let terminator_name = &self
559 .index
560 .properties
561 .last()
562 .ok_or(Error::Query(
563 QuerySyntaxError::InvalidWhereClauseComponents(
564 "range_summable index must have at least one property",
565 ),
566 ))?
567 .name;
568
569 let mut base_path: Vec<Vec<u8>> = vec![
570 vec![RootTree::DataContractDocuments as u8],
571 self.contract_id.to_vec(),
572 vec![1u8],
573 self.document_type_name.as_bytes().to_vec(),
574 ];
575
576 let mut in_outer_keys: Option<Vec<Vec<u8>>> = None;
583 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
584
585 for prop in prefix_props {
586 let clause = self
587 .where_clauses
588 .iter()
589 .find(|wc| wc.field == prop.name)
590 .ok_or(Error::Query(
591 QuerySyntaxError::InvalidWhereClauseComponents(
592 "distinct_sum_path_query: missing where clause for an index \
593 prefix property",
594 ),
595 ))?;
596
597 match clause.operator {
598 WhereOperator::Equal => {
599 let serialized = self.document_type.serialize_value_for_key(
600 prop.name.as_str(),
601 &clause.value,
602 platform_version,
603 )?;
604 if in_outer_keys.is_some() {
605 subquery_path_extension.push(prop.name.as_bytes().to_vec());
606 subquery_path_extension.push(serialized);
607 } else {
608 base_path.push(prop.name.as_bytes().to_vec());
609 base_path.push(serialized);
610 }
611 }
612 WhereOperator::In => {
613 if in_outer_keys.is_some() {
614 return Err(Error::Query(
615 QuerySyntaxError::InvalidWhereClauseComponents(
616 "distinct_sum_path_query: at most one `In` clause is supported \
617 on prefix properties",
618 ),
619 ));
620 }
621 base_path.push(prop.name.as_bytes().to_vec());
624 let in_values = clause.in_values().into_data_with_error()??;
625 let mut keys: Vec<Vec<u8>> = in_values
626 .iter()
627 .map(|v| {
628 self.document_type.serialize_value_for_key(
629 prop.name.as_str(),
630 v,
631 platform_version,
632 )
633 })
634 .collect::<Result<_, _>>()?;
635 keys.sort();
642 in_outer_keys = Some(keys);
643 }
644 _ => {
645 return Err(Error::Query(
646 QuerySyntaxError::InvalidWhereClauseComponents(
647 "distinct_sum_path_query: prefix properties must use `==` or `in`",
648 ),
649 ));
650 }
651 }
652 }
653
654 match in_outer_keys {
655 None => {
656 base_path.push(terminator_name.as_bytes().to_vec());
659 let mut query = Query::new_with_direction(left_to_right);
660 query.insert_item(range_item);
661 Ok(PathQuery::new(
662 base_path,
663 SizedQuery::new(query, limit, None),
664 ))
665 }
666 Some(keys) => {
667 let mut outer_query = Query::new_with_direction(left_to_right);
675 for key in keys {
676 outer_query.insert_key(key);
677 }
678 subquery_path_extension.push(terminator_name.as_bytes().to_vec());
679
680 let mut subquery = Query::new_with_direction(left_to_right);
681 subquery.insert_item(range_item);
682
683 outer_query.set_subquery_path(subquery_path_extension);
684 outer_query.set_subquery(subquery);
685
686 Ok(PathQuery::new(
687 base_path,
688 SizedQuery::new(outer_query, limit, None),
689 ))
690 }
691 }
692 }
693
694 pub fn carrier_aggregate_sum_path_query(
743 &self,
744 limit: Option<u16>,
745 left_to_right: bool,
746 platform_version: &PlatformVersion,
747 ) -> Result<PathQuery, Error> {
748 let terminator_prop_name = &self
761 .index
762 .properties
763 .last()
764 .ok_or(Error::Query(
765 QuerySyntaxError::InvalidWhereClauseComponents(
766 "range_summable index must have at least one property",
767 ),
768 ))?
769 .name;
770 let terminator_clause = self
771 .where_clauses
772 .iter()
773 .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
774 .ok_or(Error::Query(
775 QuerySyntaxError::InvalidWhereClauseComponents(
776 "carrier_aggregate_sum_path_query requires a range where-clause on the \
777 terminator property of the chosen index",
778 ),
779 ))?;
780 let inner_range_item =
781 self.range_clause_to_query_item(terminator_clause, platform_version)?;
782
783 let mut base_path: Vec<Vec<u8>> = vec![
784 vec![RootTree::DataContractDocuments as u8],
785 self.contract_id.to_vec(),
786 vec![1u8],
787 self.document_type_name.as_bytes().to_vec(),
788 ];
789 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
790
791 enum Carrier {
796 Pending,
797 In(WhereClause),
798 Range(WhereClause),
799 }
800 let mut carrier = Carrier::Pending;
801 let prefix_and_carrier_props = &self.index.properties[..self.index.properties.len() - 1];
802
803 for prop in prefix_and_carrier_props {
804 let clause = self
805 .where_clauses
806 .iter()
807 .find(|wc| wc.field == prop.name)
808 .ok_or(Error::Query(
809 QuerySyntaxError::InvalidWhereClauseComponents(
810 "carrier-aggregate sum proof: missing where clause for an index prefix \
811 property",
812 ),
813 ))?;
814 match (&carrier, clause.operator) {
815 (Carrier::Pending, WhereOperator::Equal) => {
816 base_path.push(prop.name.as_bytes().to_vec());
817 base_path.push(self.document_type.serialize_value_for_key(
818 prop.name.as_str(),
819 &clause.value,
820 platform_version,
821 )?);
822 }
823 (Carrier::Pending, WhereOperator::In) => {
824 base_path.push(prop.name.as_bytes().to_vec());
825 carrier = Carrier::In(clause.clone());
826 }
827 (Carrier::Pending, op) if is_range_operator(op) => {
828 base_path.push(prop.name.as_bytes().to_vec());
829 carrier = Carrier::Range(clause.clone());
830 }
831 (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => {
832 subquery_path_extension.push(prop.name.as_bytes().to_vec());
833 subquery_path_extension.push(self.document_type.serialize_value_for_key(
834 prop.name.as_str(),
835 &clause.value,
836 platform_version,
837 )?);
838 }
839 (Carrier::In(_) | Carrier::Range(_), _) => {
840 return Err(Error::Query(
841 QuerySyntaxError::InvalidWhereClauseComponents(
842 "carrier-aggregate sum proof: at most one carrier clause (In or \
843 range) is supported on prefix properties; subsequent prefix \
844 clauses must use `==`",
845 ),
846 ));
847 }
848 _ => {
849 return Err(Error::Query(
850 QuerySyntaxError::InvalidWhereClauseComponents(
851 "carrier-aggregate sum proof: prefix property operator unsupported",
852 ),
853 ));
854 }
855 }
856 }
857 subquery_path_extension.push(terminator_prop_name.as_bytes().to_vec());
858
859 let mut outer_query = Query::new_with_direction(left_to_right);
860 match carrier {
861 Carrier::Pending => {
862 return Err(Error::Query(
863 QuerySyntaxError::InvalidWhereClauseComponents(
864 "carrier-aggregate sum proof: an In or range clause must appear on a \
865 prefix property of the chosen index to act as the carrier dimension",
866 ),
867 ));
868 }
869 Carrier::In(in_clause) => {
870 let in_values = in_clause.in_values().into_data_with_error()??;
875 let mut serialized_in_keys: Vec<Vec<u8>> = in_values
876 .iter()
877 .map(|v| {
878 self.document_type.serialize_value_for_key(
879 in_clause.field.as_str(),
880 v,
881 platform_version,
882 )
883 })
884 .collect::<Result<_, _>>()?;
885 serialized_in_keys.sort();
886 serialized_in_keys.dedup();
887 for key in serialized_in_keys {
888 outer_query.insert_key(key);
889 }
890 }
891 Carrier::Range(range_clause) => {
892 let outer_range_item =
896 self.range_clause_to_query_item(&range_clause, platform_version)?;
897 outer_query.items.push(outer_range_item);
898 }
899 }
900 outer_query.set_subquery_path(subquery_path_extension);
901 outer_query.set_subquery(Query::new_aggregate_sum_on_range(inner_range_item));
902
903 Ok(PathQuery::new(
910 base_path,
911 SizedQuery::new(outer_query, limit, None),
912 ))
913 }
914
915 pub fn carrier_aggregate_count_and_sum_path_query(
937 &self,
938 limit: Option<u16>,
939 left_to_right: bool,
940 platform_version: &PlatformVersion,
941 ) -> Result<PathQuery, Error> {
942 if !self.index.range_countable {
943 return Err(Error::Query(QuerySyntaxError::Unsupported(
944 "carrier_aggregate_count_and_sum_path_query: index must declare BOTH \
945 `rangeCountable: true` AND `rangeSummable: true` to produce a PCPS \
946 (ProvableCountProvableSumTree) property-name tree."
947 .to_string(),
948 )));
949 }
950
951 let terminator_prop_name = &self
952 .index
953 .properties
954 .last()
955 .ok_or(Error::Query(
956 QuerySyntaxError::InvalidWhereClauseComponents(
957 "range_countable + range_summable index must have at least one property",
958 ),
959 ))?
960 .name;
961 let terminator_clause = self
962 .where_clauses
963 .iter()
964 .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
965 .ok_or(Error::Query(
966 QuerySyntaxError::InvalidWhereClauseComponents(
967 "carrier_aggregate_count_and_sum_path_query requires a range where-clause \
968 on the terminator property of the chosen index",
969 ),
970 ))?;
971 let inner_range_item =
972 self.range_clause_to_query_item(terminator_clause, platform_version)?;
973
974 let mut base_path: Vec<Vec<u8>> = vec![
975 vec![RootTree::DataContractDocuments as u8],
976 self.contract_id.to_vec(),
977 vec![1u8],
978 self.document_type_name.as_bytes().to_vec(),
979 ];
980 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
981
982 enum Carrier {
984 Pending,
985 In(WhereClause),
986 Range(WhereClause),
987 }
988 let mut carrier = Carrier::Pending;
989 let prefix_and_carrier_props = &self.index.properties[..self.index.properties.len() - 1];
990
991 for prop in prefix_and_carrier_props {
992 let clause = self
993 .where_clauses
994 .iter()
995 .find(|wc| wc.field == prop.name)
996 .ok_or(Error::Query(
997 QuerySyntaxError::InvalidWhereClauseComponents(
998 "carrier-aggregate count-and-sum proof: missing where clause for an index \
999 prefix property",
1000 ),
1001 ))?;
1002 match (&carrier, clause.operator) {
1003 (Carrier::Pending, WhereOperator::Equal) => {
1004 base_path.push(prop.name.as_bytes().to_vec());
1005 base_path.push(self.document_type.serialize_value_for_key(
1006 prop.name.as_str(),
1007 &clause.value,
1008 platform_version,
1009 )?);
1010 }
1011 (Carrier::Pending, WhereOperator::In) => {
1012 base_path.push(prop.name.as_bytes().to_vec());
1013 carrier = Carrier::In(clause.clone());
1014 }
1015 (Carrier::Pending, op) if is_range_operator(op) => {
1016 base_path.push(prop.name.as_bytes().to_vec());
1017 carrier = Carrier::Range(clause.clone());
1018 }
1019 (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => {
1020 subquery_path_extension.push(prop.name.as_bytes().to_vec());
1021 subquery_path_extension.push(self.document_type.serialize_value_for_key(
1022 prop.name.as_str(),
1023 &clause.value,
1024 platform_version,
1025 )?);
1026 }
1027 (Carrier::In(_) | Carrier::Range(_), _) => {
1028 return Err(Error::Query(
1029 QuerySyntaxError::InvalidWhereClauseComponents(
1030 "carrier-aggregate count-and-sum proof: at most one carrier clause \
1031 (In or range) is supported on prefix properties; subsequent prefix \
1032 clauses must use `==`",
1033 ),
1034 ));
1035 }
1036 _ => {
1037 return Err(Error::Query(
1038 QuerySyntaxError::InvalidWhereClauseComponents(
1039 "carrier-aggregate count-and-sum proof: prefix property operator \
1040 unsupported",
1041 ),
1042 ));
1043 }
1044 }
1045 }
1046 subquery_path_extension.push(terminator_prop_name.as_bytes().to_vec());
1047
1048 let mut outer_query = Query::new_with_direction(left_to_right);
1049 match carrier {
1050 Carrier::Pending => {
1051 return Err(Error::Query(
1052 QuerySyntaxError::InvalidWhereClauseComponents(
1053 "carrier-aggregate count-and-sum proof: an In or range clause must \
1054 appear on a prefix property of the chosen index to act as the carrier \
1055 dimension",
1056 ),
1057 ));
1058 }
1059 Carrier::In(in_clause) => {
1060 let in_values = in_clause.in_values().into_data_with_error()??;
1061 let mut serialized_in_keys: Vec<Vec<u8>> = in_values
1062 .iter()
1063 .map(|v| {
1064 self.document_type.serialize_value_for_key(
1065 in_clause.field.as_str(),
1066 v,
1067 platform_version,
1068 )
1069 })
1070 .collect::<Result<_, _>>()?;
1071 serialized_in_keys.sort();
1072 serialized_in_keys.dedup();
1073 for key in serialized_in_keys {
1074 outer_query.insert_key(key);
1075 }
1076 }
1077 Carrier::Range(range_clause) => {
1078 let outer_range_item =
1079 self.range_clause_to_query_item(&range_clause, platform_version)?;
1080 outer_query.items.push(outer_range_item);
1081 }
1082 }
1083 outer_query.set_subquery_path(subquery_path_extension);
1084 outer_query.set_subquery(grovedb::Query::new_aggregate_count_and_sum_on_range(
1085 inner_range_item,
1086 ));
1087
1088 Ok(PathQuery::new(
1089 base_path,
1090 SizedQuery::new(outer_query, limit, None),
1091 ))
1092 }
1093}
1094
1095#[cfg(any(feature = "server", feature = "verify"))]
1101impl<'a> DriveDocumentSumQuery<'a> {
1102 pub fn point_lookup_sum_path_query_static(
1106 contract: &DataContract,
1107 document_type: DocumentTypeRef,
1108 sum_property: &str,
1109 where_clauses: &[WhereClause],
1110 platform_version: &PlatformVersion,
1111 ) -> Result<PathQuery, Error> {
1112 use crate::query::drive_document_sum_query::index_picker::find_summable_index_for_where_clauses;
1113 use dpp::data_contract::accessors::v0::DataContractV0Getters;
1114 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1115
1116 let index = find_summable_index_for_where_clauses(
1117 document_type.indexes(),
1118 where_clauses,
1119 sum_property,
1120 )
1121 .ok_or_else(|| {
1122 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
1123 "no `summable: \"<prop>\"` index exactly matches the where-clause fields. \
1124 Define a more specific summable index (with `summable: \"<prop>\"` whose \
1125 properties exactly equal the clauses) or use `prove=false`."
1126 .to_string(),
1127 ))
1128 })?;
1129 let q = DriveDocumentSumQuery {
1130 document_type,
1131 contract_id: contract.id().to_buffer(),
1132 document_type_name: document_type.name().clone(),
1133 index,
1134 where_clauses: where_clauses.to_vec(),
1135 sum_property: sum_property.to_string(),
1136 };
1137 q.point_lookup_sum_path_query(platform_version)
1138 }
1139
1140 pub fn aggregate_sum_path_query_static(
1144 contract: &DataContract,
1145 document_type: DocumentTypeRef,
1146 sum_property: &str,
1147 where_clauses: &[WhereClause],
1148 platform_version: &PlatformVersion,
1149 ) -> Result<PathQuery, Error> {
1150 use crate::query::drive_document_sum_query::index_picker::find_range_summable_index_for_where_clauses;
1151 use dpp::data_contract::accessors::v0::DataContractV0Getters;
1152 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1153
1154 let index = find_range_summable_index_for_where_clauses(
1155 document_type.indexes(),
1156 where_clauses,
1157 sum_property,
1158 )
1159 .ok_or_else(|| {
1160 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
1161 "no `rangeSummable: true` index covers the where-clause shape (Equal/In \
1162 prefix exactly + range on the index's last property). Define one or use \
1163 `prove=false`."
1164 .to_string(),
1165 ))
1166 })?;
1167 let q = DriveDocumentSumQuery {
1168 document_type,
1169 contract_id: contract.id().to_buffer(),
1170 document_type_name: document_type.name().clone(),
1171 index,
1172 where_clauses: where_clauses.to_vec(),
1173 sum_property: sum_property.to_string(),
1174 };
1175 q.aggregate_sum_path_query(platform_version)
1176 }
1177
1178 pub fn carrier_aggregate_sum_path_query_static(
1187 contract: &DataContract,
1188 document_type: DocumentTypeRef,
1189 sum_property: &str,
1190 where_clauses: &[WhereClause],
1191 limit: Option<u16>,
1192 left_to_right: bool,
1193 platform_version: &PlatformVersion,
1194 ) -> Result<PathQuery, Error> {
1195 use crate::query::drive_document_sum_query::index_picker::find_range_summable_index_for_where_clauses;
1196 use dpp::data_contract::accessors::v0::DataContractV0Getters;
1197 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1198
1199 let index = find_range_summable_index_for_where_clauses(
1200 document_type.indexes(),
1201 where_clauses,
1202 sum_property,
1203 )
1204 .ok_or_else(|| {
1205 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
1206 "no `rangeSummable: true` index covers the where-clause shape for the \
1207 carrier-aggregate sum carrier (Equal/In prefix + In-or-range carrier + \
1208 range on the index's last property). Define one or use `prove=false`."
1209 .to_string(),
1210 ))
1211 })?;
1212 let q = DriveDocumentSumQuery {
1213 document_type,
1214 contract_id: contract.id().to_buffer(),
1215 document_type_name: document_type.name().clone(),
1216 index,
1217 where_clauses: where_clauses.to_vec(),
1218 sum_property: sum_property.to_string(),
1219 };
1220 q.carrier_aggregate_sum_path_query(limit, left_to_right, platform_version)
1221 }
1222}
1223
1224#[cfg(test)]
1240mod carrier_path_query_tests {
1241 use super::*;
1242 use crate::query::WhereOperator;
1243 use assert_matches::assert_matches;
1244 use dpp::data_contract::accessors::v0::DataContractV0Getters;
1245 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1246 use dpp::data_contract::DataContract;
1247 use dpp::tests::json_document::json_document_to_contract;
1248 use grovedb::QueryItem;
1249
1250 fn load_tip_jar_contract(platform_version: &PlatformVersion) -> DataContract {
1251 json_document_to_contract(
1256 "tests/supporting_files/contract/tip-jar/tip-jar-contract.json",
1257 false,
1258 platform_version,
1259 )
1260 .expect("tip-jar contract fixture loads")
1261 }
1262
1263 fn pick_doc_type<'a>(
1275 contract: &'a DataContract,
1276 doc_type_name: &str,
1277 ) -> dpp::data_contract::document_type::DocumentTypeRef<'a> {
1278 contract
1279 .document_type_for_name(doc_type_name)
1280 .expect("document type exists in tip-jar fixture")
1281 }
1282
1283 fn recipient_a() -> Vec<u8> {
1287 let mut v = vec![0x80u8; 32];
1289 v[31] = 0x01;
1290 v
1291 }
1292 fn recipient_b() -> Vec<u8> {
1293 let mut v = vec![0x10u8; 32];
1295 v[31] = 0x02;
1296 v
1297 }
1298
1299 #[test]
1304 fn carrier_aggregate_sum_in_on_carrier_range_on_terminator() {
1305 let platform_version = PlatformVersion::latest();
1306 let contract = load_tip_jar_contract(platform_version);
1307 let doc_type = pick_doc_type(&contract, "tip");
1308 let index = doc_type
1309 .indexes()
1310 .get("byRecipientTime")
1311 .expect("byRecipientTime index exists on tip doc type");
1312
1313 let in_values = vec![
1318 dpp::platform_value::Value::Bytes(recipient_a()),
1319 dpp::platform_value::Value::Bytes(recipient_b()),
1320 ];
1321 let where_clauses = vec![
1322 WhereClause {
1323 field: "recipient".to_string(),
1324 operator: WhereOperator::In,
1325 value: dpp::platform_value::Value::Array(in_values.clone()),
1326 },
1327 WhereClause {
1328 field: "sentAt".to_string(),
1329 operator: WhereOperator::GreaterThan,
1330 value: dpp::platform_value::Value::U64(0),
1331 },
1332 ];
1333 let q = DriveDocumentSumQuery {
1334 document_type: doc_type,
1335 contract_id: contract.id().to_buffer(),
1336 document_type_name: doc_type.name().clone(),
1337 index,
1338 where_clauses,
1339 sum_property: "amount".to_string(),
1340 };
1341
1342 let pq = q
1343 .carrier_aggregate_sum_path_query(None, true, platform_version)
1344 .expect("carrier-aggregate sum path query builds");
1345
1346 assert!(
1350 pq.path.len() >= 5,
1351 "expected base_path to extend through the In-bearing prop's name subtree"
1352 );
1353 assert_eq!(
1354 pq.path.last().expect("base_path non-empty"),
1355 b"recipient",
1356 "outer path must stop at the In-bearing prop's property-name subtree"
1357 );
1358
1359 let outer_items = &pq.query.query.items;
1363 assert_eq!(outer_items.len(), 2, "one outer Key per In value");
1364 for item in outer_items {
1365 assert_matches!(item, QueryItem::Key(_));
1366 }
1367 if let (QueryItem::Key(a), QueryItem::Key(b)) = (&outer_items[0], &outer_items[1]) {
1368 assert!(a < b, "outer Keys must be sorted lex-ascending");
1369 }
1370
1371 let sub_path = pq
1373 .query
1374 .query
1375 .default_subquery_branch
1376 .subquery_path
1377 .as_ref()
1378 .expect("subquery_path set");
1379 assert_eq!(sub_path, &vec![b"sentAt".to_vec()]);
1380
1381 let subquery = pq
1383 .query
1384 .query
1385 .default_subquery_branch
1386 .subquery
1387 .as_ref()
1388 .expect("subquery set");
1389 assert_eq!(subquery.items.len(), 1);
1390 assert_matches!(subquery.items[0], QueryItem::AggregateSumOnRange(_));
1391 }
1392
1393 #[test]
1397 fn carrier_aggregate_sum_limit_flows_into_sized_query() {
1398 let platform_version = PlatformVersion::latest();
1399 let contract = load_tip_jar_contract(platform_version);
1400 let doc_type = pick_doc_type(&contract, "tip");
1401 let index = doc_type
1402 .indexes()
1403 .get("byRecipientTime")
1404 .expect("byRecipientTime index exists on tip doc type");
1405
1406 let where_clauses = vec![
1407 WhereClause {
1408 field: "recipient".to_string(),
1409 operator: WhereOperator::In,
1410 value: dpp::platform_value::Value::Array(vec![
1411 dpp::platform_value::Value::Bytes(recipient_a()),
1412 dpp::platform_value::Value::Bytes(recipient_b()),
1413 ]),
1414 },
1415 WhereClause {
1416 field: "sentAt".to_string(),
1417 operator: WhereOperator::GreaterThan,
1418 value: dpp::platform_value::Value::U64(0),
1419 },
1420 ];
1421 let q = DriveDocumentSumQuery {
1422 document_type: doc_type,
1423 contract_id: contract.id().to_buffer(),
1424 document_type_name: doc_type.name().clone(),
1425 index,
1426 where_clauses,
1427 sum_property: "amount".to_string(),
1428 };
1429
1430 let pq = q
1431 .carrier_aggregate_sum_path_query(Some(7), true, platform_version)
1432 .expect("carrier-aggregate sum path query builds with limit");
1433 assert_eq!(pq.query.limit, Some(7), "outer SizedQuery::limit threads");
1434 }
1435
1436 #[test]
1438 fn carrier_aggregate_sum_rejects_missing_terminator_range() {
1439 let platform_version = PlatformVersion::latest();
1440 let contract = load_tip_jar_contract(platform_version);
1441 let doc_type = pick_doc_type(&contract, "tip");
1442 let index = doc_type
1443 .indexes()
1444 .get("byRecipientTime")
1445 .expect("byRecipientTime index exists on tip doc type");
1446
1447 let where_clauses = vec![WhereClause {
1448 field: "recipient".to_string(),
1449 operator: WhereOperator::In,
1450 value: dpp::platform_value::Value::Array(vec![dpp::platform_value::Value::Bytes(
1451 recipient_a(),
1452 )]),
1453 }];
1454 let q = DriveDocumentSumQuery {
1455 document_type: doc_type,
1456 contract_id: contract.id().to_buffer(),
1457 document_type_name: doc_type.name().clone(),
1458 index,
1459 where_clauses,
1460 sum_property: "amount".to_string(),
1461 };
1462
1463 let err = q
1464 .carrier_aggregate_sum_path_query(None, true, platform_version)
1465 .expect_err("missing range clause must be rejected");
1466 let msg = format!("{err:?}");
1467 assert!(
1468 msg.contains("requires a range where-clause"),
1469 "unexpected error: {msg}"
1470 );
1471 }
1472
1473 #[test]
1476 fn carrier_aggregate_sum_rejects_missing_carrier() {
1477 let platform_version = PlatformVersion::latest();
1478 let contract = load_tip_jar_contract(platform_version);
1479 let doc_type = pick_doc_type(&contract, "tip");
1480 let index = doc_type
1481 .indexes()
1482 .get("byRecipientTime")
1483 .expect("byRecipientTime index exists on tip doc type");
1484
1485 let where_clauses = vec![
1490 WhereClause {
1491 field: "recipient".to_string(),
1492 operator: WhereOperator::Equal,
1493 value: dpp::platform_value::Value::Bytes(recipient_a()),
1494 },
1495 WhereClause {
1496 field: "sentAt".to_string(),
1497 operator: WhereOperator::GreaterThan,
1498 value: dpp::platform_value::Value::U64(0),
1499 },
1500 ];
1501 let q = DriveDocumentSumQuery {
1502 document_type: doc_type,
1503 contract_id: contract.id().to_buffer(),
1504 document_type_name: doc_type.name().clone(),
1505 index,
1506 where_clauses,
1507 sum_property: "amount".to_string(),
1508 };
1509
1510 let err = q
1511 .carrier_aggregate_sum_path_query(None, true, platform_version)
1512 .expect_err("Equal-only prefix must be rejected by carrier builder");
1513 let msg = format!("{err:?}");
1514 assert!(msg.contains("carrier dimension"), "unexpected error: {msg}");
1515 }
1516}