drive/query/drive_document_having_query/execute_range.rs
1//! The two having-range executors on [`DriveDocumentHavingQuery`]: a
2//! direct value-bounded read of the axis secondary, and generation of
3//! the equivalent proof.
4//!
5//! Both are thin — all of the work happens inside grovedb, which seeks
6//! straight to the encoded bounds in the pre-sorted secondary Merk. No
7//! value trees are opened, no documents are materialized, and the cost
8//! is `O(log n + k)` in the number of *matching* groups returned, never
9//! in the total group population.
10//!
11//! Whole module is gated `feature = "server"` via the parent's
12//! `pub mod execute_range;` declaration.
13
14use super::super::drive_document_ranked_query::branches::{
15 axis_keys_to_ranked, decompose_branch_paths, read_branched_union,
16};
17use super::super::drive_document_ranked_query::RankedEntry;
18use super::DriveDocumentHavingQuery;
19use crate::drive::Drive;
20use crate::error::drive::DriveError;
21use crate::error::Error;
22use dpp::version::PlatformVersion;
23use grovedb::query_result_type::QueryResultType;
24use grovedb::{PathQuery, PathQueryRun, TransactionArg};
25use grovedb_costs::CostContext;
26use grovedb_query::AxisQuery;
27
28impl DriveDocumentHavingQuery<'_> {
29 /// Read the matching groups directly from the axis secondary: every
30 /// group whose aggregate falls inside the bounds, up to `limit`, in
31 /// axis order in the walk direction.
32 ///
33 /// Fewer than `limit` entries is normal (fewer groups match) and is
34 /// not an error; exactly `limit` entries may mean the match set was
35 /// cut.
36 ///
37 /// Missing paths follow the ranked surface's rule. A pinned prefix
38 /// no document has written yet (a `timeRange` bucket before its
39 /// first document, a `hashtag` nobody has used) is an empty match
40 /// set, not an error: grovedb answers a single-path axis read over
41 /// a path that does not exist with the traversal's empty result on
42 /// the read and the proof alike. On an `IN`-pinned request, an
43 /// element whose branch chain is missing at ANY depth — the branch
44 /// key, or any deeper pinned segment under a *present* key —
45 /// contributes an **empty branch** (union semantics, exactly as the
46 /// proved envelope authenticates it), and the union is served from
47 /// one committed state (a `None` read runs under a grovedb snapshot
48 /// read transaction). What stays an error is a path that exists but
49 /// does not lead to an indexed tree carrying the axis. An index with
50 /// no documents has the tree, with an empty secondary, and yields an
51 /// empty entry list.
52 pub fn execute_range_no_proof(
53 &self,
54 drive: &Drive,
55 transaction: TransactionArg,
56 platform_version: &PlatformVersion,
57 ) -> Result<Vec<RankedEntry>, Error> {
58 if self.prefix_branches.len() > 1 {
59 // ONE grovedb call for the whole union, pinned to one
60 // committed state — the entire sequence lives in the ranked
61 // surface's `branches::read_branched_union`, shared with the
62 // ranked executor so the two cannot drift.
63 let paths = (0..self.prefix_branches.len())
64 .map(|branch| self.indexed_property_name_tree_path(branch))
65 .collect::<Result<Vec<_>, Error>>()?;
66 let axis = self.bounds.axis();
67 let (lo, hi) = self.bounds.inclusive_bounds_i128();
68 return read_branched_union(
69 &drive.grove,
70 "having",
71 &self.prefix_branches,
72 &paths,
73 axis,
74 AxisQuery::bounded(axis.into(), lo, hi, self.limit, self.descending),
75 self.limit as usize,
76 self.descending,
77 transaction,
78 &platform_version.drive.grove_version,
79 );
80 }
81 self.execute_range_no_proof_branch(0, drive, transaction, platform_version)
82 }
83
84 /// One branch's in-bound page — the entire pre-`IN` executor,
85 /// parameterized by which prefix branch's terminal tree it walks.
86 fn execute_range_no_proof_branch(
87 &self,
88 branch: usize,
89 drive: &Drive,
90 transaction: TransactionArg,
91 platform_version: &PlatformVersion,
92 ) -> Result<Vec<RankedEntry>, Error> {
93 let grove_version = &platform_version.drive.grove_version;
94 let path = self.indexed_property_name_tree_path(branch)?;
95 let axis = self.bounds.axis();
96 let (lo, hi) = self.bounds.inclusive_bounds_i128();
97
98 // Costs are destructured away rather than `.unwrap()`-ed, same
99 // as the ranked executors: `CostContext::unwrap` is infallible
100 // but reads like a panicking unwrap at the call site.
101 let path_query = PathQuery::new_axis(
102 path,
103 AxisQuery::bounded(axis.into(), lo, hi, self.limit, self.descending).keys_only(),
104 );
105 let CostContext { value, cost: _ } = drive.grove.run_path_query(
106 &path_query,
107 true,
108 true,
109 true,
110 QueryResultType::QueryKeyElementPairResultType,
111 transaction,
112 grove_version,
113 );
114 let run = value.map_err(|e| Error::GroveDB(Box::new(e)))?;
115 let PathQueryRun::AxisKeys { keys, skipped: _ } = run else {
116 return Err(Error::Drive(DriveError::CorruptedDriveState(
117 "a keys-only having read returned a different result shape".to_string(),
118 )));
119 };
120 let entries = axis_keys_to_ranked(axis, keys)?;
121 if entries.len() > self.limit as usize {
122 return Err(Error::Drive(DriveError::CorruptedDriveState(format!(
123 "having {axis:?} read returned {} entries for limit = {}",
124 entries.len(),
125 self.limit
126 ))));
127 }
128 Ok(entries)
129 }
130
131 /// Generate the grovedb indexed-axis range proof for this query.
132 ///
133 /// The envelope commits the in-range secondary entries, the
134 /// primary's root hash, the sibling axes' root hashes, and a
135 /// per-ancestor attestation chain up to the grovedb root — so the
136 /// client reconstructs the platform root hash from it. The bounds,
137 /// direction and limit bind by RECONSTRUCTION: the verifier rebuilds
138 /// the same `Bounded` axis `PathQuery` from the request
139 /// ([`AxisRangeBounds::inclusive_bounds_i128`]) and re-executes the
140 /// proof against it — which is why the bounds are validated rather
141 /// than clamped upstream, and why completeness needs no extra
142 /// machinery: a Merk range proof over a sorted keyspace commits its
143 /// boundaries, so an in-range group the server omitted fails
144 /// reconstruction.
145 ///
146 /// Verified by
147 /// [`DriveDocumentHavingQuery::verify_having_range_proof`](crate::query::DriveDocumentHavingQuery::verify_having_range_proof).
148 pub fn execute_range_with_proof(
149 &self,
150 drive: &Drive,
151 transaction: TransactionArg,
152 platform_version: &PlatformVersion,
153 ) -> Result<Vec<u8>, Error> {
154 // Same fail-closed rule as the ranked prover: grovedb's
155 // `prove_query` proves committed state only and cannot see the
156 // caller's transaction — single-prefix and branched alike.
157 if transaction.is_some() {
158 return Err(Error::Drive(DriveError::NotSupported(
159 "a having-range proof is generated from committed state only: grovedb's \
160 prove_query cannot see the caller's transaction — commit first",
161 )));
162 }
163 if self.prefix_branches.len() > 1 {
164 // One grovedb **branched** envelope — see the ranked
165 // executor's multi-branch arm for the shape.
166 let grove_version = &platform_version.drive.grove_version;
167 let paths = (0..self.prefix_branches.len())
168 .map(|branch| self.indexed_property_name_tree_path(branch))
169 .collect::<Result<Vec<_>, Error>>()?;
170 let (prefix, keys, suffix) = decompose_branch_paths(&paths)?;
171 let (lo, hi) = self.bounds.inclusive_bounds_i128();
172 let path_query = PathQuery::new_branched_axis(
173 prefix,
174 keys,
175 suffix,
176 AxisQuery::bounded(
177 self.bounds.axis().into(),
178 lo,
179 hi,
180 self.limit,
181 self.descending,
182 ),
183 );
184 let CostContext { value, cost: _ } =
185 drive.grove.prove_query(&path_query, None, grove_version);
186 return value.map_err(|e| Error::GroveDB(Box::new(e)));
187 }
188 self.execute_range_with_proof_branch(0, drive, platform_version)
189 }
190
191 /// One branch's proof — the entire pre-`IN` prover, parameterized by
192 /// the prefix branch.
193 fn execute_range_with_proof_branch(
194 &self,
195 branch: usize,
196 drive: &Drive,
197 platform_version: &PlatformVersion,
198 ) -> Result<Vec<u8>, Error> {
199 let grove_version = &platform_version.drive.grove_version;
200 let path = self.indexed_property_name_tree_path(branch)?;
201 let (lo, hi) = self.bounds.inclusive_bounds_i128();
202 let path_query = PathQuery::new_axis_bounded(
203 path,
204 self.bounds.axis().into(),
205 lo,
206 hi,
207 self.limit,
208 self.descending,
209 );
210 // Same destructure-don't-unwrap rationale as the no-proof arm.
211 let CostContext { value, cost: _ } =
212 drive.grove.prove_query(&path_query, None, grove_version);
213 value.map_err(|e| Error::GroveDB(Box::new(e)))
214 }
215}