Skip to main content

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::{present_entries_on_axis, 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 read_bounds = self.read_bounds();
67            let axis = read_bounds.axis();
68            let (lo, hi) = read_bounds.inclusive_bounds_i128();
69            return read_branched_union(
70                &drive.grove,
71                "having",
72                &self.prefix_branches,
73                &paths,
74                axis,
75                AxisQuery::bounded(axis.into(), lo, hi, self.limit, self.descending),
76                self.limit as usize,
77                self.descending,
78                transaction,
79                &platform_version.drive.grove_version,
80            )
81            .map(|entries| present_entries_on_axis(self.bounds.axis(), entries));
82        }
83        self.execute_range_no_proof_branch(0, drive, transaction, platform_version)
84    }
85
86    /// One branch's in-bound page — the entire pre-`IN` executor,
87    /// parameterized by which prefix branch's terminal tree it walks.
88    fn execute_range_no_proof_branch(
89        &self,
90        branch: usize,
91        drive: &Drive,
92        transaction: TransactionArg,
93        platform_version: &PlatformVersion,
94    ) -> Result<Vec<RankedEntry>, Error> {
95        let grove_version = &platform_version.drive.grove_version;
96        let path = self.indexed_property_name_tree_path(branch)?;
97        let read_bounds = self.read_bounds();
98        let axis = read_bounds.axis();
99        let (lo, hi) = read_bounds.inclusive_bounds_i128();
100
101        // Costs are destructured away rather than `.unwrap()`-ed, same
102        // as the ranked executors: `CostContext::unwrap` is infallible
103        // but reads like a panicking unwrap at the call site.
104        let path_query = PathQuery::new_axis(
105            path,
106            AxisQuery::bounded(axis.into(), lo, hi, self.limit, self.descending).keys_only(),
107        );
108        let CostContext { value, cost: _ } = drive.grove.run_path_query(
109            &path_query,
110            true,
111            true,
112            true,
113            QueryResultType::QueryKeyElementPairResultType,
114            transaction,
115            grove_version,
116        );
117        let run = value.map_err(|e| Error::GroveDB(Box::new(e)))?;
118        let PathQueryRun::AxisKeys { keys, skipped: _ } = run else {
119            return Err(Error::Drive(DriveError::CorruptedDriveState(
120                "a keys-only having read returned a different result shape".to_string(),
121            )));
122        };
123        let entries = axis_keys_to_ranked(axis, keys)?;
124        if entries.len() > self.limit as usize {
125            return Err(Error::Drive(DriveError::CorruptedDriveState(format!(
126                "having {axis:?} read returned {} entries for limit = {}",
127                entries.len(),
128                self.limit
129            ))));
130        }
131        Ok(present_entries_on_axis(self.bounds.axis(), entries))
132    }
133
134    /// Generate the grovedb indexed-axis range proof for this query.
135    ///
136    /// The envelope commits the in-range secondary entries, the
137    /// primary's root hash, the sibling axes' root hashes, and a
138    /// per-ancestor attestation chain up to the grovedb root — so the
139    /// client reconstructs the platform root hash from it. The bounds,
140    /// direction and limit bind by RECONSTRUCTION: the verifier rebuilds
141    /// the same `Bounded` axis `PathQuery` from the request
142    /// ([`AxisRangeBounds::inclusive_bounds_i128`]) and re-executes the
143    /// proof against it — which is why the bounds are validated rather
144    /// than clamped upstream, and why completeness needs no extra
145    /// machinery: a Merk range proof over a sorted keyspace commits its
146    /// boundaries, so an in-range group the server omitted fails
147    /// reconstruction.
148    ///
149    /// Verified by
150    /// [`DriveDocumentHavingQuery::verify_having_range_proof`](crate::query::DriveDocumentHavingQuery::verify_having_range_proof).
151    pub fn execute_range_with_proof(
152        &self,
153        drive: &Drive,
154        transaction: TransactionArg,
155        platform_version: &PlatformVersion,
156    ) -> Result<Vec<u8>, Error> {
157        // Same fail-closed rule as the ranked prover: grovedb's
158        // `prove_query` proves committed state only and cannot see the
159        // caller's transaction — single-prefix and branched alike.
160        if transaction.is_some() {
161            return Err(Error::Drive(DriveError::NotSupported(
162                "a having-range proof is generated from committed state only: grovedb's \
163                 prove_query cannot see the caller's transaction — commit first",
164            )));
165        }
166        if self.prefix_branches.len() > 1 {
167            // One grovedb **branched** envelope — see the ranked
168            // executor's multi-branch arm for the shape.
169            let grove_version = &platform_version.drive.grove_version;
170            let paths = (0..self.prefix_branches.len())
171                .map(|branch| self.indexed_property_name_tree_path(branch))
172                .collect::<Result<Vec<_>, Error>>()?;
173            let (prefix, keys, suffix) = decompose_branch_paths(&paths)?;
174            let read_bounds = self.read_bounds();
175            let (lo, hi) = read_bounds.inclusive_bounds_i128();
176            let path_query = PathQuery::new_branched_axis(
177                prefix,
178                keys,
179                suffix,
180                AxisQuery::bounded(
181                    read_bounds.axis().into(),
182                    lo,
183                    hi,
184                    self.limit,
185                    self.descending,
186                ),
187            );
188            let CostContext { value, cost: _ } =
189                drive.grove.prove_query(&path_query, None, grove_version);
190            return value.map_err(|e| Error::GroveDB(Box::new(e)));
191        }
192        self.execute_range_with_proof_branch(0, drive, platform_version)
193    }
194
195    /// One branch's proof — the entire pre-`IN` prover, parameterized by
196    /// the prefix branch.
197    fn execute_range_with_proof_branch(
198        &self,
199        branch: usize,
200        drive: &Drive,
201        platform_version: &PlatformVersion,
202    ) -> Result<Vec<u8>, Error> {
203        let grove_version = &platform_version.drive.grove_version;
204        let path = self.indexed_property_name_tree_path(branch)?;
205        let read_bounds = self.read_bounds();
206        let (lo, hi) = read_bounds.inclusive_bounds_i128();
207        let path_query = PathQuery::new_axis_bounded(
208            path,
209            read_bounds.axis().into(),
210            lo,
211            hi,
212            self.limit,
213            self.descending,
214        );
215        // Same destructure-don't-unwrap rationale as the no-proof arm.
216        let CostContext { value, cost: _ } =
217            drive.grove.prove_query(&path_query, None, grove_version);
218        value.map_err(|e| Error::GroveDB(Box::new(e)))
219    }
220}