Skip to main content

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}