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