Skip to main content

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}