1use crate::drive::RootTree;
22use crate::error::query::QuerySyntaxError;
23use crate::error::Error;
24use crate::query::drive_document_sum_query::index_picker::prefix_to_last_sum_reads;
25use crate::query::drive_document_sum_query::{is_range_operator, DriveDocumentSumQuery};
26use crate::query::ResolvedTimeRange;
27use crate::query::{
28 pins_reach_chain, prefix_to_last_path_query, refuse_a_range_total_through_a_ranked_index,
29};
30use crate::query::{WhereClause, WhereOperator};
31use dpp::data_contract::document_type::methods::DocumentTypeV0Methods;
37use dpp::data_contract::document_type::DocumentTypeRef;
38use dpp::data_contract::DataContract;
39use dpp::version::PlatformVersion;
40use grovedb::{PathQuery, Query, QueryItem, SizedQuery};
41
42const SUM_TREE_KEY: u8 = 0;
46
47#[cfg(any(feature = "server", feature = "verify"))]
48impl<'a> DriveDocumentSumQuery<'a> {
49 pub fn primary_key_sum_path_query(
57 contract_id: [u8; 32],
58 document_type_name: &str,
59 ) -> PathQuery {
60 let path = vec![
61 vec![RootTree::DataContractDocuments as u8],
62 contract_id.to_vec(),
63 vec![1u8],
64 document_type_name.as_bytes().to_vec(),
65 ];
66 let mut query = Query::new();
67 query.insert_key(vec![SUM_TREE_KEY]);
68 PathQuery::new(path, SizedQuery::new(query, None, None))
69 }
70
71 pub fn point_lookup_sum_path_query(
76 &self,
77 platform_version: &PlatformVersion,
78 ) -> Result<PathQuery, Error> {
79 if self.index.properties.is_empty() {
80 return Err(Error::Query(
81 QuerySyntaxError::InvalidWhereClauseComponents(
82 "point_lookup_sum_path_query: index must have at least one property",
83 ),
84 ));
85 }
86
87 let mut base_path: Vec<Vec<u8>> = vec![
88 vec![RootTree::DataContractDocuments as u8],
89 self.contract_id.to_vec(),
90 vec![1u8],
91 self.document_type_name.as_bytes().to_vec(),
92 ];
93
94 let mut in_outer_keys: Option<Vec<Vec<u8>>> = None;
95 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
96
97 for (position, prop) in self.index.properties.iter().enumerate() {
98 let Some(clause) = self.where_clauses.iter().find(|wc| wc.field == prop.name) else {
99 if prefix_to_last_sum_reads(self.index, position) {
106 return Ok(prefix_to_last_path_query(
107 base_path,
108 in_outer_keys,
109 subquery_path_extension,
110 self.index.level_key_for_property(&prop.name).into_bytes(),
111 ));
112 }
113 let deepest_pin_is_sum_bearing = pins_reach_chain(
120 self.index,
121 position,
122 self.index.shallowest_sum_chain_position(),
123 );
124 let gapped = self.index.properties[position..]
125 .iter()
126 .any(|deeper| self.where_clauses.iter().any(|wc| wc.field == deeper.name));
127 if deepest_pin_is_sum_bearing && !gapped {
128 break;
129 }
130 return Err(Error::Query(
131 QuerySyntaxError::InvalidWhereClauseComponents(
132 "prove sum requires the where clauses to fully cover the \
133 summable index; one or more index properties have no matching \
134 `==` or `in` clause — define a more specific summable index \
135 (with `summable: \"<prop>\"` whose properties exactly equal \
136 the clauses) or use `prove=false`",
137 ),
138 ));
139 };
140
141 match clause.operator {
142 WhereOperator::Equal => {
143 let serialized = self.document_type.serialize_value_for_key(
144 prop.name.as_str(),
145 &clause.value,
146 platform_version,
147 )?;
148 if in_outer_keys.is_some() {
149 subquery_path_extension
150 .push(self.index.level_key_for_property(&prop.name).into_bytes());
151 subquery_path_extension.push(serialized);
152 } else {
153 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
154 base_path.push(serialized);
155 }
156 }
157 WhereOperator::In => {
158 if in_outer_keys.is_some() {
159 return Err(Error::Query(
160 QuerySyntaxError::InvalidWhereClauseComponents(
161 "prove sum: at most one `in` clause is supported on the \
162 covering summable index",
163 ),
164 ));
165 }
166 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
167 let in_values = clause.in_values().into_data_with_error()??;
168 let mut keys: Vec<Vec<u8>> = in_values
169 .iter()
170 .map(|v| {
171 self.document_type.serialize_value_for_key(
172 prop.name.as_str(),
173 v,
174 platform_version,
175 )
176 })
177 .collect::<Result<_, _>>()?;
178 keys.sort();
179 in_outer_keys = Some(keys);
180 }
181 _ => {
182 return Err(Error::Query(
183 QuerySyntaxError::InvalidWhereClauseComponents(
184 "point_lookup_sum_path_query: index properties must use \
185 `==` or `in`",
186 ),
187 ));
188 }
189 }
190 }
191
192 let sum_tree_terminator = self.index.summed_value_name().is_some();
201
202 match in_outer_keys {
203 None => {
204 let mut query = Query::new();
206 if sum_tree_terminator {
207 let last_value = base_path.pop().expect(
212 "Equal-only loop pushes (name, value) per prop; \
213 base_path must hold the terminator's serialized value",
214 );
215 query.insert_key(last_value);
216 } else {
217 query.insert_key(vec![SUM_TREE_KEY]);
218 }
219 Ok(PathQuery::new(
220 base_path,
221 SizedQuery::new(query, None, None),
222 ))
223 }
224 Some(keys) => {
225 let mut outer_query = Query::new();
227 for key in keys {
228 outer_query.insert_key(key);
229 }
230
231 if subquery_path_extension.is_empty() {
232 if sum_tree_terminator {
233 } else {
236 let mut subquery = Query::new();
237 subquery.insert_key(vec![SUM_TREE_KEY]);
238 outer_query.set_subquery(subquery);
239 }
240 } else {
241 let mut subquery = Query::new();
242 if sum_tree_terminator {
243 let termval = subquery_path_extension.pop().expect(
244 "trailing-Equal loop pushes (name, value) pairs; \
245 non-empty extension's tail must be the terminator's \
246 serialized value",
247 );
248 subquery.insert_key(termval);
249 } else {
250 subquery.insert_key(vec![SUM_TREE_KEY]);
251 }
252 outer_query.set_subquery_path(subquery_path_extension);
253 outer_query.set_subquery(subquery);
254 }
255
256 Ok(PathQuery::new(
257 base_path,
258 SizedQuery::new(outer_query, None, None),
259 ))
260 }
261 }
262 }
263
264 pub fn aggregate_sum_path_query(
270 &self,
271 platform_version: &PlatformVersion,
272 ) -> Result<PathQuery, Error> {
273 self.refuse_a_range_sum_total()?;
274 let terminator_prop_name = &self
279 .index
280 .properties
281 .last()
282 .ok_or(Error::Query(
283 QuerySyntaxError::InvalidWhereClauseComponents(
284 "range_summable index must have at least one property",
285 ),
286 ))?
287 .name;
288 let range_clause = self
289 .where_clauses
290 .iter()
291 .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
292 .ok_or(Error::Query(
293 QuerySyntaxError::InvalidWhereClauseComponents(
294 "aggregate_sum_path_query requires a range where-clause on the index terminator property",
295 ),
296 ))?;
297 let query_item = self.range_clause_to_query_item(range_clause, platform_version)?;
298
299 let mut path = vec![
300 vec![RootTree::DataContractDocuments as u8],
301 self.contract_id.to_vec(),
302 vec![1u8],
303 self.document_type_name.as_bytes().to_vec(),
304 ];
305 let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
306 for prop in prefix_props {
307 let clause = self
308 .where_clauses
309 .iter()
310 .find(|wc| wc.field == prop.name)
311 .ok_or(Error::Query(
312 QuerySyntaxError::InvalidWhereClauseComponents(
313 "aggregate-sum proof: missing where clause for an index prefix property",
314 ),
315 ))?;
316 if clause.operator != WhereOperator::Equal {
317 return Err(Error::Query(
318 QuerySyntaxError::InvalidWhereClauseComponents(
319 "aggregate-sum proof: prefix properties must use `==` (no `in`); use \
320 `group_by = [in_field, range_field]` (carrier-aggregate variant) for \
321 compound In-on-prefix sum queries",
322 ),
323 ));
324 }
325 path.push(self.index.level_key_for_property(&prop.name).into_bytes());
326 path.push(self.document_type.serialize_value_for_key(
327 prop.name.as_str(),
328 &clause.value,
329 platform_version,
330 )?);
331 }
332 let range_prop_name = &self
333 .index
334 .properties
335 .last()
336 .ok_or(Error::Query(
337 QuerySyntaxError::InvalidWhereClauseComponents(
338 "range_summable index must have at least one property",
339 ),
340 ))?
341 .name;
342 path.push(
343 self.index
344 .level_key_for_property(range_prop_name)
345 .into_bytes(),
346 );
347
348 let query = Query::new_aggregate_sum_on_range(query_item);
350 Ok(PathQuery::new(path, SizedQuery::new(query, None, None)))
351 }
352
353 fn refuse_a_range_sum_total(&self) -> Result<(), Error> {
357 refuse_a_range_total_through_a_ranked_index(self.document_type, self.index)
358 }
359
360 pub fn aggregate_count_and_sum_path_query(
365 &self,
366 platform_version: &PlatformVersion,
367 ) -> Result<PathQuery, Error> {
368 self.refuse_a_range_sum_total()?;
369 if !self.index.range_countable {
370 return Err(Error::Query(QuerySyntaxError::Unsupported(
371 "aggregate_count_and_sum_path_query: index must declare BOTH \
372 `rangeCountable: true` AND `rangeSummable: true` to produce a PCPS \
373 (ProvableCountProvableSumTree) property-name tree."
374 .to_string(),
375 )));
376 }
377
378 let terminator_prop_name = &self
381 .index
382 .properties
383 .last()
384 .ok_or(Error::Query(
385 QuerySyntaxError::InvalidWhereClauseComponents(
386 "PCPS index must have at least one property",
387 ),
388 ))?
389 .name;
390 let range_clause = self
391 .where_clauses
392 .iter()
393 .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
394 .ok_or(Error::Query(
395 QuerySyntaxError::InvalidWhereClauseComponents(
396 "aggregate_count_and_sum_path_query requires a range where-clause on the index terminator property",
397 ),
398 ))?;
399 let query_item = self.range_clause_to_query_item(range_clause, platform_version)?;
400
401 let mut path = vec![
402 vec![RootTree::DataContractDocuments as u8],
403 self.contract_id.to_vec(),
404 vec![1u8],
405 self.document_type_name.as_bytes().to_vec(),
406 ];
407 let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
408 for prop in prefix_props {
409 let clause = self
410 .where_clauses
411 .iter()
412 .find(|wc| wc.field == prop.name)
413 .ok_or(Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
414 "aggregate-count-and-sum proof: missing where clause for an index prefix property",
415 )))?;
416 if clause.operator != WhereOperator::Equal {
417 return Err(Error::Query(
418 QuerySyntaxError::InvalidWhereClauseComponents(
419 "aggregate-count-and-sum proof: prefix properties must use `==` (no `in`)",
420 ),
421 ));
422 }
423 path.push(self.index.level_key_for_property(&prop.name).into_bytes());
424 path.push(self.document_type.serialize_value_for_key(
425 prop.name.as_str(),
426 &clause.value,
427 platform_version,
428 )?);
429 }
430 let range_prop_name = &self
431 .index
432 .properties
433 .last()
434 .ok_or(Error::Query(
435 QuerySyntaxError::InvalidWhereClauseComponents(
436 "range_countable + range_summable index must have at least one property",
437 ),
438 ))?
439 .name;
440 path.push(
441 self.index
442 .level_key_for_property(range_prop_name)
443 .into_bytes(),
444 );
445
446 let query = grovedb::Query::new_aggregate_count_and_sum_on_range(query_item);
447 Ok(PathQuery::new(
448 path,
449 grovedb::SizedQuery::new(query, None, None),
450 ))
451 }
452
453 fn range_clause_to_query_item(
463 &self,
464 clause: &WhereClause,
465 platform_version: &PlatformVersion,
466 ) -> Result<QueryItem, Error> {
467 let serialize = |v: &dpp::platform_value::Value| -> Result<Vec<u8>, Error> {
468 Ok(self.document_type.serialize_value_for_key(
469 clause.field.as_str(),
470 v,
471 platform_version,
472 )?)
473 };
474 let serialize_pair = || -> Result<(Vec<u8>, Vec<u8>), Error> {
475 let arr = clause.value.as_array().ok_or_else(|| {
476 Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
477 "range bounds value must be a 2-element array",
478 ))
479 })?;
480 if arr.len() != 2 {
481 return Err(Error::Query(
482 QuerySyntaxError::InvalidWhereClauseComponents(
483 "range bounds value must be a 2-element array",
484 ),
485 ));
486 }
487 let a = serialize(&arr[0])?;
488 let b = serialize(&arr[1])?;
489 if a > b {
490 return Err(Error::Query(
491 QuerySyntaxError::InvalidWhereClauseComponents(
492 "range lower bound must be <= upper bound",
493 ),
494 ));
495 }
496 Ok((a, b))
497 };
498
499 Ok(match clause.operator {
500 WhereOperator::GreaterThan => {
501 let v = serialize(&clause.value)?;
502 QueryItem::RangeAfter(v..)
503 }
504 WhereOperator::GreaterThanOrEquals => {
505 let v = serialize(&clause.value)?;
506 QueryItem::RangeFrom(v..)
507 }
508 WhereOperator::LessThan => {
509 let v = serialize(&clause.value)?;
510 QueryItem::RangeTo(..v)
511 }
512 WhereOperator::LessThanOrEquals => {
513 let v = serialize(&clause.value)?;
514 QueryItem::RangeToInclusive(..=v)
515 }
516 WhereOperator::Between => {
517 let (a, b) = serialize_pair()?;
518 QueryItem::RangeInclusive(a..=b)
519 }
520 WhereOperator::BetweenExcludeBounds => {
521 let (a, b) = serialize_pair()?;
522 QueryItem::RangeAfterTo(a..b)
523 }
524 WhereOperator::BetweenExcludeLeft => {
525 let (a, b) = serialize_pair()?;
526 QueryItem::RangeAfterToInclusive(a..=b)
527 }
528 WhereOperator::BetweenExcludeRight => {
529 let (a, b) = serialize_pair()?;
530 QueryItem::Range(a..b)
531 }
532 WhereOperator::StartsWith => {
533 let left_key = serialize(&clause.value)?;
534 let mut right_key = left_key.clone();
535 if right_key.is_empty() {
536 return Err(Error::Query(QuerySyntaxError::InvalidStartsWithClause(
537 "startsWith prefix must have at least one byte",
538 )));
539 }
540 let mut i = right_key.len();
547 while i > 0 && right_key[i - 1] == 0xFF {
548 i -= 1;
549 }
550 if i == 0 {
551 return Err(Error::Query(QuerySyntaxError::InvalidStartsWithClause(
552 "startsWith prefix is all 0xFF bytes; cannot form half-open upper bound",
553 )));
554 }
555 right_key.truncate(i);
556 *right_key
557 .last_mut()
558 .expect("non-empty after truncate to non-zero length") += 1;
559 QueryItem::Range(left_key..right_key)
560 }
561 _ => {
562 return Err(Error::Query(
563 QuerySyntaxError::InvalidWhereClauseComponents(
564 "range_clause_to_query_item called on a non-range operator",
565 ),
566 ));
567 }
568 })
569 }
570
571 pub fn distinct_sum_path_query(
594 &self,
595 limit: Option<u16>,
596 left_to_right: bool,
597 platform_version: &PlatformVersion,
598 ) -> Result<PathQuery, Error> {
599 let range_clause = self
600 .where_clauses
601 .iter()
602 .find(|wc| is_range_operator(wc.operator))
603 .ok_or(Error::Query(
604 QuerySyntaxError::InvalidWhereClauseComponents(
605 "distinct_sum_path_query requires a range where-clause",
606 ),
607 ))?;
608 let range_item = self.range_clause_to_query_item(range_clause, platform_version)?;
609
610 let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
611 let terminator_name = &self
612 .index
613 .properties
614 .last()
615 .ok_or(Error::Query(
616 QuerySyntaxError::InvalidWhereClauseComponents(
617 "range_summable index must have at least one property",
618 ),
619 ))?
620 .name;
621
622 let mut base_path: Vec<Vec<u8>> = vec![
623 vec![RootTree::DataContractDocuments as u8],
624 self.contract_id.to_vec(),
625 vec![1u8],
626 self.document_type_name.as_bytes().to_vec(),
627 ];
628
629 let mut in_outer_keys: Option<Vec<Vec<u8>>> = None;
636 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
637
638 for prop in prefix_props {
639 let clause = self
640 .where_clauses
641 .iter()
642 .find(|wc| wc.field == prop.name)
643 .ok_or(Error::Query(
644 QuerySyntaxError::InvalidWhereClauseComponents(
645 "distinct_sum_path_query: missing where clause for an index \
646 prefix property",
647 ),
648 ))?;
649
650 match clause.operator {
651 WhereOperator::Equal => {
652 let serialized = self.document_type.serialize_value_for_key(
653 prop.name.as_str(),
654 &clause.value,
655 platform_version,
656 )?;
657 if in_outer_keys.is_some() {
658 subquery_path_extension
659 .push(self.index.level_key_for_property(&prop.name).into_bytes());
660 subquery_path_extension.push(serialized);
661 } else {
662 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
663 base_path.push(serialized);
664 }
665 }
666 WhereOperator::In => {
667 if in_outer_keys.is_some() {
668 return Err(Error::Query(
669 QuerySyntaxError::InvalidWhereClauseComponents(
670 "distinct_sum_path_query: at most one `In` clause is supported \
671 on prefix properties",
672 ),
673 ));
674 }
675 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
678 let in_values = clause.in_values().into_data_with_error()??;
679 let mut keys: Vec<Vec<u8>> = in_values
680 .iter()
681 .map(|v| {
682 self.document_type.serialize_value_for_key(
683 prop.name.as_str(),
684 v,
685 platform_version,
686 )
687 })
688 .collect::<Result<_, _>>()?;
689 keys.sort();
696 in_outer_keys = Some(keys);
697 }
698 _ => {
699 return Err(Error::Query(
700 QuerySyntaxError::InvalidWhereClauseComponents(
701 "distinct_sum_path_query: prefix properties must use `==` or `in`",
702 ),
703 ));
704 }
705 }
706 }
707
708 match in_outer_keys {
709 None => {
710 base_path.push(terminator_name.as_bytes().to_vec());
713 let mut query = Query::new_with_direction(left_to_right);
714 query.insert_item(range_item);
715 Ok(PathQuery::new(
716 base_path,
717 SizedQuery::new(query, limit, None),
718 ))
719 }
720 Some(keys) => {
721 let mut outer_query = Query::new_with_direction(left_to_right);
729 for key in keys {
730 outer_query.insert_key(key);
731 }
732 subquery_path_extension.push(terminator_name.as_bytes().to_vec());
733
734 let mut subquery = Query::new_with_direction(left_to_right);
735 subquery.insert_item(range_item);
736
737 outer_query.set_subquery_path(subquery_path_extension);
738 outer_query.set_subquery(subquery);
739
740 Ok(PathQuery::new(
741 base_path,
742 SizedQuery::new(outer_query, limit, None),
743 ))
744 }
745 }
746 }
747
748 pub fn carrier_aggregate_sum_path_query(
797 &self,
798 limit: Option<u16>,
799 left_to_right: bool,
800 platform_version: &PlatformVersion,
801 ) -> Result<PathQuery, Error> {
802 self.refuse_a_range_sum_total()?;
804 let terminator_prop_name = &self
817 .index
818 .properties
819 .last()
820 .ok_or(Error::Query(
821 QuerySyntaxError::InvalidWhereClauseComponents(
822 "range_summable index must have at least one property",
823 ),
824 ))?
825 .name;
826 let terminator_clause = self
827 .where_clauses
828 .iter()
829 .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
830 .ok_or(Error::Query(
831 QuerySyntaxError::InvalidWhereClauseComponents(
832 "carrier_aggregate_sum_path_query requires a range where-clause on the \
833 terminator property of the chosen index",
834 ),
835 ))?;
836 let inner_range_item =
837 self.range_clause_to_query_item(terminator_clause, platform_version)?;
838
839 let mut base_path: Vec<Vec<u8>> = vec![
840 vec![RootTree::DataContractDocuments as u8],
841 self.contract_id.to_vec(),
842 vec![1u8],
843 self.document_type_name.as_bytes().to_vec(),
844 ];
845 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
846
847 enum Carrier {
852 Pending,
853 In(WhereClause),
854 Range(WhereClause),
855 }
856 let mut carrier = Carrier::Pending;
857 let prefix_and_carrier_props = &self.index.properties[..self.index.properties.len() - 1];
858
859 for prop in prefix_and_carrier_props {
860 let clause = self
861 .where_clauses
862 .iter()
863 .find(|wc| wc.field == prop.name)
864 .ok_or(Error::Query(
865 QuerySyntaxError::InvalidWhereClauseComponents(
866 "carrier-aggregate sum proof: missing where clause for an index prefix \
867 property",
868 ),
869 ))?;
870 match (&carrier, clause.operator) {
871 (Carrier::Pending, WhereOperator::Equal) => {
872 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
873 base_path.push(self.document_type.serialize_value_for_key(
874 prop.name.as_str(),
875 &clause.value,
876 platform_version,
877 )?);
878 }
879 (Carrier::Pending, WhereOperator::In) => {
880 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
881 carrier = Carrier::In(clause.clone());
882 }
883 (Carrier::Pending, op) if is_range_operator(op) => {
884 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
885 carrier = Carrier::Range(clause.clone());
886 }
887 (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => {
888 subquery_path_extension
889 .push(self.index.level_key_for_property(&prop.name).into_bytes());
890 subquery_path_extension.push(self.document_type.serialize_value_for_key(
891 prop.name.as_str(),
892 &clause.value,
893 platform_version,
894 )?);
895 }
896 (Carrier::In(_) | Carrier::Range(_), _) => {
897 return Err(Error::Query(
898 QuerySyntaxError::InvalidWhereClauseComponents(
899 "carrier-aggregate sum proof: at most one carrier clause (In or \
900 range) is supported on prefix properties; subsequent prefix \
901 clauses must use `==`",
902 ),
903 ));
904 }
905 _ => {
906 return Err(Error::Query(
907 QuerySyntaxError::InvalidWhereClauseComponents(
908 "carrier-aggregate sum proof: prefix property operator unsupported",
909 ),
910 ));
911 }
912 }
913 }
914 subquery_path_extension.push(
915 self.index
916 .level_key_for_property(terminator_prop_name)
917 .into_bytes(),
918 );
919
920 let mut outer_query = Query::new_with_direction(left_to_right);
921 match carrier {
922 Carrier::Pending => {
923 return Err(Error::Query(
924 QuerySyntaxError::InvalidWhereClauseComponents(
925 "carrier-aggregate sum proof: an In or range clause must appear on a \
926 prefix property of the chosen index to act as the carrier dimension",
927 ),
928 ));
929 }
930 Carrier::In(in_clause) => {
931 let in_values = in_clause.in_values().into_data_with_error()??;
936 let mut serialized_in_keys: Vec<Vec<u8>> = in_values
937 .iter()
938 .map(|v| {
939 self.document_type.serialize_value_for_key(
940 in_clause.field.as_str(),
941 v,
942 platform_version,
943 )
944 })
945 .collect::<Result<_, _>>()?;
946 serialized_in_keys.sort();
947 serialized_in_keys.dedup();
948 for key in serialized_in_keys {
949 outer_query.insert_key(key);
950 }
951 }
952 Carrier::Range(range_clause) => {
953 let outer_range_item =
957 self.range_clause_to_query_item(&range_clause, platform_version)?;
958 outer_query.items.push(outer_range_item);
959 }
960 }
961 outer_query.set_subquery_path(subquery_path_extension);
962 outer_query.set_subquery(Query::new_aggregate_sum_on_range(inner_range_item));
963
964 Ok(PathQuery::new(
971 base_path,
972 SizedQuery::new(outer_query, limit, None),
973 ))
974 }
975
976 pub fn carrier_aggregate_count_and_sum_path_query(
998 &self,
999 limit: Option<u16>,
1000 left_to_right: bool,
1001 platform_version: &PlatformVersion,
1002 ) -> Result<PathQuery, Error> {
1003 self.refuse_a_range_sum_total()?;
1005 if !self.index.range_countable {
1006 return Err(Error::Query(QuerySyntaxError::Unsupported(
1007 "carrier_aggregate_count_and_sum_path_query: index must declare BOTH \
1008 `rangeCountable: true` AND `rangeSummable: true` to produce a PCPS \
1009 (ProvableCountProvableSumTree) property-name tree."
1010 .to_string(),
1011 )));
1012 }
1013
1014 let terminator_prop_name = &self
1015 .index
1016 .properties
1017 .last()
1018 .ok_or(Error::Query(
1019 QuerySyntaxError::InvalidWhereClauseComponents(
1020 "range_countable + range_summable index must have at least one property",
1021 ),
1022 ))?
1023 .name;
1024 let terminator_clause = self
1025 .where_clauses
1026 .iter()
1027 .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
1028 .ok_or(Error::Query(
1029 QuerySyntaxError::InvalidWhereClauseComponents(
1030 "carrier_aggregate_count_and_sum_path_query requires a range where-clause \
1031 on the terminator property of the chosen index",
1032 ),
1033 ))?;
1034 let inner_range_item =
1035 self.range_clause_to_query_item(terminator_clause, platform_version)?;
1036
1037 let mut base_path: Vec<Vec<u8>> = vec![
1038 vec![RootTree::DataContractDocuments as u8],
1039 self.contract_id.to_vec(),
1040 vec![1u8],
1041 self.document_type_name.as_bytes().to_vec(),
1042 ];
1043 let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
1044
1045 enum Carrier {
1047 Pending,
1048 In(WhereClause),
1049 Range(WhereClause),
1050 }
1051 let mut carrier = Carrier::Pending;
1052 let prefix_and_carrier_props = &self.index.properties[..self.index.properties.len() - 1];
1053
1054 for prop in prefix_and_carrier_props {
1055 let clause = self
1056 .where_clauses
1057 .iter()
1058 .find(|wc| wc.field == prop.name)
1059 .ok_or(Error::Query(
1060 QuerySyntaxError::InvalidWhereClauseComponents(
1061 "carrier-aggregate count-and-sum proof: missing where clause for an index \
1062 prefix property",
1063 ),
1064 ))?;
1065 match (&carrier, clause.operator) {
1066 (Carrier::Pending, WhereOperator::Equal) => {
1067 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
1068 base_path.push(self.document_type.serialize_value_for_key(
1069 prop.name.as_str(),
1070 &clause.value,
1071 platform_version,
1072 )?);
1073 }
1074 (Carrier::Pending, WhereOperator::In) => {
1075 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
1076 carrier = Carrier::In(clause.clone());
1077 }
1078 (Carrier::Pending, op) if is_range_operator(op) => {
1079 base_path.push(self.index.level_key_for_property(&prop.name).into_bytes());
1080 carrier = Carrier::Range(clause.clone());
1081 }
1082 (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => {
1083 subquery_path_extension
1084 .push(self.index.level_key_for_property(&prop.name).into_bytes());
1085 subquery_path_extension.push(self.document_type.serialize_value_for_key(
1086 prop.name.as_str(),
1087 &clause.value,
1088 platform_version,
1089 )?);
1090 }
1091 (Carrier::In(_) | Carrier::Range(_), _) => {
1092 return Err(Error::Query(
1093 QuerySyntaxError::InvalidWhereClauseComponents(
1094 "carrier-aggregate count-and-sum proof: at most one carrier clause \
1095 (In or range) is supported on prefix properties; subsequent prefix \
1096 clauses must use `==`",
1097 ),
1098 ));
1099 }
1100 _ => {
1101 return Err(Error::Query(
1102 QuerySyntaxError::InvalidWhereClauseComponents(
1103 "carrier-aggregate count-and-sum proof: prefix property operator \
1104 unsupported",
1105 ),
1106 ));
1107 }
1108 }
1109 }
1110 subquery_path_extension.push(
1111 self.index
1112 .level_key_for_property(terminator_prop_name)
1113 .into_bytes(),
1114 );
1115
1116 let mut outer_query = Query::new_with_direction(left_to_right);
1117 match carrier {
1118 Carrier::Pending => {
1119 return Err(Error::Query(
1120 QuerySyntaxError::InvalidWhereClauseComponents(
1121 "carrier-aggregate count-and-sum proof: an In or range clause must \
1122 appear on a prefix property of the chosen index to act as the carrier \
1123 dimension",
1124 ),
1125 ));
1126 }
1127 Carrier::In(in_clause) => {
1128 let in_values = in_clause.in_values().into_data_with_error()??;
1129 let mut serialized_in_keys: Vec<Vec<u8>> = in_values
1130 .iter()
1131 .map(|v| {
1132 self.document_type.serialize_value_for_key(
1133 in_clause.field.as_str(),
1134 v,
1135 platform_version,
1136 )
1137 })
1138 .collect::<Result<_, _>>()?;
1139 serialized_in_keys.sort();
1140 serialized_in_keys.dedup();
1141 for key in serialized_in_keys {
1142 outer_query.insert_key(key);
1143 }
1144 }
1145 Carrier::Range(range_clause) => {
1146 let outer_range_item =
1147 self.range_clause_to_query_item(&range_clause, platform_version)?;
1148 outer_query.items.push(outer_range_item);
1149 }
1150 }
1151 outer_query.set_subquery_path(subquery_path_extension);
1152 outer_query.set_subquery(grovedb::Query::new_aggregate_count_and_sum_on_range(
1153 inner_range_item,
1154 ));
1155
1156 Ok(PathQuery::new(
1157 base_path,
1158 SizedQuery::new(outer_query, limit, None),
1159 ))
1160 }
1161}
1162
1163#[cfg(any(feature = "server", feature = "verify"))]
1169impl<'a> DriveDocumentSumQuery<'a> {
1170 pub fn point_lookup_sum_path_query_static(
1174 contract: &DataContract,
1175 document_type: DocumentTypeRef,
1176 sum_property: &str,
1177 where_clauses: &[WhereClause],
1178 resolved_time_ranges: &[ResolvedTimeRange],
1179 platform_version: &PlatformVersion,
1180 ) -> Result<PathQuery, Error> {
1181 use crate::query::drive_document_sum_query::index_picker::find_summable_index_for_where_clauses;
1182 use dpp::data_contract::accessors::v0::DataContractV0Getters;
1183 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1184
1185 let index = find_summable_index_for_where_clauses(
1186 document_type.indexes(),
1187 where_clauses,
1188 sum_property,
1189 resolved_time_ranges,
1190 )
1191 .ok_or_else(|| {
1192 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
1193 "no `summable: \"<prop>\"` index exactly matches the where-clause fields. \
1194 Define a more specific summable index (with `summable: \"<prop>\"` whose \
1195 properties exactly equal the clauses) or use `prove=false`."
1196 .to_string(),
1197 ))
1198 })?;
1199 let q = DriveDocumentSumQuery {
1200 document_type,
1201 contract_id: contract.id().to_buffer(),
1202 document_type_name: document_type.name().clone(),
1203 index,
1204 where_clauses: where_clauses.to_vec(),
1205 sum_property: sum_property.to_string(),
1206 };
1207 q.point_lookup_sum_path_query(platform_version)
1208 }
1209
1210 pub fn aggregate_sum_path_query_static(
1214 contract: &DataContract,
1215 document_type: DocumentTypeRef,
1216 sum_property: &str,
1217 where_clauses: &[WhereClause],
1218 resolved_time_ranges: &[ResolvedTimeRange],
1219 platform_version: &PlatformVersion,
1220 ) -> Result<PathQuery, Error> {
1221 use crate::query::drive_document_sum_query::index_picker::find_range_summable_index_for_where_clauses;
1222 use dpp::data_contract::accessors::v0::DataContractV0Getters;
1223 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1224
1225 let index = find_range_summable_index_for_where_clauses(
1226 document_type.indexes(),
1227 where_clauses,
1228 sum_property,
1229 resolved_time_ranges,
1230 )
1231 .ok_or_else(|| {
1232 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
1233 "no `rangeSummable: true` index covers the where-clause shape (Equal/In \
1234 prefix exactly + range on the index's last property). Define one or use \
1235 `prove=false`."
1236 .to_string(),
1237 ))
1238 })?;
1239 let q = DriveDocumentSumQuery {
1240 document_type,
1241 contract_id: contract.id().to_buffer(),
1242 document_type_name: document_type.name().clone(),
1243 index,
1244 where_clauses: where_clauses.to_vec(),
1245 sum_property: sum_property.to_string(),
1246 };
1247 q.aggregate_sum_path_query(platform_version)
1248 }
1249
1250 #[allow(clippy::too_many_arguments)]
1259 pub fn carrier_aggregate_sum_path_query_static(
1260 contract: &DataContract,
1261 document_type: DocumentTypeRef,
1262 sum_property: &str,
1263 where_clauses: &[WhereClause],
1264 resolved_time_ranges: &[ResolvedTimeRange],
1265 limit: Option<u16>,
1266 left_to_right: bool,
1267 platform_version: &PlatformVersion,
1268 ) -> Result<PathQuery, Error> {
1269 use crate::query::drive_document_sum_query::index_picker::find_range_summable_index_for_where_clauses;
1270 use dpp::data_contract::accessors::v0::DataContractV0Getters;
1271 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1272
1273 let index = find_range_summable_index_for_where_clauses(
1274 document_type.indexes(),
1275 where_clauses,
1276 sum_property,
1277 resolved_time_ranges,
1278 )
1279 .ok_or_else(|| {
1280 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
1281 "no `rangeSummable: true` index covers the where-clause shape for the \
1282 carrier-aggregate sum carrier (Equal/In prefix + In-or-range carrier + \
1283 range on the index's last property). Define one or use `prove=false`."
1284 .to_string(),
1285 ))
1286 })?;
1287 let q = DriveDocumentSumQuery {
1288 document_type,
1289 contract_id: contract.id().to_buffer(),
1290 document_type_name: document_type.name().clone(),
1291 index,
1292 where_clauses: where_clauses.to_vec(),
1293 sum_property: sum_property.to_string(),
1294 };
1295 q.carrier_aggregate_sum_path_query(limit, left_to_right, platform_version)
1296 }
1297}
1298
1299#[cfg(test)]
1315mod carrier_path_query_tests {
1316 use super::*;
1317 use crate::query::WhereOperator;
1318 use assert_matches::assert_matches;
1319 use dpp::data_contract::accessors::v0::DataContractV0Getters;
1320 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1321 use dpp::data_contract::DataContract;
1322 use dpp::tests::json_document::json_document_to_contract;
1323 use grovedb::QueryItem;
1324
1325 fn load_tip_jar_contract(platform_version: &PlatformVersion) -> DataContract {
1326 json_document_to_contract(
1331 "tests/supporting_files/contract/tip-jar/tip-jar-contract.json",
1332 false,
1333 platform_version,
1334 )
1335 .expect("tip-jar contract fixture loads")
1336 }
1337
1338 fn pick_doc_type<'a>(
1350 contract: &'a DataContract,
1351 doc_type_name: &str,
1352 ) -> dpp::data_contract::document_type::DocumentTypeRef<'a> {
1353 contract
1354 .document_type_for_name(doc_type_name)
1355 .expect("document type exists in tip-jar fixture")
1356 }
1357
1358 fn recipient_a() -> Vec<u8> {
1362 let mut v = vec![0x80u8; 32];
1364 v[31] = 0x01;
1365 v
1366 }
1367 fn recipient_b() -> Vec<u8> {
1368 let mut v = vec![0x10u8; 32];
1370 v[31] = 0x02;
1371 v
1372 }
1373
1374 #[test]
1379 fn carrier_aggregate_sum_in_on_carrier_range_on_terminator() {
1380 let platform_version = PlatformVersion::latest();
1381 let contract = load_tip_jar_contract(platform_version);
1382 let doc_type = pick_doc_type(&contract, "tip");
1383 let index = doc_type
1384 .indexes()
1385 .get("byRecipientTime")
1386 .expect("byRecipientTime index exists on tip doc type");
1387
1388 let in_values = vec![
1393 dpp::platform_value::Value::Bytes(recipient_a()),
1394 dpp::platform_value::Value::Bytes(recipient_b()),
1395 ];
1396 let where_clauses = vec![
1397 WhereClause {
1398 field: "recipient".to_string(),
1399 operator: WhereOperator::In,
1400 value: dpp::platform_value::Value::Array(in_values.clone()),
1401 },
1402 WhereClause {
1403 field: "sentAt".to_string(),
1404 operator: WhereOperator::GreaterThan,
1405 value: dpp::platform_value::Value::U64(0),
1406 },
1407 ];
1408 let q = DriveDocumentSumQuery {
1409 document_type: doc_type,
1410 contract_id: contract.id().to_buffer(),
1411 document_type_name: doc_type.name().clone(),
1412 index,
1413 where_clauses,
1414 sum_property: "amount".to_string(),
1415 };
1416
1417 let pq = q
1418 .carrier_aggregate_sum_path_query(None, true, platform_version)
1419 .expect("carrier-aggregate sum path query builds");
1420
1421 assert!(
1425 pq.path.len() >= 5,
1426 "expected base_path to extend through the In-bearing prop's name subtree"
1427 );
1428 assert_eq!(
1429 pq.path.last().expect("base_path non-empty"),
1430 b"recipient",
1431 "outer path must stop at the In-bearing prop's property-name subtree"
1432 );
1433
1434 let outer_items = &pq.query.query.items;
1438 assert_eq!(outer_items.len(), 2, "one outer Key per In value");
1439 for item in outer_items {
1440 assert_matches!(item, QueryItem::Key(_));
1441 }
1442 if let (QueryItem::Key(a), QueryItem::Key(b)) = (&outer_items[0], &outer_items[1]) {
1443 assert!(a < b, "outer Keys must be sorted lex-ascending");
1444 }
1445
1446 let sub_path = pq
1448 .query
1449 .query
1450 .default_subquery_branch
1451 .subquery_path
1452 .as_ref()
1453 .expect("subquery_path set");
1454 assert_eq!(sub_path, &vec![b"sentAt".to_vec()]);
1455
1456 let subquery = pq
1458 .query
1459 .query
1460 .default_subquery_branch
1461 .subquery
1462 .as_ref()
1463 .expect("subquery set");
1464 assert_eq!(subquery.items.len(), 1);
1465 assert_matches!(subquery.items[0], QueryItem::AggregateSumOnRange(_));
1466 }
1467
1468 #[test]
1472 fn carrier_aggregate_sum_limit_flows_into_sized_query() {
1473 let platform_version = PlatformVersion::latest();
1474 let contract = load_tip_jar_contract(platform_version);
1475 let doc_type = pick_doc_type(&contract, "tip");
1476 let index = doc_type
1477 .indexes()
1478 .get("byRecipientTime")
1479 .expect("byRecipientTime index exists on tip doc type");
1480
1481 let where_clauses = vec![
1482 WhereClause {
1483 field: "recipient".to_string(),
1484 operator: WhereOperator::In,
1485 value: dpp::platform_value::Value::Array(vec![
1486 dpp::platform_value::Value::Bytes(recipient_a()),
1487 dpp::platform_value::Value::Bytes(recipient_b()),
1488 ]),
1489 },
1490 WhereClause {
1491 field: "sentAt".to_string(),
1492 operator: WhereOperator::GreaterThan,
1493 value: dpp::platform_value::Value::U64(0),
1494 },
1495 ];
1496 let q = DriveDocumentSumQuery {
1497 document_type: doc_type,
1498 contract_id: contract.id().to_buffer(),
1499 document_type_name: doc_type.name().clone(),
1500 index,
1501 where_clauses,
1502 sum_property: "amount".to_string(),
1503 };
1504
1505 let pq = q
1506 .carrier_aggregate_sum_path_query(Some(7), true, platform_version)
1507 .expect("carrier-aggregate sum path query builds with limit");
1508 assert_eq!(pq.query.limit, Some(7), "outer SizedQuery::limit threads");
1509 }
1510
1511 #[test]
1513 fn carrier_aggregate_sum_rejects_missing_terminator_range() {
1514 let platform_version = PlatformVersion::latest();
1515 let contract = load_tip_jar_contract(platform_version);
1516 let doc_type = pick_doc_type(&contract, "tip");
1517 let index = doc_type
1518 .indexes()
1519 .get("byRecipientTime")
1520 .expect("byRecipientTime index exists on tip doc type");
1521
1522 let where_clauses = vec![WhereClause {
1523 field: "recipient".to_string(),
1524 operator: WhereOperator::In,
1525 value: dpp::platform_value::Value::Array(vec![dpp::platform_value::Value::Bytes(
1526 recipient_a(),
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("missing range clause must be rejected");
1541 let msg = format!("{err:?}");
1542 assert!(
1543 msg.contains("requires a range where-clause"),
1544 "unexpected error: {msg}"
1545 );
1546 }
1547
1548 #[test]
1551 fn carrier_aggregate_sum_rejects_missing_carrier() {
1552 let platform_version = PlatformVersion::latest();
1553 let contract = load_tip_jar_contract(platform_version);
1554 let doc_type = pick_doc_type(&contract, "tip");
1555 let index = doc_type
1556 .indexes()
1557 .get("byRecipientTime")
1558 .expect("byRecipientTime index exists on tip doc type");
1559
1560 let where_clauses = vec![
1565 WhereClause {
1566 field: "recipient".to_string(),
1567 operator: WhereOperator::Equal,
1568 value: dpp::platform_value::Value::Bytes(recipient_a()),
1569 },
1570 WhereClause {
1571 field: "sentAt".to_string(),
1572 operator: WhereOperator::GreaterThan,
1573 value: dpp::platform_value::Value::U64(0),
1574 },
1575 ];
1576 let q = DriveDocumentSumQuery {
1577 document_type: doc_type,
1578 contract_id: contract.id().to_buffer(),
1579 document_type_name: doc_type.name().clone(),
1580 index,
1581 where_clauses,
1582 sum_property: "amount".to_string(),
1583 };
1584
1585 let err = q
1586 .carrier_aggregate_sum_path_query(None, true, platform_version)
1587 .expect_err("Equal-only prefix must be rejected by carrier builder");
1588 let msg = format!("{err:?}");
1589 assert!(msg.contains("carrier dimension"), "unexpected error: {msg}");
1590 }
1591}