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 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}