Skip to main content

drive/query/drive_document_count_query/
path_query.rs

1//! Path-query builders for the count query.
2//!
3//! These are the **load-bearing prover/verifier-agreement boundary**:
4//! the bytes these builders produce must match byte-for-byte between
5//! the prover and the verifier, or the merk-root recomputation
6//! fails. Touching anything here without updating both the
7//! server-side prove executor AND the SDK's verifier path-query
8//! reconstruction simultaneously is a bug waiting to happen.
9//!
10//! All three builders are gated `#[cfg(any(feature = "server",
11//! feature = "verify"))]` so the verifier crate (which only enables
12//! `verify`) can reach them via `DriveDocumentCountQuery::*` method
13//! syntax.
14
15#![cfg(any(feature = "server", feature = "verify"))]
16
17use super::super::conditions::{WhereClause, WhereOperator};
18use super::DriveDocumentCountQuery;
19use crate::drive::RootTree;
20use crate::error::query::QuerySyntaxError;
21use crate::error::Error;
22use dpp::data_contract::document_type::methods::DocumentTypeV0Methods;
23use dpp::version::PlatformVersion;
24use grovedb::{PathQuery, Query, QueryItem, SizedQuery};
25
26impl DriveDocumentCountQuery<'_> {
27    /// Convert a single range where-clause + value into the grovedb
28    /// `QueryItem` used to walk children of the property-name
29    /// `ProvableCountTree`. The clause's value is serialized via the
30    /// document type's `serialize_value_for_key`, which produces the
31    /// canonical bytes used everywhere else in the index path.
32    ///
33    /// Range mappings:
34    /// - `>` → `RangeAfter(value..)` (exclusive lower)
35    /// - `>=` → `RangeFrom(value..)` (inclusive lower)
36    /// - `<` → `RangeTo(..value)` (exclusive upper)
37    /// - `<=` → `RangeToInclusive(..=value)` (inclusive upper)
38    /// - `between [a, b]` → `RangeInclusive(a..=b)` (inclusive both)
39    /// - `between (a, b)` → `RangeAfterTo(a..b)` (exclusive both — the
40    ///   inner range is half-open in grovedb terms; this models
41    ///   exclude-bounds)
42    /// - `between (a, b]` → `RangeAfterToInclusive(a..=b)`
43    /// - `between [a, b)` → `Range(a..b)`
44    /// - `startsWith "p"` → `Range(serialize("p")..serialize("p") with
45    ///   last byte +1)` — same byte-incremented half-open encoding the
46    ///   normal docs path uses (see `conditions.rs:1129`'s `StartsWith`
47    ///   arm). `value_shape_ok` constrains the prefix to `Value::Text`,
48    ///   and valid UTF-8 never contains `0xFF`, so the `+1` doesn't
49    ///   overflow for valid string keys; the unlikely 0xFF-tail case is
50    ///   caught via `checked_add` and rejected with a clear error.
51    fn range_clause_to_query_item(
52        &self,
53        clause: &WhereClause,
54        platform_version: &PlatformVersion,
55    ) -> Result<QueryItem, Error> {
56        let serialize = |v: &dpp::platform_value::Value| -> Result<Vec<u8>, Error> {
57            Ok(self.document_type.serialize_value_for_key(
58                clause.field.as_str(),
59                v,
60                platform_version,
61            )?)
62        };
63        // Shared helper for all four `between*` operators. The
64        // operator the caller used (`between`, `betweenExcludeBounds`,
65        // etc.) is not woven into error messages because
66        // `InvalidWhereClauseComponents` takes `&'static str` — a
67        // String-typed error variant would let us do that, but the
68        // existing static-string contract is fine to live with: the
69        // arm name (`WhereOperator::Between` etc.) is visible in
70        // backtraces if a malformed payload reaches this far, and
71        // mode detection has already filtered out non-range operators.
72        let serialize_pair = || -> Result<(Vec<u8>, Vec<u8>), Error> {
73            let arr = clause.value.as_array().ok_or_else(|| {
74                Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
75                    "range bounds value must be a 2-element array",
76                ))
77            })?;
78            if arr.len() != 2 {
79                return Err(Error::Query(
80                    QuerySyntaxError::InvalidWhereClauseComponents(
81                        "range bounds value must be a 2-element array",
82                    ),
83                ));
84            }
85            let a = serialize(&arr[0])?;
86            let b = serialize(&arr[1])?;
87            if a > b {
88                return Err(Error::Query(
89                    QuerySyntaxError::InvalidWhereClauseComponents(
90                        "range lower bound must be <= upper bound",
91                    ),
92                ));
93            }
94            Ok((a, b))
95        };
96
97        Ok(match clause.operator {
98            WhereOperator::GreaterThan => {
99                let v = serialize(&clause.value)?;
100                QueryItem::RangeAfter(v..)
101            }
102            WhereOperator::GreaterThanOrEquals => {
103                let v = serialize(&clause.value)?;
104                QueryItem::RangeFrom(v..)
105            }
106            WhereOperator::LessThan => {
107                let v = serialize(&clause.value)?;
108                QueryItem::RangeTo(..v)
109            }
110            WhereOperator::LessThanOrEquals => {
111                let v = serialize(&clause.value)?;
112                QueryItem::RangeToInclusive(..=v)
113            }
114            WhereOperator::Between => {
115                let (a, b) = serialize_pair()?;
116                QueryItem::RangeInclusive(a..=b)
117            }
118            WhereOperator::BetweenExcludeBounds => {
119                let (a, b) = serialize_pair()?;
120                QueryItem::RangeAfterTo(a..b)
121            }
122            WhereOperator::BetweenExcludeLeft => {
123                let (a, b) = serialize_pair()?;
124                QueryItem::RangeAfterToInclusive(a..=b)
125            }
126            WhereOperator::BetweenExcludeRight => {
127                let (a, b) = serialize_pair()?;
128                QueryItem::Range(a..b)
129            }
130            WhereOperator::StartsWith => {
131                let left_key = serialize(&clause.value)?;
132                let mut right_key = left_key.clone();
133                // Byte-increment the last byte to form the half-open
134                // upper bound `[prefix, prefix+1)`. Mirrors the
135                // normal-docs encoding in `conditions.rs:1129`'s
136                // `StartsWith` arm; we use `checked_add` so the
137                // pathological `0xFF`-tail input fails loudly instead
138                // of wrapping silently (UTF-8 never contains 0xFF so
139                // valid string keys never hit this).
140                let last = right_key.last_mut().ok_or_else(|| {
141                    Error::Query(QuerySyntaxError::InvalidStartsWithClause(
142                        "startsWith prefix must have at least one byte",
143                    ))
144                })?;
145                *last = last.checked_add(1).ok_or_else(|| {
146                    Error::Query(QuerySyntaxError::InvalidStartsWithClause(
147                        "startsWith prefix ends in 0xFF; cannot form half-open upper bound",
148                    ))
149                })?;
150                QueryItem::Range(left_key..right_key)
151            }
152            _ => {
153                return Err(Error::Query(
154                    QuerySyntaxError::InvalidWhereClauseComponents(
155                        "range_clause_to_query_item called on a non-range operator",
156                    ),
157                ));
158            }
159        })
160    }
161
162    /// Build the grovedb `PathQuery` for an `AggregateCountOnRange`
163    /// query against this count query's `range_countable` index.
164    ///
165    /// Shared between the server-side prove path
166    /// ([`Self::execute_aggregate_count_with_proof`]) and the client-
167    /// side verify path (the SDK's `FromProof<DocumentQuery>` for
168    /// `DocumentCount`, via the shared `verify_aggregate_count`
169    /// helper). Both sides must produce the *exact same* `PathQuery`
170    /// for verification to recompute the same merk root.
171    ///
172    /// Aggregate-count specifically restricts prefix props to `Equal`:
173    /// grovedb's `AggregateCountOnRange` primitive wraps a *single*
174    /// inner range and emits one aggregate `u64` — there's no way for
175    /// it to cartesian-fork over multiple In values at the merk
176    /// layer. For per-distinct-value counts with In on prefix, use
177    /// [`Self::distinct_count_path_query`] instead.
178    ///
179    /// Errors:
180    /// - No range where-clause / multiple range where-clauses →
181    ///   `InvalidWhereClauseComponents`
182    /// - `In` on a prefix property → `InvalidWhereClauseComponents`
183    ///   (aggregate primitive can't fork)
184    /// - Missing prefix clause → `InvalidWhereClauseComponents`
185    pub fn aggregate_count_path_query(
186        &self,
187        platform_version: &PlatformVersion,
188    ) -> Result<PathQuery, Error> {
189        let range_clause = self
190            .where_clauses
191            .iter()
192            .find(|wc| Self::is_range_operator(wc.operator))
193            .ok_or(Error::Query(
194                QuerySyntaxError::InvalidWhereClauseComponents(
195                    "aggregate_count_path_query requires a range where-clause",
196                ),
197            ))?;
198        let query_item = self.range_clause_to_query_item(range_clause, platform_version)?;
199
200        let mut path = vec![
201            vec![RootTree::DataContractDocuments as u8],
202            self.contract_id.to_vec(),
203            vec![1u8],
204            self.document_type_name.as_bytes().to_vec(),
205        ];
206        let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
207        for prop in prefix_props {
208            let clause = self
209                .where_clauses
210                .iter()
211                .find(|wc| wc.field == prop.name)
212                .ok_or(Error::Query(
213                    QuerySyntaxError::InvalidWhereClauseComponents(
214                        "aggregate-count proof: missing where clause for an index prefix property",
215                    ),
216                ))?;
217            if clause.operator != WhereOperator::Equal {
218                return Err(Error::Query(
219                    QuerySyntaxError::InvalidWhereClauseComponents(
220                        "aggregate-count proof: prefix properties must use `==` (no `in`); \
221                         use a two-field `group_by = [in_field, range_field]` for compound \
222                         In-on-prefix queries",
223                    ),
224                ));
225            }
226            path.push(prop.name.as_bytes().to_vec());
227            path.push(self.document_type.serialize_value_for_key(
228                prop.name.as_str(),
229                &clause.value,
230                platform_version,
231            )?);
232        }
233        let range_prop_name = &self
234            .index
235            .properties
236            .last()
237            .ok_or(Error::Query(
238                QuerySyntaxError::InvalidWhereClauseComponents(
239                    "range_countable index must have at least one property",
240                ),
241            ))?
242            .name;
243        path.push(range_prop_name.as_bytes().to_vec());
244
245        Ok(PathQuery::new_aggregate_count_on_range(path, query_item))
246    }
247
248    /// Build the grovedb `PathQuery` for a **carrier**
249    /// `AggregateCountOnRange` proof — one outer Key per `In`
250    /// value, each terminating in an ACOR boundary walk over the
251    /// per-branch range subtree. Returns one `(in_key, u64)` pair
252    /// per resolved In branch via
253    /// [`grovedb::GroveDb::query_aggregate_count_per_key`] (no-
254    /// proof) and
255    /// [`grovedb::GroveDb::verify_aggregate_count_query_per_key`]
256    /// (verify).
257    ///
258    /// Required where-clause shape (validated upstream by
259    /// [`Self::detect_mode`] routing to
260    /// [`DocumentCountMode::RangeAggregateCarrierProof`]):
261    /// - Exactly one `In` clause on the In-property
262    /// - Exactly one range clause on the *terminator* property of
263    ///   a `range_countable: true` index whose first property is
264    ///   the In-property
265    /// - Any prefix properties between In and range must use
266    ///   `==` (mirror of [`Self::aggregate_count_path_query`]'s
267    ///   non-In prefix rule)
268    ///
269    /// Path-query structure:
270    /// - Outer path stops one level above the In-bearing property
271    ///   subtree's children (`@/doc_prefix/0x01/doctype/<In-prop>`).
272    /// - Outer Query: `Key(in_value_0)`, `Key(in_value_1)`, … in
273    ///   lex-asc serialized order (grovedb's multi-key walker
274    ///   invariant).
275    /// - `subquery_path`: the terminator property name (and any
276    ///   trailing `==` clause names between In and range, in
277    ///   index order).
278    /// - `subquery`: `Query::new_aggregate_count_on_range(range_item)`.
279    ///
280    /// Enabled by [grovedb PR #663](https://github.com/dashpay/grovedb/pull/663).
281    /// Before that PR, `AggregateCountOnRange` was required to be
282    /// the only item in its query and could not appear under a
283    /// `subquery` field — the dispatcher rejected this shape with
284    /// "range count queries with an `in` clause are not supported on
285    /// the aggregate prove path".
286    ///
287    /// Errors:
288    /// - No range where-clause / multiple range where-clauses →
289    ///   `InvalidWhereClauseComponents`
290    /// - No In where-clause → `InvalidWhereClauseComponents`
291    /// - In on a non-prefix property → `InvalidWhereClauseComponents`
292    /// - Prefix property between In and range uses non-Equal →
293    ///   `InvalidWhereClauseComponents`
294    pub fn carrier_aggregate_count_path_query(
295        &self,
296        limit: Option<u16>,
297        left_to_right: bool,
298        platform_version: &PlatformVersion,
299    ) -> Result<PathQuery, Error> {
300        // The terminator property (last in the index) carries the
301        // ACOR target range. The "carrier" property — the one whose
302        // clause becomes the outer Query items — is either:
303        // - An `In` clause (G7 shape: one Key per In value)
304        // - A range clause on a prefix prop (G8 shape: one QueryItem
305        //   bounding the outer range, with `SizedQuery::limit` capping
306        //   how many outer matches the carrier walks — see
307        //   [grovedb PR #664](https://github.com/dashpay/grovedb/pull/664))
308        //
309        // The terminator's clause must be a range and is converted to
310        // the inner ACOR `QueryItem`. Any properties between the
311        // carrier and the terminator must use `==` and extend the
312        // subquery_path.
313        let terminator_prop_name = &self
314            .index
315            .properties
316            .last()
317            .ok_or(Error::Query(
318                QuerySyntaxError::InvalidWhereClauseComponents(
319                    "range_countable index must have at least one property",
320                ),
321            ))?
322            .name;
323        let terminator_clause = self
324            .where_clauses
325            .iter()
326            .find(|wc| wc.field == *terminator_prop_name && Self::is_range_operator(wc.operator))
327            .ok_or(Error::Query(
328                QuerySyntaxError::InvalidWhereClauseComponents(
329                    "carrier_aggregate_count_path_query requires a range where-clause on the \
330                     terminator property of the chosen index",
331                ),
332            ))?;
333        let inner_range_item =
334            self.range_clause_to_query_item(terminator_clause, platform_version)?;
335
336        let mut base_path: Vec<Vec<u8>> = vec![
337            vec![RootTree::DataContractDocuments as u8],
338            self.contract_id.to_vec(),
339            vec![1u8],
340            self.document_type_name.as_bytes().to_vec(),
341        ];
342        let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
343
344        // Carrier clause state: either `None` (not seen yet, still on
345        // the `==`-prefix run), `Some(In)` (G7), or `Some(Range)` (G8).
346        enum Carrier {
347            Pending,
348            In(WhereClause),
349            Range(WhereClause),
350        }
351        let mut carrier = Carrier::Pending;
352        let prefix_and_carrier_props = &self.index.properties[..self.index.properties.len() - 1];
353
354        for prop in prefix_and_carrier_props {
355            let clause = self
356                .where_clauses
357                .iter()
358                .find(|wc| wc.field == prop.name)
359                .ok_or(
360                Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
361                    "carrier-aggregate proof: missing where clause for an index prefix property",
362                )),
363            )?;
364            match (&carrier, clause.operator) {
365                (Carrier::Pending, WhereOperator::Equal) => {
366                    base_path.push(prop.name.as_bytes().to_vec());
367                    base_path.push(self.document_type.serialize_value_for_key(
368                        prop.name.as_str(),
369                        &clause.value,
370                        platform_version,
371                    )?);
372                }
373                (Carrier::Pending, WhereOperator::In) => {
374                    base_path.push(prop.name.as_bytes().to_vec());
375                    carrier = Carrier::In(clause.clone());
376                }
377                (Carrier::Pending, op) if Self::is_range_operator(op) => {
378                    base_path.push(prop.name.as_bytes().to_vec());
379                    carrier = Carrier::Range(clause.clone());
380                }
381                (Carrier::In(_) | Carrier::Range(_), WhereOperator::Equal) => {
382                    subquery_path_extension.push(prop.name.as_bytes().to_vec());
383                    subquery_path_extension.push(self.document_type.serialize_value_for_key(
384                        prop.name.as_str(),
385                        &clause.value,
386                        platform_version,
387                    )?);
388                }
389                (Carrier::In(_) | Carrier::Range(_), _) => {
390                    return Err(Error::Query(
391                        QuerySyntaxError::InvalidWhereClauseComponents(
392                            "carrier-aggregate proof: at most one carrier clause (In or range) \
393                         is supported on prefix properties; subsequent prefix clauses must \
394                         use `==`",
395                        ),
396                    ));
397                }
398                _ => {
399                    return Err(Error::Query(
400                        QuerySyntaxError::InvalidWhereClauseComponents(
401                            "carrier-aggregate proof: prefix property operator unsupported",
402                        ),
403                    ));
404                }
405            }
406        }
407        subquery_path_extension.push(terminator_prop_name.as_bytes().to_vec());
408
409        let mut outer_query = Query::new_with_direction(left_to_right);
410        match carrier {
411            Carrier::Pending => {
412                return Err(Error::Query(
413                    QuerySyntaxError::InvalidWhereClauseComponents(
414                        "carrier-aggregate proof: an In or range clause must appear on a prefix \
415                     property of the chosen index to act as the carrier dimension",
416                    ),
417                ));
418            }
419            Carrier::In(in_clause) => {
420                // Build one Key per In value, sorted lex-ascending
421                // (grovedb's multi-key walker invariant per PR #663).
422                let in_values = in_clause.in_values().into_data_with_error()??;
423                let mut serialized_in_keys: Vec<Vec<u8>> = in_values
424                    .iter()
425                    .map(|v| {
426                        self.document_type.serialize_value_for_key(
427                            in_clause.field.as_str(),
428                            v,
429                            platform_version,
430                        )
431                    })
432                    .collect::<Result<_, _>>()?;
433                serialized_in_keys.sort();
434                serialized_in_keys.dedup();
435                for key in serialized_in_keys {
436                    outer_query.insert_key(key);
437                }
438            }
439            Carrier::Range(range_clause) => {
440                // Single QueryItem bounding the outer range. The
441                // carrier walks this range and emits one `(key, u64)`
442                // pair per matched outer key.
443                let outer_range_item =
444                    self.range_clause_to_query_item(&range_clause, platform_version)?;
445                outer_query.items.push(outer_range_item);
446            }
447        }
448        outer_query.set_subquery_path(subquery_path_extension);
449        outer_query.set_subquery(Query::new_aggregate_count_on_range(inner_range_item));
450
451        // `SizedQuery::limit` is permitted on carriers as of grovedb
452        // PR #664; for In-outer carriers the |IN| array already
453        // bounds the result so `limit` is typically `None`, but for
454        // Range-outer carriers `limit` caps the outer walk and is
455        // load-bearing for proof bytes.
456        Ok(PathQuery::new(
457            base_path,
458            SizedQuery::new(outer_query, limit, None),
459        ))
460    }
461
462    /// Build the grovedb `PathQuery` for a *regular* range query
463    /// against this count query's `range_countable` index — the
464    /// distinct-counts variant. Used by:
465    /// - the server's prove-distinct executor
466    ///   ([`Self::execute_distinct_count_with_proof`])
467    /// - the server's no-proof range executor
468    ///   ([`Self::execute_range_count_no_proof`])
469    /// - the SDK's per-key-count verifier
470    ///   ([`drive_proof_verifier::verify_distinct_count_proof`])
471    ///
472    /// **In-on-prefix support via grovedb subqueries.** Where
473    /// [`Self::aggregate_count_path_query`] rejects In on prefix
474    /// (the aggregate merk primitive can't cartesian-fork), this
475    /// builder uses grovedb's native subquery primitive:
476    ///
477    /// - **Flat shape** (no In on prefix, only Equal): path includes
478    ///   the range terminator; outer Query has the range item.
479    /// - **Compound shape** (one In on prefix): path stops at the
480    ///   In-bearing prop's property-name subtree; outer Query has
481    ///   one `Key(value)` item per In value; `set_subquery_path`
482    ///   carries any post-In Equal-clause `(name, value)` pairs plus
483    ///   the terminator name; `set_subquery` is the range item.
484    ///
485    /// Both shapes return `(path, branched-or-flat Query)` and feed
486    /// the same `grove_get_raw_path_query` / `get_proved_path_query`
487    /// pipelines downstream. The compound shape replaces the
488    /// pre-existing cartesian-fork loop in
489    /// `execute_range_count_no_proof`.
490    ///
491    /// `limit` IS load-bearing for prove-path verification: the
492    /// prover bounds the proof at `limit` matched keys, and the
493    /// verifier must build the exact same `PathQuery` (including
494    /// this cap) for the merk-root recomputation to match. The
495    /// dispatcher pre-validates `limit ≤ max_query_limit` on the
496    /// prove path, so unbounded queries can't reach this builder
497    /// with `Some(...)` greater than the cap. The no-proof path
498    /// passes `None` (full walk) so cross-In-fork merging sees
499    /// every emitted element before the result-set-level limit is
500    /// applied in post-processing.
501    ///
502    /// `left_to_right` controls grovedb's iteration direction:
503    /// `true` (the default, used for ascending `order_by_ascending`)
504    /// walks the range from low key to high key; `false` reverses.
505    /// On the prove path this is load-bearing: the path query's
506    /// `Query.left_to_right` is part of the serialized PathQuery
507    /// bytes, so the prover and verifier must agree on the value or
508    /// the merk-root recomputation fails. For compound queries the
509    /// flag is applied to BOTH the outer In-keys Query and the
510    /// inner range subquery, so descending iteration walks
511    /// `(in_key_desc, key_desc)` tuples (matching what
512    /// `RangeCountOptions::order_by_ascending = false` callers
513    /// expect).
514    ///
515    /// Errors:
516    /// - No range where-clause / multiple range where-clauses
517    /// - Multiple In clauses on prefix props
518    /// - Non-Equal-non-In operator on a prefix prop
519    /// - Missing prefix clause
520    pub fn distinct_count_path_query(
521        &self,
522        limit: Option<u16>,
523        left_to_right: bool,
524        platform_version: &PlatformVersion,
525    ) -> Result<PathQuery, Error> {
526        let range_clause = self
527            .where_clauses
528            .iter()
529            .find(|wc| Self::is_range_operator(wc.operator))
530            .ok_or(Error::Query(
531                QuerySyntaxError::InvalidWhereClauseComponents(
532                    "distinct_count_path_query requires a range where-clause",
533                ),
534            ))?;
535        let range_item = self.range_clause_to_query_item(range_clause, platform_version)?;
536
537        let prefix_props = &self.index.properties[..self.index.properties.len() - 1];
538        let terminator_name = &self
539            .index
540            .properties
541            .last()
542            .ok_or(Error::Query(
543                QuerySyntaxError::InvalidWhereClauseComponents(
544                    "range_countable index must have at least one property",
545                ),
546            ))?
547            .name;
548
549        let mut base_path: Vec<Vec<u8>> = vec![
550            vec![RootTree::DataContractDocuments as u8],
551            self.contract_id.to_vec(),
552            vec![1u8],
553            self.document_type_name.as_bytes().to_vec(),
554        ];
555
556        // `Some(keys)` once an In clause has been encountered on a
557        // prefix property. From that point on, subsequent Equal
558        // clauses go into `subquery_path_extension` rather than
559        // `base_path`. Only one In allowed (multiple Ins would
560        // multiply the fork count beyond what a single Query can
561        // express via `set_subquery_path`).
562        let mut in_outer_keys: Option<Vec<Vec<u8>>> = None;
563        let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
564
565        for prop in prefix_props {
566            let clause = self
567                .where_clauses
568                .iter()
569                .find(|wc| wc.field == prop.name)
570                .ok_or(Error::Query(
571                    QuerySyntaxError::InvalidWhereClauseComponents(
572                        "distinct_count_path_query: missing where clause for an index \
573                         prefix property",
574                    ),
575                ))?;
576
577            match clause.operator {
578                WhereOperator::Equal => {
579                    let serialized = self.document_type.serialize_value_for_key(
580                        prop.name.as_str(),
581                        &clause.value,
582                        platform_version,
583                    )?;
584                    if in_outer_keys.is_some() {
585                        subquery_path_extension.push(prop.name.as_bytes().to_vec());
586                        subquery_path_extension.push(serialized);
587                    } else {
588                        base_path.push(prop.name.as_bytes().to_vec());
589                        base_path.push(serialized);
590                    }
591                }
592                WhereOperator::In => {
593                    if in_outer_keys.is_some() {
594                        return Err(Error::Query(
595                            QuerySyntaxError::InvalidWhereClauseComponents(
596                                "distinct_count_path_query: at most one `In` clause is supported \
597                                 on prefix properties",
598                            ),
599                        ));
600                    }
601                    // Path stops at the In-bearing prop's property-
602                    // name subtree; outer Query lives at that level.
603                    base_path.push(prop.name.as_bytes().to_vec());
604                    let in_values = clause.in_values().into_data_with_error()??;
605                    let mut keys: Vec<Vec<u8>> = in_values
606                        .iter()
607                        .map(|v| {
608                            self.document_type.serialize_value_for_key(
609                                prop.name.as_str(),
610                                v,
611                                platform_version,
612                            )
613                        })
614                        .collect::<Result<_, _>>()?;
615                    // Sort the serialized In keys lex-ascending before
616                    // building the outer Query. This is load-bearing
617                    // for both correctness and DoS-resistance:
618                    // - **Order parity**: grovedb iterates `Key` items
619                    //   in insert order. Without sorting, the emitted
620                    //   `(in_key, key)` tuples come out in user-input
621                    //   order on the prefix dimension, which diverges
622                    //   from the documented lex-asc order contract on
623                    //   the no-proof distinct path (which sorts post-
624                    //   walk) and forces a per-side sort step.
625                    // - **`left_to_right`-driven descent**: with sorted
626                    //   keys, `left_to_right = false` walks the outer
627                    //   In dimension lex-descending — what the caller
628                    //   asked for. Without the sort, descending
629                    //   `left_to_right` just reverses user-input
630                    //   order, which is gibberish.
631                    // - **Pushed-limit safety**: callers that push the
632                    //   path-query limit (no-proof distinct mode) get
633                    //   the bottom-N or top-N entries by lex order,
634                    //   which is the documented limit-on-distinct
635                    //   semantics. With unsorted keys, the path-query
636                    //   limit would give the first-N entries in user-
637                    //   input order — useless for distinct pagination.
638                    //
639                    // Both the prover and the verifier go through this
640                    // builder, so the byte-equality contract still
641                    // holds — the sort happens identically on both
642                    // sides.
643                    keys.sort();
644                    in_outer_keys = Some(keys);
645                }
646                _ => {
647                    return Err(Error::Query(
648                        QuerySyntaxError::InvalidWhereClauseComponents(
649                            "distinct_count_path_query: prefix properties must use `==` or `in`",
650                        ),
651                    ));
652                }
653            }
654        }
655
656        match in_outer_keys {
657            None => {
658                // Flat shape — path includes terminator, single
659                // range-only Query.
660                base_path.push(terminator_name.as_bytes().to_vec());
661                let mut query = Query::new_with_direction(left_to_right);
662                query.insert_item(range_item);
663                Ok(PathQuery::new(
664                    base_path,
665                    SizedQuery::new(query, limit, None),
666                ))
667            }
668            Some(keys) => {
669                // Compound shape — outer Query has one Key per In
670                // value at the In-bearing prop's property-name
671                // subtree. `subquery_path` carries any post-In Equal
672                // pairs + terminator. Subquery is the range item.
673                //
674                // `left_to_right` applies to BOTH the outer Query
675                // and the subquery so descending iteration walks
676                // `(in_key_desc, key_desc)` tuples — otherwise we'd
677                // get e.g. In keys ascending but per-fork terminator
678                // values descending, which is a weird order no
679                // user would expect.
680                let mut outer_query = Query::new_with_direction(left_to_right);
681                for key in keys {
682                    outer_query.insert_key(key);
683                }
684                subquery_path_extension.push(terminator_name.as_bytes().to_vec());
685
686                let mut subquery = Query::new_with_direction(left_to_right);
687                subquery.insert_item(range_item);
688
689                outer_query.set_subquery_path(subquery_path_extension);
690                outer_query.set_subquery(subquery);
691
692                Ok(PathQuery::new(
693                    base_path,
694                    SizedQuery::new(outer_query, limit, None),
695                ))
696            }
697        }
698    }
699
700    /// Build the grovedb `PathQuery` for a point-lookup count proof
701    /// against a `countable: true` index. Returns one element per
702    /// covered branch whose `count_value` is the per-branch document
703    /// count.
704    ///
705    /// Shared between the server-side prove path
706    /// ([`Self::execute_point_lookup_count_with_proof`]) and the
707    /// client-side verify path
708    /// ([`Self::verify_point_lookup_count_proof`]). Both sides must
709    /// produce the *exact same* `PathQuery` for the merk-root
710    /// recomputation to match.
711    ///
712    /// ## Two terminator shapes depending on `range_countable`
713    ///
714    /// The proof's terminal element is at one of two layers, picked
715    /// from [`Index::range_countable`]:
716    ///
717    /// - **Normal `countable: true`** (NOT `range_countable`): the
718    ///   terminator's value tree is a `NormalTree`, and the doc-count
719    ///   `CountTree` sits inside it at the conventional `[0]` child.
720    ///   Proof targets `[..., last_field, last_value, 0]`.
721    /// - **`range_countable: true`**: the terminator's value tree is
722    ///   itself a `CountTree` (continuation property-name subtrees
723    ///   sit beneath as `Element::NonCounted` so they don't pollute
724    ///   the parent count — see `add_indices_for_index_level_for_contract_operations_v0`).
725    ///   The value tree's own `count_value_or_default()` already IS
726    ///   the per-branch doc count, so the proof targets the value
727    ///   tree directly at `[..., last_field, last_value]` and saves
728    ///   one merk-path layer per covered branch.
729    ///
730    /// Concretely the optimization replaces a trailing `Key([0])`
731    /// with `Key(last_value)` against `[..., last_field]` (Equal-
732    /// only, no In) — or against the In-bearing prop's property-name
733    /// subtree (In on terminator) — or replaces the trailing pair in
734    /// `set_subquery_path` (In on prefix + trailing Equals that reach
735    /// the terminator). The query shape stays in the same Query/
736    /// subquery topology so byte-equality across prover and verifier
737    /// is preserved by construction.
738    ///
739    /// ## Shape support
740    ///
741    /// The builder requires the where clauses to **fully cover** the
742    /// index — every property in `self.index.properties` must have a
743    /// matching `Equal` or `In` clause. Partial-coverage shapes
744    /// (where some index properties have no matching clause) require
745    /// a recursive subquery enumeration that this builder does not
746    /// implement (and that the strict picker already rejects upstream).
747    ///
748    /// **`In` may appear at any position in the index.** Equal
749    /// clauses before the In contribute to `base_path`; Equal clauses
750    /// after the In feed `set_subquery_path` on the outer Query so the
751    /// descent under each matched In value lands at the right
752    /// CountTree leaf. At most one `In` clause per query (multiple
753    /// would cartesian-fork beyond what a single `set_subquery`
754    /// expresses).
755    ///
756    /// This is **more permissive than the regular document query
757    /// path's `Index::matches` rule** (`packages/rs-dpp/src/
758    /// data_contract/document_type/index/mod.rs:503`), which restricts
759    /// `In` to the last or before-last index property because its
760    /// path-construction code positionally zips intermediate index
761    /// names with Equal-clause values (see
762    /// `DriveDocumentQuery::get_non_primary_key_path_query`). The
763    /// count path doesn't have that constraint: it's a pure CountTree
764    /// element lookup with no document-key terminator descent, no
765    /// `order_by` interpretation, and no `limit/offset` semantics, so
766    /// `set_subquery_path` with an arbitrary trailing tail just
767    /// works. Both no-proof ([`Self::execute_no_proof`]) and prove
768    /// ([`Self::execute_point_lookup_count_with_proof`]) executors
769    /// route through this single builder, so they accept the same
770    /// query shapes by construction.
771    ///
772    /// Output shapes (`countable` / `range_countable` differ only in
773    /// whether the trailing `Key([0])` is replaced by `Key(last_value)`):
774    /// - **Equal-only, fully covered**:
775    ///   - `countable`: path `[..., last_field, last_value]`, single `Key([0])`.
776    ///   - `range_countable`: path `[..., last_field]`, single
777    ///     `Key(last_value)`.
778    /// - **Equal prefix + `In` (any position) [+ trailing Equals]**:
779    ///   compound query with `base_path` ending at the In-bearing
780    ///   property's property-name subtree (Equal clauses before the
781    ///   In are baked into `base_path`); outer Query has one `Key`
782    ///   per In value (sorted lex-asc for prove/no-proof parity and
783    ///   pushed-limit safety — same convention as
784    ///   [`Self::distinct_count_path_query`]).
785    ///   - **In on terminator**:
786    ///     - `countable`: subquery `Key([0])` under each In value's
787    ///       value tree (`set_subquery_path` unset).
788    ///     - `range_countable`: outer `Key`s already point at the
789    ///       CountTree value trees themselves; no subquery is set.
790    ///   - **In on a prefix + trailing Equals reaching the
791    ///     terminator**: `set_subquery_path` carries the post-In
792    ///     Equal `(name, value)` pairs in index order:
793    ///     - `countable`: full pairs, subquery `Key([0])`.
794    ///     - `range_countable`: last pair's `value` is hoisted out as
795    ///       the subquery's single `Key(value)`; `set_subquery_path`
796    ///       ends at the terminator's property-name segment.
797    ///
798    /// ## Errors
799    ///
800    /// Rejects shapes the builder doesn't support:
801    /// - Partial coverage (uncovered index property)
802    /// - More than one `In` clause
803    /// - Any non-`Equal` / non-`In` operator (defense-in-depth; mode
804    ///   detection already filters these out)
805    ///
806    /// [`Index::range_countable`]: dpp::data_contract::document_type::index::Index::range_countable
807    pub fn point_lookup_count_path_query(
808        &self,
809        platform_version: &PlatformVersion,
810    ) -> Result<PathQuery, Error> {
811        if self.index.properties.is_empty() {
812            return Err(Error::Query(
813                QuerySyntaxError::InvalidWhereClauseComponents(
814                    "point_lookup_count_path_query: index must have at least one property",
815                ),
816            ));
817        }
818
819        let mut base_path: Vec<Vec<u8>> = vec![
820            vec![RootTree::DataContractDocuments as u8],
821            self.contract_id.to_vec(),
822            vec![1u8],
823            self.document_type_name.as_bytes().to_vec(),
824        ];
825
826        // `in_outer_keys` is populated when we encounter the (single)
827        // `In` clause. Equal clauses *before* the In contribute to
828        // `base_path`; Equal clauses *after* the In feed
829        // `subquery_path_extension`, which becomes the outer Query's
830        // `set_subquery_path` — i.e., the descent under each matched
831        // In value walks `[trailing_field_1, trailing_value_1, ...,
832        // trailing_field_n, trailing_value_n]` before the
833        // selector subquery (either `Key([0])` for normal countable
834        // or a `Key(terminator_value)` lift for range_countable —
835        // see the post-loop selector decision below) picks off the
836        // count-bearing element.
837        //
838        // No position restriction on the In clause: any index
839        // position works because the count path doesn't have the
840        // positional path-construction assumption the regular
841        // document query path makes (see this method's docstring for
842        // the divergence rationale).
843        let mut in_outer_keys: Option<Vec<Vec<u8>>> = None;
844        let mut subquery_path_extension: Vec<Vec<u8>> = vec![];
845
846        for prop in self.index.properties.iter() {
847            let clause = self
848                .where_clauses
849                .iter()
850                .find(|wc| wc.field == prop.name)
851                .ok_or_else(|| {
852                    Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
853                        "prove count requires the where clauses to fully cover the \
854                         countable index; one or more index properties have no \
855                         matching `==` or `in` clause — use a more specific index \
856                         (define a `countable: true` index whose properties exactly \
857                         match the clauses) or use `prove=false`",
858                    ))
859                })?;
860
861            match clause.operator {
862                WhereOperator::Equal => {
863                    let serialized = self.document_type.serialize_value_for_key(
864                        prop.name.as_str(),
865                        &clause.value,
866                        platform_version,
867                    )?;
868                    if in_outer_keys.is_some() {
869                        // Trailing Equal after the (already-seen) In:
870                        // descend through it as part of the subquery
871                        // path. Any number of these may accumulate —
872                        // one for each Equal that sits *after* the In
873                        // in the index ordering.
874                        subquery_path_extension.push(prop.name.as_bytes().to_vec());
875                        subquery_path_extension.push(serialized);
876                    } else {
877                        base_path.push(prop.name.as_bytes().to_vec());
878                        base_path.push(serialized);
879                    }
880                }
881                WhereOperator::In => {
882                    if in_outer_keys.is_some() {
883                        return Err(Error::Query(
884                            QuerySyntaxError::InvalidWhereClauseComponents(
885                                "prove count: at most one `in` clause is supported on \
886                                 the covering countable index",
887                            ),
888                        ));
889                    }
890                    // Stops `base_path` at the In-bearing property's
891                    // property-name subtree; outer Query lives at
892                    // that level. Any trailing Equal property then
893                    // routes through `subquery_path_extension`.
894                    base_path.push(prop.name.as_bytes().to_vec());
895                    let in_values = clause.in_values().into_data_with_error()??;
896                    let mut keys: Vec<Vec<u8>> = in_values
897                        .iter()
898                        .map(|v| {
899                            self.document_type.serialize_value_for_key(
900                                prop.name.as_str(),
901                                v,
902                                platform_version,
903                            )
904                        })
905                        .collect::<Result<_, _>>()?;
906                    // Sort lex-asc for prove/no-proof entry-order
907                    // parity and so the pushed-limit (if any) gives
908                    // the documented "first N by lex" semantics.
909                    // Same convention as `distinct_count_path_query`.
910                    keys.sort();
911                    in_outer_keys = Some(keys);
912                }
913                _ => {
914                    return Err(Error::Query(
915                        QuerySyntaxError::InvalidWhereClauseComponents(
916                            "point_lookup_count_path_query: index properties must use \
917                             `==` or `in`",
918                        ),
919                    ));
920                }
921            }
922        }
923
924        // Whether the terminator's value tree is itself a `CountTree`
925        // (carries the per-branch doc count directly) vs. a
926        // `NormalTree` whose `[0]` child is the `CountTree`. Drives
927        // the selector-element decision below.
928        //
929        // The insertion side
930        // (`add_indices_for_index_level_for_contract_operations_v0`)
931        // makes the terminator value tree a `CountTree` for **any**
932        // countable index — not just `range_countable: true`. Both
933        // tiers (`Countable` and `CountableAllowingOffset`) layout
934        // the value tree the same way: a `CountTree` whose count
935        // equals the `[0]` ref-bucket's doc count (continuations
936        // wrapped `NonCounted` so they don't pollute the parent).
937        // `range_countable` only additionally upgrades the
938        // property-name tree to `ProvableCountTree` for
939        // `AggregateCountOnRange` queries — that's orthogonal to the
940        // point-lookup proof shape.
941        //
942        // So gate the optimization on `countable.is_countable()`:
943        // every countable index uses the compact shape. The picker
944        // upstream already requires the index to be countable to be
945        // selected (`find_countable_index_for_where_clauses` / the
946        // range_countable picker for range shapes), so reaching this
947        // builder with a non-countable index would be a bug — but
948        // we keep the gate explicit for clarity.
949        //
950        // The loop above already enforces full coverage of every
951        // index property, so the terminator is always proven; this
952        // flag is the only differentiator between the two output
953        // shapes.
954        let count_tree_terminator = self.index.countable.is_countable();
955
956        // CountTree storage convention for non-countable indexes
957        // (defensive — picker upstream filters these out): the count
958        // lives at the `[0]` child of the value
959        // tree. See the book's "Count Trees and Provable Counts"
960        // chapter for the layout.
961        const COUNT_TREE_KEY: u8 = 0;
962
963        match in_outer_keys {
964            None => {
965                // Equal-only, fully covered.
966                //
967                // - normal countable: `base_path` ends at
968                //   `[..., last_field, last_value]`; query asks for
969                //   the single key `[0]` (the CountTree under the
970                //   value tree).
971                // - `range_countable`: peel the trailing `last_value`
972                //   off `base_path` and use it as the query's Key.
973                //   The resolved element is the value tree itself
974                //   (a CountTree), and its `count_value_or_default()`
975                //   is the per-branch count — one merk layer shorter
976                //   per resolved branch than the `[0]` shape.
977                let mut query = Query::new();
978                if count_tree_terminator {
979                    // The Equal loop always pushes (name, value) per
980                    // prop, so `base_path` has at least the trailing
981                    // serialized `last_value` to lift. The expect()
982                    // here would fire only if the loop above changed
983                    // its push contract — a load-bearing invariant
984                    // checked by every test in this module that
985                    // routes through this builder.
986                    let last_value = base_path.pop().expect(
987                        "Equal-only loop pushes (name, value) per prop; \
988                         base_path must hold the terminator's serialized value",
989                    );
990                    query.insert_key(last_value);
991                } else {
992                    query.insert_key(vec![COUNT_TREE_KEY]);
993                }
994                Ok(PathQuery::new(
995                    base_path,
996                    SizedQuery::new(query, None, None),
997                ))
998            }
999            Some(keys) => {
1000                // Compound shape. `base_path` ends at the In-bearing
1001                // property's property-name subtree; the outer Query
1002                // enumerates serialized In values; the subquery
1003                // (when present) descends from each matched In value
1004                // to the count-bearing element.
1005                //
1006                // `subquery_path_extension` carries 0..N segments,
1007                // one `(prop_name, serialized_value)` pair per Equal
1008                // clause that sits *after* the In in the index
1009                // ordering. The exact subquery topology depends on
1010                // both whether trailing Equals exist AND whether the
1011                // terminator is range_countable; see the inline
1012                // branches below.
1013                let mut outer_query = Query::new();
1014                for key in keys {
1015                    outer_query.insert_key(key);
1016                }
1017
1018                if subquery_path_extension.is_empty() {
1019                    // **In on the terminator** (no trailing Equals).
1020                    if count_tree_terminator {
1021                        // Outer `Key`s already point at the terminator
1022                        // value trees, which are themselves CountTrees.
1023                        // No subquery is needed — grovedb returns one
1024                        // element per matched outer Key.
1025                    } else {
1026                        // Normal countable: descend one more layer
1027                        // under each matched In value's NormalTree
1028                        // value tree to grab the `Key([0])` CountTree
1029                        // child.
1030                        let mut subquery = Query::new();
1031                        subquery.insert_key(vec![COUNT_TREE_KEY]);
1032                        outer_query.set_subquery(subquery);
1033                    }
1034                } else {
1035                    // **In on a prefix + trailing Equals** that
1036                    // collectively reach the terminator.
1037                    let mut subquery = Query::new();
1038                    if count_tree_terminator {
1039                        // The terminator's serialized value is the
1040                        // last element pushed into
1041                        // `subquery_path_extension` (the trailing-
1042                        // Equal loop pushes `[name, value, ...,
1043                        // termname, termval]`). Lift `termval` out
1044                        // as the subquery's Key so the descent stops
1045                        // at the terminator's property-name subtree
1046                        // and the subquery resolves the CountTree
1047                        // value tree directly. `subquery_path_extension`
1048                        // is left at an odd length on purpose — it
1049                        // ends with the terminator's `name` segment,
1050                        // exactly where the subquery's `Key(termval)`
1051                        // picks up.
1052                        let termval = subquery_path_extension.pop().expect(
1053                            "trailing-Equal loop pushes (name, value) pairs; \
1054                             non-empty extension's tail must be the terminator's \
1055                             serialized value",
1056                        );
1057                        subquery.insert_key(termval);
1058                    } else {
1059                        // Normal countable: subquery descends to the
1060                        // `Key([0])` CountTree at the resolved leaf,
1061                        // with the full `(name, value)` pairs of the
1062                        // trailing Equals consumed by
1063                        // `set_subquery_path`.
1064                        subquery.insert_key(vec![COUNT_TREE_KEY]);
1065                    }
1066                    outer_query.set_subquery_path(subquery_path_extension);
1067                    outer_query.set_subquery(subquery);
1068                }
1069
1070                // `SizedQuery::new(_, None, None)` is intentional —
1071                // PointLookupProof always returns ALL In branches.
1072                // The handler rejects `limit` upstream on this path
1073                // (see [`CountMode::accepts_limit`]'s `GroupByIn`
1074                // arm) because the In array is already capped at 100
1075                // by `WhereClause::in_values()`, and a partial-In
1076                // selection isn't representable in this `SizedQuery`
1077                // shape without rebuilding the verifier to know
1078                // which subset got truncated.
1079                Ok(PathQuery::new(
1080                    base_path,
1081                    SizedQuery::new(outer_query, None, None),
1082                ))
1083            }
1084        }
1085    }
1086
1087    /// Build the grovedb `PathQuery` for proving the document type's
1088    /// primary-key `CountTree` element at `[contract_doc, contract_id,
1089    /// 1, doctype, 0]`. Used for unfiltered total counts when the
1090    /// document type has `documents_countable: true` — the
1091    /// type-level CountTree's `count_value` IS the total document
1092    /// count, no index walk needed.
1093    ///
1094    /// Shared between the server-side prove path
1095    /// ([`Drive::execute_document_count_point_lookup_proof`]'s
1096    /// documents_countable fast path) and the client-side verify path
1097    /// ([`Self::verify_primary_key_count_tree_proof`]). Both sides
1098    /// produce the exact same `PathQuery` for merk-root recomputation.
1099    ///
1100    /// Free function rather than a method on `DriveDocumentCountQuery`
1101    /// because the documents_countable case isn't tied to any index —
1102    /// it operates at the doctype level directly.
1103    pub fn primary_key_count_tree_path_query(
1104        contract_id: [u8; 32],
1105        document_type_name: &str,
1106    ) -> PathQuery {
1107        let path = vec![
1108            vec![RootTree::DataContractDocuments as u8],
1109            contract_id.to_vec(),
1110            vec![1u8],
1111            document_type_name.as_bytes().to_vec(),
1112        ];
1113        let mut query = Query::new();
1114        query.insert_key(vec![0]);
1115        PathQuery::new(path, SizedQuery::new(query, None, None))
1116    }
1117}