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::ResolvedTimeRange;
26use crate::query::{WhereClause, WhereOperator};
27use dpp::data_contract::document_type::methods::DocumentTypeV0Methods;
33use dpp::data_contract::document_type::DocumentTypeRef;
34use dpp::data_contract::DataContract;
35use dpp::version::PlatformVersion;
36use grovedb::{PathQuery, Query, QueryItem, SizedQuery};
37
38const SUM_TREE_KEY: u8 = 0;
42
43#[cfg(any(feature = "server", feature = "verify"))]
44impl<'a> DriveDocumentSumQuery<'a> {
45 pub fn primary_key_sum_path_query(
53 contract_id: [u8; 32],
54 document_type_name: &str,
55 ) -> PathQuery {
56 let path = vec![
57 vec![RootTree::DataContractDocuments as u8],
58 contract_id.to_vec(),
59 vec![1u8],
60 document_type_name.as_bytes().to_vec(),
61 ];
62 let mut query = Query::new();
63 query.insert_key(vec![SUM_TREE_KEY]);
64 PathQuery::new(path, SizedQuery::new(query, None, None))
65 }
66
67 pub fn point_lookup_sum_path_query(
72 &self,
73 platform_version: &PlatformVersion,
74 ) -> Result<PathQuery, Error> {
75 if self.index.properties.is_empty() {
76 return Err(Error::Query(
77 QuerySyntaxError::InvalidWhereClauseComponents(
78 "point_lookup_sum_path_query: index must have at least one property",
79 ),
80 ));
81 }
82
83 let mut base_path: Vec<Vec<u8>> = vec![
84 vec![RootTree::DataContractDocuments as u8],
85 self.contract_id.to_vec(),
86 vec![1u8],
87 self.document_type_name.as_bytes().to_vec(),
88 ];
89
90 let mut in_outer_keys: Option<Vec<Vec<u8>>> = None;
91 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
92
93 for prop in self.index.properties.iter() {
94 let clause = self
95 .where_clauses
96 .iter()
97 .find(|wc| wc.field == prop.name)
98 .ok_or_else(|| {
99 Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
100 "prove sum requires the where clauses to fully cover the \
101 summable index; one or more index properties have no matching \
102 `==` or `in` clause — define a more specific summable index \
103 (with `summable: \"<prop>\"` whose properties exactly equal \
104 the clauses) or use `prove=false`",
105 ))
106 })?;
107
108 match clause.operator {
109 WhereOperator::Equal => {
110 let serialized = self.document_type.serialize_value_for_key(
111 prop.name.as_str(),
112 &clause.value,
113 platform_version,
114 )?;
115 if in_outer_keys.is_some() {
116 subquery_path_extension
117 .push(self.index.level_key_for_property(&prop.name).into_bytes());
118 subquery_path_extension.push(serialized);
119 } else {
120 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
121 base_path.push(serialized);
122 }
123 }
124 WhereOperator::In => {
125 if in_outer_keys.is_some() {
126 return Err(Error::Query(
127 QuerySyntaxError::InvalidWhereClauseComponents(
128 "prove sum: at most one `in` clause is supported on the \
129 covering summable index",
130 ),
131 ));
132 }
133 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
134 let in_values = clause.in_values().into_data_with_error()??;
135 let mut keys: Vec<Vec<u8>> = in_values
136 .iter()
137 .map(|v| {
138 self.document_type.serialize_value_for_key(
139 prop.name.as_str(),
140 v,
141 platform_version,
142 )
143 })
144 .collect::<Result<_, _>>()?;
145 keys.sort();
146 in_outer_keys = Some(keys);
147 }
148 _ => {
149 return Err(Error::Query(
150 QuerySyntaxError::InvalidWhereClauseComponents(
151 "point_lookup_sum_path_query: index properties must use \
152 `==` or `in`",
153 ),
154 ));
155 }
156 }
157 }
158
159 let sum_tree_terminator = self.index.summable.is_some();
167
168 match in_outer_keys {
169 None => {
170 let mut query = Query::new();
172 if sum_tree_terminator {
173 let last_value = base_path.pop().expect(
178 "Equal-only loop pushes (name, value) per prop; \
179 base_path must hold the terminator's serialized value",
180 );
181 query.insert_key(last_value);
182 } else {
183 query.insert_key(vec![SUM_TREE_KEY]);
184 }
185 Ok(PathQuery::new(
186 base_path,
187 SizedQuery::new(query, None, None),
188 ))
189 }
190 Some(keys) => {
191 let mut outer_query = Query::new();
193 for key in keys {
194 outer_query.insert_key(key);
195 }
196
197 if subquery_path_extension.is_empty() {
198 if sum_tree_terminator {
199 } else {
202 let mut subquery = Query::new();
203 subquery.insert_key(vec![SUM_TREE_KEY]);
204 outer_query.set_subquery(subquery);
205 }
206 } else {
207 let mut subquery = Query::new();
208 if sum_tree_terminator {
209 let termval = subquery_path_extension.pop().expect(
210 "trailing-Equal loop pushes (name, value) pairs; \
211 non-empty extension's tail must be the terminator's \
212 serialized value",
213 );
214 subquery.insert_key(termval);
215 } else {
216 subquery.insert_key(vec![SUM_TREE_KEY]);
217 }
218 outer_query.set_subquery_path(subquery_path_extension);
219 outer_query.set_subquery(subquery);
220 }
221
222 Ok(PathQuery::new(
223 base_path,
224 SizedQuery::new(outer_query, None, None),
225 ))
226 }
227 }
228 }
229
230 pub fn aggregate_sum_path_query(
236 &self,
237 platform_version: &PlatformVersion,
238 ) -> Result<PathQuery, Error> {
239 let terminator_prop_name = &self
244 .index
245 .properties
246 .last()
247 .ok_or(Error::Query(
248 QuerySyntaxError::InvalidWhereClauseComponents(
249 "range_summable index must have at least one property",
250 ),
251 ))?
252 .name;
253 let range_clause = self
254 .where_clauses
255 .iter()
256 .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
257 .ok_or(Error::Query(
258 QuerySyntaxError::InvalidWhereClauseComponents(
259 "aggregate_sum_path_query requires a range where-clause on the index terminator property",
260 ),
261 ))?;
262 let query_item = self.range_clause_to_query_item(range_clause, platform_version)?;
263
264 let mut path = vec![
265 vec![RootTree::DataContractDocuments as u8],
266 self.contract_id.to_vec(),
267 vec![1u8],
268 self.document_type_name.as_bytes().to_vec(),
269 ];
270 let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
271 for prop in prefix_props {
272 let clause = self
273 .where_clauses
274 .iter()
275 .find(|wc| wc.field == prop.name)
276 .ok_or(Error::Query(
277 QuerySyntaxError::InvalidWhereClauseComponents(
278 "aggregate-sum proof: missing where clause for an index prefix property",
279 ),
280 ))?;
281 if clause.operator != WhereOperator::Equal {
282 return Err(Error::Query(
283 QuerySyntaxError::InvalidWhereClauseComponents(
284 "aggregate-sum proof: prefix properties must use `==` (no `in`); use \
285 `group_by = [in_field, range_field]` (carrier-aggregate variant) for \
286 compound In-on-prefix sum queries",
287 ),
288 ));
289 }
290 path.push(self.index.level_key_for_property(&prop.name).into_bytes());
291 path.push(self.document_type.serialize_value_for_key(
292 prop.name.as_str(),
293 &clause.value,
294 platform_version,
295 )?);
296 }
297 let range_prop_name = &self
298 .index
299 .properties
300 .last()
301 .ok_or(Error::Query(
302 QuerySyntaxError::InvalidWhereClauseComponents(
303 "range_summable index must have at least one property",
304 ),
305 ))?
306 .name;
307 path.push(
308 self.index
309 .level_key_for_property(range_prop_name)
310 .into_bytes(),
311 );
312
313 let query = Query::new_aggregate_sum_on_range(query_item);
315 Ok(PathQuery::new(path, SizedQuery::new(query, None, None)))
316 }
317
318 pub fn aggregate_count_and_sum_path_query(
323 &self,
324 platform_version: &PlatformVersion,
325 ) -> Result<PathQuery, Error> {
326 if !self.index.range_countable {
327 return Err(Error::Query(QuerySyntaxError::Unsupported(
328 "aggregate_count_and_sum_path_query: index must declare BOTH \
329 `rangeCountable: true` AND `rangeSummable: true` to produce a PCPS \
330 (ProvableCountProvableSumTree) property-name tree."
331 .to_string(),
332 )));
333 }
334
335 let terminator_prop_name = &self
338 .index
339 .properties
340 .last()
341 .ok_or(Error::Query(
342 QuerySyntaxError::InvalidWhereClauseComponents(
343 "PCPS index must have at least one property",
344 ),
345 ))?
346 .name;
347 let range_clause = self
348 .where_clauses
349 .iter()
350 .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
351 .ok_or(Error::Query(
352 QuerySyntaxError::InvalidWhereClauseComponents(
353 "aggregate_count_and_sum_path_query requires a range where-clause on the index terminator property",
354 ),
355 ))?;
356 let query_item = self.range_clause_to_query_item(range_clause, platform_version)?;
357
358 let mut path = vec![
359 vec![RootTree::DataContractDocuments as u8],
360 self.contract_id.to_vec(),
361 vec![1u8],
362 self.document_type_name.as_bytes().to_vec(),
363 ];
364 let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
365 for prop in prefix_props {
366 let clause = self
367 .where_clauses
368 .iter()
369 .find(|wc| wc.field == prop.name)
370 .ok_or(Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
371 "aggregate-count-and-sum proof: missing where clause for an index prefix property",
372 )))?;
373 if clause.operator != WhereOperator::Equal {
374 return Err(Error::Query(
375 QuerySyntaxError::InvalidWhereClauseComponents(
376 "aggregate-count-and-sum proof: prefix properties must use `==` (no `in`)",
377 ),
378 ));
379 }
380 path.push(self.index.level_key_for_property(&prop.name).into_bytes());
381 path.push(self.document_type.serialize_value_for_key(
382 prop.name.as_str(),
383 &clause.value,
384 platform_version,
385 )?);
386 }
387 let range_prop_name = &self
388 .index
389 .properties
390 .last()
391 .ok_or(Error::Query(
392 QuerySyntaxError::InvalidWhereClauseComponents(
393 "range_countable + range_summable index must have at least one property",
394 ),
395 ))?
396 .name;
397 path.push(
398 self.index
399 .level_key_for_property(range_prop_name)
400 .into_bytes(),
401 );
402
403 let query = grovedb::Query::new_aggregate_count_and_sum_on_range(query_item);
404 Ok(PathQuery::new(
405 path,
406 grovedb::SizedQuery::new(query, None, None),
407 ))
408 }
409
410 fn range_clause_to_query_item(
420 &self,
421 clause: &WhereClause,
422 platform_version: &PlatformVersion,
423 ) -> Result<QueryItem, Error> {
424 let serialize = |v: &dpp::platform_value::Value| -> Result<Vec<u8>, Error> {
425 Ok(self.document_type.serialize_value_for_key(
426 clause.field.as_str(),
427 v,
428 platform_version,
429 )?)
430 };
431 let serialize_pair = || -> Result<(Vec<u8>, Vec<u8>), Error> {
432 let arr = clause.value.as_array().ok_or_else(|| {
433 Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
434 "range bounds value must be a 2-element array",
435 ))
436 })?;
437 if arr.len() != 2 {
438 return Err(Error::Query(
439 QuerySyntaxError::InvalidWhereClauseComponents(
440 "range bounds value must be a 2-element array",
441 ),
442 ));
443 }
444 let a = serialize(&arr[0])?;
445 let b = serialize(&arr[1])?;
446 if a > b {
447 return Err(Error::Query(
448 QuerySyntaxError::InvalidWhereClauseComponents(
449 "range lower bound must be <= upper bound",
450 ),
451 ));
452 }
453 Ok((a, b))
454 };
455
456 Ok(match clause.operator {
457 WhereOperator::GreaterThan => {
458 let v = serialize(&clause.value)?;
459 QueryItem::RangeAfter(v..)
460 }
461 WhereOperator::GreaterThanOrEquals => {
462 let v = serialize(&clause.value)?;
463 QueryItem::RangeFrom(v..)
464 }
465 WhereOperator::LessThan => {
466 let v = serialize(&clause.value)?;
467 QueryItem::RangeTo(..v)
468 }
469 WhereOperator::LessThanOrEquals => {
470 let v = serialize(&clause.value)?;
471 QueryItem::RangeToInclusive(..=v)
472 }
473 WhereOperator::Between => {
474 let (a, b) = serialize_pair()?;
475 QueryItem::RangeInclusive(a..=b)
476 }
477 WhereOperator::BetweenExcludeBounds => {
478 let (a, b) = serialize_pair()?;
479 QueryItem::RangeAfterTo(a..b)
480 }
481 WhereOperator::BetweenExcludeLeft => {
482 let (a, b) = serialize_pair()?;
483 QueryItem::RangeAfterToInclusive(a..=b)
484 }
485 WhereOperator::BetweenExcludeRight => {
486 let (a, b) = serialize_pair()?;
487 QueryItem::Range(a..b)
488 }
489 WhereOperator::StartsWith => {
490 let left_key = serialize(&clause.value)?;
491 let mut right_key = left_key.clone();
492 if right_key.is_empty() {
493 return Err(Error::Query(QuerySyntaxError::InvalidStartsWithClause(
494 "startsWith prefix must have at least one byte",
495 )));
496 }
497 let mut i = right_key.len();
504 while i > 0 && right_key[i - 1] == 0xFF {
505 i -= 1;
506 }
507 if i == 0 {
508 return Err(Error::Query(QuerySyntaxError::InvalidStartsWithClause(
509 "startsWith prefix is all 0xFF bytes; cannot form half-open upper bound",
510 )));
511 }
512 right_key.truncate(i);
513 *right_key
514 .last_mut()
515 .expect("non-empty after truncate to non-zero length") += 1;
516 QueryItem::Range(left_key..right_key)
517 }
518 _ => {
519 return Err(Error::Query(
520 QuerySyntaxError::InvalidWhereClauseComponents(
521 "range_clause_to_query_item called on a non-range operator",
522 ),
523 ));
524 }
525 })
526 }
527
528 pub fn distinct_sum_path_query(
551 &self,
552 limit: Option<u16>,
553 left_to_right: bool,
554 platform_version: &PlatformVersion,
555 ) -> Result<PathQuery, Error> {
556 let range_clause = self
557 .where_clauses
558 .iter()
559 .find(|wc| is_range_operator(wc.operator))
560 .ok_or(Error::Query(
561 QuerySyntaxError::InvalidWhereClauseComponents(
562 "distinct_sum_path_query requires a range where-clause",
563 ),
564 ))?;
565 let range_item = self.range_clause_to_query_item(range_clause, platform_version)?;
566
567 let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
568 let terminator_name = &self
569 .index
570 .properties
571 .last()
572 .ok_or(Error::Query(
573 QuerySyntaxError::InvalidWhereClauseComponents(
574 "range_summable index must have at least one property",
575 ),
576 ))?
577 .name;
578
579 let mut base_path: Vec<Vec<u8>> = vec![
580 vec![RootTree::DataContractDocuments as u8],
581 self.contract_id.to_vec(),
582 vec![1u8],
583 self.document_type_name.as_bytes().to_vec(),
584 ];
585
586 let mut in_outer_keys: Option<Vec<Vec<u8>>> = None;
593 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
594
595 for prop in prefix_props {
596 let clause = self
597 .where_clauses
598 .iter()
599 .find(|wc| wc.field == prop.name)
600 .ok_or(Error::Query(
601 QuerySyntaxError::InvalidWhereClauseComponents(
602 "distinct_sum_path_query: missing where clause for an index \
603 prefix property",
604 ),
605 ))?;
606
607 match clause.operator {
608 WhereOperator::Equal => {
609 let serialized = self.document_type.serialize_value_for_key(
610 prop.name.as_str(),
611 &clause.value,
612 platform_version,
613 )?;
614 if in_outer_keys.is_some() {
615 subquery_path_extension
616 .push(self.index.level_key_for_property(&prop.name).into_bytes());
617 subquery_path_extension.push(serialized);
618 } else {
619 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
620 base_path.push(serialized);
621 }
622 }
623 WhereOperator::In => {
624 if in_outer_keys.is_some() {
625 return Err(Error::Query(
626 QuerySyntaxError::InvalidWhereClauseComponents(
627 "distinct_sum_path_query: at most one `In` clause is supported \
628 on prefix properties",
629 ),
630 ));
631 }
632 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
635 let in_values = clause.in_values().into_data_with_error()??;
636 let mut keys: Vec<Vec<u8>> = in_values
637 .iter()
638 .map(|v| {
639 self.document_type.serialize_value_for_key(
640 prop.name.as_str(),
641 v,
642 platform_version,
643 )
644 })
645 .collect::<Result<_, _>>()?;
646 keys.sort();
653 in_outer_keys = Some(keys);
654 }
655 _ => {
656 return Err(Error::Query(
657 QuerySyntaxError::InvalidWhereClauseComponents(
658 "distinct_sum_path_query: prefix properties must use `==` or `in`",
659 ),
660 ));
661 }
662 }
663 }
664
665 match in_outer_keys {
666 None => {
667 base_path.push(terminator_name.as_bytes().to_vec());
670 let mut query = Query::new_with_direction(left_to_right);
671 query.insert_item(range_item);
672 Ok(PathQuery::new(
673 base_path,
674 SizedQuery::new(query, limit, None),
675 ))
676 }
677 Some(keys) => {
678 let mut outer_query = Query::new_with_direction(left_to_right);
686 for key in keys {
687 outer_query.insert_key(key);
688 }
689 subquery_path_extension.push(terminator_name.as_bytes().to_vec());
690
691 let mut subquery = Query::new_with_direction(left_to_right);
692 subquery.insert_item(range_item);
693
694 outer_query.set_subquery_path(subquery_path_extension);
695 outer_query.set_subquery(subquery);
696
697 Ok(PathQuery::new(
698 base_path,
699 SizedQuery::new(outer_query, limit, None),
700 ))
701 }
702 }
703 }
704
705 pub fn carrier_aggregate_sum_path_query(
754 &self,
755 limit: Option<u16>,
756 left_to_right: bool,
757 platform_version: &PlatformVersion,
758 ) -> Result<PathQuery, Error> {
759 let terminator_prop_name = &self
772 .index
773 .properties
774 .last()
775 .ok_or(Error::Query(
776 QuerySyntaxError::InvalidWhereClauseComponents(
777 "range_summable index must have at least one property",
778 ),
779 ))?
780 .name;
781 let terminator_clause = self
782 .where_clauses
783 .iter()
784 .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
785 .ok_or(Error::Query(
786 QuerySyntaxError::InvalidWhereClauseComponents(
787 "carrier_aggregate_sum_path_query requires a range where-clause on the \
788 terminator property of the chosen index",
789 ),
790 ))?;
791 let inner_range_item =
792 self.range_clause_to_query_item(terminator_clause, platform_version)?;
793
794 let mut base_path: Vec<Vec<u8>> = vec![
795 vec![RootTree::DataContractDocuments as u8],
796 self.contract_id.to_vec(),
797 vec![1u8],
798 self.document_type_name.as_bytes().to_vec(),
799 ];
800 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
801
802 enum Carrier {
807 Pending,
808 In(WhereClause),
809 Range(WhereClause),
810 }
811 let mut carrier = Carrier::Pending;
812 let prefix_and_carrier_props = &self.index.properties[..self.index.properties.len() - 1];
813
814 for prop in prefix_and_carrier_props {
815 let clause = self
816 .where_clauses
817 .iter()
818 .find(|wc| wc.field == prop.name)
819 .ok_or(Error::Query(
820 QuerySyntaxError::InvalidWhereClauseComponents(
821 "carrier-aggregate sum proof: missing where clause for an index prefix \
822 property",
823 ),
824 ))?;
825 match (&carrier, clause.operator) {
826 (Carrier::Pending, WhereOperator::Equal) => {
827 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
828 base_path.push(self.document_type.serialize_value_for_key(
829 prop.name.as_str(),
830 &clause.value,
831 platform_version,
832 )?);
833 }
834 (Carrier::Pending, WhereOperator::In) => {
835 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
836 carrier = Carrier::In(clause.clone());
837 }
838 (Carrier::Pending, op) if is_range_operator(op) => {
839 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
840 carrier = Carrier::Range(clause.clone());
841 }
842 (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => {
843 subquery_path_extension
844 .push(self.index.level_key_for_property(&prop.name).into_bytes());
845 subquery_path_extension.push(self.document_type.serialize_value_for_key(
846 prop.name.as_str(),
847 &clause.value,
848 platform_version,
849 )?);
850 }
851 (Carrier::In(_) | Carrier::Range(_), _) => {
852 return Err(Error::Query(
853 QuerySyntaxError::InvalidWhereClauseComponents(
854 "carrier-aggregate sum proof: at most one carrier clause (In or \
855 range) is supported on prefix properties; subsequent prefix \
856 clauses must use `==`",
857 ),
858 ));
859 }
860 _ => {
861 return Err(Error::Query(
862 QuerySyntaxError::InvalidWhereClauseComponents(
863 "carrier-aggregate sum proof: prefix property operator unsupported",
864 ),
865 ));
866 }
867 }
868 }
869 subquery_path_extension.push(
870 self.index
871 .level_key_for_property(terminator_prop_name)
872 .into_bytes(),
873 );
874
875 let mut outer_query = Query::new_with_direction(left_to_right);
876 match carrier {
877 Carrier::Pending => {
878 return Err(Error::Query(
879 QuerySyntaxError::InvalidWhereClauseComponents(
880 "carrier-aggregate sum proof: an In or range clause must appear on a \
881 prefix property of the chosen index to act as the carrier dimension",
882 ),
883 ));
884 }
885 Carrier::In(in_clause) => {
886 let in_values = in_clause.in_values().into_data_with_error()??;
891 let mut serialized_in_keys: Vec<Vec<u8>> = in_values
892 .iter()
893 .map(|v| {
894 self.document_type.serialize_value_for_key(
895 in_clause.field.as_str(),
896 v,
897 platform_version,
898 )
899 })
900 .collect::<Result<_, _>>()?;
901 serialized_in_keys.sort();
902 serialized_in_keys.dedup();
903 for key in serialized_in_keys {
904 outer_query.insert_key(key);
905 }
906 }
907 Carrier::Range(range_clause) => {
908 let outer_range_item =
912 self.range_clause_to_query_item(&range_clause, platform_version)?;
913 outer_query.items.push(outer_range_item);
914 }
915 }
916 outer_query.set_subquery_path(subquery_path_extension);
917 outer_query.set_subquery(Query::new_aggregate_sum_on_range(inner_range_item));
918
919 Ok(PathQuery::new(
926 base_path,
927 SizedQuery::new(outer_query, limit, None),
928 ))
929 }
930
931 pub fn carrier_aggregate_count_and_sum_path_query(
953 &self,
954 limit: Option<u16>,
955 left_to_right: bool,
956 platform_version: &PlatformVersion,
957 ) -> Result<PathQuery, Error> {
958 if !self.index.range_countable {
959 return Err(Error::Query(QuerySyntaxError::Unsupported(
960 "carrier_aggregate_count_and_sum_path_query: index must declare BOTH \
961 `rangeCountable: true` AND `rangeSummable: true` to produce a PCPS \
962 (ProvableCountProvableSumTree) property-name tree."
963 .to_string(),
964 )));
965 }
966
967 let terminator_prop_name = &self
968 .index
969 .properties
970 .last()
971 .ok_or(Error::Query(
972 QuerySyntaxError::InvalidWhereClauseComponents(
973 "range_countable + range_summable index must have at least one property",
974 ),
975 ))?
976 .name;
977 let terminator_clause = self
978 .where_clauses
979 .iter()
980 .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
981 .ok_or(Error::Query(
982 QuerySyntaxError::InvalidWhereClauseComponents(
983 "carrier_aggregate_count_and_sum_path_query requires a range where-clause \
984 on the terminator property of the chosen index",
985 ),
986 ))?;
987 let inner_range_item =
988 self.range_clause_to_query_item(terminator_clause, platform_version)?;
989
990 let mut base_path: Vec<Vec<u8>> = vec![
991 vec![RootTree::DataContractDocuments as u8],
992 self.contract_id.to_vec(),
993 vec![1u8],
994 self.document_type_name.as_bytes().to_vec(),
995 ];
996 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
997
998 enum Carrier {
1000 Pending,
1001 In(WhereClause),
1002 Range(WhereClause),
1003 }
1004 let mut carrier = Carrier::Pending;
1005 let prefix_and_carrier_props = &self.index.properties[..self.index.properties.len() - 1];
1006
1007 for prop in prefix_and_carrier_props {
1008 let clause = self
1009 .where_clauses
1010 .iter()
1011 .find(|wc| wc.field == prop.name)
1012 .ok_or(Error::Query(
1013 QuerySyntaxError::InvalidWhereClauseComponents(
1014 "carrier-aggregate count-and-sum proof: missing where clause for an index \
1015 prefix property",
1016 ),
1017 ))?;
1018 match (&carrier, clause.operator) {
1019 (Carrier::Pending, WhereOperator::Equal) => {
1020 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
1021 base_path.push(self.document_type.serialize_value_for_key(
1022 prop.name.as_str(),
1023 &clause.value,
1024 platform_version,
1025 )?);
1026 }
1027 (Carrier::Pending, WhereOperator::In) => {
1028 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
1029 carrier = Carrier::In(clause.clone());
1030 }
1031 (Carrier::Pending, op) if is_range_operator(op) => {
1032 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
1033 carrier = Carrier::Range(clause.clone());
1034 }
1035 (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => {
1036 subquery_path_extension
1037 .push(self.index.level_key_for_property(&prop.name).into_bytes());
1038 subquery_path_extension.push(self.document_type.serialize_value_for_key(
1039 prop.name.as_str(),
1040 &clause.value,
1041 platform_version,
1042 )?);
1043 }
1044 (Carrier::In(_) | Carrier::Range(_), _) => {
1045 return Err(Error::Query(
1046 QuerySyntaxError::InvalidWhereClauseComponents(
1047 "carrier-aggregate count-and-sum proof: at most one carrier clause \
1048 (In or range) is supported on prefix properties; subsequent prefix \
1049 clauses must use `==`",
1050 ),
1051 ));
1052 }
1053 _ => {
1054 return Err(Error::Query(
1055 QuerySyntaxError::InvalidWhereClauseComponents(
1056 "carrier-aggregate count-and-sum proof: prefix property operator \
1057 unsupported",
1058 ),
1059 ));
1060 }
1061 }
1062 }
1063 subquery_path_extension.push(
1064 self.index
1065 .level_key_for_property(terminator_prop_name)
1066 .into_bytes(),
1067 );
1068
1069 let mut outer_query = Query::new_with_direction(left_to_right);
1070 match carrier {
1071 Carrier::Pending => {
1072 return Err(Error::Query(
1073 QuerySyntaxError::InvalidWhereClauseComponents(
1074 "carrier-aggregate count-and-sum proof: an In or range clause must \
1075 appear on a prefix property of the chosen index to act as the carrier \
1076 dimension",
1077 ),
1078 ));
1079 }
1080 Carrier::In(in_clause) => {
1081 let in_values = in_clause.in_values().into_data_with_error()??;
1082 let mut serialized_in_keys: Vec<Vec<u8>> = in_values
1083 .iter()
1084 .map(|v| {
1085 self.document_type.serialize_value_for_key(
1086 in_clause.field.as_str(),
1087 v,
1088 platform_version,
1089 )
1090 })
1091 .collect::<Result<_, _>>()?;
1092 serialized_in_keys.sort();
1093 serialized_in_keys.dedup();
1094 for key in serialized_in_keys {
1095 outer_query.insert_key(key);
1096 }
1097 }
1098 Carrier::Range(range_clause) => {
1099 let outer_range_item =
1100 self.range_clause_to_query_item(&range_clause, platform_version)?;
1101 outer_query.items.push(outer_range_item);
1102 }
1103 }
1104 outer_query.set_subquery_path(subquery_path_extension);
1105 outer_query.set_subquery(grovedb::Query::new_aggregate_count_and_sum_on_range(
1106 inner_range_item,
1107 ));
1108
1109 Ok(PathQuery::new(
1110 base_path,
1111 SizedQuery::new(outer_query, limit, None),
1112 ))
1113 }
1114}
1115
1116#[cfg(any(feature = "server", feature = "verify"))]
1122impl<'a> DriveDocumentSumQuery<'a> {
1123 pub fn point_lookup_sum_path_query_static(
1127 contract: &DataContract,
1128 document_type: DocumentTypeRef,
1129 sum_property: &str,
1130 where_clauses: &[WhereClause],
1131 resolved_time_ranges: &[ResolvedTimeRange],
1132 platform_version: &PlatformVersion,
1133 ) -> Result<PathQuery, Error> {
1134 use crate::query::drive_document_sum_query::index_picker::find_summable_index_for_where_clauses;
1135 use dpp::data_contract::accessors::v0::DataContractV0Getters;
1136 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1137
1138 let index = find_summable_index_for_where_clauses(
1139 document_type.indexes(),
1140 where_clauses,
1141 sum_property,
1142 resolved_time_ranges,
1143 )
1144 .ok_or_else(|| {
1145 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
1146 "no `summable: \"<prop>\"` index exactly matches the where-clause fields. \
1147 Define a more specific summable index (with `summable: \"<prop>\"` whose \
1148 properties exactly equal the clauses) or use `prove=false`."
1149 .to_string(),
1150 ))
1151 })?;
1152 let q = DriveDocumentSumQuery {
1153 document_type,
1154 contract_id: contract.id().to_buffer(),
1155 document_type_name: document_type.name().clone(),
1156 index,
1157 where_clauses: where_clauses.to_vec(),
1158 sum_property: sum_property.to_string(),
1159 };
1160 q.point_lookup_sum_path_query(platform_version)
1161 }
1162
1163 pub fn aggregate_sum_path_query_static(
1167 contract: &DataContract,
1168 document_type: DocumentTypeRef,
1169 sum_property: &str,
1170 where_clauses: &[WhereClause],
1171 resolved_time_ranges: &[ResolvedTimeRange],
1172 platform_version: &PlatformVersion,
1173 ) -> Result<PathQuery, Error> {
1174 use crate::query::drive_document_sum_query::index_picker::find_range_summable_index_for_where_clauses;
1175 use dpp::data_contract::accessors::v0::DataContractV0Getters;
1176 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1177
1178 let index = find_range_summable_index_for_where_clauses(
1179 document_type.indexes(),
1180 where_clauses,
1181 sum_property,
1182 resolved_time_ranges,
1183 )
1184 .ok_or_else(|| {
1185 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
1186 "no `rangeSummable: true` index covers the where-clause shape (Equal/In \
1187 prefix exactly + range on the index's last property). Define one or use \
1188 `prove=false`."
1189 .to_string(),
1190 ))
1191 })?;
1192 let q = DriveDocumentSumQuery {
1193 document_type,
1194 contract_id: contract.id().to_buffer(),
1195 document_type_name: document_type.name().clone(),
1196 index,
1197 where_clauses: where_clauses.to_vec(),
1198 sum_property: sum_property.to_string(),
1199 };
1200 q.aggregate_sum_path_query(platform_version)
1201 }
1202
1203 #[allow(clippy::too_many_arguments)]
1212 pub fn carrier_aggregate_sum_path_query_static(
1213 contract: &DataContract,
1214 document_type: DocumentTypeRef,
1215 sum_property: &str,
1216 where_clauses: &[WhereClause],
1217 resolved_time_ranges: &[ResolvedTimeRange],
1218 limit: Option<u16>,
1219 left_to_right: bool,
1220 platform_version: &PlatformVersion,
1221 ) -> Result<PathQuery, Error> {
1222 use crate::query::drive_document_sum_query::index_picker::find_range_summable_index_for_where_clauses;
1223 use dpp::data_contract::accessors::v0::DataContractV0Getters;
1224 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1225
1226 let index = find_range_summable_index_for_where_clauses(
1227 document_type.indexes(),
1228 where_clauses,
1229 sum_property,
1230 resolved_time_ranges,
1231 )
1232 .ok_or_else(|| {
1233 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
1234 "no `rangeSummable: true` index covers the where-clause shape for the \
1235 carrier-aggregate sum carrier (Equal/In prefix + In-or-range carrier + \
1236 range on the index's last property). Define one or use `prove=false`."
1237 .to_string(),
1238 ))
1239 })?;
1240 let q = DriveDocumentSumQuery {
1241 document_type,
1242 contract_id: contract.id().to_buffer(),
1243 document_type_name: document_type.name().clone(),
1244 index,
1245 where_clauses: where_clauses.to_vec(),
1246 sum_property: sum_property.to_string(),
1247 };
1248 q.carrier_aggregate_sum_path_query(limit, left_to_right, platform_version)
1249 }
1250}
1251
1252#[cfg(test)]
1268mod carrier_path_query_tests {
1269 use super::*;
1270 use crate::query::WhereOperator;
1271 use assert_matches::assert_matches;
1272 use dpp::data_contract::accessors::v0::DataContractV0Getters;
1273 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1274 use dpp::data_contract::DataContract;
1275 use dpp::tests::json_document::json_document_to_contract;
1276 use grovedb::QueryItem;
1277
1278 fn load_tip_jar_contract(platform_version: &PlatformVersion) -> DataContract {
1279 json_document_to_contract(
1284 "tests/supporting_files/contract/tip-jar/tip-jar-contract.json",
1285 false,
1286 platform_version,
1287 )
1288 .expect("tip-jar contract fixture loads")
1289 }
1290
1291 fn pick_doc_type<'a>(
1303 contract: &'a DataContract,
1304 doc_type_name: &str,
1305 ) -> dpp::data_contract::document_type::DocumentTypeRef<'a> {
1306 contract
1307 .document_type_for_name(doc_type_name)
1308 .expect("document type exists in tip-jar fixture")
1309 }
1310
1311 fn recipient_a() -> Vec<u8> {
1315 let mut v = vec![0x80u8; 32];
1317 v[31] = 0x01;
1318 v
1319 }
1320 fn recipient_b() -> Vec<u8> {
1321 let mut v = vec![0x10u8; 32];
1323 v[31] = 0x02;
1324 v
1325 }
1326
1327 #[test]
1332 fn carrier_aggregate_sum_in_on_carrier_range_on_terminator() {
1333 let platform_version = PlatformVersion::latest();
1334 let contract = load_tip_jar_contract(platform_version);
1335 let doc_type = pick_doc_type(&contract, "tip");
1336 let index = doc_type
1337 .indexes()
1338 .get("byRecipientTime")
1339 .expect("byRecipientTime index exists on tip doc type");
1340
1341 let in_values = vec![
1346 dpp::platform_value::Value::Bytes(recipient_a()),
1347 dpp::platform_value::Value::Bytes(recipient_b()),
1348 ];
1349 let where_clauses = vec![
1350 WhereClause {
1351 field: "recipient".to_string(),
1352 operator: WhereOperator::In,
1353 value: dpp::platform_value::Value::Array(in_values.clone()),
1354 },
1355 WhereClause {
1356 field: "sentAt".to_string(),
1357 operator: WhereOperator::GreaterThan,
1358 value: dpp::platform_value::Value::U64(0),
1359 },
1360 ];
1361 let q = DriveDocumentSumQuery {
1362 document_type: doc_type,
1363 contract_id: contract.id().to_buffer(),
1364 document_type_name: doc_type.name().clone(),
1365 index,
1366 where_clauses,
1367 sum_property: "amount".to_string(),
1368 };
1369
1370 let pq = q
1371 .carrier_aggregate_sum_path_query(None, true, platform_version)
1372 .expect("carrier-aggregate sum path query builds");
1373
1374 assert!(
1378 pq.path.len() >= 5,
1379 "expected base_path to extend through the In-bearing prop's name subtree"
1380 );
1381 assert_eq!(
1382 pq.path.last().expect("base_path non-empty"),
1383 b"recipient",
1384 "outer path must stop at the In-bearing prop's property-name subtree"
1385 );
1386
1387 let outer_items = &pq.query.query.items;
1391 assert_eq!(outer_items.len(), 2, "one outer Key per In value");
1392 for item in outer_items {
1393 assert_matches!(item, QueryItem::Key(_));
1394 }
1395 if let (QueryItem::Key(a), QueryItem::Key(b)) = (&outer_items[0], &outer_items[1]) {
1396 assert!(a < b, "outer Keys must be sorted lex-ascending");
1397 }
1398
1399 let sub_path = pq
1401 .query
1402 .query
1403 .default_subquery_branch
1404 .subquery_path
1405 .as_ref()
1406 .expect("subquery_path set");
1407 assert_eq!(sub_path, &vec![b"sentAt".to_vec()]);
1408
1409 let subquery = pq
1411 .query
1412 .query
1413 .default_subquery_branch
1414 .subquery
1415 .as_ref()
1416 .expect("subquery set");
1417 assert_eq!(subquery.items.len(), 1);
1418 assert_matches!(subquery.items[0], QueryItem::AggregateSumOnRange(_));
1419 }
1420
1421 #[test]
1425 fn carrier_aggregate_sum_limit_flows_into_sized_query() {
1426 let platform_version = PlatformVersion::latest();
1427 let contract = load_tip_jar_contract(platform_version);
1428 let doc_type = pick_doc_type(&contract, "tip");
1429 let index = doc_type
1430 .indexes()
1431 .get("byRecipientTime")
1432 .expect("byRecipientTime index exists on tip doc type");
1433
1434 let where_clauses = vec![
1435 WhereClause {
1436 field: "recipient".to_string(),
1437 operator: WhereOperator::In,
1438 value: dpp::platform_value::Value::Array(vec![
1439 dpp::platform_value::Value::Bytes(recipient_a()),
1440 dpp::platform_value::Value::Bytes(recipient_b()),
1441 ]),
1442 },
1443 WhereClause {
1444 field: "sentAt".to_string(),
1445 operator: WhereOperator::GreaterThan,
1446 value: dpp::platform_value::Value::U64(0),
1447 },
1448 ];
1449 let q = DriveDocumentSumQuery {
1450 document_type: doc_type,
1451 contract_id: contract.id().to_buffer(),
1452 document_type_name: doc_type.name().clone(),
1453 index,
1454 where_clauses,
1455 sum_property: "amount".to_string(),
1456 };
1457
1458 let pq = q
1459 .carrier_aggregate_sum_path_query(Some(7), true, platform_version)
1460 .expect("carrier-aggregate sum path query builds with limit");
1461 assert_eq!(pq.query.limit, Some(7), "outer SizedQuery::limit threads");
1462 }
1463
1464 #[test]
1466 fn carrier_aggregate_sum_rejects_missing_terminator_range() {
1467 let platform_version = PlatformVersion::latest();
1468 let contract = load_tip_jar_contract(platform_version);
1469 let doc_type = pick_doc_type(&contract, "tip");
1470 let index = doc_type
1471 .indexes()
1472 .get("byRecipientTime")
1473 .expect("byRecipientTime index exists on tip doc type");
1474
1475 let where_clauses = vec![WhereClause {
1476 field: "recipient".to_string(),
1477 operator: WhereOperator::In,
1478 value: dpp::platform_value::Value::Array(vec![dpp::platform_value::Value::Bytes(
1479 recipient_a(),
1480 )]),
1481 }];
1482 let q = DriveDocumentSumQuery {
1483 document_type: doc_type,
1484 contract_id: contract.id().to_buffer(),
1485 document_type_name: doc_type.name().clone(),
1486 index,
1487 where_clauses,
1488 sum_property: "amount".to_string(),
1489 };
1490
1491 let err = q
1492 .carrier_aggregate_sum_path_query(None, true, platform_version)
1493 .expect_err("missing range clause must be rejected");
1494 let msg = format!("{err:?}");
1495 assert!(
1496 msg.contains("requires a range where-clause"),
1497 "unexpected error: {msg}"
1498 );
1499 }
1500
1501 #[test]
1504 fn carrier_aggregate_sum_rejects_missing_carrier() {
1505 let platform_version = PlatformVersion::latest();
1506 let contract = load_tip_jar_contract(platform_version);
1507 let doc_type = pick_doc_type(&contract, "tip");
1508 let index = doc_type
1509 .indexes()
1510 .get("byRecipientTime")
1511 .expect("byRecipientTime index exists on tip doc type");
1512
1513 let where_clauses = vec![
1518 WhereClause {
1519 field: "recipient".to_string(),
1520 operator: WhereOperator::Equal,
1521 value: dpp::platform_value::Value::Bytes(recipient_a()),
1522 },
1523 WhereClause {
1524 field: "sentAt".to_string(),
1525 operator: WhereOperator::GreaterThan,
1526 value: dpp::platform_value::Value::U64(0),
1527 },
1528 ];
1529 let q = DriveDocumentSumQuery {
1530 document_type: doc_type,
1531 contract_id: contract.id().to_buffer(),
1532 document_type_name: doc_type.name().clone(),
1533 index,
1534 where_clauses,
1535 sum_property: "amount".to_string(),
1536 };
1537
1538 let err = q
1539 .carrier_aggregate_sum_path_query(None, true, platform_version)
1540 .expect_err("Equal-only prefix must be rejected by carrier builder");
1541 let msg = format!("{err:?}");
1542 assert!(msg.contains("carrier dimension"), "unexpected error: {msg}");
1543 }
1544}