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