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::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};
31// `serialize_value_for_key` is a `DocumentTypeV0Methods` method, NOT
32// `DocumentTypeBasicMethods` (which is the trait of versionless basic
33// helpers). The serializer routes through a versioned dispatcher
34// (`serialize_value_for_key_v0` + friends), so it lives on the
35// versioned-methods trait.
36use 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
42/// Storage convention: the count/sum tree under a non-rangeSummable
43/// value tree lives at child key `[0]` (the ref bucket). Same convention
44/// as count's `COUNT_TREE_KEY`.
45const SUM_TREE_KEY: u8 = 0;
46
47#[cfg(any(feature = "server", feature = "verify"))]
48impl<'a> DriveDocumentSumQuery<'a> {
49    /// Build the `PathQuery` for the primary-key SumTree fast path
50    /// (used when `documents_summable` is set and the query has no
51    /// `where` clauses).
52    ///
53    /// Mirrors count's `primary_key_count_tree_path_query` signature
54    /// — takes the two scalar arguments (`contract_id`,
55    /// `document_type_name`) that are the only fields actually used.
56    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    /// Instance-method form of [`Self::point_lookup_sum_path_query_static`]
72    /// — uses `self.index` (already resolved by the picker) rather than
73    /// re-picking from the document type. Mirrors count's
74    /// `point_lookup_count_path_query` shape.
75    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                // Only the LAST property carries no clause on a
100                // `summableOffCountIndex` index that reads its last
101                // property's tree ([`prefix_to_last_sum_reads`], checked
102                // before the sum-chain form): the sum is that tree's own
103                // element, read by its level key from the last pin's value
104                // tree, the shape count's builds over the same tree.
105                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                // Sum-chain value-tree read: when the deepest pinned
114                // property's level sits at or below the index's shallowest
115                // sum or average `at` level (a `summableOffCountIndex`
116                // index), its value trees sum their whole subtree, and the
117                // selector below reads the deepest pin's value tree as the
118                // terminator's. The pins must form a contiguous prefix.
119                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        // Sum-tree terminator optimization: every summable terminator's
193        // value tree is a SumTree (continuations NonCounted-wrapped),
194        // so the proof can stop at the value tree without descending
195        // to the `[0]` ref bucket. Mirror of count's
196        // `count_tree_terminator` gate (uses `is_countable()` on count
197        // side; on the sum side, `summable.is_some()` is the right
198        // discriminator). A `summableOffCountIndex` index keeps a `SumItem`
199        // counter at that key instead, read the same way.
200        let sum_tree_terminator = self.index.summed_value_name().is_some();
201
202        match in_outer_keys {
203            None => {
204                // Equal-only, fully covered.
205                let mut query = Query::new();
206                if sum_tree_terminator {
207                    // Lift the last serialized value off the path: the
208                    // terminator's value tree is a SumTree directly, so
209                    // we ask for it as a Key under the property-name
210                    // subtree.
211                    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                // Compound shape with In at some position.
226                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                        // Outer Keys already point at the SumTree value
234                        // trees themselves; no subquery needed.
235                    } 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    /// Instance-method form: builds the `AggregateSumOnRange` path
265    /// query against `self.index` (resolved upstream by the
266    /// `find_range_summable_index_for_where_clauses` picker). The
267    /// terminator's range clause is required; prefix properties must
268    /// use `==`.
269    pub fn aggregate_sum_path_query(
270        &self,
271        platform_version: &PlatformVersion,
272    ) -> Result<PathQuery, Error> {
273        self.refuse_a_range_sum_total()?;
274        // Bind the range clause to the index's *terminator* property so a
275        // request with multiple range-like clauses (e.g. `prefix > x AND
276        // terminator > y`) picks the right one. The previous predicate
277        // returned the first range operator and could pick the prefix.
278        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        // grovedb PR 670 surface: `Query::new_aggregate_sum_on_range`.
349        let query = Query::new_aggregate_sum_on_range(query_item);
350        Ok(PathQuery::new(path, SizedQuery::new(query, None, None)))
351    }
352
353    /// Refuses an index whose path passes through a ranked level, which no
354    /// range total can be read through (see
355    /// [`refuse_a_range_total_through_a_ranked_index`]).
356    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    /// Instance-method form: builds the combined PCPS
361    /// `AggregateCountAndSumOnRange` path query against `self.index`.
362    /// Requires the index to declare BOTH `rangeCountable: true` AND
363    /// `rangeSummable: true`.
364    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        // Bind to the terminator property — see the sibling
379        // `aggregate_sum_path_query` comment.
380        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    /// Convert a single range where-clause + value into the grovedb
454    /// `QueryItem` used to walk children of the property-name
455    /// `ProvableSumTree`. The clause's value is serialized via the
456    /// document type's `serialize_value_for_key`, which produces the
457    /// canonical bytes used everywhere else in the index path.
458    ///
459    /// Identical to count's analog — sum-agnostic operator mapping.
460    /// See count's `range_clause_to_query_item` for the per-operator
461    /// docs.
462    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                // Byte-wise carry propagation. Strip trailing 0xFFs (they
541                // already cover the entire byte range) and increment the
542                // first non-0xFF byte from the right. This correctly
543                // handles prefixes like [0x12, 0xFF] → upper bound [0x13].
544                // Only fail if every byte is 0xFF (no representable
545                // exclusive upper bound).
546                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    /// Build the grovedb `PathQuery` for a per-distinct-key range-sum
572    /// proof / no-proof walk against this query's `rangeSummable`
573    /// index. Sum analog of count's `distinct_count_path_query` — the
574    /// path-query shape is structurally identical (range on the
575    /// terminator + outer `Key`s per `In` value on a prefix prop, if
576    /// any). The only difference is at proof-emission time:
577    /// the terminator's value tree is a `SumTree` (vs `CountTree` on
578    /// the count side), so grovedb emits `KVSum` ops instead of
579    /// `KVCount`. The path-query bytes the prover and verifier
580    /// reconstruct are the same on both sides.
581    ///
582    /// `left_to_right` flips both the outer Query (when there's an
583    /// `In` on prefix) and the subquery direction so the iteration
584    /// walks `(in_key, terminator_key)` tuples in the requested
585    /// order — descending on `left_to_right = false` walks the In
586    /// dimension lex-descending too, not just the inner range.
587    ///
588    /// Errors:
589    /// - No range where-clause / multiple range where-clauses
590    /// - Multiple In clauses on prefix props
591    /// - Non-Equal-non-In operator on a prefix prop
592    /// - Missing prefix clause
593    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        // `Some(keys)` once an In clause has been encountered on a
630        // prefix property. From that point on, subsequent Equal
631        // clauses go into `subquery_path_extension` rather than
632        // `base_path`. Only one In allowed (multiple Ins would
633        // multiply the fork count beyond what a single Query can
634        // express via `set_subquery_path`).
635        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                    // Path stops at the In-bearing prop's property-
676                    // name subtree; outer Query lives at that level.
677                    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                    // Same sort + parity rationale as count's
690                    // `distinct_count_path_query` — see the long
691                    // docstring there. Prover and verifier share
692                    // this builder so the sort happens identically
693                    // on both sides; without it, descending walks
694                    // and pushed-limit pagination produce gibberish.
695                    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                // Flat shape — path includes terminator, single
711                // range-only Query.
712                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                // Compound shape — outer Query has one Key per In
722                // value at the In-bearing prop's property-name
723                // subtree. `subquery_path` carries any post-In
724                // Equal pairs + terminator. Subquery is the range
725                // item. `left_to_right` applies to both layers so
726                // descending iteration walks `(in_key_desc,
727                // key_desc)` tuples consistently.
728                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    /// Build the grovedb `PathQuery` for a **carrier**
749    /// `AggregateSumOnRange` proof — one outer Key per `In`
750    /// value (or one outer QueryItem per outer-range match), each
751    /// terminating in an ASOR boundary walk over the per-branch
752    /// range subtree. Returns one `(in_key, i64)` pair per resolved
753    /// In branch via [`grovedb::GroveDb::query_aggregate_sum_per_key`]
754    /// (no-proof) and
755    /// [`grovedb::GroveDb::verify_aggregate_sum_query_per_key`]
756    /// (verify), once those primitives ship.
757    ///
758    /// Required where-clause shape (validated upstream by
759    /// [`crate::query::drive_document_sum_query::drive_dispatcher::detect_sum_mode`]
760    /// routing to [`DocumentSumMode::RangeAggregateCarrierProof`]):
761    /// - Exactly one `In` clause on the In-property
762    /// - Exactly one range clause on the *terminator* property of
763    ///   a `rangeSummable: true` index whose first property is
764    ///   the In-property
765    /// - Any prefix properties between In and range must use
766    ///   `==` (mirror of [`Self::aggregate_sum_path_query`]'s
767    ///   non-In prefix rule)
768    ///
769    /// Path-query structure (mirror of count's analog —
770    /// [`crate::query::drive_document_count_query::path_query::DriveDocumentCountQuery::carrier_aggregate_count_path_query`]):
771    /// - Outer path stops one level above the In-bearing property
772    ///   subtree's children (`@/doc_prefix/0x01/doctype/<In-prop>`).
773    /// - Outer Query: `Key(in_value_0)`, `Key(in_value_1)`, … in
774    ///   lex-asc serialized order (grovedb's multi-key walker
775    ///   invariant — required for prove/verify byte-parity).
776    /// - `subquery_path`: the terminator property name (and any
777    ///   trailing `==` clause names between In and range, in
778    ///   index order).
779    /// - `subquery`: `Query::new_aggregate_sum_on_range(range_item)`.
780    ///
781    /// Both the executor and the verifier consume the `PathQuery`
782    /// this builder produces. Grovedb PR #670 (head `e98bab5f`)
783    /// landed carrier-`AggregateSumOnRange` support
784    /// (`Query::validate_carrier_aggregate_sum_on_range` and
785    /// `GroveDb::verify_aggregate_sum_query_per_key`), so the
786    /// builder's output flows directly through `prove_query` and the
787    /// verifier on both sides.
788    ///
789    /// Errors:
790    /// - No range where-clause / multiple range where-clauses →
791    ///   `InvalidWhereClauseComponents`
792    /// - No In where-clause → `InvalidWhereClauseComponents`
793    /// - In on a non-prefix property → `InvalidWhereClauseComponents`
794    /// - Prefix property between In and range uses non-Equal →
795    ///   `InvalidWhereClauseComponents`
796    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        // No range total through a ranked level (see the helper).
803        self.refuse_a_range_sum_total()?;
804        // The terminator property (last in the index) carries the
805        // ASOR target range. The "carrier" property — the one whose
806        // clause becomes the outer Query items — is either:
807        // - An `In` clause (G7 shape: one Key per In value)
808        // - A range clause on a prefix prop (G8 shape: one QueryItem
809        //   bounding the outer range, with `SizedQuery::limit` capping
810        //   how many outer matches the carrier walks)
811        //
812        // The terminator's clause must be a range and is converted to
813        // the inner ASOR `QueryItem`. Any properties between the
814        // carrier and the terminator must use `==` and extend the
815        // subquery_path.
816        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        // Carrier clause state: either `None` (not seen yet, still on
848        // the `==`-prefix run), `Some(In)` (G7), or `Some(Range)` (G8).
849        // Mirror of count's analog (drive_document_count_query/
850        // path_query.rs's `Carrier` enum).
851        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                // Build one Key per In value, sorted lex-ascending —
932                // grovedb's multi-key walker invariant (same convention
933                // as count's carrier and the SDK's verifier-side
934                // rebuild).
935                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                // Single QueryItem bounding the outer range. The
954                // carrier walks this range and emits one `(key, i64)`
955                // pair per matched outer key.
956                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        // `SizedQuery::limit` mirrors count's carrier:
965        // - For In-outer carriers the |IN| array already bounds the
966        //   result, so `limit` is typically `None`.
967        // - For Range-outer carriers `limit` caps the outer walk and
968        //   is load-bearing for proof bytes — must match prover/
969        //   verifier for the merk-root recomputation.
970        Ok(PathQuery::new(
971            base_path,
972            SizedQuery::new(outer_query, limit, None),
973        ))
974    }
975
976    /// Combined PCPS (`ProvableCountProvableSumTree`) carrier variant:
977    /// outer In or outer range, inner range carrying both per-bucket
978    /// count AND per-bucket sum via grovedb's
979    /// `AggregateCountAndSumOnRange` primitive. The terminator
980    /// property's value tree must be PCPS (the index must declare
981    /// BOTH `rangeCountable: true` AND `rangeSummable: true`).
982    ///
983    /// PCPS-only — `ProvableSumTree` / `ProvableCountTree` /
984    /// `ProvableCountSumTree` (the per-axis or root-only sum
985    /// variants) reject the query item at the prover. Returns one
986    /// `(outer_key, u64 count, i64 sum)` triple per resolved In
987    /// branch. Verified client-side via
988    /// `GroveDb::verify_aggregate_count_and_sum_query_per_key`
989    /// (grovedb develop (PR #670 merged; head `e98bab5f` as of this PR)).
990    ///
991    /// Same outer/subquery topology as
992    /// [`Self::carrier_aggregate_sum_path_query`] — the only
993    /// difference is the inner aggregation primitive
994    /// (`Query::new_aggregate_count_and_sum_on_range` vs.
995    /// `Query::new_aggregate_sum_on_range`) and the additional
996    /// PCPS gate.
997    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        // No range total through a ranked level (see the helper).
1004        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        // Same Carrier state-machine as the sum-only variant.
1046        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// ─── Static / free-function wrappers for the bench + verifier-side
1164// rebuild. These re-pick the covering index from the document type
1165// (vs. the instance methods above which use the already-resolved
1166// `self.index`). ────────────────────────────────────────────────────
1167
1168#[cfg(any(feature = "server", feature = "verify"))]
1169impl<'a> DriveDocumentSumQuery<'a> {
1170    /// Static wrapper for the bench / verifier-side rebuild. Calls
1171    /// the instance method via a temporary `DriveDocumentSumQuery`
1172    /// built from the picked covering index.
1173    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    /// Static wrapper for the bench / verifier-side rebuild — picks the
1211    /// covering range-summable index and delegates to the instance
1212    /// method.
1213    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    /// Static wrapper for the bench / verifier-side rebuild — picks
1251    /// the covering range-summable index and delegates to the carrier
1252    /// instance method. Mirror of count's analog
1253    /// [`crate::query::drive_document_count_query::path_query::DriveDocumentCountQuery::carrier_aggregate_count_path_query`]'s
1254    /// implicit static surface via the executor.
1255    /// Used by the SDK verifier-side rebuild via
1256    /// `GroveDb::verify_aggregate_sum_query_per_key` (grovedb PR #670
1257    /// head `e98bab5f`).
1258    #[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// ── Carrier-shape unit tests ───────────────────────────────────────
1300//
1301// The carrier builder is pure Rust data construction — no grovedb
1302// interaction — so it can be exercised today regardless of the upstream
1303// grovedb prover gating. Tests assert the structural invariants the
1304// prover/verifier will require once the sister PR lands:
1305// - outer path stops at the In-bearing property-name subtree;
1306// - outer Query has Key items in lex-asc serialized order;
1307// - default_subquery_branch.subquery is a single
1308//   `AggregateSumOnRange(inner)`;
1309// - subquery_path is the (post-In Equals + terminator name) chain.
1310//
1311// These tests pin the carrier path-query shape so a future refactor of
1312// the builder body can't silently drift from what the verifier will
1313// rebuild on its side.
1314#[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        // The tip-jar contract has a rangeSummable index on
1327        // `(recipient, sentAt)` (`byRecipientTime`) with
1328        // `summable: "amount"` — exactly the shape the carrier targets:
1329        // outer In on `recipient`, inner range on `sentAt`.
1330        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    /// Helper — given a contract and a `(doctype, index)` name pair,
1339    /// resolve the [`DocumentTypeRef`] for the doctype.
1340    ///
1341    /// The matching `Index` is fetched inside each test via
1342    /// `doc_type.indexes().get(index_name)` rather than returned
1343    /// alongside `doc_type` here, because the index reference is
1344    /// bound to the doc_type's lifetime (not the contract's), so
1345    /// returning both from a helper would create a self-referential
1346    /// tuple. The two-step pattern (`pick_doc_type` here, then
1347    /// `.indexes().get(...)` at the call site) is the same pattern
1348    /// count's tests use.
1349    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    /// Two recipient byte-array values for In-on-carrier tests. We
1359    /// pick values in non-lex order so the builder's sort step is
1360    /// observable in the resulting Key item order.
1361    fn recipient_a() -> Vec<u8> {
1362        // Bytes starting with 0x80 — lex-greater.
1363        let mut v = vec![0x80u8; 32];
1364        v[31] = 0x01;
1365        v
1366    }
1367    fn recipient_b() -> Vec<u8> {
1368        // Bytes starting with 0x10 — lex-less.
1369        let mut v = vec![0x10u8; 32];
1370        v[31] = 0x02;
1371        v
1372    }
1373
1374    /// G7 — In on carrier + range on terminator. Asserts outer Query
1375    /// has one `Key` per In value (lex-sorted), subquery is
1376    /// `AggregateSumOnRange(inner_range)`, and subquery_path is just
1377    /// the terminator's property-name segment.
1378    #[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        // `byRecipientTime` is `[recipient, sentAt]` with
1389        // `summable: "amount"` + `rangeSummable: true`. Provide the
1390        // In values out of lex order so the builder's lex-sort is
1391        // observable.
1392        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        // base_path = [contract-docs-root, contract_id, 0x01,
1422        // doctype_name, "recipient"]. The outer Keys live under the
1423        // "recipient" property-name subtree.
1424        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        // Outer Query: one Key per In value, lex-sorted (the
1435        // builder's `.sort()` step turns the unsorted user input into
1436        // the prover/verifier-agreement lex-asc order).
1437        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        // Subquery_path = ["sentAt"] (just the terminator's name).
1447        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        // Subquery is `AggregateSumOnRange(inner_range)`.
1457        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    /// G7 — same as above but `limit = Some(N)` flows into
1469    /// `SizedQuery::limit` so the prover/verifier sides agree on the
1470    /// outer-walk cap byte-for-byte.
1471    #[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    /// Missing terminator range → `InvalidWhereClauseComponents`.
1512    #[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    /// Missing carrier (no In or outer range on a prefix prop) →
1549    /// `InvalidWhereClauseComponents`.
1550    #[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        // Equal on prefix + range on terminator — *no* carrier. This
1561        // is the `aggregate_sum_path_query` shape, not the carrier
1562        // shape; the carrier builder must reject because the carrier
1563        // state stays `Pending` through the prefix loop.
1564        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}