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::{WhereClause, WhereOperator};
26// `serialize_value_for_key` is a `DocumentTypeV0Methods` method, NOT
27// `DocumentTypeBasicMethods` (which is the trait of versionless basic
28// helpers). The serializer routes through a versioned dispatcher
29// (`serialize_value_for_key_v0` + friends), so it lives on the
30// versioned-methods trait.
31use dpp::data_contract::document_type::methods::DocumentTypeV0Methods;
32use dpp::data_contract::document_type::DocumentTypeRef;
33use dpp::data_contract::DataContract;
34use dpp::version::PlatformVersion;
35use grovedb::{PathQuery, Query, QueryItem, SizedQuery};
36
37/// Storage convention: the count/sum tree under a non-rangeSummable
38/// value tree lives at child key `[0]` (the ref bucket). Same convention
39/// as count's `COUNT_TREE_KEY`.
40const SUM_TREE_KEY: u8 = 0;
41
42#[cfg(any(feature = "server", feature = "verify"))]
43impl<'a> DriveDocumentSumQuery<'a> {
44    /// Build the `PathQuery` for the primary-key SumTree fast path
45    /// (used when `documents_summable` is set and the query has no
46    /// `where` clauses).
47    ///
48    /// Mirrors count's `primary_key_count_tree_path_query` signature
49    /// — takes the two scalar arguments (`contract_id`,
50    /// `document_type_name`) that are the only fields actually used.
51    pub fn primary_key_sum_path_query(
52        contract_id: [u8; 32],
53        document_type_name: &str,
54    ) -> PathQuery {
55        let path = vec![
56            vec![RootTree::DataContractDocuments as u8],
57            contract_id.to_vec(),
58            vec![1u8],
59            document_type_name.as_bytes().to_vec(),
60        ];
61        let mut query = Query::new();
62        query.insert_key(vec![SUM_TREE_KEY]);
63        PathQuery::new(path, SizedQuery::new(query, None, None))
64    }
65
66    /// Instance-method form of [`Self::point_lookup_sum_path_query_static`]
67    /// — uses `self.index` (already resolved by the picker) rather than
68    /// re-picking from the document type. Mirrors count's
69    /// `point_lookup_count_path_query` shape.
70    pub fn point_lookup_sum_path_query(
71        &self,
72        platform_version: &PlatformVersion,
73    ) -> Result<PathQuery, Error> {
74        if self.index.properties.is_empty() {
75            return Err(Error::Query(
76                QuerySyntaxError::InvalidWhereClauseComponents(
77                    "point_lookup_sum_path_query: index must have at least one property",
78                ),
79            ));
80        }
81
82        let mut base_path: Vec<Vec<u8>> = vec![
83            vec![RootTree::DataContractDocuments as u8],
84            self.contract_id.to_vec(),
85            vec![1u8],
86            self.document_type_name.as_bytes().to_vec(),
87        ];
88
89        let mut in_outer_keys: Option<Vec<Vec<u8>>> = None;
90        let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
91
92        for prop in self.index.properties.iter() {
93            let clause = self
94                .where_clauses
95                .iter()
96                .find(|wc| wc.field == prop.name)
97                .ok_or_else(|| {
98                    Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
99                        "prove sum requires the where clauses to fully cover the \
100                         summable index; one or more index properties have no matching \
101                         `==` or `in` clause — define a more specific summable index \
102                         (with `summable: \"<prop>\"` whose properties exactly equal \
103                         the clauses) or use `prove=false`",
104                    ))
105                })?;
106
107            match clause.operator {
108                WhereOperator::Equal => {
109                    let serialized = self.document_type.serialize_value_for_key(
110                        prop.name.as_str(),
111                        &clause.value,
112                        platform_version,
113                    )?;
114                    if in_outer_keys.is_some() {
115                        subquery_path_extension.push(prop.name.as_bytes().to_vec());
116                        subquery_path_extension.push(serialized);
117                    } else {
118                        base_path.push(prop.name.as_bytes().to_vec());
119                        base_path.push(serialized);
120                    }
121                }
122                WhereOperator::In => {
123                    if in_outer_keys.is_some() {
124                        return Err(Error::Query(
125                            QuerySyntaxError::InvalidWhereClauseComponents(
126                                "prove sum: at most one `in` clause is supported on the \
127                                 covering summable index",
128                            ),
129                        ));
130                    }
131                    base_path.push(prop.name.as_bytes().to_vec());
132                    let in_values = clause.in_values().into_data_with_error()??;
133                    let mut keys: Vec<Vec<u8>> = in_values
134                        .iter()
135                        .map(|v| {
136                            self.document_type.serialize_value_for_key(
137                                prop.name.as_str(),
138                                v,
139                                platform_version,
140                            )
141                        })
142                        .collect::<Result<_, _>>()?;
143                    keys.sort();
144                    in_outer_keys = Some(keys);
145                }
146                _ => {
147                    return Err(Error::Query(
148                        QuerySyntaxError::InvalidWhereClauseComponents(
149                            "point_lookup_sum_path_query: index properties must use \
150                             `==` or `in`",
151                        ),
152                    ));
153                }
154            }
155        }
156
157        // Sum-tree terminator optimization: every summable terminator's
158        // value tree is a SumTree (continuations NonCounted-wrapped),
159        // so the proof can stop at the value tree without descending
160        // to the `[0]` ref bucket. Mirror of count's
161        // `count_tree_terminator` gate (uses `is_countable()` on count
162        // side; on the sum side, `summable.is_some()` is the right
163        // discriminator).
164        let sum_tree_terminator = self.index.summable.is_some();
165
166        match in_outer_keys {
167            None => {
168                // Equal-only, fully covered.
169                let mut query = Query::new();
170                if sum_tree_terminator {
171                    // Lift the last serialized value off the path: the
172                    // terminator's value tree is a SumTree directly, so
173                    // we ask for it as a Key under the property-name
174                    // subtree.
175                    let last_value = base_path.pop().expect(
176                        "Equal-only loop pushes (name, value) per prop; \
177                         base_path must hold the terminator's serialized value",
178                    );
179                    query.insert_key(last_value);
180                } else {
181                    query.insert_key(vec![SUM_TREE_KEY]);
182                }
183                Ok(PathQuery::new(
184                    base_path,
185                    SizedQuery::new(query, None, None),
186                ))
187            }
188            Some(keys) => {
189                // Compound shape with In at some position.
190                let mut outer_query = Query::new();
191                for key in keys {
192                    outer_query.insert_key(key);
193                }
194
195                if subquery_path_extension.is_empty() {
196                    if sum_tree_terminator {
197                        // Outer Keys already point at the SumTree value
198                        // trees themselves; no subquery needed.
199                    } else {
200                        let mut subquery = Query::new();
201                        subquery.insert_key(vec![SUM_TREE_KEY]);
202                        outer_query.set_subquery(subquery);
203                    }
204                } else {
205                    let mut subquery = Query::new();
206                    if sum_tree_terminator {
207                        let termval = subquery_path_extension.pop().expect(
208                            "trailing-Equal loop pushes (name, value) pairs; \
209                             non-empty extension's tail must be the terminator's \
210                             serialized value",
211                        );
212                        subquery.insert_key(termval);
213                    } else {
214                        subquery.insert_key(vec![SUM_TREE_KEY]);
215                    }
216                    outer_query.set_subquery_path(subquery_path_extension);
217                    outer_query.set_subquery(subquery);
218                }
219
220                Ok(PathQuery::new(
221                    base_path,
222                    SizedQuery::new(outer_query, None, None),
223                ))
224            }
225        }
226    }
227
228    /// Instance-method form: builds the `AggregateSumOnRange` path
229    /// query against `self.index` (resolved upstream by the
230    /// `find_range_summable_index_for_where_clauses` picker). The
231    /// terminator's range clause is required; prefix properties must
232    /// use `==`.
233    pub fn aggregate_sum_path_query(
234        &self,
235        platform_version: &PlatformVersion,
236    ) -> Result<PathQuery, Error> {
237        // Bind the range clause to the index's *terminator* property so a
238        // request with multiple range-like clauses (e.g. `prefix > x AND
239        // terminator > y`) picks the right one. The previous predicate
240        // returned the first range operator and could pick the prefix.
241        let terminator_prop_name = &self
242            .index
243            .properties
244            .last()
245            .ok_or(Error::Query(
246                QuerySyntaxError::InvalidWhereClauseComponents(
247                    "range_summable index must have at least one property",
248                ),
249            ))?
250            .name;
251        let range_clause = self
252            .where_clauses
253            .iter()
254            .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
255            .ok_or(Error::Query(
256                QuerySyntaxError::InvalidWhereClauseComponents(
257                    "aggregate_sum_path_query requires a range where-clause on the index terminator property",
258                ),
259            ))?;
260        let query_item = self.range_clause_to_query_item(range_clause, platform_version)?;
261
262        let mut path = vec![
263            vec![RootTree::DataContractDocuments as u8],
264            self.contract_id.to_vec(),
265            vec![1u8],
266            self.document_type_name.as_bytes().to_vec(),
267        ];
268        let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
269        for prop in prefix_props {
270            let clause = self
271                .where_clauses
272                .iter()
273                .find(|wc| wc.field == prop.name)
274                .ok_or(Error::Query(
275                    QuerySyntaxError::InvalidWhereClauseComponents(
276                        "aggregate-sum proof: missing where clause for an index prefix property",
277                    ),
278                ))?;
279            if clause.operator != WhereOperator::Equal {
280                return Err(Error::Query(
281                    QuerySyntaxError::InvalidWhereClauseComponents(
282                        "aggregate-sum proof: prefix properties must use `==` (no `in`); use \
283                         `group_by = [in_field, range_field]` (carrier-aggregate variant) for \
284                         compound In-on-prefix sum queries",
285                    ),
286                ));
287            }
288            path.push(prop.name.as_bytes().to_vec());
289            path.push(self.document_type.serialize_value_for_key(
290                prop.name.as_str(),
291                &clause.value,
292                platform_version,
293            )?);
294        }
295        let range_prop_name = &self
296            .index
297            .properties
298            .last()
299            .ok_or(Error::Query(
300                QuerySyntaxError::InvalidWhereClauseComponents(
301                    "range_summable index must have at least one property",
302                ),
303            ))?
304            .name;
305        path.push(range_prop_name.as_bytes().to_vec());
306
307        // grovedb PR 670 surface: `Query::new_aggregate_sum_on_range`.
308        let query = Query::new_aggregate_sum_on_range(query_item);
309        Ok(PathQuery::new(path, SizedQuery::new(query, None, None)))
310    }
311
312    /// Instance-method form: builds the combined PCPS
313    /// `AggregateCountAndSumOnRange` path query against `self.index`.
314    /// Requires the index to declare BOTH `rangeCountable: true` AND
315    /// `rangeSummable: true`.
316    pub fn aggregate_count_and_sum_path_query(
317        &self,
318        platform_version: &PlatformVersion,
319    ) -> Result<PathQuery, Error> {
320        if !self.index.range_countable {
321            return Err(Error::Query(QuerySyntaxError::Unsupported(
322                "aggregate_count_and_sum_path_query: index must declare BOTH \
323                 `rangeCountable: true` AND `rangeSummable: true` to produce a PCPS \
324                 (ProvableCountProvableSumTree) property-name tree."
325                    .to_string(),
326            )));
327        }
328
329        // Bind to the terminator property — see the sibling
330        // `aggregate_sum_path_query` comment.
331        let terminator_prop_name = &self
332            .index
333            .properties
334            .last()
335            .ok_or(Error::Query(
336                QuerySyntaxError::InvalidWhereClauseComponents(
337                    "PCPS index must have at least one property",
338                ),
339            ))?
340            .name;
341        let range_clause = self
342            .where_clauses
343            .iter()
344            .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
345            .ok_or(Error::Query(
346                QuerySyntaxError::InvalidWhereClauseComponents(
347                    "aggregate_count_and_sum_path_query requires a range where-clause on the index terminator property",
348                ),
349            ))?;
350        let query_item = self.range_clause_to_query_item(range_clause, platform_version)?;
351
352        let mut path = vec![
353            vec![RootTree::DataContractDocuments as u8],
354            self.contract_id.to_vec(),
355            vec![1u8],
356            self.document_type_name.as_bytes().to_vec(),
357        ];
358        let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
359        for prop in prefix_props {
360            let clause = self
361                .where_clauses
362                .iter()
363                .find(|wc| wc.field == prop.name)
364                .ok_or(Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
365                    "aggregate-count-and-sum proof: missing where clause for an index prefix property",
366                )))?;
367            if clause.operator != WhereOperator::Equal {
368                return Err(Error::Query(
369                    QuerySyntaxError::InvalidWhereClauseComponents(
370                        "aggregate-count-and-sum proof: prefix properties must use `==` (no `in`)",
371                    ),
372                ));
373            }
374            path.push(prop.name.as_bytes().to_vec());
375            path.push(self.document_type.serialize_value_for_key(
376                prop.name.as_str(),
377                &clause.value,
378                platform_version,
379            )?);
380        }
381        let range_prop_name = &self
382            .index
383            .properties
384            .last()
385            .ok_or(Error::Query(
386                QuerySyntaxError::InvalidWhereClauseComponents(
387                    "range_countable + range_summable index must have at least one property",
388                ),
389            ))?
390            .name;
391        path.push(range_prop_name.as_bytes().to_vec());
392
393        let query = grovedb::Query::new_aggregate_count_and_sum_on_range(query_item);
394        Ok(PathQuery::new(
395            path,
396            grovedb::SizedQuery::new(query, None, None),
397        ))
398    }
399
400    /// Convert a single range where-clause + value into the grovedb
401    /// `QueryItem` used to walk children of the property-name
402    /// `ProvableSumTree`. The clause's value is serialized via the
403    /// document type's `serialize_value_for_key`, which produces the
404    /// canonical bytes used everywhere else in the index path.
405    ///
406    /// Identical to count's analog — sum-agnostic operator mapping.
407    /// See count's `range_clause_to_query_item` for the per-operator
408    /// docs.
409    fn range_clause_to_query_item(
410        &self,
411        clause: &WhereClause,
412        platform_version: &PlatformVersion,
413    ) -> Result<QueryItem, Error> {
414        let serialize = |v: &dpp::platform_value::Value| -> Result<Vec<u8>, Error> {
415            Ok(self.document_type.serialize_value_for_key(
416                clause.field.as_str(),
417                v,
418                platform_version,
419            )?)
420        };
421        let serialize_pair = || -> Result<(Vec<u8>, Vec<u8>), Error> {
422            let arr = clause.value.as_array().ok_or_else(|| {
423                Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
424                    "range bounds value must be a 2-element array",
425                ))
426            })?;
427            if arr.len() != 2 {
428                return Err(Error::Query(
429                    QuerySyntaxError::InvalidWhereClauseComponents(
430                        "range bounds value must be a 2-element array",
431                    ),
432                ));
433            }
434            let a = serialize(&arr[0])?;
435            let b = serialize(&arr[1])?;
436            if a > b {
437                return Err(Error::Query(
438                    QuerySyntaxError::InvalidWhereClauseComponents(
439                        "range lower bound must be <= upper bound",
440                    ),
441                ));
442            }
443            Ok((a, b))
444        };
445
446        Ok(match clause.operator {
447            WhereOperator::GreaterThan => {
448                let v = serialize(&clause.value)?;
449                QueryItem::RangeAfter(v..)
450            }
451            WhereOperator::GreaterThanOrEquals => {
452                let v = serialize(&clause.value)?;
453                QueryItem::RangeFrom(v..)
454            }
455            WhereOperator::LessThan => {
456                let v = serialize(&clause.value)?;
457                QueryItem::RangeTo(..v)
458            }
459            WhereOperator::LessThanOrEquals => {
460                let v = serialize(&clause.value)?;
461                QueryItem::RangeToInclusive(..=v)
462            }
463            WhereOperator::Between => {
464                let (a, b) = serialize_pair()?;
465                QueryItem::RangeInclusive(a..=b)
466            }
467            WhereOperator::BetweenExcludeBounds => {
468                let (a, b) = serialize_pair()?;
469                QueryItem::RangeAfterTo(a..b)
470            }
471            WhereOperator::BetweenExcludeLeft => {
472                let (a, b) = serialize_pair()?;
473                QueryItem::RangeAfterToInclusive(a..=b)
474            }
475            WhereOperator::BetweenExcludeRight => {
476                let (a, b) = serialize_pair()?;
477                QueryItem::Range(a..b)
478            }
479            WhereOperator::StartsWith => {
480                let left_key = serialize(&clause.value)?;
481                let mut right_key = left_key.clone();
482                if right_key.is_empty() {
483                    return Err(Error::Query(QuerySyntaxError::InvalidStartsWithClause(
484                        "startsWith prefix must have at least one byte",
485                    )));
486                }
487                // Byte-wise carry propagation. Strip trailing 0xFFs (they
488                // already cover the entire byte range) and increment the
489                // first non-0xFF byte from the right. This correctly
490                // handles prefixes like [0x12, 0xFF] → upper bound [0x13].
491                // Only fail if every byte is 0xFF (no representable
492                // exclusive upper bound).
493                let mut i = right_key.len();
494                while i > 0 && right_key[i - 1] == 0xFF {
495                    i -= 1;
496                }
497                if i == 0 {
498                    return Err(Error::Query(QuerySyntaxError::InvalidStartsWithClause(
499                        "startsWith prefix is all 0xFF bytes; cannot form half-open upper bound",
500                    )));
501                }
502                right_key.truncate(i);
503                *right_key
504                    .last_mut()
505                    .expect("non-empty after truncate to non-zero length") += 1;
506                QueryItem::Range(left_key..right_key)
507            }
508            _ => {
509                return Err(Error::Query(
510                    QuerySyntaxError::InvalidWhereClauseComponents(
511                        "range_clause_to_query_item called on a non-range operator",
512                    ),
513                ));
514            }
515        })
516    }
517
518    /// Build the grovedb `PathQuery` for a per-distinct-key range-sum
519    /// proof / no-proof walk against this query's `rangeSummable`
520    /// index. Sum analog of count's `distinct_count_path_query` — the
521    /// path-query shape is structurally identical (range on the
522    /// terminator + outer `Key`s per `In` value on a prefix prop, if
523    /// any). The only difference is at proof-emission time:
524    /// the terminator's value tree is a `SumTree` (vs `CountTree` on
525    /// the count side), so grovedb emits `KVSum` ops instead of
526    /// `KVCount`. The path-query bytes the prover and verifier
527    /// reconstruct are the same on both sides.
528    ///
529    /// `left_to_right` flips both the outer Query (when there's an
530    /// `In` on prefix) and the subquery direction so the iteration
531    /// walks `(in_key, terminator_key)` tuples in the requested
532    /// order — descending on `left_to_right = false` walks the In
533    /// dimension lex-descending too, not just the inner range.
534    ///
535    /// Errors:
536    /// - No range where-clause / multiple range where-clauses
537    /// - Multiple In clauses on prefix props
538    /// - Non-Equal-non-In operator on a prefix prop
539    /// - Missing prefix clause
540    pub fn distinct_sum_path_query(
541        &self,
542        limit: Option<u16>,
543        left_to_right: bool,
544        platform_version: &PlatformVersion,
545    ) -> Result<PathQuery, Error> {
546        let range_clause = self
547            .where_clauses
548            .iter()
549            .find(|wc| is_range_operator(wc.operator))
550            .ok_or(Error::Query(
551                QuerySyntaxError::InvalidWhereClauseComponents(
552                    "distinct_sum_path_query requires a range where-clause",
553                ),
554            ))?;
555        let range_item = self.range_clause_to_query_item(range_clause, platform_version)?;
556
557        let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
558        let terminator_name = &self
559            .index
560            .properties
561            .last()
562            .ok_or(Error::Query(
563                QuerySyntaxError::InvalidWhereClauseComponents(
564                    "range_summable index must have at least one property",
565                ),
566            ))?
567            .name;
568
569        let mut base_path: Vec<Vec<u8>> = vec![
570            vec![RootTree::DataContractDocuments as u8],
571            self.contract_id.to_vec(),
572            vec![1u8],
573            self.document_type_name.as_bytes().to_vec(),
574        ];
575
576        // `Some(keys)` once an In clause has been encountered on a
577        // prefix property. From that point on, subsequent Equal
578        // clauses go into `subquery_path_extension` rather than
579        // `base_path`. Only one In allowed (multiple Ins would
580        // multiply the fork count beyond what a single Query can
581        // express via `set_subquery_path`).
582        let mut in_outer_keys: Option<Vec<Vec<u8>>> = None;
583        let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
584
585        for prop in prefix_props {
586            let clause = self
587                .where_clauses
588                .iter()
589                .find(|wc| wc.field == prop.name)
590                .ok_or(Error::Query(
591                    QuerySyntaxError::InvalidWhereClauseComponents(
592                        "distinct_sum_path_query: missing where clause for an index \
593                         prefix property",
594                    ),
595                ))?;
596
597            match clause.operator {
598                WhereOperator::Equal => {
599                    let serialized = self.document_type.serialize_value_for_key(
600                        prop.name.as_str(),
601                        &clause.value,
602                        platform_version,
603                    )?;
604                    if in_outer_keys.is_some() {
605                        subquery_path_extension.push(prop.name.as_bytes().to_vec());
606                        subquery_path_extension.push(serialized);
607                    } else {
608                        base_path.push(prop.name.as_bytes().to_vec());
609                        base_path.push(serialized);
610                    }
611                }
612                WhereOperator::In => {
613                    if in_outer_keys.is_some() {
614                        return Err(Error::Query(
615                            QuerySyntaxError::InvalidWhereClauseComponents(
616                                "distinct_sum_path_query: at most one `In` clause is supported \
617                                 on prefix properties",
618                            ),
619                        ));
620                    }
621                    // Path stops at the In-bearing prop's property-
622                    // name subtree; outer Query lives at that level.
623                    base_path.push(prop.name.as_bytes().to_vec());
624                    let in_values = clause.in_values().into_data_with_error()??;
625                    let mut keys: Vec<Vec<u8>> = in_values
626                        .iter()
627                        .map(|v| {
628                            self.document_type.serialize_value_for_key(
629                                prop.name.as_str(),
630                                v,
631                                platform_version,
632                            )
633                        })
634                        .collect::<Result<_, _>>()?;
635                    // Same sort + parity rationale as count's
636                    // `distinct_count_path_query` — see the long
637                    // docstring there. Prover and verifier share
638                    // this builder so the sort happens identically
639                    // on both sides; without it, descending walks
640                    // and pushed-limit pagination produce gibberish.
641                    keys.sort();
642                    in_outer_keys = Some(keys);
643                }
644                _ => {
645                    return Err(Error::Query(
646                        QuerySyntaxError::InvalidWhereClauseComponents(
647                            "distinct_sum_path_query: prefix properties must use `==` or `in`",
648                        ),
649                    ));
650                }
651            }
652        }
653
654        match in_outer_keys {
655            None => {
656                // Flat shape — path includes terminator, single
657                // range-only Query.
658                base_path.push(terminator_name.as_bytes().to_vec());
659                let mut query = Query::new_with_direction(left_to_right);
660                query.insert_item(range_item);
661                Ok(PathQuery::new(
662                    base_path,
663                    SizedQuery::new(query, limit, None),
664                ))
665            }
666            Some(keys) => {
667                // Compound shape — outer Query has one Key per In
668                // value at the In-bearing prop's property-name
669                // subtree. `subquery_path` carries any post-In
670                // Equal pairs + terminator. Subquery is the range
671                // item. `left_to_right` applies to both layers so
672                // descending iteration walks `(in_key_desc,
673                // key_desc)` tuples consistently.
674                let mut outer_query = Query::new_with_direction(left_to_right);
675                for key in keys {
676                    outer_query.insert_key(key);
677                }
678                subquery_path_extension.push(terminator_name.as_bytes().to_vec());
679
680                let mut subquery = Query::new_with_direction(left_to_right);
681                subquery.insert_item(range_item);
682
683                outer_query.set_subquery_path(subquery_path_extension);
684                outer_query.set_subquery(subquery);
685
686                Ok(PathQuery::new(
687                    base_path,
688                    SizedQuery::new(outer_query, limit, None),
689                ))
690            }
691        }
692    }
693
694    /// Build the grovedb `PathQuery` for a **carrier**
695    /// `AggregateSumOnRange` proof — one outer Key per `In`
696    /// value (or one outer QueryItem per outer-range match), each
697    /// terminating in an ASOR boundary walk over the per-branch
698    /// range subtree. Returns one `(in_key, i64)` pair per resolved
699    /// In branch via [`grovedb::GroveDb::query_aggregate_sum_per_key`]
700    /// (no-proof) and
701    /// [`grovedb::GroveDb::verify_aggregate_sum_query_per_key`]
702    /// (verify), once those primitives ship.
703    ///
704    /// Required where-clause shape (validated upstream by
705    /// [`crate::query::drive_document_sum_query::drive_dispatcher::detect_sum_mode`]
706    /// routing to [`DocumentSumMode::RangeAggregateCarrierProof`]):
707    /// - Exactly one `In` clause on the In-property
708    /// - Exactly one range clause on the *terminator* property of
709    ///   a `rangeSummable: true` index whose first property is
710    ///   the In-property
711    /// - Any prefix properties between In and range must use
712    ///   `==` (mirror of [`Self::aggregate_sum_path_query`]'s
713    ///   non-In prefix rule)
714    ///
715    /// Path-query structure (mirror of count's analog —
716    /// [`crate::query::drive_document_count_query::path_query::DriveDocumentCountQuery::carrier_aggregate_count_path_query`]):
717    /// - Outer path stops one level above the In-bearing property
718    ///   subtree's children (`@/doc_prefix/0x01/doctype/<In-prop>`).
719    /// - Outer Query: `Key(in_value_0)`, `Key(in_value_1)`, … in
720    ///   lex-asc serialized order (grovedb's multi-key walker
721    ///   invariant — required for prove/verify byte-parity).
722    /// - `subquery_path`: the terminator property name (and any
723    ///   trailing `==` clause names between In and range, in
724    ///   index order).
725    /// - `subquery`: `Query::new_aggregate_sum_on_range(range_item)`.
726    ///
727    /// Both the executor and the verifier consume the `PathQuery`
728    /// this builder produces. Grovedb PR #670 (head `e98bab5f`)
729    /// landed carrier-`AggregateSumOnRange` support
730    /// (`Query::validate_carrier_aggregate_sum_on_range` and
731    /// `GroveDb::verify_aggregate_sum_query_per_key`), so the
732    /// builder's output flows directly through `prove_query` and the
733    /// verifier on both sides.
734    ///
735    /// Errors:
736    /// - No range where-clause / multiple range where-clauses →
737    ///   `InvalidWhereClauseComponents`
738    /// - No In where-clause → `InvalidWhereClauseComponents`
739    /// - In on a non-prefix property → `InvalidWhereClauseComponents`
740    /// - Prefix property between In and range uses non-Equal →
741    ///   `InvalidWhereClauseComponents`
742    pub fn carrier_aggregate_sum_path_query(
743        &self,
744        limit: Option<u16>,
745        left_to_right: bool,
746        platform_version: &PlatformVersion,
747    ) -> Result<PathQuery, Error> {
748        // The terminator property (last in the index) carries the
749        // ASOR target range. The "carrier" property — the one whose
750        // clause becomes the outer Query items — is either:
751        // - An `In` clause (G7 shape: one Key per In value)
752        // - A range clause on a prefix prop (G8 shape: one QueryItem
753        //   bounding the outer range, with `SizedQuery::limit` capping
754        //   how many outer matches the carrier walks)
755        //
756        // The terminator's clause must be a range and is converted to
757        // the inner ASOR `QueryItem`. Any properties between the
758        // carrier and the terminator must use `==` and extend the
759        // subquery_path.
760        let terminator_prop_name = &self
761            .index
762            .properties
763            .last()
764            .ok_or(Error::Query(
765                QuerySyntaxError::InvalidWhereClauseComponents(
766                    "range_summable index must have at least one property",
767                ),
768            ))?
769            .name;
770        let terminator_clause = self
771            .where_clauses
772            .iter()
773            .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
774            .ok_or(Error::Query(
775                QuerySyntaxError::InvalidWhereClauseComponents(
776                    "carrier_aggregate_sum_path_query requires a range where-clause on the \
777                     terminator property of the chosen index",
778                ),
779            ))?;
780        let inner_range_item =
781            self.range_clause_to_query_item(terminator_clause, platform_version)?;
782
783        let mut base_path: Vec<Vec<u8>> = vec![
784            vec![RootTree::DataContractDocuments as u8],
785            self.contract_id.to_vec(),
786            vec![1u8],
787            self.document_type_name.as_bytes().to_vec(),
788        ];
789        let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
790
791        // Carrier clause state: either `None` (not seen yet, still on
792        // the `==`-prefix run), `Some(In)` (G7), or `Some(Range)` (G8).
793        // Mirror of count's analog (drive_document_count_query/
794        // path_query.rs's `Carrier` enum).
795        enum Carrier {
796            Pending,
797            In(WhereClause),
798            Range(WhereClause),
799        }
800        let mut carrier = Carrier::Pending;
801        let prefix_and_carrier_props = &self.index.properties[..self.index.properties.len() - 1];
802
803        for prop in prefix_and_carrier_props {
804            let clause = self
805                .where_clauses
806                .iter()
807                .find(|wc| wc.field == prop.name)
808                .ok_or(Error::Query(
809                    QuerySyntaxError::InvalidWhereClauseComponents(
810                        "carrier-aggregate sum proof: missing where clause for an index prefix \
811                     property",
812                    ),
813                ))?;
814            match (&carrier, clause.operator) {
815                (Carrier::Pending, WhereOperator::Equal) => {
816                    base_path.push(prop.name.as_bytes().to_vec());
817                    base_path.push(self.document_type.serialize_value_for_key(
818                        prop.name.as_str(),
819                        &clause.value,
820                        platform_version,
821                    )?);
822                }
823                (Carrier::Pending, WhereOperator::In) => {
824                    base_path.push(prop.name.as_bytes().to_vec());
825                    carrier = Carrier::In(clause.clone());
826                }
827                (Carrier::Pending, op) if is_range_operator(op) => {
828                    base_path.push(prop.name.as_bytes().to_vec());
829                    carrier = Carrier::Range(clause.clone());
830                }
831                (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => {
832                    subquery_path_extension.push(prop.name.as_bytes().to_vec());
833                    subquery_path_extension.push(self.document_type.serialize_value_for_key(
834                        prop.name.as_str(),
835                        &clause.value,
836                        platform_version,
837                    )?);
838                }
839                (Carrier::In(_) | Carrier::Range(_), _) => {
840                    return Err(Error::Query(
841                        QuerySyntaxError::InvalidWhereClauseComponents(
842                            "carrier-aggregate sum proof: at most one carrier clause (In or \
843                             range) is supported on prefix properties; subsequent prefix \
844                             clauses must use `==`",
845                        ),
846                    ));
847                }
848                _ => {
849                    return Err(Error::Query(
850                        QuerySyntaxError::InvalidWhereClauseComponents(
851                            "carrier-aggregate sum proof: prefix property operator unsupported",
852                        ),
853                    ));
854                }
855            }
856        }
857        subquery_path_extension.push(terminator_prop_name.as_bytes().to_vec());
858
859        let mut outer_query = Query::new_with_direction(left_to_right);
860        match carrier {
861            Carrier::Pending => {
862                return Err(Error::Query(
863                    QuerySyntaxError::InvalidWhereClauseComponents(
864                        "carrier-aggregate sum proof: an In or range clause must appear on a \
865                         prefix property of the chosen index to act as the carrier dimension",
866                    ),
867                ));
868            }
869            Carrier::In(in_clause) => {
870                // Build one Key per In value, sorted lex-ascending —
871                // grovedb's multi-key walker invariant (same convention
872                // as count's carrier and the SDK's verifier-side
873                // rebuild).
874                let in_values = in_clause.in_values().into_data_with_error()??;
875                let mut serialized_in_keys: Vec<Vec<u8>> = in_values
876                    .iter()
877                    .map(|v| {
878                        self.document_type.serialize_value_for_key(
879                            in_clause.field.as_str(),
880                            v,
881                            platform_version,
882                        )
883                    })
884                    .collect::<Result<_, _>>()?;
885                serialized_in_keys.sort();
886                serialized_in_keys.dedup();
887                for key in serialized_in_keys {
888                    outer_query.insert_key(key);
889                }
890            }
891            Carrier::Range(range_clause) => {
892                // Single QueryItem bounding the outer range. The
893                // carrier walks this range and emits one `(key, i64)`
894                // pair per matched outer key.
895                let outer_range_item =
896                    self.range_clause_to_query_item(&range_clause, platform_version)?;
897                outer_query.items.push(outer_range_item);
898            }
899        }
900        outer_query.set_subquery_path(subquery_path_extension);
901        outer_query.set_subquery(Query::new_aggregate_sum_on_range(inner_range_item));
902
903        // `SizedQuery::limit` mirrors count's carrier:
904        // - For In-outer carriers the |IN| array already bounds the
905        //   result, so `limit` is typically `None`.
906        // - For Range-outer carriers `limit` caps the outer walk and
907        //   is load-bearing for proof bytes — must match prover/
908        //   verifier for the merk-root recomputation.
909        Ok(PathQuery::new(
910            base_path,
911            SizedQuery::new(outer_query, limit, None),
912        ))
913    }
914
915    /// Combined PCPS (`ProvableCountProvableSumTree`) carrier variant:
916    /// outer In or outer range, inner range carrying both per-bucket
917    /// count AND per-bucket sum via grovedb's
918    /// `AggregateCountAndSumOnRange` primitive. The terminator
919    /// property's value tree must be PCPS (the index must declare
920    /// BOTH `rangeCountable: true` AND `rangeSummable: true`).
921    ///
922    /// PCPS-only — `ProvableSumTree` / `ProvableCountTree` /
923    /// `ProvableCountSumTree` (the per-axis or root-only sum
924    /// variants) reject the query item at the prover. Returns one
925    /// `(outer_key, u64 count, i64 sum)` triple per resolved In
926    /// branch. Verified client-side via
927    /// `GroveDb::verify_aggregate_count_and_sum_query_per_key`
928    /// (grovedb develop (PR #670 merged; head `e98bab5f` as of this PR)).
929    ///
930    /// Same outer/subquery topology as
931    /// [`Self::carrier_aggregate_sum_path_query`] — the only
932    /// difference is the inner aggregation primitive
933    /// (`Query::new_aggregate_count_and_sum_on_range` vs.
934    /// `Query::new_aggregate_sum_on_range`) and the additional
935    /// PCPS gate.
936    pub fn carrier_aggregate_count_and_sum_path_query(
937        &self,
938        limit: Option<u16>,
939        left_to_right: bool,
940        platform_version: &PlatformVersion,
941    ) -> Result<PathQuery, Error> {
942        if !self.index.range_countable {
943            return Err(Error::Query(QuerySyntaxError::Unsupported(
944                "carrier_aggregate_count_and_sum_path_query: index must declare BOTH \
945                 `rangeCountable: true` AND `rangeSummable: true` to produce a PCPS \
946                 (ProvableCountProvableSumTree) property-name tree."
947                    .to_string(),
948            )));
949        }
950
951        let terminator_prop_name = &self
952            .index
953            .properties
954            .last()
955            .ok_or(Error::Query(
956                QuerySyntaxError::InvalidWhereClauseComponents(
957                    "range_countable + range_summable index must have at least one property",
958                ),
959            ))?
960            .name;
961        let terminator_clause = self
962            .where_clauses
963            .iter()
964            .find(|wc| wc.field == *terminator_prop_name && is_range_operator(wc.operator))
965            .ok_or(Error::Query(
966                QuerySyntaxError::InvalidWhereClauseComponents(
967                    "carrier_aggregate_count_and_sum_path_query requires a range where-clause \
968                     on the terminator property of the chosen index",
969                ),
970            ))?;
971        let inner_range_item =
972            self.range_clause_to_query_item(terminator_clause, platform_version)?;
973
974        let mut base_path: Vec<Vec<u8>> = vec![
975            vec![RootTree::DataContractDocuments as u8],
976            self.contract_id.to_vec(),
977            vec![1u8],
978            self.document_type_name.as_bytes().to_vec(),
979        ];
980        let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
981
982        // Same Carrier state-machine as the sum-only variant.
983        enum Carrier {
984            Pending,
985            In(WhereClause),
986            Range(WhereClause),
987        }
988        let mut carrier = Carrier::Pending;
989        let prefix_and_carrier_props = &self.index.properties[..self.index.properties.len() - 1];
990
991        for prop in prefix_and_carrier_props {
992            let clause = self
993                .where_clauses
994                .iter()
995                .find(|wc| wc.field == prop.name)
996                .ok_or(Error::Query(
997                    QuerySyntaxError::InvalidWhereClauseComponents(
998                        "carrier-aggregate count-and-sum proof: missing where clause for an index \
999                     prefix property",
1000                    ),
1001                ))?;
1002            match (&carrier, clause.operator) {
1003                (Carrier::Pending, WhereOperator::Equal) => {
1004                    base_path.push(prop.name.as_bytes().to_vec());
1005                    base_path.push(self.document_type.serialize_value_for_key(
1006                        prop.name.as_str(),
1007                        &clause.value,
1008                        platform_version,
1009                    )?);
1010                }
1011                (Carrier::Pending, WhereOperator::In) => {
1012                    base_path.push(prop.name.as_bytes().to_vec());
1013                    carrier = Carrier::In(clause.clone());
1014                }
1015                (Carrier::Pending, op) if is_range_operator(op) => {
1016                    base_path.push(prop.name.as_bytes().to_vec());
1017                    carrier = Carrier::Range(clause.clone());
1018                }
1019                (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => {
1020                    subquery_path_extension.push(prop.name.as_bytes().to_vec());
1021                    subquery_path_extension.push(self.document_type.serialize_value_for_key(
1022                        prop.name.as_str(),
1023                        &clause.value,
1024                        platform_version,
1025                    )?);
1026                }
1027                (Carrier::In(_) | Carrier::Range(_), _) => {
1028                    return Err(Error::Query(
1029                        QuerySyntaxError::InvalidWhereClauseComponents(
1030                            "carrier-aggregate count-and-sum proof: at most one carrier clause \
1031                             (In or range) is supported on prefix properties; subsequent prefix \
1032                             clauses must use `==`",
1033                        ),
1034                    ));
1035                }
1036                _ => {
1037                    return Err(Error::Query(
1038                        QuerySyntaxError::InvalidWhereClauseComponents(
1039                            "carrier-aggregate count-and-sum proof: prefix property operator \
1040                             unsupported",
1041                        ),
1042                    ));
1043                }
1044            }
1045        }
1046        subquery_path_extension.push(terminator_prop_name.as_bytes().to_vec());
1047
1048        let mut outer_query = Query::new_with_direction(left_to_right);
1049        match carrier {
1050            Carrier::Pending => {
1051                return Err(Error::Query(
1052                    QuerySyntaxError::InvalidWhereClauseComponents(
1053                        "carrier-aggregate count-and-sum proof: an In or range clause must \
1054                         appear on a prefix property of the chosen index to act as the carrier \
1055                         dimension",
1056                    ),
1057                ));
1058            }
1059            Carrier::In(in_clause) => {
1060                let in_values = in_clause.in_values().into_data_with_error()??;
1061                let mut serialized_in_keys: Vec<Vec<u8>> = in_values
1062                    .iter()
1063                    .map(|v| {
1064                        self.document_type.serialize_value_for_key(
1065                            in_clause.field.as_str(),
1066                            v,
1067                            platform_version,
1068                        )
1069                    })
1070                    .collect::<Result<_, _>>()?;
1071                serialized_in_keys.sort();
1072                serialized_in_keys.dedup();
1073                for key in serialized_in_keys {
1074                    outer_query.insert_key(key);
1075                }
1076            }
1077            Carrier::Range(range_clause) => {
1078                let outer_range_item =
1079                    self.range_clause_to_query_item(&range_clause, platform_version)?;
1080                outer_query.items.push(outer_range_item);
1081            }
1082        }
1083        outer_query.set_subquery_path(subquery_path_extension);
1084        outer_query.set_subquery(grovedb::Query::new_aggregate_count_and_sum_on_range(
1085            inner_range_item,
1086        ));
1087
1088        Ok(PathQuery::new(
1089            base_path,
1090            SizedQuery::new(outer_query, limit, None),
1091        ))
1092    }
1093}
1094
1095// ─── Static / free-function wrappers for the bench + verifier-side
1096// rebuild. These re-pick the covering index from the document type
1097// (vs. the instance methods above which use the already-resolved
1098// `self.index`). ────────────────────────────────────────────────────
1099
1100#[cfg(any(feature = "server", feature = "verify"))]
1101impl<'a> DriveDocumentSumQuery<'a> {
1102    /// Static wrapper for the bench / verifier-side rebuild. Calls
1103    /// the instance method via a temporary `DriveDocumentSumQuery`
1104    /// built from the picked covering index.
1105    pub fn point_lookup_sum_path_query_static(
1106        contract: &DataContract,
1107        document_type: DocumentTypeRef,
1108        sum_property: &str,
1109        where_clauses: &[WhereClause],
1110        platform_version: &PlatformVersion,
1111    ) -> Result<PathQuery, Error> {
1112        use crate::query::drive_document_sum_query::index_picker::find_summable_index_for_where_clauses;
1113        use dpp::data_contract::accessors::v0::DataContractV0Getters;
1114        use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1115
1116        let index = find_summable_index_for_where_clauses(
1117            document_type.indexes(),
1118            where_clauses,
1119            sum_property,
1120        )
1121        .ok_or_else(|| {
1122            Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
1123                "no `summable: \"<prop>\"` index exactly matches the where-clause fields. \
1124                 Define a more specific summable index (with `summable: \"<prop>\"` whose \
1125                 properties exactly equal the clauses) or use `prove=false`."
1126                    .to_string(),
1127            ))
1128        })?;
1129        let q = DriveDocumentSumQuery {
1130            document_type,
1131            contract_id: contract.id().to_buffer(),
1132            document_type_name: document_type.name().clone(),
1133            index,
1134            where_clauses: where_clauses.to_vec(),
1135            sum_property: sum_property.to_string(),
1136        };
1137        q.point_lookup_sum_path_query(platform_version)
1138    }
1139
1140    /// Static wrapper for the bench / verifier-side rebuild — picks the
1141    /// covering range-summable index and delegates to the instance
1142    /// method.
1143    pub fn aggregate_sum_path_query_static(
1144        contract: &DataContract,
1145        document_type: DocumentTypeRef,
1146        sum_property: &str,
1147        where_clauses: &[WhereClause],
1148        platform_version: &PlatformVersion,
1149    ) -> Result<PathQuery, Error> {
1150        use crate::query::drive_document_sum_query::index_picker::find_range_summable_index_for_where_clauses;
1151        use dpp::data_contract::accessors::v0::DataContractV0Getters;
1152        use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1153
1154        let index = find_range_summable_index_for_where_clauses(
1155            document_type.indexes(),
1156            where_clauses,
1157            sum_property,
1158        )
1159        .ok_or_else(|| {
1160            Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
1161                "no `rangeSummable: true` index covers the where-clause shape (Equal/In \
1162                 prefix exactly + range on the index's last property). Define one or use \
1163                 `prove=false`."
1164                    .to_string(),
1165            ))
1166        })?;
1167        let q = DriveDocumentSumQuery {
1168            document_type,
1169            contract_id: contract.id().to_buffer(),
1170            document_type_name: document_type.name().clone(),
1171            index,
1172            where_clauses: where_clauses.to_vec(),
1173            sum_property: sum_property.to_string(),
1174        };
1175        q.aggregate_sum_path_query(platform_version)
1176    }
1177
1178    /// Static wrapper for the bench / verifier-side rebuild — picks
1179    /// the covering range-summable index and delegates to the carrier
1180    /// instance method. Mirror of count's analog
1181    /// [`crate::query::drive_document_count_query::path_query::DriveDocumentCountQuery::carrier_aggregate_count_path_query`]'s
1182    /// implicit static surface via the executor.
1183    /// Used by the SDK verifier-side rebuild via
1184    /// `GroveDb::verify_aggregate_sum_query_per_key` (grovedb PR #670
1185    /// head `e98bab5f`).
1186    pub fn carrier_aggregate_sum_path_query_static(
1187        contract: &DataContract,
1188        document_type: DocumentTypeRef,
1189        sum_property: &str,
1190        where_clauses: &[WhereClause],
1191        limit: Option<u16>,
1192        left_to_right: bool,
1193        platform_version: &PlatformVersion,
1194    ) -> Result<PathQuery, Error> {
1195        use crate::query::drive_document_sum_query::index_picker::find_range_summable_index_for_where_clauses;
1196        use dpp::data_contract::accessors::v0::DataContractV0Getters;
1197        use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1198
1199        let index = find_range_summable_index_for_where_clauses(
1200            document_type.indexes(),
1201            where_clauses,
1202            sum_property,
1203        )
1204        .ok_or_else(|| {
1205            Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
1206                "no `rangeSummable: true` index covers the where-clause shape for the \
1207                 carrier-aggregate sum carrier (Equal/In prefix + In-or-range carrier + \
1208                 range on the index's last property). Define one or use `prove=false`."
1209                    .to_string(),
1210            ))
1211        })?;
1212        let q = DriveDocumentSumQuery {
1213            document_type,
1214            contract_id: contract.id().to_buffer(),
1215            document_type_name: document_type.name().clone(),
1216            index,
1217            where_clauses: where_clauses.to_vec(),
1218            sum_property: sum_property.to_string(),
1219        };
1220        q.carrier_aggregate_sum_path_query(limit, left_to_right, platform_version)
1221    }
1222}
1223
1224// ── Carrier-shape unit tests ───────────────────────────────────────
1225//
1226// The carrier builder is pure Rust data construction — no grovedb
1227// interaction — so it can be exercised today regardless of the upstream
1228// grovedb prover gating. Tests assert the structural invariants the
1229// prover/verifier will require once the sister PR lands:
1230// - outer path stops at the In-bearing property-name subtree;
1231// - outer Query has Key items in lex-asc serialized order;
1232// - default_subquery_branch.subquery is a single
1233//   `AggregateSumOnRange(inner)`;
1234// - subquery_path is the (post-In Equals + terminator name) chain.
1235//
1236// These tests pin the carrier path-query shape so a future refactor of
1237// the builder body can't silently drift from what the verifier will
1238// rebuild on its side.
1239#[cfg(test)]
1240mod carrier_path_query_tests {
1241    use super::*;
1242    use crate::query::WhereOperator;
1243    use assert_matches::assert_matches;
1244    use dpp::data_contract::accessors::v0::DataContractV0Getters;
1245    use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
1246    use dpp::data_contract::DataContract;
1247    use dpp::tests::json_document::json_document_to_contract;
1248    use grovedb::QueryItem;
1249
1250    fn load_tip_jar_contract(platform_version: &PlatformVersion) -> DataContract {
1251        // The tip-jar contract has a rangeSummable index on
1252        // `(recipient, sentAt)` (`byRecipientTime`) with
1253        // `summable: "amount"` — exactly the shape the carrier targets:
1254        // outer In on `recipient`, inner range on `sentAt`.
1255        json_document_to_contract(
1256            "tests/supporting_files/contract/tip-jar/tip-jar-contract.json",
1257            false,
1258            platform_version,
1259        )
1260        .expect("tip-jar contract fixture loads")
1261    }
1262
1263    /// Helper — given a contract and a `(doctype, index)` name pair,
1264    /// resolve the [`DocumentTypeRef`] for the doctype.
1265    ///
1266    /// The matching `Index` is fetched inside each test via
1267    /// `doc_type.indexes().get(index_name)` rather than returned
1268    /// alongside `doc_type` here, because the index reference is
1269    /// bound to the doc_type's lifetime (not the contract's), so
1270    /// returning both from a helper would create a self-referential
1271    /// tuple. The two-step pattern (`pick_doc_type` here, then
1272    /// `.indexes().get(...)` at the call site) is the same pattern
1273    /// count's tests use.
1274    fn pick_doc_type<'a>(
1275        contract: &'a DataContract,
1276        doc_type_name: &str,
1277    ) -> dpp::data_contract::document_type::DocumentTypeRef<'a> {
1278        contract
1279            .document_type_for_name(doc_type_name)
1280            .expect("document type exists in tip-jar fixture")
1281    }
1282
1283    /// Two recipient byte-array values for In-on-carrier tests. We
1284    /// pick values in non-lex order so the builder's sort step is
1285    /// observable in the resulting Key item order.
1286    fn recipient_a() -> Vec<u8> {
1287        // Bytes starting with 0x80 — lex-greater.
1288        let mut v = vec![0x80u8; 32];
1289        v[31] = 0x01;
1290        v
1291    }
1292    fn recipient_b() -> Vec<u8> {
1293        // Bytes starting with 0x10 — lex-less.
1294        let mut v = vec![0x10u8; 32];
1295        v[31] = 0x02;
1296        v
1297    }
1298
1299    /// G7 — In on carrier + range on terminator. Asserts outer Query
1300    /// has one `Key` per In value (lex-sorted), subquery is
1301    /// `AggregateSumOnRange(inner_range)`, and subquery_path is just
1302    /// the terminator's property-name segment.
1303    #[test]
1304    fn carrier_aggregate_sum_in_on_carrier_range_on_terminator() {
1305        let platform_version = PlatformVersion::latest();
1306        let contract = load_tip_jar_contract(platform_version);
1307        let doc_type = pick_doc_type(&contract, "tip");
1308        let index = doc_type
1309            .indexes()
1310            .get("byRecipientTime")
1311            .expect("byRecipientTime index exists on tip doc type");
1312
1313        // `byRecipientTime` is `[recipient, sentAt]` with
1314        // `summable: "amount"` + `rangeSummable: true`. Provide the
1315        // In values out of lex order so the builder's lex-sort is
1316        // observable.
1317        let in_values = vec![
1318            dpp::platform_value::Value::Bytes(recipient_a()),
1319            dpp::platform_value::Value::Bytes(recipient_b()),
1320        ];
1321        let where_clauses = vec![
1322            WhereClause {
1323                field: "recipient".to_string(),
1324                operator: WhereOperator::In,
1325                value: dpp::platform_value::Value::Array(in_values.clone()),
1326            },
1327            WhereClause {
1328                field: "sentAt".to_string(),
1329                operator: WhereOperator::GreaterThan,
1330                value: dpp::platform_value::Value::U64(0),
1331            },
1332        ];
1333        let q = DriveDocumentSumQuery {
1334            document_type: doc_type,
1335            contract_id: contract.id().to_buffer(),
1336            document_type_name: doc_type.name().clone(),
1337            index,
1338            where_clauses,
1339            sum_property: "amount".to_string(),
1340        };
1341
1342        let pq = q
1343            .carrier_aggregate_sum_path_query(None, true, platform_version)
1344            .expect("carrier-aggregate sum path query builds");
1345
1346        // base_path = [contract-docs-root, contract_id, 0x01,
1347        // doctype_name, "recipient"]. The outer Keys live under the
1348        // "recipient" property-name subtree.
1349        assert!(
1350            pq.path.len() >= 5,
1351            "expected base_path to extend through the In-bearing prop's name subtree"
1352        );
1353        assert_eq!(
1354            pq.path.last().expect("base_path non-empty"),
1355            b"recipient",
1356            "outer path must stop at the In-bearing prop's property-name subtree"
1357        );
1358
1359        // Outer Query: one Key per In value, lex-sorted (the
1360        // builder's `.sort()` step turns the unsorted user input into
1361        // the prover/verifier-agreement lex-asc order).
1362        let outer_items = &pq.query.query.items;
1363        assert_eq!(outer_items.len(), 2, "one outer Key per In value");
1364        for item in outer_items {
1365            assert_matches!(item, QueryItem::Key(_));
1366        }
1367        if let (QueryItem::Key(a), QueryItem::Key(b)) = (&outer_items[0], &outer_items[1]) {
1368            assert!(a < b, "outer Keys must be sorted lex-ascending");
1369        }
1370
1371        // Subquery_path = ["sentAt"] (just the terminator's name).
1372        let sub_path = pq
1373            .query
1374            .query
1375            .default_subquery_branch
1376            .subquery_path
1377            .as_ref()
1378            .expect("subquery_path set");
1379        assert_eq!(sub_path, &vec![b"sentAt".to_vec()]);
1380
1381        // Subquery is `AggregateSumOnRange(inner_range)`.
1382        let subquery = pq
1383            .query
1384            .query
1385            .default_subquery_branch
1386            .subquery
1387            .as_ref()
1388            .expect("subquery set");
1389        assert_eq!(subquery.items.len(), 1);
1390        assert_matches!(subquery.items[0], QueryItem::AggregateSumOnRange(_));
1391    }
1392
1393    /// G7 — same as above but `limit = Some(N)` flows into
1394    /// `SizedQuery::limit` so the prover/verifier sides agree on the
1395    /// outer-walk cap byte-for-byte.
1396    #[test]
1397    fn carrier_aggregate_sum_limit_flows_into_sized_query() {
1398        let platform_version = PlatformVersion::latest();
1399        let contract = load_tip_jar_contract(platform_version);
1400        let doc_type = pick_doc_type(&contract, "tip");
1401        let index = doc_type
1402            .indexes()
1403            .get("byRecipientTime")
1404            .expect("byRecipientTime index exists on tip doc type");
1405
1406        let where_clauses = vec![
1407            WhereClause {
1408                field: "recipient".to_string(),
1409                operator: WhereOperator::In,
1410                value: dpp::platform_value::Value::Array(vec![
1411                    dpp::platform_value::Value::Bytes(recipient_a()),
1412                    dpp::platform_value::Value::Bytes(recipient_b()),
1413                ]),
1414            },
1415            WhereClause {
1416                field: "sentAt".to_string(),
1417                operator: WhereOperator::GreaterThan,
1418                value: dpp::platform_value::Value::U64(0),
1419            },
1420        ];
1421        let q = DriveDocumentSumQuery {
1422            document_type: doc_type,
1423            contract_id: contract.id().to_buffer(),
1424            document_type_name: doc_type.name().clone(),
1425            index,
1426            where_clauses,
1427            sum_property: "amount".to_string(),
1428        };
1429
1430        let pq = q
1431            .carrier_aggregate_sum_path_query(Some(7), true, platform_version)
1432            .expect("carrier-aggregate sum path query builds with limit");
1433        assert_eq!(pq.query.limit, Some(7), "outer SizedQuery::limit threads");
1434    }
1435
1436    /// Missing terminator range → `InvalidWhereClauseComponents`.
1437    #[test]
1438    fn carrier_aggregate_sum_rejects_missing_terminator_range() {
1439        let platform_version = PlatformVersion::latest();
1440        let contract = load_tip_jar_contract(platform_version);
1441        let doc_type = pick_doc_type(&contract, "tip");
1442        let index = doc_type
1443            .indexes()
1444            .get("byRecipientTime")
1445            .expect("byRecipientTime index exists on tip doc type");
1446
1447        let where_clauses = vec![WhereClause {
1448            field: "recipient".to_string(),
1449            operator: WhereOperator::In,
1450            value: dpp::platform_value::Value::Array(vec![dpp::platform_value::Value::Bytes(
1451                recipient_a(),
1452            )]),
1453        }];
1454        let q = DriveDocumentSumQuery {
1455            document_type: doc_type,
1456            contract_id: contract.id().to_buffer(),
1457            document_type_name: doc_type.name().clone(),
1458            index,
1459            where_clauses,
1460            sum_property: "amount".to_string(),
1461        };
1462
1463        let err = q
1464            .carrier_aggregate_sum_path_query(None, true, platform_version)
1465            .expect_err("missing range clause must be rejected");
1466        let msg = format!("{err:?}");
1467        assert!(
1468            msg.contains("requires a range where-clause"),
1469            "unexpected error: {msg}"
1470        );
1471    }
1472
1473    /// Missing carrier (no In or outer range on a prefix prop) →
1474    /// `InvalidWhereClauseComponents`.
1475    #[test]
1476    fn carrier_aggregate_sum_rejects_missing_carrier() {
1477        let platform_version = PlatformVersion::latest();
1478        let contract = load_tip_jar_contract(platform_version);
1479        let doc_type = pick_doc_type(&contract, "tip");
1480        let index = doc_type
1481            .indexes()
1482            .get("byRecipientTime")
1483            .expect("byRecipientTime index exists on tip doc type");
1484
1485        // Equal on prefix + range on terminator — *no* carrier. This
1486        // is the `aggregate_sum_path_query` shape, not the carrier
1487        // shape; the carrier builder must reject because the carrier
1488        // state stays `Pending` through the prefix loop.
1489        let where_clauses = vec![
1490            WhereClause {
1491                field: "recipient".to_string(),
1492                operator: WhereOperator::Equal,
1493                value: dpp::platform_value::Value::Bytes(recipient_a()),
1494            },
1495            WhereClause {
1496                field: "sentAt".to_string(),
1497                operator: WhereOperator::GreaterThan,
1498                value: dpp::platform_value::Value::U64(0),
1499            },
1500        ];
1501        let q = DriveDocumentSumQuery {
1502            document_type: doc_type,
1503            contract_id: contract.id().to_buffer(),
1504            document_type_name: doc_type.name().clone(),
1505            index,
1506            where_clauses,
1507            sum_property: "amount".to_string(),
1508        };
1509
1510        let err = q
1511            .carrier_aggregate_sum_path_query(None, true, platform_version)
1512            .expect_err("Equal-only prefix must be rejected by carrier builder");
1513        let msg = format!("{err:?}");
1514        assert!(msg.contains("carrier dimension"), "unexpected error: {msg}");
1515    }
1516}