drive/query/drive_document_ranked_query/drive_dispatcher.rs
1//! Top-level dispatcher for the ranked
2//! (`ORDER BY <aggregate> LIMIT n OFFSET m`) request.
3//!
4//! Owns the pipeline: validate → resolve the ranking → pick the covering
5//! index → execute → wrap. The drive-abci handler builds a
6//! [`DocumentRankedRequest`] and calls
7//! [`Drive::execute_document_ranked_request`]; everything past contract
8//! lookup lives here.
9//!
10//! Both [`DocumentRankedRequest`] and [`DocumentRankedResponse`] are the
11//! ABI for this dispatcher — public so drive-abci can name the
12//! input/output types without reaching into the executor surface.
13//!
14//! Module is gated `feature = "server"` via the parent's
15//! `pub mod drive_dispatcher;` declaration.
16
17use super::mode_detection::detect_ranked_mode;
18use super::{RankedPage, RankedPaginationInputs};
19use crate::drive::Drive;
20use crate::error::query::QuerySyntaxError;
21use crate::error::Error;
22use crate::query::having::HavingClause;
23use crate::query::projection::SelectProjection;
24use crate::query::ResolvedTimeRange;
25use crate::query::{OrderClause, WhereClause};
26use dpp::data_contract::accessors::v0::DataContractV0Getters;
27use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
28use dpp::data_contract::document_type::DocumentTypeRef;
29use dpp::data_contract::DataContract;
30use dpp::version::PlatformVersion;
31use grovedb::TransactionArg;
32
33/// All inputs required by [`Drive::execute_document_ranked_request`].
34/// Built by the gRPC handler from a `GetDocumentsRequestV1` after
35/// wire-decoding + contract lookup.
36///
37/// The list-shaped inputs are borrowed slices rather than owned `Vec`s
38/// because the dispatcher only ever reads them — nothing here is
39/// canonicalized or rewritten the way count's where-clauses are, so
40/// taking ownership would just force the handler into a clone.
41///
42/// `having` and `start_at` are carried even though a ranked request
43/// must leave both empty: drive owns the rejection, so the contract is
44/// enforced identically no matter which upstream path built the
45/// request. See [`super::mode_detection::detect_ranked_mode_v0`] for
46/// why each is refused rather than ignored.
47pub struct DocumentRankedRequest<'a> {
48 /// Live contract (already loaded by the handler).
49 pub contract: &'a DataContract,
50 /// Resolved document type within `contract`.
51 pub document_type: DocumentTypeRef<'a>,
52 /// The single `GROUP BY` property. Must be the covering ranked
53 /// index's trailing property.
54 pub group_by: &'a [String],
55 /// The projection being ranked: `COUNT(*)`, `SUM(field)` or
56 /// `AVG(field)`.
57 pub select: SelectProjection,
58 /// The `HAVING` clauses. Must be empty — boolean per-group
59 /// predicates cannot yet be combined with an aggregate ordering.
60 pub having: &'a [HavingClause],
61 /// The `ORDER BY` clauses. Exactly one, naming the selected
62 /// aggregate (`$count` for `COUNT(*)`, otherwise the select's
63 /// field); its direction is the ranking direction.
64 pub order_by: &'a [OrderClause],
65 /// Structured `where` clauses. Empty for the single-property form;
66 /// pins on the covering compound index's leading properties for
67 /// the pinned-prefix form: one equality pin per property, of which
68 /// at most one may instead be a bounded `IN` (one branch per
69 /// element, merged; entries then carry `in_key`).
70 pub where_clauses: &'a [WhereClause],
71 /// The provenance of any `where_clauses` equality produced by
72 /// `IN_TIME_RANGE` resolution (see
73 /// [`crate::query::DriveDocumentQuery::resolved_time_ranges`]). At most
74 /// one, and its resolved bucket-start equality must appear among
75 /// `where_clauses` as the pin on the covering index's bucketed first
76 /// property. Index selection consumes it through
77 /// [`crate::query::index_admissible_for_resolved_time_range`]: a
78 /// resolved request is served only by the index bucketing that field
79 /// with exactly that grid, and a raw request never by a bucketed
80 /// index. Ranked levels sit strictly BELOW the bucketed one (contract
81 /// validation guarantees it), so the walk reads the pinned window's
82 /// own per-prefix secondary — one window, each document once,
83 /// regardless of grid overlap.
84 pub resolved_time_ranges: &'a [ResolvedTimeRange],
85 /// Request `limit` — the ranking's `k`. **Required**; there is no
86 /// server default a verifying client could reproduce.
87 pub limit: Option<u32>,
88 /// Request `offset` — the rank the page starts at. Optional
89 /// (`None` ⇒ 0), unbounded above.
90 pub offset: Option<u32>,
91 /// Whether the request carried a `start_at` / `start_after` cursor.
92 /// Must be `false`.
93 pub has_start_at: bool,
94 /// Whether to produce a proof instead of materializing entries.
95 pub prove: bool,
96}
97
98/// Output shape of [`Drive::execute_document_ranked_request`].
99///
100/// - `Entries(RankedPage)` — the page's entries **in ranking order**
101/// plus the rank it starts at; the abci handler maps this straight
102/// onto the wire without re-sorting.
103/// - `Proof(Vec<u8>)` — grovedb indexed-axis paginated proof bytes the
104/// client verifies with
105/// [`DriveDocumentRankedQuery::verify_ranked_top_k_proof`](crate::query::DriveDocumentRankedQuery::verify_ranked_top_k_proof),
106/// which recovers the same [`RankedPage`].
107///
108/// There is deliberately no `Aggregate` variant: even `LIMIT 1` returns
109/// a one-element entry list, because the caller needs the *group* as
110/// much as the value ("which restaurant is best", not just "what the
111/// best score is").
112#[derive(Debug, Clone)]
113pub enum DocumentRankedResponse {
114 /// One page of ranked groups, best-first for `DESC` and worst-first
115 /// for `ASC`, with the rank the page starts at.
116 Entries(RankedPage),
117 /// Grovedb indexed-axis paginated top-k proof bytes.
118 Proof(Vec<u8>),
119}
120
121impl Drive {
122 /// Single entry point for a ranked document request.
123 ///
124 /// 1. [`detect_ranked_mode`] validates the request shape and resolves
125 /// `(axis, descending, k, offset, group property, aggregate
126 /// field)`.
127 /// 2. The matching executor picks the covering ranked index and runs
128 /// the read or the proof.
129 /// 3. The result is wrapped in [`DocumentRankedResponse`].
130 ///
131 /// Errors:
132 /// - Request-shape failures (wrong `group_by` arity, an `order_by`
133 /// that does not name the `select`'s aggregate, a missing or
134 /// out-of-range `limit`, a malformed `where` pin, a `having`) come back
135 /// as `Error::Query(QuerySyntaxError::*)` —
136 /// see [`super::mode_detection::detect_ranked_mode_v0`] for the
137 /// full grammar.
138 /// - "No index declares this ranking axis" comes back as
139 /// `Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty)`
140 /// naming the missing contract keyword.
141 /// - Everything else (grovedb, versioning) surfaces as its native
142 /// `Error` variant.
143 pub fn execute_document_ranked_request(
144 &self,
145 request: DocumentRankedRequest,
146 transaction: TransactionArg,
147 platform_version: &PlatformVersion,
148 ) -> Result<DocumentRankedResponse, Error> {
149 // A transform's source must be its index's first property, so no
150 // single index can serve two resolved buckets; rejected before
151 // routing, mirroring `DriveDocumentQuery::select_best_index`.
152 if request.resolved_time_ranges.len() > 1 {
153 return Err(Error::Query(QuerySyntaxError::Unsupported(format!(
154 "at most one time-range selection (IN_TIME_RANGE) is supported per ranked \
155 query; this one resolves {:?}, and no single index can bucket more than \
156 one field",
157 request.resolved_time_ranges
158 ))));
159 }
160
161 let mode = detect_ranked_mode(
162 &request.select,
163 request.group_by,
164 request.having,
165 request.order_by,
166 request.where_clauses,
167 RankedPaginationInputs {
168 limit: request.limit,
169 offset: request.offset,
170 has_start_at: request.has_start_at,
171 },
172 platform_version,
173 )?;
174
175 let contract_id = request.contract.id_ref().to_buffer();
176 let document_type_name = request.document_type.name().to_string();
177
178 if request.prove {
179 Ok(DocumentRankedResponse::Proof(
180 self.execute_document_ranked_top_k_proof(
181 contract_id,
182 request.document_type,
183 document_type_name,
184 &mode,
185 request.resolved_time_ranges,
186 transaction,
187 platform_version,
188 )?,
189 ))
190 } else {
191 Ok(DocumentRankedResponse::Entries(
192 self.execute_document_ranked_top_k_no_proof(
193 contract_id,
194 request.document_type,
195 document_type_name,
196 &mode,
197 request.resolved_time_ranges,
198 transaction,
199 platform_version,
200 )?,
201 ))
202 }
203 }
204}