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 fields among `where_clauses` whose equality clause was produced by
72 /// `IN_TIME_RANGE` resolution (see
73 /// [`crate::query::DriveDocumentQuery::resolved_time_ranges`]).
74 /// Must be empty: `where_clauses` must be empty, so there is nothing to
75 /// have resolved, and ranking over bucket keys is undesigned — a document
76 /// belongs to `overlap_factor` buckets at once, so it would contribute to
77 /// that many groups. Carried (and rejected) here for the same reason
78 /// `where_clauses` is: drive owns the rejection regardless of which
79 /// upstream path built the request.
80 pub resolved_time_ranges: &'a [ResolvedTimeRange],
81 /// Request `limit` — the ranking's `k`. **Required**; there is no
82 /// server default a verifying client could reproduce.
83 pub limit: Option<u32>,
84 /// Request `offset` — the rank the page starts at. Optional
85 /// (`None` ⇒ 0), unbounded above.
86 pub offset: Option<u32>,
87 /// Whether the request carried a `start_at` / `start_after` cursor.
88 /// Must be `false`.
89 pub has_start_at: bool,
90 /// Whether to produce a proof instead of materializing entries.
91 pub prove: bool,
92}
93
94/// Output shape of [`Drive::execute_document_ranked_request`].
95///
96/// - `Entries(RankedPage)` — the page's entries **in ranking order**
97/// plus the rank it starts at; the abci handler maps this straight
98/// onto the wire without re-sorting.
99/// - `Proof(Vec<u8>)` — grovedb indexed-axis paginated proof bytes the
100/// client verifies with
101/// [`DriveDocumentRankedQuery::verify_ranked_top_k_proof`](crate::query::DriveDocumentRankedQuery::verify_ranked_top_k_proof),
102/// which recovers the same [`RankedPage`].
103///
104/// There is deliberately no `Aggregate` variant: even `LIMIT 1` returns
105/// a one-element entry list, because the caller needs the *group* as
106/// much as the value ("which restaurant is best", not just "what the
107/// best score is").
108#[derive(Debug, Clone)]
109pub enum DocumentRankedResponse {
110 /// One page of ranked groups, best-first for `DESC` and worst-first
111 /// for `ASC`, with the rank the page starts at.
112 Entries(RankedPage),
113 /// Grovedb indexed-axis paginated top-k proof bytes.
114 Proof(Vec<u8>),
115}
116
117impl Drive {
118 /// Single entry point for a ranked document request.
119 ///
120 /// 1. [`detect_ranked_mode`] validates the request shape and resolves
121 /// `(axis, descending, k, offset, group property, aggregate
122 /// field)`.
123 /// 2. The matching executor picks the covering ranked index and runs
124 /// the read or the proof.
125 /// 3. The result is wrapped in [`DocumentRankedResponse`].
126 ///
127 /// Errors:
128 /// - Request-shape failures (wrong `group_by` arity, an `order_by`
129 /// that does not name the `select`'s aggregate, a missing or
130 /// out-of-range `limit`, a `where` clause, a `having`) come back
131 /// as `Error::Query(QuerySyntaxError::*)` —
132 /// see [`super::mode_detection::detect_ranked_mode_v0`] for the
133 /// full grammar.
134 /// - "No index declares this ranking axis" comes back as
135 /// `Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty)`
136 /// naming the missing contract keyword.
137 /// - Everything else (grovedb, versioning) surfaces as its native
138 /// `Error` variant.
139 pub fn execute_document_ranked_request(
140 &self,
141 request: DocumentRankedRequest,
142 transaction: TransactionArg,
143 platform_version: &PlatformVersion,
144 ) -> Result<DocumentRankedResponse, Error> {
145 // Unreachable behind the empty-`where_clauses` rule `detect_ranked_mode`
146 // enforces below — a resolved equality is a where clause — but stated
147 // here so the ranked surface's exclusion of bucketed indexes is a
148 // rejection rather than a silent fallback to another index.
149 if !request.resolved_time_ranges.is_empty() {
150 return Err(Error::Query(QuerySyntaxError::Unsupported(
151 "a ranked query cannot carry a time-range (IN_TIME_RANGE) selection: ranking \
152 groups by an index's only property, and a document belongs to every bucket \
153 that contains its timestamp, so it would be ranked into several groups at once"
154 .to_string(),
155 )));
156 }
157
158 let mode = detect_ranked_mode(
159 &request.select,
160 request.group_by,
161 request.having,
162 request.order_by,
163 request.where_clauses,
164 RankedPaginationInputs {
165 limit: request.limit,
166 offset: request.offset,
167 has_start_at: request.has_start_at,
168 },
169 platform_version,
170 )?;
171
172 let contract_id = request.contract.id_ref().to_buffer();
173 let document_type_name = request.document_type.name().to_string();
174
175 if request.prove {
176 Ok(DocumentRankedResponse::Proof(
177 self.execute_document_ranked_top_k_proof(
178 contract_id,
179 request.document_type,
180 document_type_name,
181 &mode,
182 transaction,
183 platform_version,
184 )?,
185 ))
186 } else {
187 Ok(DocumentRankedResponse::Entries(
188 self.execute_document_ranked_top_k_no_proof(
189 contract_id,
190 request.document_type,
191 document_type_name,
192 &mode,
193 transaction,
194 platform_version,
195 )?,
196 ))
197 }
198 }
199}