Skip to main content

drive/query/drive_document_sum_query/
path_query.rs

1//! Path-query builders for the sum surface. Single source of truth for
2//! the `PathQuery` shape both the prover (in `executors/*`) and the
3//! verifier (in tests + bench's `display_proofs`) construct.
4//!
5//! Parallels [`crate::query::drive_document_count_query::path_query`] —
6//! the bench's `display_proofs` function directly calls these as the
7//! verifier-side rebuild, so each builder MUST produce the byte-for-byte
8//! same `PathQuery` the prover used. Drift breaks every proof
9//! verification.
10//!
11//! Two shapes exist for each builder:
12//! - **Instance methods on `impl DriveDocumentSumQuery<'_>`** — called by
13//!   the per-mode executors which have already resolved the covering
14//!   index via the picker. These use `self.contract_id`,
15//!   `self.document_type`, `self.index`, etc.
16//! - **Static associated functions** — called by the bench's
17//!   `display_proofs` (verifier-side rebuild) and tests. These re-pick
18//!   the covering index from the document type's index map so callers
19//!   don't have to thread it through.
20
21use 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};
27// `serialize_value_for_key` is a `DocumentTypeV0Methods` method, NOT
28// `DocumentTypeBasicMethods` (which is the trait of versionless basic
29// helpers). The serializer routes through a versioned dispatcher
30// (`serialize_value_for_key_v0` + friends), so it lives on the
31// versioned-methods trait.
32use 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
38/// Storage convention: the count/sum tree under a non-rangeSummable
39/// value tree lives at child key `[0]` (the ref bucket). Same convention
40/// as count's `COUNT_TREE_KEY`.
41const SUM_TREE_KEY: u8 = 0;
42
43#[cfg(any(feature = "server", feature = "verify"))]
44impl<'a> DriveDocumentSumQuery<'a> {
45    /// Build the `PathQuery` for the primary-key SumTree fast path
46    /// (used when `documents_summable` is set and the query has no
47    /// `where` clauses).
48    ///
49    /// Mirrors count's `primary_key_count_tree_path_query` signature
50    /// — takes the two scalar arguments (`contract_id`,
51    /// `document_type_name`) that are the only fields actually used.
52    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    /// Instance-method form of [`Self::point_lookup_sum_path_query_static`]
68    /// — uses `self.index` (already resolved by the picker) rather than
69    /// re-picking from the document type. Mirrors count's
70    /// `point_lookup_count_path_query` shape.
71    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        // Sum-tree terminator optimization: every summable terminator's
160        // value tree is a SumTree (continuations NonCounted-wrapped),
161        // so the proof can stop at the value tree without descending
162        // to the `[0]` ref bucket. Mirror of count's
163        // `count_tree_terminator` gate (uses `is_countable()` on count
164        // side; on the sum side, `summable.is_some()` is the right
165        // discriminator).
166        let sum_tree_terminator = self.index.summable.is_some();
167
168        match in_outer_keys {
169            None => {
170                // Equal-only, fully covered.
171                let mut query = Query::new();
172                if sum_tree_terminator {
173                    // Lift the last serialized value off the path: the
174                    // terminator's value tree is a SumTree directly, so
175                    // we ask for it as a Key under the property-name
176                    // subtree.
177                    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                // Compound shape with In at some position.
192                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                        // Outer Keys already point at the SumTree value
200                        // trees themselves; no subquery needed.
201                    } 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    /// Instance-method form: builds the `AggregateSumOnRange` path
231    /// query against `self.index` (resolved upstream by the
232    /// `find_range_summable_index_for_where_clauses` picker). The
233    /// terminator's range clause is required; prefix properties must
234    /// use `==`.
235    pub fn aggregate_sum_path_query(
236        &self,
237        platform_version: &PlatformVersion,
238    ) -> Result<PathQuery, Error> {
239        // Bind the range clause to the index's *terminator* property so a
240        // request with multiple range-like clauses (e.g. `prefix > x AND
241        // terminator > y`) picks the right one. The previous predicate
242        // returned the first range operator and could pick the prefix.
243        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        // grovedb PR 670 surface: `Query::new_aggregate_sum_on_range`.
314        let query = Query::new_aggregate_sum_on_range(query_item);
315        Ok(PathQuery::new(path, SizedQuery::new(query, None, None)))
316    }
317
318    /// Instance-method form: builds the combined PCPS
319    /// `AggregateCountAndSumOnRange` path query against `self.index`.
320    /// Requires the index to declare BOTH `rangeCountable: true` AND
321    /// `rangeSummable: true`.
322    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        // Bind to the terminator property — see the sibling
336        // `aggregate_sum_path_query` comment.
337        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    /// Convert a single range where-clause + value into the grovedb
411    /// `QueryItem` used to walk children of the property-name
412    /// `ProvableSumTree`. The clause's value is serialized via the
413    /// document type's `serialize_value_for_key`, which produces the
414    /// canonical bytes used everywhere else in the index path.
415    ///
416    /// Identical to count's analog — sum-agnostic operator mapping.
417    /// See count's `range_clause_to_query_item` for the per-operator
418    /// docs.
419    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                // Byte-wise carry propagation. Strip trailing 0xFFs (they
498                // already cover the entire byte range) and increment the
499                // first non-0xFF byte from the right. This correctly
500                // handles prefixes like [0x12, 0xFF] → upper bound [0x13].
501                // Only fail if every byte is 0xFF (no representable
502                // exclusive upper bound).
503                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    /// Build the grovedb `PathQuery` for a per-distinct-key range-sum
529    /// proof / no-proof walk against this query's `rangeSummable`
530    /// index. Sum analog of count's `distinct_count_path_query` — the
531    /// path-query shape is structurally identical (range on the
532    /// terminator + outer `Key`s per `In` value on a prefix prop, if
533    /// any). The only difference is at proof-emission time:
534    /// the terminator's value tree is a `SumTree` (vs `CountTree` on
535    /// the count side), so grovedb emits `KVSum` ops instead of
536    /// `KVCount`. The path-query bytes the prover and verifier
537    /// reconstruct are the same on both sides.
538    ///
539    /// `left_to_right` flips both the outer Query (when there's an
540    /// `In` on prefix) and the subquery direction so the iteration
541    /// walks `(in_key, terminator_key)` tuples in the requested
542    /// order — descending on `left_to_right = false` walks the In
543    /// dimension lex-descending too, not just the inner range.
544    ///
545    /// Errors:
546    /// - No range where-clause / multiple range where-clauses
547    /// - Multiple In clauses on prefix props
548    /// - Non-Equal-non-In operator on a prefix prop
549    /// - Missing prefix clause
550    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        // `Some(keys)` once an In clause has been encountered on a
587        // prefix property. From that point on, subsequent Equal
588        // clauses go into `subquery_path_extension` rather than
589        // `base_path`. Only one In allowed (multiple Ins would
590        // multiply the fork count beyond what a single Query can
591        // express via `set_subquery_path`).
592        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                    // Path stops at the In-bearing prop's property-
633                    // name subtree; outer Query lives at that level.
634                    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                    // Same sort + parity rationale as count's
647                    // `distinct_count_path_query` — see the long
648                    // docstring there. Prover and verifier share
649                    // this builder so the sort happens identically
650                    // on both sides; without it, descending walks
651                    // and pushed-limit pagination produce gibberish.
652                    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                // Flat shape — path includes terminator, single
668                // range-only Query.
669                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                // Compound shape — outer Query has one Key per In
679                // value at the In-bearing prop's property-name
680                // subtree. `subquery_path` carries any post-In
681                // Equal pairs + terminator. Subquery is the range
682                // item. `left_to_right` applies to both layers so
683                // descending iteration walks `(in_key_desc,
684                // key_desc)` tuples consistently.
685                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    /// Build the grovedb `PathQuery` for a **carrier**
706    /// `AggregateSumOnRange` proof — one outer Key per `In`
707    /// value (or one outer QueryItem per outer-range match), each
708    /// terminating in an ASOR boundary walk over the per-branch
709    /// range subtree. Returns one `(in_key, i64)` pair per resolved
710    /// In branch via [`grovedb::GroveDb::query_aggregate_sum_per_key`]
711    /// (no-proof) and
712    /// [`grovedb::GroveDb::verify_aggregate_sum_query_per_key`]
713    /// (verify), once those primitives ship.
714    ///
715    /// Required where-clause shape (validated upstream by
716    /// [`crate::query::drive_document_sum_query::drive_dispatcher::detect_sum_mode`]
717    /// routing to [`DocumentSumMode::RangeAggregateCarrierProof`]):
718    /// - Exactly one `In` clause on the In-property
719    /// - Exactly one range clause on the *terminator* property of
720    ///   a `rangeSummable: true` index whose first property is
721    ///   the In-property
722    /// - Any prefix properties between In and range must use
723    ///   `==` (mirror of [`Self::aggregate_sum_path_query`]'s
724    ///   non-In prefix rule)
725    ///
726    /// Path-query structure (mirror of count's analog —
727    /// [`crate::query::drive_document_count_query::path_query::DriveDocumentCountQuery::carrier_aggregate_count_path_query`]):
728    /// - Outer path stops one level above the In-bearing property
729    ///   subtree's children (`@/doc_prefix/0x01/doctype/<In-prop>`).
730    /// - Outer Query: `Key(in_value_0)`, `Key(in_value_1)`, … in
731    ///   lex-asc serialized order (grovedb's multi-key walker
732    ///   invariant — required for prove/verify byte-parity).
733    /// - `subquery_path`: the terminator property name (and any
734    ///   trailing `==` clause names between In and range, in
735    ///   index order).
736    /// - `subquery`: `Query::new_aggregate_sum_on_range(range_item)`.
737    ///
738    /// Both the executor and the verifier consume the `PathQuery`
739    /// this builder produces. Grovedb PR #670 (head `e98bab5f`)
740    /// landed carrier-`AggregateSumOnRange` support
741    /// (`Query::validate_carrier_aggregate_sum_on_range` and
742    /// `GroveDb::verify_aggregate_sum_query_per_key`), so the
743    /// builder's output flows directly through `prove_query` and the
744    /// verifier on both sides.
745    ///
746    /// Errors:
747    /// - No range where-clause / multiple range where-clauses →
748    ///   `InvalidWhereClauseComponents`
749    /// - No In where-clause → `InvalidWhereClauseComponents`
750    /// - In on a non-prefix property → `InvalidWhereClauseComponents`
751    /// - Prefix property between In and range uses non-Equal →
752    ///   `InvalidWhereClauseComponents`
753    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        // The terminator property (last in the index) carries the
760        // ASOR target range. The "carrier" property — the one whose
761        // clause becomes the outer Query items — is either:
762        // - An `In` clause (G7 shape: one Key per In value)
763        // - A range clause on a prefix prop (G8 shape: one QueryItem
764        //   bounding the outer range, with `SizedQuery::limit` capping
765        //   how many outer matches the carrier walks)
766        //
767        // The terminator's clause must be a range and is converted to
768        // the inner ASOR `QueryItem`. Any properties between the
769        // carrier and the terminator must use `==` and extend the
770        // subquery_path.
771        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        // Carrier clause state: either `None` (not seen yet, still on
803        // the `==`-prefix run), `Some(In)` (G7), or `Some(Range)` (G8).
804        // Mirror of count's analog (drive_document_count_query/
805        // path_query.rs's `Carrier` enum).
806        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                // Build one Key per In value, sorted lex-ascending —
887                // grovedb's multi-key walker invariant (same convention
888                // as count's carrier and the SDK's verifier-side
889                // rebuild).
890                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                // Single QueryItem bounding the outer range. The
909                // carrier walks this range and emits one `(key, i64)`
910                // pair per matched outer key.
911                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        // `SizedQuery::limit` mirrors count's carrier:
920        // - For In-outer carriers the |IN| array already bounds the
921        //   result, so `limit` is typically `None`.
922        // - For Range-outer carriers `limit` caps the outer walk and
923        //   is load-bearing for proof bytes — must match prover/
924        //   verifier for the merk-root recomputation.
925        Ok(PathQuery::new(
926            base_path,
927            SizedQuery::new(outer_query, limit, None),
928        ))
929    }
930
931    /// Combined PCPS (`ProvableCountProvableSumTree`) carrier variant:
932    /// outer In or outer range, inner range carrying both per-bucket
933    /// count AND per-bucket sum via grovedb's
934    /// `AggregateCountAndSumOnRange` primitive. The terminator
935    /// property's value tree must be PCPS (the index must declare
936    /// BOTH `rangeCountable: true` AND `rangeSummable: true`).
937    ///
938    /// PCPS-only — `ProvableSumTree` / `ProvableCountTree` /
939    /// `ProvableCountSumTree` (the per-axis or root-only sum
940    /// variants) reject the query item at the prover. Returns one
941    /// `(outer_key, u64 count, i64 sum)` triple per resolved In
942    /// branch. Verified client-side via
943    /// `GroveDb::verify_aggregate_count_and_sum_query_per_key`
944    /// (grovedb develop (PR #670 merged; head `e98bab5f` as of this PR)).
945    ///
946    /// Same outer/subquery topology as
947    /// [`Self::carrier_aggregate_sum_path_query`] — the only
948    /// difference is the inner aggregation primitive
949    /// (`Query::new_aggregate_count_and_sum_on_range` vs.
950    /// `Query::new_aggregate_sum_on_range`) and the additional
951    /// PCPS gate.
952    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        // Same Carrier state-machine as the sum-only variant.
999        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// ─── Static / free-function wrappers for the bench + verifier-side
1117// rebuild. These re-pick the covering index from the document type
1118// (vs. the instance methods above which use the already-resolved
1119// `self.index`). ────────────────────────────────────────────────────
1120
1121#[cfg(any(feature = "server", feature = "verify"))]
1122impl<'a> DriveDocumentSumQuery<'a> {
1123    /// Static wrapper for the bench / verifier-side rebuild. Calls
1124    /// the instance method via a temporary `DriveDocumentSumQuery`
1125    /// built from the picked covering index.
1126    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    /// Static wrapper for the bench / verifier-side rebuild — picks the
1164    /// covering range-summable index and delegates to the instance
1165    /// method.
1166    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    /// Static wrapper for the bench / verifier-side rebuild — picks
1204    /// the covering range-summable index and delegates to the carrier
1205    /// instance method. Mirror of count's analog
1206    /// [`crate::query::drive_document_count_query::path_query::DriveDocumentCountQuery::carrier_aggregate_count_path_query`]'s
1207    /// implicit static surface via the executor.
1208    /// Used by the SDK verifier-side rebuild via
1209    /// `GroveDb::verify_aggregate_sum_query_per_key` (grovedb PR #670
1210    /// head `e98bab5f`).
1211    #[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// ── Carrier-shape unit tests ───────────────────────────────────────
1253//
1254// The carrier builder is pure Rust data construction — no grovedb
1255// interaction — so it can be exercised today regardless of the upstream
1256// grovedb prover gating. Tests assert the structural invariants the
1257// prover/verifier will require once the sister PR lands:
1258// - outer path stops at the In-bearing property-name subtree;
1259// - outer Query has Key items in lex-asc serialized order;
1260// - default_subquery_branch.subquery is a single
1261//   `AggregateSumOnRange(inner)`;
1262// - subquery_path is the (post-In Equals + terminator name) chain.
1263//
1264// These tests pin the carrier path-query shape so a future refactor of
1265// the builder body can't silently drift from what the verifier will
1266// rebuild on its side.
1267#[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        // The tip-jar contract has a rangeSummable index on
1280        // `(recipient, sentAt)` (`byRecipientTime`) with
1281        // `summable: "amount"` — exactly the shape the carrier targets:
1282        // outer In on `recipient`, inner range on `sentAt`.
1283        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    /// Helper — given a contract and a `(doctype, index)` name pair,
1292    /// resolve the [`DocumentTypeRef`] for the doctype.
1293    ///
1294    /// The matching `Index` is fetched inside each test via
1295    /// `doc_type.indexes().get(index_name)` rather than returned
1296    /// alongside `doc_type` here, because the index reference is
1297    /// bound to the doc_type's lifetime (not the contract's), so
1298    /// returning both from a helper would create a self-referential
1299    /// tuple. The two-step pattern (`pick_doc_type` here, then
1300    /// `.indexes().get(...)` at the call site) is the same pattern
1301    /// count's tests use.
1302    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    /// Two recipient byte-array values for In-on-carrier tests. We
1312    /// pick values in non-lex order so the builder's sort step is
1313    /// observable in the resulting Key item order.
1314    fn recipient_a() -> Vec<u8> {
1315        // Bytes starting with 0x80 — lex-greater.
1316        let mut v = vec![0x80u8; 32];
1317        v[31] = 0x01;
1318        v
1319    }
1320    fn recipient_b() -> Vec<u8> {
1321        // Bytes starting with 0x10 — lex-less.
1322        let mut v = vec![0x10u8; 32];
1323        v[31] = 0x02;
1324        v
1325    }
1326
1327    /// G7 — In on carrier + range on terminator. Asserts outer Query
1328    /// has one `Key` per In value (lex-sorted), subquery is
1329    /// `AggregateSumOnRange(inner_range)`, and subquery_path is just
1330    /// the terminator's property-name segment.
1331    #[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        // `byRecipientTime` is `[recipient, sentAt]` with
1342        // `summable: "amount"` + `rangeSummable: true`. Provide the
1343        // In values out of lex order so the builder's lex-sort is
1344        // observable.
1345        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        // base_path = [contract-docs-root, contract_id, 0x01,
1375        // doctype_name, "recipient"]. The outer Keys live under the
1376        // "recipient" property-name subtree.
1377        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        // Outer Query: one Key per In value, lex-sorted (the
1388        // builder's `.sort()` step turns the unsorted user input into
1389        // the prover/verifier-agreement lex-asc order).
1390        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        // Subquery_path = ["sentAt"] (just the terminator's name).
1400        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        // Subquery is `AggregateSumOnRange(inner_range)`.
1410        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    /// G7 — same as above but `limit = Some(N)` flows into
1422    /// `SizedQuery::limit` so the prover/verifier sides agree on the
1423    /// outer-walk cap byte-for-byte.
1424    #[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    /// Missing terminator range → `InvalidWhereClauseComponents`.
1465    #[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    /// Missing carrier (no In or outer range on a prefix prop) →
1502    /// `InvalidWhereClauseComponents`.
1503    #[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        // Equal on prefix + range on terminator — *no* carrier. This
1514        // is the `aggregate_sum_path_query` shape, not the carrier
1515        // shape; the carrier builder must reject because the carrier
1516        // state stays `Pending` through the prefix loop.
1517        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}