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