drive/query/drive_document_having_query/drive_dispatcher.rs
1//! [`DocumentHavingRequest`] / [`DocumentHavingResponse`] and the
2//! having-range dispatcher on `impl Drive` — the ABI drive-abci's
3//! routing layer names.
4
5use super::super::drive_document_ranked_query::{RankedEntry, RankedPaginationInputs};
6use super::mode_detection::detect_having_mode;
7use crate::drive::Drive;
8use crate::error::query::QuerySyntaxError;
9use crate::error::Error;
10use crate::query::having::HavingClause;
11use crate::query::projection::SelectProjection;
12use crate::query::{OrderClause, ResolvedTimeRange, WhereClause};
13use dpp::data_contract::accessors::v0::DataContractV0Getters;
14use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
15use dpp::data_contract::document_type::DocumentTypeRef;
16use dpp::data_contract::DataContract;
17use dpp::version::PlatformVersion;
18use grovedb::TransactionArg;
19
20/// All inputs required by [`Drive::execute_document_having_request`].
21/// Built by the gRPC handler from a `GetDocumentsRequestV1` after
22/// wire-decoding + contract lookup — the same construction pattern as
23/// [`super::super::drive_document_ranked_query::DocumentRankedRequest`].
24///
25/// `offset` and `start_at` are carried even though a having-range
26/// request must leave both empty: drive owns the rejection, so the
27/// contract is enforced identically no matter which upstream path built
28/// the request. See [`super::mode_detection::detect_having_mode_v0`]
29/// for why each is refused rather than ignored.
30pub struct DocumentHavingRequest<'a> {
31 /// Live contract (already loaded by the handler).
32 pub contract: &'a DataContract,
33 /// Resolved document type within `contract`.
34 pub document_type: DocumentTypeRef<'a>,
35 /// The single `GROUP BY` property. Must be the ranked index's only
36 /// property.
37 pub group_by: &'a [String],
38 /// The projection whose aggregate the `having` clause bounds:
39 /// `COUNT(*)`, `SUM(field)` or `AVG(field)`.
40 pub select: SelectProjection,
41 /// The `HAVING` clauses. Exactly one, bounding the selected
42 /// aggregate.
43 pub having: &'a [HavingClause],
44 /// The `ORDER BY` clauses. Empty (ascending default) or exactly
45 /// one, naming the selected aggregate.
46 pub order_by: &'a [OrderClause],
47 /// Structured `where` clauses. Empty for the single-property form;
48 /// pins on the covering compound index's leading properties for
49 /// the pinned-prefix form: one equality pin per property, of which
50 /// at most one may instead be a bounded `IN` (one branch per
51 /// element, merged; entries then carry `in_key`).
52 pub where_clauses: &'a [WhereClause],
53 /// The provenance of any `where_clauses` equality produced by
54 /// `IN_TIME_RANGE` resolution (see
55 /// [`crate::query::DriveDocumentQuery::resolved_time_ranges`]). At most
56 /// one; index selection consumes it through
57 /// [`crate::query::index_admissible_for_resolved_time_range`], which
58 /// routes the resolved bucket-start pin to exactly the grid it was
59 /// resolved against — and keeps raw requests off bucketed indexes,
60 /// where a hand-written equality would authenticate raw-timestamp
61 /// matches at the bucket boundary instead of window membership.
62 pub resolved_time_ranges: &'a [ResolvedTimeRange],
63 /// Request `limit`. **Required**; `1 ..= MAX_HAVING_LIMIT`.
64 pub limit: Option<u32>,
65 /// Request `offset`. Must be `None` — the range walk has no skip.
66 pub offset: Option<u32>,
67 /// Whether the request carried a `start_at` / `start_after` cursor.
68 /// Must be `false`.
69 pub has_start_at: bool,
70 /// Whether to produce a proof instead of materializing entries.
71 pub prove: bool,
72}
73
74/// Output shape of [`Drive::execute_document_having_request`].
75///
76/// - `Entries` — the matching groups **in axis order in the walk
77/// direction**; the abci handler maps this straight onto the wire's
78/// ranked-entries shape (with no rank base) without re-sorting.
79/// - `Proof(Vec<u8>)` — grovedb indexed-axis range proof bytes the
80/// client verifies with
81/// [`DriveDocumentHavingQuery::verify_having_range_proof`](crate::query::DriveDocumentHavingQuery::verify_having_range_proof),
82/// which recovers the same entry list.
83#[derive(Debug, Clone)]
84pub enum DocumentHavingResponse {
85 /// The groups whose aggregate falls inside the bound, cut at the
86 /// request's limit.
87 Entries(Vec<RankedEntry>),
88 /// Grovedb indexed-axis range proof bytes.
89 Proof(Vec<u8>),
90}
91
92impl Drive {
93 /// Single entry point for a having-range document request.
94 ///
95 /// 1. [`detect_having_mode`] validates the request shape and
96 /// resolves the `(bounds, descending, limit, group property,
97 /// aggregate field)` tuple.
98 /// 2. The matching executor picks the covering ranked index and runs
99 /// the read or the proof.
100 /// 3. The result is wrapped in [`DocumentHavingResponse`].
101 ///
102 /// Errors:
103 /// - Request-shape failures (wrong `group_by` arity, a clause on an
104 /// aggregate the select does not project, an untranslatable
105 /// operator, a missing or out-of-range `limit`, a `where`, an
106 /// `offset`) come back as `Error::Query(QuerySyntaxError::*)` —
107 /// see [`super::mode_detection::detect_having_mode_v0`] for the
108 /// full grammar.
109 /// - "No index declares this axis" comes back as
110 /// `Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty)`
111 /// naming the missing contract keyword.
112 /// - Everything else (grovedb, versioning) surfaces as its native
113 /// `Error` variant.
114 pub fn execute_document_having_request(
115 &self,
116 request: DocumentHavingRequest,
117 transaction: TransactionArg,
118 platform_version: &PlatformVersion,
119 ) -> Result<DocumentHavingResponse, Error> {
120 // A transform's source must be its index's first property, so no
121 // single index can serve two resolved buckets; rejected before
122 // routing, mirroring the ranked dispatcher. A single resolution is
123 // consumed by index selection (the picker's admissibility rule
124 // routes it to exactly the grid it was resolved against, and keeps
125 // raw requests off bucketed indexes) — the resolved bucket-start
126 // equality pins the bucketed first level like any other leading
127 // property.
128 if request.resolved_time_ranges.len() > 1 {
129 return Err(Error::Query(QuerySyntaxError::Unsupported(format!(
130 "at most one time-range selection (IN_TIME_RANGE) is supported per \
131 having-range query; this one resolves {:?}, and no single index can \
132 bucket more than one field",
133 request.resolved_time_ranges
134 ))));
135 }
136 let mode = detect_having_mode(
137 &request.select,
138 request.group_by,
139 request.having,
140 request.order_by,
141 request.where_clauses,
142 RankedPaginationInputs {
143 limit: request.limit,
144 offset: request.offset,
145 has_start_at: request.has_start_at,
146 },
147 platform_version,
148 )?;
149
150 let contract_id = request.contract.id_ref().to_buffer();
151 let document_type_name = request.document_type.name().to_string();
152
153 if request.prove {
154 Ok(DocumentHavingResponse::Proof(
155 self.execute_document_having_range_proof(
156 contract_id,
157 request.document_type,
158 document_type_name,
159 &mode,
160 request.resolved_time_ranges,
161 transaction,
162 platform_version,
163 )?,
164 ))
165 } else {
166 Ok(DocumentHavingResponse::Entries(
167 self.execute_document_having_range_no_proof(
168 contract_id,
169 request.document_type,
170 document_type_name,
171 &mode,
172 request.resolved_time_ranges,
173 transaction,
174 platform_version,
175 )?,
176 ))
177 }
178 }
179}