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