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 fields among `where_clauses` whose equality clause was produced by
54 /// `IN_TIME_RANGE` resolution (see
55 /// [`crate::query::DriveDocumentQuery::resolved_time_ranges`]).
56 /// Must be empty: HAVING's equality prefixes pin plain ranked indexes
57 /// (its picker excludes transformed ones), so a resolved bucket-start
58 /// equality would authenticate raw-timestamp matches at the bucket
59 /// boundary instead of window membership. Carried (and rejected) here
60 /// for the same reason the ranked request carries it: drive owns the
61 /// rejection regardless of which upstream path built the request.
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 // Before mode detection reads `where_clauses`: a resolved bucket
121 // equality is indistinguishable from a hand-written equality pin, and
122 // the pinned-prefix form would accept it against a plain ranked index
123 // over raw timestamps — a validly-proven answer to a different
124 // question. See `DocumentHavingRequest::resolved_time_ranges`.
125 if !request.resolved_time_ranges.is_empty() {
126 return Err(Error::Query(QuerySyntaxError::Unsupported(
127 "a HAVING query cannot carry a time-range (IN_TIME_RANGE) selection: its \
128 equality prefixes pin plain ranked indexes, so a resolved bucket-start \
129 equality would match raw timestamps at the bucket boundary instead of the \
130 selected window"
131 .to_string(),
132 )));
133 }
134 let mode = detect_having_mode(
135 &request.select,
136 request.group_by,
137 request.having,
138 request.order_by,
139 request.where_clauses,
140 RankedPaginationInputs {
141 limit: request.limit,
142 offset: request.offset,
143 has_start_at: request.has_start_at,
144 },
145 platform_version,
146 )?;
147
148 let contract_id = request.contract.id_ref().to_buffer();
149 let document_type_name = request.document_type.name().to_string();
150
151 if request.prove {
152 Ok(DocumentHavingResponse::Proof(
153 self.execute_document_having_range_proof(
154 contract_id,
155 request.document_type,
156 document_type_name,
157 &mode,
158 transaction,
159 platform_version,
160 )?,
161 ))
162 } else {
163 Ok(DocumentHavingResponse::Entries(
164 self.execute_document_having_range_no_proof(
165 contract_id,
166 request.document_type,
167 document_type_name,
168 &mode,
169 transaction,
170 platform_version,
171 )?,
172 ))
173 }
174 }
175}