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}