dash_platform_queries/documents/document_query.rs
1//! Method to query documents from the Drive.
2
3use std::sync::Arc;
4
5use super::composite_document_query::{drive_sub_queries, sub_queries_to_proto, CompositeSubQuery};
6use crate::error::Error;
7use dapi_grpc::platform::v0::get_documents_request::Version::{V0, V1};
8use dapi_grpc::platform::v0::{
9 self as platform_proto,
10 get_documents_request::{
11 document_field_value,
12 get_documents_request_v0::Start,
13 get_documents_request_v1::{select, Select as ProtoSelect, Start as V1Start},
14 having_aggregate, having_clause, order_clause,
15 time_range_selection::{Grid as ProtoTimeRangeGrid, Selector as ProtoTimeRangeSelector},
16 DocumentFieldValue as ProtoDocumentFieldValue, GetDocumentsRequestV0,
17 GetDocumentsRequestV1, HavingAggregate as ProtoHavingAggregate,
18 HavingClause as ProtoHavingClause, OrderClause as ProtoOrderClause,
19 TimeRangeSelection as ProtoTimeRangeSelection, WhereClause as ProtoWhereClause,
20 WhereOperator as ProtoWhereOperator,
21 },
22 GetDocumentsRequest, Proof, ResponseMetadata,
23};
24use dash_context_provider::ContextProvider;
25use dpp::dashcore::Network;
26use dpp::version::{PlatformVersion, TryFromPlatformVersioned};
27use dpp::{
28 data_contract::{
29 accessors::v0::DataContractV0Getters, document_type::accessors::DocumentTypeV0Getters,
30 },
31 document::Document,
32 platform_value::{platform_value, Value},
33 prelude::{DataContract, Identifier},
34 InvalidVectorSizeError, ProtocolError,
35};
36use drive::config::DEFAULT_QUERY_LIMIT;
37use drive::query::drive_document_ranked_query::mode_detection::ranked_order_key;
38use drive::query::{
39 resolve_time_range_bucket_clause, validate_resolved_time_range_clause_shapes,
40 DriveDocumentQuery, HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator,
41 HavingRightOperand, InternalClauses, OrderClause, ResolvedTimeRange, SelectFunction,
42 SelectProjection, TimeRangeGridSpec, TimeRangeSelector, WhereClause, WhereOperator,
43};
44use drive_proof_verifier::{types::Documents, FromProof};
45
46// TODO: remove DocumentQuery once ContextProvider that provides data contracts is merged.
47
48/// One pending `IN_TIME_RANGE` selection: the timestamp field, the
49/// `"newest"` / `"oldest"` selector, and — when the contract buckets the
50/// field with more than one `timeRange` grid — the grid it targets (`None`
51/// means the field's sole grid, and the server rejects the bare form on a
52/// multi-grid field as ambiguous).
53#[derive(Debug, Clone, PartialEq, Eq)]
54#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))]
55pub struct TimeRangeClause {
56 /// The bucketed timestamp field the selection is on.
57 pub field: String,
58 /// Which active window to resolve to.
59 pub selector: TimeRangeSelector,
60 /// The grid targeted, in the contract's declared seconds; `None` when
61 /// the field carries a single grid.
62 #[cfg_attr(feature = "mocks", serde(default))]
63 pub grid: Option<TimeRangeGridSpec>,
64}
65
66/// Request that is used to query documents from the Dash Platform.
67///
68/// This is an abstraction layer built on top of [GetDocumentsRequest] to address issues with missing details
69/// required to correctly verify proofs returned by the Dash Platform.
70///
71/// Conversions are implemented between this type, [GetDocumentsRequest] and [DriveDocumentQuery] using [TryFrom] trait.
72///
73/// Add related document or count queries with [`Self::with_sub_query`].
74/// Fetch these compositions as [`drive_proof_verifier::CompositeDocuments`]
75/// to receive both the page and its verified sub-results.
76#[derive(Debug, Clone, PartialEq, dash_platform_macros::Mockable)]
77#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))]
78pub struct DocumentQuery {
79 /// SQL-shaped `SELECT` projection — `(function, field)` pair.
80 /// `Documents` returns matched rows; `Count` / `Sum` / `Avg`
81 /// return either a single aggregate (empty `group_by`) or
82 /// per-group entries (non-empty `group_by`). Defaults to
83 /// `SelectProjection::documents()` so callers that don't opt
84 /// into the SQL-shaped surface get plain document-fetch
85 /// semantics.
86 ///
87 /// `#[serde(default)]` here (and on `group_by` / `having`
88 /// below) is wire-format-compat for mock vectors captured
89 /// before the SQL-shaped surface was added: default
90 /// `SelectProjection` is `documents()`, `Vec` defaults to
91 /// empty — together those mean an old fixture without these
92 /// fields deserializes to the documents-fetch shape it was
93 /// originally captured under. New fixtures should serialize
94 /// the fields explicitly.
95 #[cfg_attr(feature = "mocks", serde(default))]
96 pub select: SelectProjection,
97 /// Data contract
98 pub data_contract: Arc<DataContract>,
99 /// Document type for the data contract
100 pub document_type_name: String,
101 /// `where` clauses for the query
102 pub where_clauses: Vec<WhereClause>,
103 /// Time-range (`IN_TIME_RANGE`) selections on a timestamp field covered
104 /// by a `timeRange` index. These are emitted as `IN_TIME_RANGE` clauses
105 /// on the v1 wire and resolved server-side from the current block time;
106 /// the verifier re-derives the same bucket from the quorum-signed
107 /// response metadata time. v1-only (the v0 wire has no `IN_TIME_RANGE`
108 /// operator). See [`Self::with_time_range`] and
109 /// [`Self::with_time_range_grid`].
110 #[cfg_attr(feature = "mocks", serde(default))]
111 pub time_range_clauses: Vec<TimeRangeClause>,
112 /// SQL `GROUP BY` field names, in left-to-right order. Empty =
113 /// no explicit grouping (aggregate count for `select=Count`).
114 /// Only meaningful when `select=Count`; non-empty with
115 /// `select=Documents` is rejected by the server as unsupported.
116 #[cfg_attr(feature = "mocks", serde(default))]
117 pub group_by: Vec<String>,
118 /// SQL `HAVING` clauses — **boolean** aggregate filters that apply
119 /// to the grouped rows produced by `select = Count | Sum | Avg`
120 /// with a non-empty `group_by`. Unlike `where_clauses`, the left
121 /// side is an aggregate (`COUNT(*)`, `SUM(field)`, `AVG(field)`)
122 /// rather than a raw row field. See [`HavingClause`] /
123 /// [`drive::query::HavingAggregate`] /
124 /// [`drive::query::HavingOperator`] for the catalogs. Multiple
125 /// entries combine with implicit `AND`.
126 ///
127 /// **Served from protocol version 14, for exactly one clause
128 /// bounding the selected aggregate** with a contiguous-range
129 /// operator (`=`, `>`, `>=`, `<`, `<=`, `BETWEEN*`) — the
130 /// having-range surface, fetched as
131 /// [`DocumentHavingEntries`](drive_proof_verifier::DocumentHavingEntries)
132 /// and served as a value-bounded range read of the covering ranked
133 /// index's axis secondary (the index must declare the matching
134 /// `rankedCountable` / `rankedSummable` / `rankedAverageable`
135 /// keyword). Everything else — multiple clauses (implicit AND), a
136 /// clause on an aggregate the select does not project, `!=` / `IN`
137 /// — is still rejected with `QuerySyntaxError::Unsupported`, as is
138 /// any non-empty value at protocol version 13 and earlier.
139 ///
140 /// **`having` does not express ranking.** "The n highest-scoring
141 /// groups" is [`Self::order_by_selected_aggregate`] +
142 /// [`Self::with_limit`] — SQL's own `ORDER BY <agg> DESC LIMIT n`
143 /// — which is also served from protocol version 14. The two
144 /// compose only in the one shape the having grammar allows: an
145 /// `ORDER BY` naming the selected aggregate sets the having
146 /// range's walk direction.
147 #[cfg_attr(feature = "mocks", serde(default))]
148 pub having: Vec<HavingClause>,
149 /// `order_by` clauses for the query.
150 ///
151 /// For `select = Documents` these order the matched rows. For the
152 /// **ranked** surface a single clause naming the selected
153 /// aggregate orders the *groups* — see
154 /// [`Self::order_by_selected_aggregate`], which builds it.
155 pub order_by_clauses: Vec<OrderClause>,
156 /// queryset limit. `0` is the sentinel for "unset / default" and
157 /// is translated to `None` on the V1 wire (`optional uint32`).
158 pub limit: u32,
159 /// SQL `OFFSET` — how many result rows to skip before the returned
160 /// page. `None` leaves the field unset on the wire.
161 ///
162 /// Served on exactly one path: the **ranked** surface (protocol
163 /// version 14+), where it skips that many *ranks*, so
164 /// `.order_by_selected_aggregate(Descending).with_limit(1)
165 /// .with_offset(4)` is the 5th-best group. Everywhere else the
166 /// server rejects a set offset with `Unsupported("OFFSET
167 /// pagination is not yet implemented")`.
168 ///
169 /// `#[serde(default)]` for the same mock-vector compatibility
170 /// reason as `select` / `group_by` / `having`: a fixture captured
171 /// before offsets existed deserializes to `None`.
172 #[cfg_attr(feature = "mocks", serde(default))]
173 pub offset: Option<u32>,
174 /// first object to start with
175 pub start: Option<Start>,
176 /// Related document and count queries derived from this page. Empty for
177 /// ordinary document or aggregate queries. Fetch nonempty compositions
178 /// as [`drive_proof_verifier::CompositeDocuments`].
179 #[cfg_attr(feature = "mocks", serde(default))]
180 pub sub_queries: Vec<CompositeSubQuery>,
181}
182
183/// Which end of a ranking a
184/// [`DocumentQuery::order_by_selected_aggregate`] call walks from.
185///
186/// A named pair rather than a bare `ascending: bool`, because the two
187/// readings of a ranked query — "the best n" and "the worst n" — are
188/// what callers actually think in, and `false` meaning "best first" at
189/// a call site is exactly the sort of thing that gets flipped in
190/// review without anyone noticing.
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum RankingDirection {
193 /// `ORDER BY <aggregate> DESC` — walk from the largest aggregate
194 /// down. The "top n" reading: entry 0 is the highest-scoring group.
195 Descending,
196 /// `ORDER BY <aggregate> ASC` — walk from the smallest aggregate
197 /// up. The "bottom n" reading: entry 0 is the lowest-scoring group.
198 Ascending,
199}
200
201impl DocumentQuery {
202 /// Create new DocumentQuery for provided contract and document type name.
203 pub fn new<C: Into<Arc<DataContract>>>(
204 contract: C,
205 document_type_name: &str,
206 ) -> Result<Self, Error> {
207 let contract = contract.into();
208 // ensure document type name is correct
209 contract
210 .document_type_for_name(document_type_name)
211 .map_err(ProtocolError::DataContractError)?;
212
213 Ok(Self {
214 select: SelectProjection::documents(),
215 data_contract: Arc::clone(&contract),
216 document_type_name: document_type_name.to_string(),
217 where_clauses: vec![],
218 time_range_clauses: vec![],
219 group_by: Vec::new(),
220 having: Vec::new(),
221 order_by_clauses: vec![],
222 limit: 0,
223 offset: None,
224 start: None,
225 sub_queries: vec![],
226 })
227 }
228
229 /// Ordinary document and aggregate proof results cannot represent sub-queries.
230 pub(super) fn ensure_no_sub_queries(&self) -> Result<(), drive_proof_verifier::Error> {
231 if !self.sub_queries.is_empty() {
232 return Err(drive_proof_verifier::Error::RequestError {
233 error: "this result type cannot return sub-queries; fetch the query as CompositeDocuments".to_string(),
234 });
235 }
236 Ok(())
237 }
238
239 /// Create new document query based on a [DriveDocumentQuery].
240 ///
241 /// Preserves sub-queries, including their contracts and bindings.
242 ///
243 /// Fails when the drive query carries time-range resolution provenance
244 /// (`resolved_time_ranges`): the resolved bucket equality cannot be
245 /// represented without it — see the `TryFrom` impl. Build the query
246 /// with [`Self::with_time_range`] / [`Self::with_time_range_grid`]
247 /// instead for time-range selections.
248 pub fn new_with_drive_query(d: &DriveDocumentQuery) -> Result<Self, crate::error::Error> {
249 Self::try_from(d)
250 }
251
252 /// Point to a specific document ID.
253 pub fn with_document_id(self, document_id: &Identifier) -> Self {
254 let clause = WhereClause {
255 field: "$id".to_string(),
256 operator: WhereOperator::Equal,
257 value: platform_value!(document_id),
258 };
259
260 self.with_where(clause)
261 }
262
263 /// Add new where clause to the query.
264 ///
265 /// Existing where clauses will be preserved.
266 pub fn with_where(mut self, clause: WhereClause) -> Self {
267 self.where_clauses.push(clause);
268
269 self
270 }
271
272 /// Restrict the query to a single time-range bucket of `field`
273 /// (a timestamp covered by a `timeRange` index). The relative selectors
274 /// ([`TimeRangeSelector::Newest`] / [`TimeRangeSelector::Oldest`]) pick a
275 /// currently active range, resolved server-side from the current block
276 /// time; the proof verifier re-derives the identical bucket from the
277 /// quorum-signed response metadata time. A
278 /// [`TimeRangeSelector::ByStart`] selection names a window absolutely —
279 /// current or historic — by its grid-aligned start, which both sides
280 /// read straight from the query. Emitted as an `IN_TIME_RANGE` clause
281 /// on the v1 wire. Requires protocol version 14+ — the first version
282 /// whose contract grammar hosts `timeRange` indexes.
283 ///
284 /// The bare selector is unambiguous only while exactly one grid buckets
285 /// `field`; when the contract declares several grids over it, use
286 /// [`Self::with_time_range_grid`] to name one.
287 ///
288 /// Existing time-range selections are preserved.
289 pub fn with_time_range(
290 mut self,
291 field: impl Into<String>,
292 selector: TimeRangeSelector,
293 ) -> Self {
294 self.time_range_clauses.push(TimeRangeClause {
295 field: field.into(),
296 selector,
297 grid: None,
298 });
299 self
300 }
301
302 /// [`Self::with_time_range`] naming a specific grid — required when the
303 /// contract buckets `field` with more than one `timeRange` grid. The
304 /// spec's `range` / `step` / `phase` are the contract's own declared
305 /// seconds, verbatim.
306 ///
307 /// Existing time-range selections are preserved.
308 pub fn with_time_range_grid(
309 mut self,
310 field: impl Into<String>,
311 selector: TimeRangeSelector,
312 grid: TimeRangeGridSpec,
313 ) -> Self {
314 self.time_range_clauses.push(TimeRangeClause {
315 field: field.into(),
316 selector,
317 grid: Some(grid),
318 });
319 self
320 }
321
322 /// Add order by clause to the query.
323 ///
324 /// Existing order by clauses will be preserved.
325 pub fn with_order_by(mut self, clause: OrderClause) -> Self {
326 self.order_by_clauses.push(clause);
327
328 self
329 }
330
331 /// Set the SQL-shaped `SELECT` projection.
332 ///
333 /// Construct the [`SelectProjection`] via its helpers:
334 /// [`SelectProjection::documents`] (the default — matched
335 /// rows), [`SelectProjection::count_star`] for `COUNT(*)`,
336 /// [`SelectProjection::count_field`] for `COUNT(field)`,
337 /// [`SelectProjection::sum`] for `SUM(field)`,
338 /// [`SelectProjection::avg`] for `AVG(field)`. Pair the
339 /// count/sum/avg projections with [`DocumentCount::fetch`]
340 /// (single aggregate, empty `group_by`) or
341 /// [`DocumentSplitCounts::fetch`] (per-group entries,
342 /// non-empty `group_by`).
343 ///
344 /// Server capability today: `Documents`, `COUNT(*)`,
345 /// `SUM(<field>)`, and `AVG(<field>)` are evaluated
346 /// end-to-end. `COUNT(<field>)`, `MIN(<field>)`, and
347 /// `MAX(<field>)` are accepted by the SDK but rejected by the
348 /// server with `Unsupported("SELECT … is not yet
349 /// implemented")` — the surface is shipped first and
350 /// execution lands later.
351 pub fn with_select(mut self, select: SelectProjection) -> Self {
352 self.select = select;
353 self
354 }
355
356 /// Set the `GROUP BY` field to a single field name.
357 ///
358 /// Convenience wrapper around [`Self::with_group_by_fields`].
359 /// Replaces any previously set `group_by`. Pair with
360 /// [`Self::with_select`] (e.g.
361 /// `with_select(SelectProjection::count_star())`) for the
362 /// per-group entries shape.
363 pub fn with_group_by<S: Into<String>>(mut self, field: S) -> Self {
364 self.group_by = vec![field.into()];
365 self
366 }
367
368 /// Set the full `GROUP BY` field list (replaces any previously
369 /// set `group_by`).
370 ///
371 /// Multi-field `group_by` is only accepted by the server for
372 /// `(in_field, range_field)` matching a compound `In + range`
373 /// where clause against a `rangeCountable: true` index. Other
374 /// non-empty shapes return `QuerySyntaxError::Unsupported`.
375 pub fn with_group_by_fields<I, S>(mut self, fields: I) -> Self
376 where
377 I: IntoIterator<Item = S>,
378 S: Into<String>,
379 {
380 self.group_by = fields.into_iter().map(Into::into).collect();
381 self
382 }
383
384 /// Set the `HAVING` clauses (replaces any prior value).
385 ///
386 /// From protocol version 14, a grouped aggregate query carrying
387 /// **exactly one** clause that bounds the selected aggregate with
388 /// a contiguous-range operator (`=`, `>`, `>=`, `<`, `<=`, the
389 /// `BETWEEN` variants) is served as a value-bounded range read of
390 /// the covering ranked index's axis secondary — fetch the result
391 /// through `DocumentHavingEntries::fetch`, which verifies the
392 /// proof including its completeness. The server still rejects
393 /// multiple clauses, a clause on a different aggregate than the
394 /// select's, and the non-contiguous operators (`!=`, `IN`);
395 /// protocol version 13 and earlier reject every non-empty
396 /// `having`.
397 ///
398 /// This is **not** how you ask for a ranking — see
399 /// [`Self::order_by_selected_aggregate`].
400 pub fn with_having(mut self, having: Vec<HavingClause>) -> Self {
401 self.having = having;
402 self
403 }
404
405 /// Order the `GROUP BY` groups by the aggregate this query
406 /// selects — the **ranked** surface, `ORDER BY <the selected
407 /// aggregate> [ASC|DESC]` (protocol version 14+).
408 ///
409 /// Replaces any previously set `order_by`, because a ranked query
410 /// takes exactly one ordering clause and a second one is rejected
411 /// rather than combined.
412 ///
413 /// The ordered field name is derived from the current
414 /// [`Self::select`] by rs-drive's own
415 /// [`ranked_order_key`] — `SUM(f)` / `AVG(f)` are named by `f`,
416 /// and `COUNT(*)` by the `$count` sentinel. Calling
417 /// [`Self::with_select`] *after* this method leaves a stale field
418 /// name behind and the server will refuse the request; set the
419 /// select first, which is also how the query reads.
420 ///
421 /// Pair with [`Self::with_limit`] (the ranking's `n`, `1 ..= 100`)
422 /// and optionally [`Self::with_offset`], then fetch with
423 /// [`DocumentRankedEntries`](drive_proof_verifier::DocumentRankedEntries).
424 ///
425 /// # The 5th-best group
426 ///
427 /// ```rust,ignore
428 /// # use dash_sdk::platform::{DataContract, DocumentQuery};
429 /// # use dash_sdk::platform::documents::document_query::RankingDirection;
430 /// # use dash_sdk::drive::query::SelectProjection;
431 /// # fn example(contract: DataContract) -> Result<(), dash_sdk::Error> {
432 /// // SELECT avg(grade) GROUP BY restaurantId
433 /// // ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4
434 /// let query = DocumentQuery::new(contract, "review")?
435 /// .with_select(SelectProjection::avg("grade"))
436 /// .with_group_by("restaurantId")
437 /// .order_by_selected_aggregate(RankingDirection::Descending)
438 /// .with_limit(1)
439 /// .with_offset(4);
440 /// # Ok(())
441 /// # }
442 /// ```
443 pub fn order_by_selected_aggregate(mut self, direction: RankingDirection) -> Self {
444 self.order_by_clauses = vec![OrderClause {
445 field: ranked_order_key(&self.select).to_string(),
446 ascending: matches!(direction, RankingDirection::Ascending),
447 }];
448 self
449 }
450
451 /// Set the SQL `OFFSET` — how many ranks to skip before the
452 /// returned page.
453 ///
454 /// Only the ranked surface honours it (see
455 /// [`Self::order_by_selected_aggregate`]); on every other path the
456 /// server rejects a set offset with `Unsupported`. There is no
457 /// ceiling: grovedb counts the skipped region from the subtree
458 /// aggregates instead of walking it, on both `prove` settings, so
459 /// the cost of a deep offset does not scale with the offset. It is
460 /// not identical to a shallow one — `offset = 0` keeps a sequential
461 /// fast path, a positive offset descends the tree in `O(log n)`,
462 /// and an offset at or past the population is answered from the
463 /// root without descending at all — but nothing here grows with how
464 /// far you page, which is why there is no ceiling. Only a proved
465 /// response additionally attests the count.
466 ///
467 /// An offset past the end of the ranking is a legitimate answer
468 /// rather than an error — the page comes back empty, and on a
469 /// proved fetch its `starting_rank` is the ranking's attested total
470 /// population.
471 pub fn with_offset(mut self, offset: u32) -> Self {
472 self.offset = Some(offset);
473 self
474 }
475
476 /// Set the query limit. `0` means "unset" — translated to
477 /// `None` on the V1 wire (the proto field is `optional uint32`).
478 ///
479 /// On `select=Count` with non-empty `group_by` against the
480 /// prove path, the server validates rather than clamps:
481 /// `limit > max_query_limit` is rejected with
482 /// `InvalidLimit` rather than silently truncated, since
483 /// clamping would invisibly break proof verification.
484 /// Leaving the limit unset (`0`) falls back to
485 /// `drive::config::DEFAULT_QUERY_LIMIT` on the proof verifier
486 /// side, keeping proof bytes deterministic across operators.
487 pub fn with_limit(mut self, limit: u32) -> Self {
488 self.limit = limit;
489 self
490 }
491
492 /// Convert into the wire-format [`GetDocumentsRequest`] using a
493 /// specific [`PlatformVersion`] to pick V0 vs V1. The dispatch
494 /// boundary is the document_query feature-version on the
495 /// platform_version: `0` → V0, `1` → V1.
496 pub fn try_into_request_for_version(
497 self,
498 platform_version: &PlatformVersion,
499 ) -> Result<GetDocumentsRequest, Error> {
500 GetDocumentsRequest::try_from_platform_versioned(self, platform_version)
501 }
502}
503
504impl FromProof<DocumentQuery> for Document {
505 type Request = DocumentQuery;
506 type Response = platform_proto::GetDocumentsResponse;
507 fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
508 request: I,
509 response: O,
510 network: Network,
511 platform_version: &PlatformVersion,
512 provider: &'a dyn ContextProvider,
513 ) -> Result<(Option<Self>, ResponseMetadata, Proof), drive_proof_verifier::Error>
514 where
515 Self: Sized + 'a,
516 {
517 let request: Self::Request = request.into();
518
519 let (documents, metadata, proof): (Option<Documents>, ResponseMetadata, Proof) =
520 <Documents as FromProof<Self::Request>>::maybe_from_proof_with_metadata(
521 request,
522 response,
523 network,
524 platform_version,
525 provider,
526 )?;
527
528 match documents {
529 None => Ok((None, metadata, proof)),
530 Some(docs) => match docs.len() {
531 0 | 1 => Ok((
532 docs.into_iter().next().and_then(|(_, v)| v),
533 metadata,
534 proof,
535 )),
536 n => Err(drive_proof_verifier::Error::ResponseDecodeError {
537 error: format!("expected 1 element, got {}", n),
538 }),
539 },
540 }
541 }
542}
543
544impl FromProof<DocumentQuery> for drive_proof_verifier::types::Documents {
545 type Request = DocumentQuery;
546 type Response = platform_proto::GetDocumentsResponse;
547 fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
548 request: I,
549 response: O,
550 network: Network,
551 platform_version: &PlatformVersion,
552 provider: &'a dyn ContextProvider,
553 ) -> Result<(Option<Self>, ResponseMetadata, Proof), drive_proof_verifier::Error>
554 where
555 Self: Sized + 'a,
556 {
557 let mut request: Self::Request = request.into();
558 request.ensure_no_sub_queries()?;
559 let response: Self::Response = response.into();
560
561 // A time-range (`IN_TIME_RANGE`) selection is resolved to a concrete
562 // bucket using the **quorum-signed** response metadata time — the same
563 // authoritative block time the server used to resolve it — so the
564 // reconstructed query matches the proof exactly. Resolve (and run the
565 // provenance-vs-shape guard, via the one shared normalization helper
566 // the aggregate verifiers also use) before the `DriveDocumentQuery`
567 // conversion so the engine sees ordinary equality clauses.
568 let mut resolved_time_ranges = Vec::new();
569 if !request.time_range_clauses.is_empty() {
570 // The generated `VersionedGrpcResponse::metadata()` handles both
571 // response envelopes (and any future one), so no hand-written
572 // version match is needed here.
573 use dapi_grpc::platform::VersionedGrpcResponse;
574 let time_ms = response
575 .metadata()
576 .map(|metadata| metadata.time_ms)
577 .map_err(|_| drive_proof_verifier::Error::ResponseDecodeError {
578 error: "time range query proof response is missing block-time metadata"
579 .to_string(),
580 })?;
581 resolved_time_ranges =
582 normalize_time_range_clauses_with_metadata_time(&mut request, time_ms)?;
583 }
584
585 let mut drive_query: DriveDocumentQuery =
586 (&request)
587 .try_into()
588 .map_err(|e| drive_proof_verifier::Error::RequestError {
589 error: format!("Failed to convert DocumentQuery to DriveQuery: {}", e),
590 })?;
591 // The conversion cannot recover which equalities came from resolution,
592 // so the provenance is carried across here; index selection reads it
593 // to pin the query to the index that buckets the field.
594 drive_query.resolved_time_ranges = resolved_time_ranges;
595
596 <drive_proof_verifier::types::Documents as FromProof<DriveDocumentQuery>>::maybe_from_proof_with_metadata(
597 drive_query,
598 response,
599 network,
600 platform_version,
601 provider,
602 )
603 }
604}
605
606/// Resolve a request's pending time-range (`IN_TIME_RANGE`) selections into
607/// concrete bucket-equality clauses on `request.where_clauses`, using the
608/// **quorum-signed** response metadata block time — the same authoritative
609/// time the server used — so the reconstructed query matches the proof
610/// exactly.
611///
612/// Every proof-verification path that rebuilds a drive query from a
613/// [`DocumentQuery`] must call this (or perform the identical resolution)
614/// *before* reading `request.where_clauses` for mode detection, covering-index
615/// selection, or query reconstruction: the documents path does it inline in
616/// its `FromProof` impl, and the count / sum / average aggregate helpers call
617/// this before resolving their mode. Skipping it would rebuild the query from
618/// a different shape than the prover used.
619///
620/// Returns the resolution provenance — one [`ResolvedTimeRange`] (field +
621/// exact grid) per selection, whose pushed clause is a bucket equality
622/// rather than a raw-timestamp one. Callers must carry them into index
623/// selection (`DriveDocumentQuery::resolved_time_ranges`, or the
624/// `resolved_time_ranges` argument of the aggregate index pickers):
625/// the pushed clause is an ordinary equality and nothing downstream can
626/// otherwise tell that it must be matched against the resolved grid's
627/// bucket starts.
628/// [`resolve_time_range_clauses_with_metadata_time`] followed immediately by
629/// the provenance-vs-shape guard — the two-step normalization every
630/// proof-verification path must run, in this order, before mode detection,
631/// covering-index selection, or query reconstruction. One definition so a
632/// future verifier path cannot omit either step or run them out of order:
633/// the aggregate paths once omitted the resolution entirely (valid proofs
634/// were rejected), and a path that resolved without the guard would let a
635/// caller-provided `In`/range clause on the resolved field reach the index
636/// pickers as if its raw values were bucket starts.
637pub(super) fn normalize_time_range_clauses_with_metadata_time(
638 request: &mut DocumentQuery,
639 time_ms: u64,
640) -> Result<Vec<ResolvedTimeRange>, drive_proof_verifier::Error> {
641 let resolved_time_ranges = resolve_time_range_clauses_with_metadata_time(request, time_ms)?;
642 validate_resolved_time_range_clause_shapes(&request.where_clauses, &resolved_time_ranges)
643 .map_err(|e| drive_proof_verifier::Error::RequestError {
644 error: format!("invalid time range query shape: {}", e),
645 })?;
646 Ok(resolved_time_ranges)
647}
648
649pub(super) fn resolve_time_range_clauses_with_metadata_time(
650 request: &mut DocumentQuery,
651 time_ms: u64,
652) -> Result<Vec<ResolvedTimeRange>, drive_proof_verifier::Error> {
653 if request.time_range_clauses.is_empty() {
654 return Ok(Vec::new());
655 }
656 let data_contract = Arc::clone(&request.data_contract);
657 let document_type = data_contract
658 .document_type_for_name(&request.document_type_name)
659 .map_err(|e| drive_proof_verifier::Error::RequestError {
660 error: format!("document type not found for time range query: {}", e),
661 })?;
662 let time_range_clauses = std::mem::take(&mut request.time_range_clauses);
663 let mut resolved_time_ranges = Vec::with_capacity(time_range_clauses.len());
664 for TimeRangeClause {
665 field,
666 selector,
667 grid,
668 } in time_range_clauses
669 {
670 let (clause, resolved) =
671 resolve_time_range_bucket_clause(&field, selector, grid, document_type, time_ms)
672 .map_err(|e| drive_proof_verifier::Error::RequestError {
673 error: format!("failed to resolve time range clause: {}", e),
674 })?;
675 request.where_clauses.push(clause);
676 resolved_time_ranges.push(resolved);
677 }
678 Ok(resolved_time_ranges)
679}
680
681/// Version-aware encoder. The dispatch is driven by the
682/// `drive_abci.query.document_query` feature-version on
683/// [`PlatformVersion`]: `0` → V0 wire (used by v3.0 testnet), `1` →
684/// V1 wire (introduced in v3.1).
685///
686/// V0 lacks `selects` / `group_by` / `having` / `offset` and the
687/// optional-limit semantics — callers that set those features get
688/// `Error::Config` with a clear "requires Platform v3.1+" message
689/// rather than a silently-truncated request. Time-range clauses are
690/// additionally gated on the v14 contract grammar — see the `1 =>` arm.
691impl TryFromPlatformVersioned<DocumentQuery> for GetDocumentsRequest {
692 type Error = Error;
693
694 fn try_from_platform_versioned(
695 value: DocumentQuery,
696 platform_version: &PlatformVersion,
697 ) -> Result<Self, Self::Error> {
698 if !value.sub_queries.is_empty() {
699 value.check_composite_shape()?;
700 }
701 let DocumentQuery {
702 select,
703 data_contract,
704 document_type_name,
705 where_clauses,
706 time_range_clauses,
707 group_by,
708 having,
709 order_by_clauses,
710 limit,
711 offset,
712 start,
713 sub_queries,
714 } = value;
715
716 let feature_version = platform_version
717 .drive_abci
718 .query
719 .document_query
720 .default_current_version;
721
722 tracing::debug!(
723 target: "dash_sdk::query_encoder",
724 feature_version,
725 protocol_version = platform_version.protocol_version,
726 "encoding GetDocumentsRequest"
727 );
728
729 match feature_version {
730 0 => {
731 if !sub_queries.is_empty() {
732 return Err(Error::Config(
733 "composite document queries require the V1 documents wire (Platform v3.1+)"
734 .to_string(),
735 ));
736 }
737 if !time_range_clauses.is_empty() {
738 return Err(Error::Config(
739 "time range (IN_TIME_RANGE) queries require protocol version 14+; the \
740 v0 getDocuments wire has no time-range operator"
741 .to_string(),
742 ));
743 }
744 encode_v0(
745 data_contract.id().to_vec(),
746 document_type_name,
747 where_clauses,
748 order_by_clauses,
749 limit,
750 offset,
751 start,
752 &select,
753 &group_by,
754 &having,
755 )
756 }
757 1 => {
758 // The v1 wire predates time-range indexes: protocol
759 // versions 12 and 13 also serve it, but their contract
760 // grammar (document meta-schema generations 1 and 2)
761 // cannot host a `timeRange` index. Gate on the grammar
762 // generation — the same table the server's parser reads —
763 // rather than emitting an operator a pre-v14 server
764 // rejects as an unknown discriminant.
765 let grammar_generation = platform_version
766 .dpp
767 .contract_versions
768 .document_type_versions
769 .schema
770 .document_type_schema;
771 if !time_range_clauses.is_empty() && grammar_generation < 3 {
772 return Err(Error::Config(format!(
773 "time range (IN_TIME_RANGE) queries require protocol version 14+ — the \
774 first version whose contract grammar hosts `timeRange` indexes; this \
775 network runs protocol version {}",
776 platform_version.protocol_version
777 )));
778 }
779 encode_v1(
780 data_contract.id().to_vec(),
781 document_type_name,
782 where_clauses,
783 time_range_clauses,
784 order_by_clauses,
785 limit,
786 offset,
787 start,
788 select,
789 group_by,
790 having,
791 sub_queries_to_proto(sub_queries)?,
792 )
793 }
794 n => Err(Error::Config(format!(
795 "GetDocumentsRequest wire encoder does not support feature_version={n} \
796 (drive_abci.query.document_query) on PlatformVersion v{}",
797 platform_version.protocol_version
798 ))),
799 }
800 }
801}
802
803#[allow(clippy::too_many_arguments)]
804fn encode_v1(
805 data_contract_id: Vec<u8>,
806 document_type: String,
807 where_clauses: Vec<WhereClause>,
808 time_range_clauses: Vec<TimeRangeClause>,
809 order_by_clauses: Vec<OrderClause>,
810 limit: u32,
811 offset: Option<u32>,
812 start: Option<Start>,
813 select: SelectProjection,
814 group_by: Vec<String>,
815 having: Vec<HavingClause>,
816 sub_queries: Vec<platform_proto::get_documents_request::get_documents_request_v1::SubQuery>,
817) -> Result<GetDocumentsRequest, Error> {
818 let mut where_clauses = where_clauses
819 .into_iter()
820 .map(where_clause_to_proto)
821 .collect::<Result<Vec<_>, _>>()?;
822 // Append time-range selections as `IN_TIME_RANGE` clauses carrying the
823 // typed `time_range` operand (`value` stays unset — the selection is
824 // not a field value). The grid, when named, repeats the contract's
825 // declared seconds verbatim; a zero phase is proto3's default, so a
826 // phaseless grid has exactly one wire spelling by construction. The
827 // server resolves the relative selectors to a concrete bucket from
828 // current block time and the verifier re-derives the same bucket from
829 // the signed response metadata time; a `ByStart` selection carries its
830 // window's start in the query itself, so both sides read it verbatim.
831 for TimeRangeClause {
832 field,
833 selector,
834 grid,
835 } in time_range_clauses
836 {
837 let (proto_selector, start_ms) = match selector {
838 TimeRangeSelector::Newest => (ProtoTimeRangeSelector::Newest, None),
839 TimeRangeSelector::Oldest => (ProtoTimeRangeSelector::Oldest, None),
840 TimeRangeSelector::ByStart { start_ms } => {
841 (ProtoTimeRangeSelector::ByStart, Some(start_ms))
842 }
843 };
844 where_clauses.push(ProtoWhereClause {
845 field,
846 operator: ProtoWhereOperator::InTimeRange as i32,
847 value: None,
848 time_range: Some(ProtoTimeRangeSelection {
849 selector: proto_selector as i32,
850 start_ms,
851 grid: grid.map(|spec| ProtoTimeRangeGrid {
852 range: spec.range_seconds,
853 step: spec.step_seconds,
854 phase: spec.phase_seconds,
855 }),
856 }),
857 });
858 }
859 let order_by = order_by_clauses
860 .into_iter()
861 .map(order_clause_to_proto)
862 .collect();
863 let having = having
864 .into_iter()
865 .map(having_clause_to_proto)
866 .collect::<Result<Vec<_>, _>>()?;
867 // `limit: u32` with `0` sentinel → `optional uint32` on the V1
868 // wire. `None` lets the server apply its own default; explicit
869 // `0` would be a strange "return zero rows" request.
870 let limit = if limit == 0 { None } else { Some(limit) };
871 // V0 and V1 ship separate `Start` enums even though the shape
872 // is identical. Translate at the wire boundary so the
873 // `DocumentQuery.start` field stays stable for callers already
874 // using the V0 type.
875 let start_v1 = start.map(|s| match s {
876 Start::StartAfter(b) => V1Start::StartAfter(b),
877 Start::StartAt(b) => V1Start::StartAt(b),
878 });
879
880 Ok(GetDocumentsRequest {
881 version: Some(V1(GetDocumentsRequestV1 {
882 data_contract_id,
883 document_type,
884 where_clauses,
885 order_by,
886 limit,
887 // Document fetch always proves via this conversion.
888 // Count fetch uses the same wire shape; both paths go
889 // through the `FromProof` decoders which expect the
890 // `Proof(...)` response variant. `SdkBuilder::with_proofs(false)`
891 // is consequently a no-op for both — see the blanket
892 // `Query<T> for T` impl in `packages/rs-sdk/src/platform/query.rs`
893 // for the `tracing::warn!` emitted at fetch time when
894 // proofs are disabled.
895 prove: true,
896 start: start_v1,
897 // `repeated Select selects` on the wire — single
898 // projection wraps in a one-element vec; the SDK's
899 // `DocumentQuery` carries a single `SelectProjection`
900 // because multi-projection is wire-only today.
901 selects: vec![select_to_proto(select)],
902 group_by,
903 having,
904 // Honoured on the ranked path (it is the `OFFSET` of
905 // `ORDER BY <agg> DESC LIMIT n OFFSET m`) and rejected by
906 // the server everywhere else. Passed straight through:
907 // deciding here which paths may carry an offset would put
908 // a second copy of that rule in the SDK.
909 offset,
910 chained: None,
911 sub_queries,
912 })),
913 })
914}
915
916#[allow(clippy::too_many_arguments)]
917fn encode_v0(
918 data_contract_id: Vec<u8>,
919 document_type: String,
920 where_clauses: Vec<WhereClause>,
921 order_by_clauses: Vec<OrderClause>,
922 limit: u32,
923 offset: Option<u32>,
924 start: Option<Start>,
925 select: &SelectProjection,
926 group_by: &[String],
927 having: &[HavingClause],
928) -> Result<GetDocumentsRequest, Error> {
929 // V0 only carries plain `getDocuments` semantics — reject the
930 // v1-only SQL-shaped surfaces with a typed error rather than
931 // letting the server reject them after a round-trip.
932 if !matches!(select.function, SelectFunction::Documents) {
933 return Err(Error::Config(format!(
934 "select={:?} requires Platform v3.1+ (V1 documents wire); pin/upgrade \
935 to a v3.1+ network or rebuild the query with SelectProjection::documents()",
936 select.function
937 )));
938 }
939 if !group_by.is_empty() {
940 return Err(Error::Config(
941 "group_by requires Platform v3.1+ (V1 documents wire); not supported on V0".to_string(),
942 ));
943 }
944 if !having.is_empty() {
945 return Err(Error::Config(
946 "having clauses require Platform v3.1+ (V1 documents wire); not supported on V0"
947 .to_string(),
948 ));
949 }
950 if offset.is_some() {
951 // The V0 request message has no `offset` field at all, so
952 // silently dropping it would page from rank 0 while the caller
953 // believed they had skipped ahead — the one failure mode worth
954 // an extra branch here.
955 return Err(Error::Config(
956 "offset requires Platform v3.1+ (V1 documents wire); not supported on V0".to_string(),
957 ));
958 }
959
960 // V0 carries CBOR-serialized arrays of clause components. The
961 // server decodes them via `ciborium::de::from_reader` into a
962 // `Value`, then expects `Value::Array(clauses)` where each
963 // inner clause is `[field_text, operator_text, value]` (where)
964 // or `[field_text, "asc"|"desc"]` (order_by). Build the same
965 // shape via the existing `From<WhereClause> for Value` /
966 // `From<OrderClause> for Value` impls, then serialize the
967 // top-level array.
968 let where_bytes = if where_clauses.is_empty() {
969 Vec::new()
970 } else {
971 let where_value = Value::Array(where_clauses.into_iter().map(Value::from).collect());
972 where_value.to_cbor_buffer().map_err(|e| {
973 Error::Protocol(dpp::ProtocolError::EncodingError(format!(
974 "failed to CBOR-encode v0 where clauses: {e}"
975 )))
976 })?
977 };
978 let order_by_bytes = if order_by_clauses.is_empty() {
979 Vec::new()
980 } else {
981 let order_value = Value::Array(order_by_clauses.into_iter().map(Value::from).collect());
982 order_value.to_cbor_buffer().map_err(|e| {
983 Error::Protocol(dpp::ProtocolError::EncodingError(format!(
984 "failed to CBOR-encode v0 order_by clauses: {e}"
985 )))
986 })?
987 };
988
989 Ok(GetDocumentsRequest {
990 version: Some(V0(GetDocumentsRequestV0 {
991 data_contract_id,
992 document_type,
993 r#where: where_bytes,
994 order_by: order_by_bytes,
995 // V0's `limit` is a plain u32 with 0 = "server default".
996 // V1's `optional uint32` keeps 0 as a structurally
997 // meaningless explicit-zero; we translate by clamping
998 // to 0 only when the caller meant "unset".
999 limit,
1000 start,
1001 // `prove: true` hardcoded — same rationale as `encode_v1`:
1002 // document fetch always proves; `SdkBuilder::with_proofs(false)`
1003 // is a no-op for this path because the `FromProof` decoder
1004 // expects the `Proof(...)` response variant.
1005 prove: true,
1006 })),
1007 })
1008}
1009
1010impl<'a> TryFrom<&'a DriveDocumentQuery<'a>> for DocumentQuery {
1011 type Error = crate::error::Error;
1012
1013 /// Preserves sub-queries through SDK request construction and proof verification.
1014 ///
1015 /// Fallible by necessity: a drive query carrying `resolved_time_ranges`
1016 /// holds bucket-start equalities whose meaning lives in the provenance,
1017 /// and `DocumentQuery` has no field to carry it — the original
1018 /// `IN_TIME_RANGE` selector cannot be reconstructed from the resolved
1019 /// query. Serializing such a query would silently demote the bucket
1020 /// equality to a raw-timestamp predicate: a transformed-index-only
1021 /// contract then rejects the request, while a contract with a competing
1022 /// plain index returns a different — but validly proven — result.
1023 fn try_from(value: &'a DriveDocumentQuery<'a>) -> Result<Self, Self::Error> {
1024 if !value.resolved_time_ranges.is_empty() {
1025 return Err(Error::Config(
1026 "a drive query carrying time-range resolution provenance cannot be \
1027 converted to a DocumentQuery: the resolved bucket equality would be \
1028 demoted to a raw-timestamp predicate. Build the DocumentQuery with \
1029 `with_time_range` / `with_time_range_grid` instead, so the selector \
1030 is resolved against the signed response metadata"
1031 .to_string(),
1032 ));
1033 }
1034 let data_contract = value.contract.clone();
1035 let document_type_name = value.document_type.name();
1036 let where_clauses = value.internal_clauses.clone().into();
1037 let order_by_clauses = value.order_by.iter().map(|(_, v)| v.clone()).collect();
1038 let limit = value.limit.unwrap_or(0) as u32;
1039 let offset = value.offset.map(u32::from);
1040
1041 let start = if let Some(start_at) = value.start_at {
1042 match value.start_at_included {
1043 true => Some(Start::StartAt(start_at.to_vec())),
1044 false => Some(Start::StartAfter(start_at.to_vec())),
1045 }
1046 } else {
1047 None
1048 };
1049
1050 Ok(Self {
1051 // `DriveDocumentQuery` has no SELECT/GROUP BY/HAVING/time-range
1052 // concept — it's a documents-only query. Default to the
1053 // v1 documents shape.
1054 select: SelectProjection::documents(),
1055 data_contract: Arc::new(data_contract),
1056 document_type_name: document_type_name.to_string(),
1057 where_clauses,
1058 time_range_clauses: Vec::new(),
1059 group_by: Vec::new(),
1060 having: Vec::new(),
1061 order_by_clauses,
1062 limit,
1063 offset,
1064 start,
1065 sub_queries: value
1066 .sub_queries
1067 .iter()
1068 .map(CompositeSubQuery::from)
1069 .collect(),
1070 })
1071 }
1072}
1073
1074impl<'a> TryFrom<DriveDocumentQuery<'a>> for DocumentQuery {
1075 type Error = crate::error::Error;
1076
1077 /// By-value twin of the by-reference conversion above — same
1078 /// sub-query preservation and provenance rejection, same rationale.
1079 fn try_from(value: DriveDocumentQuery<'a>) -> Result<Self, Self::Error> {
1080 DocumentQuery::try_from(&value)
1081 }
1082}
1083
1084impl<'a> TryFrom<&'a DocumentQuery> for DriveDocumentQuery<'a> {
1085 type Error = crate::error::Error;
1086
1087 fn try_from(request: &'a DocumentQuery) -> Result<Self, Self::Error> {
1088 if !request.sub_queries.is_empty() {
1089 request.check_composite_shape()?;
1090 }
1091 // A pending (unresolved) time-range selection MUST be resolved into a
1092 // concrete bucket-equality clause before a drive query can be built —
1093 // see `resolve_time_range_clauses_with_metadata_time`. Silently
1094 // dropping it here would rebuild (and verify against) a strictly
1095 // broader query than the prover ran, so refuse instead: this makes
1096 // "forgot to resolve" a loud error on every present and future call
1097 // path rather than a silent verification hole.
1098 if !request.time_range_clauses.is_empty() {
1099 return Err(Error::Config(
1100 "the query's time range (IN_TIME_RANGE) selections have not been resolved into \
1101 bucket equalities; resolve them against the response's quorum-signed metadata \
1102 time before building a drive query"
1103 .to_string(),
1104 ));
1105 }
1106
1107 // let data_contract = request.data_contract.clone();
1108 let document_type = request
1109 .data_contract
1110 .document_type_for_name(&request.document_type_name)
1111 .map_err(ProtocolError::DataContractError)?;
1112
1113 // Client-side construction groups under the latest grammar; the
1114 // server and the proof verifier enforce the network's protocol
1115 // version at path-query lowering.
1116 let internal_clauses = InternalClauses::extract_from_clauses(
1117 request.where_clauses.clone(),
1118 PlatformVersion::latest(),
1119 )
1120 .map_err(Error::Drive)?;
1121
1122 // Mirror the limit contract of the server's
1123 // `DriveDocumentQuery::from_typed_clauses` exactly: `0` (this
1124 // struct's "unset" sentinel — V0's `limit: 0`, V1's
1125 // `limit: None`) falls back to the server default, and anything
1126 // above `DEFAULT_QUERY_LIMIT` (the `config.default_query_limit`
1127 // every deployed server runs with) is refused with the server's
1128 // own `QuerySyntaxError::InvalidLimit` rather than truncated or
1129 // passed through. A `u16::try_from` alone would not do: limits
1130 // 101..=65535 fit a `u16` but the server refuses them, so a raw
1131 // `DriveDocumentQuery` carrying one would verify a proof no
1132 // honest server could have produced.
1133 let limit = match request.limit {
1134 0 => Some(DEFAULT_QUERY_LIMIT),
1135 limit if limit > u32::from(DEFAULT_QUERY_LIMIT) => {
1136 return Err(Error::Drive(drive::error::Error::Query(
1137 drive::error::query::QuerySyntaxError::InvalidLimit(format!(
1138 "limit {} greater than max limit {}",
1139 limit, DEFAULT_QUERY_LIMIT
1140 )),
1141 )));
1142 }
1143 limit => Some(limit as u16),
1144 };
1145
1146 let (start_at, start_at_included) = match request.start.as_ref() {
1147 None => (None, false),
1148 Some(Start::StartAt(at)) => (
1149 Some(at.clone().try_into().map_err(|_| {
1150 ProtocolError::InvalidVectorSizeError(InvalidVectorSizeError::new(32, at.len()))
1151 })?),
1152 true,
1153 ),
1154 Some(Start::StartAfter(after)) => (
1155 Some(after.clone().try_into().map_err(|_| {
1156 ProtocolError::InvalidVectorSizeError(InvalidVectorSizeError::new(
1157 32,
1158 after.len(),
1159 ))
1160 })?),
1161 false,
1162 ),
1163 };
1164
1165 // `DriveDocumentQuery`'s offset is a `u16`; the wire's is a
1166 // `u32` because the ranked path takes an unbounded one. A
1167 // documents query that overflows `u16` is refused rather than
1168 // truncated — silently paging from a different rank than the
1169 // caller asked for is the worst available outcome.
1170 let offset = request
1171 .offset
1172 .map(|offset| {
1173 u16::try_from(offset).map_err(|_| {
1174 Error::Config(format!(
1175 "offset {offset} does not fit a documents query's u16 offset \
1176 (max {}); offsets above that are only meaningful on the ranked \
1177 surface, which does not route through DriveDocumentQuery",
1178 u16::MAX
1179 ))
1180 })
1181 })
1182 .transpose()?;
1183
1184 let query = Self {
1185 contract: &request.data_contract,
1186 document_type,
1187 internal_clauses,
1188 offset,
1189 limit,
1190 order_by: request
1191 .order_by_clauses
1192 .clone()
1193 .into_iter()
1194 .map(|v| (v.field.clone(), v))
1195 .collect(),
1196 start_at,
1197 start_at_included,
1198 block_time_ms: None,
1199 // A `DocumentQuery` reaching here carries no unresolved
1200 // time-range selection (rejected above) and cannot tell which of
1201 // its equalities came from resolution. Callers that resolved
1202 // selections assign the fields they resolved onto the returned
1203 // query; everything else is a raw query.
1204 resolved_time_ranges: vec![],
1205 sub_queries: drive_sub_queries(request)?,
1206 };
1207
1208 Ok(query)
1209 }
1210}
1211
1212/// Convert a drive [`WhereClause`] into its wire-format proto
1213/// counterpart. The proto value variant is picked from the
1214/// `dpp::platform_value::Value` variant by primitive type — schema-
1215/// agnostic, matching the inverse direction the rs-drive-abci v1
1216/// handler runs via its `conversions::value_from_proto`.
1217///
1218/// Errors only on `Value` variants that have no wire-format
1219/// counterpart (`Map`, `EnumU8`, `EnumString`) — these aren't
1220/// produced by the SDK's typical WhereClause builders, so a
1221/// rejection here flags an unsupported caller construction at the
1222/// wire boundary rather than silently dropping the value.
1223pub(crate) fn where_clause_to_proto(clause: WhereClause) -> Result<ProtoWhereClause, Error> {
1224 Ok(ProtoWhereClause {
1225 field: clause.field,
1226 operator: where_operator_to_proto(clause.operator) as i32,
1227 value: Some(value_to_proto(clause.value)?),
1228 // The typed IN_TIME_RANGE operand; never set on an ordinary value
1229 // clause (time-range selections are encoded by `encode_v1` itself,
1230 // from `time_range_clauses`).
1231 time_range: None,
1232 })
1233}
1234
1235pub(crate) fn order_clause_to_proto(clause: OrderClause) -> ProtoOrderClause {
1236 // Drive's `OrderClause` carries a plain `field: String` —
1237 // emit the field-target variant of the wire's `target` oneof.
1238 // The aggregate-target variant (`ORDER BY COUNT(*)`) is
1239 // wire-only today; when drive's `OrderClause` gains an
1240 // aggregate target the SDK gets a parallel builder.
1241 ProtoOrderClause {
1242 target: Some(order_clause::Target::Field(clause.field)),
1243 ascending: clause.ascending,
1244 }
1245}
1246
1247/// Convert a drive [`HavingClause`] into its wire-format proto
1248/// counterpart. The inverse of `rs-drive-abci`'s
1249/// `having_clause_from_proto`. Errors only on `Value` variants
1250/// the underlying `value_to_proto` can't represent — every
1251/// `HavingOperator` / `HavingAggregateFunction` discriminant has a
1252/// 1:1 wire counterpart and is always convertible.
1253fn having_clause_to_proto(clause: HavingClause) -> Result<ProtoHavingClause, Error> {
1254 let right = match clause.right {
1255 HavingRightOperand::Value(v) => having_clause::Right::Value(value_to_proto(v)?),
1256 };
1257 Ok(ProtoHavingClause {
1258 aggregate: Some(having_aggregate_to_proto(clause.aggregate)),
1259 operator: having_operator_to_proto(clause.operator) as i32,
1260 right: Some(right),
1261 })
1262}
1263
1264fn having_aggregate_to_proto(aggregate: HavingAggregate) -> ProtoHavingAggregate {
1265 ProtoHavingAggregate {
1266 function: having_function_to_proto(aggregate.function) as i32,
1267 field: aggregate.field,
1268 }
1269}
1270
1271fn having_function_to_proto(function: HavingAggregateFunction) -> having_aggregate::Function {
1272 match function {
1273 HavingAggregateFunction::Count => having_aggregate::Function::Count,
1274 HavingAggregateFunction::Sum => having_aggregate::Function::Sum,
1275 HavingAggregateFunction::Avg => having_aggregate::Function::Avg,
1276 }
1277}
1278
1279/// Convert a drive [`SelectProjection`] into its wire-format
1280/// proto counterpart. Inverse of `rs-drive-abci`'s
1281/// `select_from_proto`. Always succeeds — every
1282/// `SelectFunction` discriminant has a 1:1 wire counterpart.
1283fn select_to_proto(select: SelectProjection) -> ProtoSelect {
1284 ProtoSelect {
1285 function: select_function_to_proto(select.function) as i32,
1286 field: select.field,
1287 }
1288}
1289
1290fn select_function_to_proto(function: SelectFunction) -> select::Function {
1291 match function {
1292 SelectFunction::Documents => select::Function::Documents,
1293 SelectFunction::Count => select::Function::Count,
1294 SelectFunction::Sum => select::Function::Sum,
1295 SelectFunction::Avg => select::Function::Avg,
1296 SelectFunction::Min => select::Function::Min,
1297 SelectFunction::Max => select::Function::Max,
1298 }
1299}
1300
1301fn having_operator_to_proto(op: HavingOperator) -> having_clause::Operator {
1302 match op {
1303 HavingOperator::Equal => having_clause::Operator::Equal,
1304 HavingOperator::NotEqual => having_clause::Operator::NotEqual,
1305 HavingOperator::GreaterThan => having_clause::Operator::GreaterThan,
1306 HavingOperator::GreaterThanOrEquals => having_clause::Operator::GreaterThanOrEquals,
1307 HavingOperator::LessThan => having_clause::Operator::LessThan,
1308 HavingOperator::LessThanOrEquals => having_clause::Operator::LessThanOrEquals,
1309 HavingOperator::Between => having_clause::Operator::Between,
1310 HavingOperator::BetweenExcludeBounds => having_clause::Operator::BetweenExcludeBounds,
1311 HavingOperator::BetweenExcludeLeft => having_clause::Operator::BetweenExcludeLeft,
1312 HavingOperator::BetweenExcludeRight => having_clause::Operator::BetweenExcludeRight,
1313 HavingOperator::In => having_clause::Operator::In,
1314 }
1315}
1316
1317fn where_operator_to_proto(op: WhereOperator) -> ProtoWhereOperator {
1318 match op {
1319 WhereOperator::Equal => ProtoWhereOperator::Equal,
1320 WhereOperator::GreaterThan => ProtoWhereOperator::GreaterThan,
1321 WhereOperator::GreaterThanOrEquals => ProtoWhereOperator::GreaterThanOrEquals,
1322 WhereOperator::LessThan => ProtoWhereOperator::LessThan,
1323 WhereOperator::LessThanOrEquals => ProtoWhereOperator::LessThanOrEquals,
1324 WhereOperator::Between => ProtoWhereOperator::Between,
1325 WhereOperator::BetweenExcludeBounds => ProtoWhereOperator::BetweenExcludeBounds,
1326 WhereOperator::BetweenExcludeLeft => ProtoWhereOperator::BetweenExcludeLeft,
1327 WhereOperator::BetweenExcludeRight => ProtoWhereOperator::BetweenExcludeRight,
1328 WhereOperator::In => ProtoWhereOperator::In,
1329 WhereOperator::StartsWith => ProtoWhereOperator::StartsWith,
1330 }
1331}
1332
1333/// Map `dpp::platform_value::Value` onto the wire-shape
1334/// [`ProtoDocumentFieldValue`]. The schema-driven decode on the
1335/// server side resolves the actual indexed type — this layer just
1336/// names the primitive.
1337///
1338/// Mapping rules:
1339/// - `Bool` → `BoolValue`
1340/// - `I8`/`I16`/`I32`/`I64` → `Int64Value` (widened)
1341/// - `U8`/`U16`/`U32`/`U64` → `Uint64Value` (widened)
1342/// - `Float` → `DoubleValue`
1343/// - `Text` → `Text`
1344/// - `Bytes`/`Bytes20`/`Bytes32`/`Bytes36`/`Identifier` → `BytesValue`
1345/// - `U128`/`I128` → `Text` (decimal string). **Not yet
1346/// round-trippable against `U128`/`I128`-typed indexed fields**:
1347/// the v1 typed-decode path (`v1/conversions.rs::value_from_proto`)
1348/// passes the text through as `Value::Text`, and the
1349/// downstream executor's strict `Value::to_integer()` then
1350/// rejects it. Schema-aware coercion (the
1351/// `DocumentPropertyType::value_from_string` path the v0 SQL
1352/// parser uses) hasn't been threaded through to the typed
1353/// path yet. The encoding is shipped because the proto needs a
1354/// home for 128-bit values; no production system contract
1355/// indexes `U128`/`I128` today. Tracked in the v1 follow-up
1356/// issue.
1357/// - `Array` → `List` (recursive, but only one level deep —
1358/// `value_to_proto` rejects nested arrays with
1359/// `EncodingError("nested DocumentFieldValue.list …")` to
1360/// match the server-side depth cap in
1361/// `v1/conversions.rs::value_from_proto_at_depth`, so wire-
1362/// malformed shapes fail at request-construction time with a
1363/// deterministic local error rather than after a transport
1364/// round-trip.
1365/// - `Null` → `NullValue(true)` (the `bool` payload is a
1366/// placeholder per the proto-side comment; only the variant
1367/// discriminant carries meaning)
1368/// - `Map`/`EnumU8`/`EnumString` → `Error` (no wire-format
1369/// counterpart for these shapes in a WhereClause operand)
1370fn value_to_proto(value: Value) -> Result<ProtoDocumentFieldValue, Error> {
1371 value_to_proto_at_depth(value, 0)
1372}
1373
1374/// Recursion-bounded form of [`value_to_proto`]. Mirrors the
1375/// server-side `value_from_proto_at_depth` contract so encoder
1376/// and decoder agree on the supported `Value` subset: `depth = 0`
1377/// is the clause-level operand; `Array` is legal once (the flat
1378/// list of scalars for `IN` / `BETWEEN*`); any deeper nesting
1379/// rejects locally instead of producing a request the server
1380/// would round-trip just to reject.
1381fn value_to_proto_at_depth(value: Value, depth: u8) -> Result<ProtoDocumentFieldValue, Error> {
1382 let variant = match value {
1383 Value::Null => document_field_value::Variant::NullValue(true),
1384 Value::Bool(b) => document_field_value::Variant::BoolValue(b),
1385 Value::I8(i) => document_field_value::Variant::Int64Value(i as i64),
1386 Value::I16(i) => document_field_value::Variant::Int64Value(i as i64),
1387 Value::I32(i) => document_field_value::Variant::Int64Value(i as i64),
1388 Value::I64(i) => document_field_value::Variant::Int64Value(i),
1389 Value::U8(u) => document_field_value::Variant::Uint64Value(u as u64),
1390 Value::U16(u) => document_field_value::Variant::Uint64Value(u as u64),
1391 Value::U32(u) => document_field_value::Variant::Uint64Value(u as u64),
1392 Value::U64(u) => document_field_value::Variant::Uint64Value(u),
1393 Value::Float(f) => document_field_value::Variant::DoubleValue(f),
1394 Value::Text(s) => document_field_value::Variant::Text(s),
1395 Value::Bytes(b) => document_field_value::Variant::BytesValue(b),
1396 Value::Bytes20(b) => document_field_value::Variant::BytesValue(b.to_vec()),
1397 Value::Bytes32(b) => document_field_value::Variant::BytesValue(b.to_vec()),
1398 Value::Bytes36(b) => document_field_value::Variant::BytesValue(b.to_vec()),
1399 Value::Identifier(b) => document_field_value::Variant::BytesValue(b.to_vec()),
1400 // u128 / i128 don't fit in `int64_value`/`uint64_value`;
1401 // encode as a decimal string. See the function-level
1402 // docstring for the U128/I128 round-trip caveat.
1403 Value::U128(u) => document_field_value::Variant::Text(u.to_string()),
1404 Value::I128(i) => document_field_value::Variant::Text(i.to_string()),
1405 Value::Array(items) => {
1406 if depth >= 1 {
1407 return Err(Error::Protocol(dpp::ProtocolError::EncodingError(
1408 "nested DocumentFieldValue.list is not supported on the v1 \
1409 query surface; `IN` / `BETWEEN*` candidate lists are flat \
1410 scalars only"
1411 .to_string(),
1412 )));
1413 }
1414 document_field_value::Variant::List(document_field_value::ValueList {
1415 values: items
1416 .into_iter()
1417 .map(|v| value_to_proto_at_depth(v, depth + 1))
1418 .collect::<Result<Vec<_>, _>>()?,
1419 })
1420 }
1421 // Catches both `Value::Map(_)` / `Value::EnumU8(_)` /
1422 // `Value::EnumString(_)` (no wire-format counterpart for
1423 // these shapes in a WhereClause operand) and any
1424 // future-added variant — `dpp::platform_value::Value` is
1425 // `#[non_exhaustive]`, so the SDK fails loudly rather
1426 // than silently dropping data the moment upstream adds a
1427 // variant we don't yet know how to encode.
1428 _ => {
1429 return Err(Error::Protocol(dpp::ProtocolError::EncodingError(format!(
1430 "Value variant has no `DocumentFieldValue` wire-format counterpart: {value:?}"
1431 ))));
1432 }
1433 };
1434 Ok(ProtoDocumentFieldValue {
1435 variant: Some(variant),
1436 })
1437}
1438
1439#[cfg(test)]
1440mod encode_version_gate_tests {
1441 //! The `IN_TIME_RANGE` operator is emitted only for protocol versions
1442 //! whose contract grammar hosts `timeRange` indexes (document
1443 //! meta-schema generation 3, first pinned by protocol version 14).
1444 //! Protocol versions 12 and 13 serve the same v1 getDocuments wire but
1445 //! predate the grammar — encoding for them must refuse up front
1446 //! instead of sending an operator the server rejects as an unknown
1447 //! discriminant.
1448
1449 use super::*;
1450 use dpp::data_contract::DataContractFactory;
1451 use dpp::platform_value::platform_value;
1452 use dpp::prelude::{DataContract, Identifier};
1453 use drive::query::TimeRangeSelector;
1454
1455 fn post_contract() -> Arc<DataContract> {
1456 let schemas = platform_value!({
1457 "post": {
1458 "type": "object",
1459 "properties": {
1460 "hashtag": { "type": "string", "maxLength": 63, "position": 0 },
1461 },
1462 "required": ["hashtag"],
1463 "additionalProperties": false,
1464 }
1465 });
1466 let contract = DataContractFactory::new(PlatformVersion::latest().protocol_version)
1467 .expect("expected a factory")
1468 .create_with_value_config(Identifier::new([7u8; 32]), 0, schemas, None, None)
1469 .expect("the post contract is well-formed")
1470 .data_contract_owned();
1471 Arc::new(contract)
1472 }
1473
1474 fn newest_time_range_query() -> DocumentQuery {
1475 DocumentQuery::new(post_contract(), "post")
1476 .expect("the fixture has this document type")
1477 .with_time_range("$createdAt", TimeRangeSelector::Newest)
1478 }
1479
1480 #[test]
1481 fn a_time_range_query_refuses_to_encode_for_protocol_version_13() {
1482 let platform_version = PlatformVersion::get(13).expect("protocol version 13 exists");
1483 let error = GetDocumentsRequest::try_from_platform_versioned(
1484 newest_time_range_query(),
1485 platform_version,
1486 )
1487 .expect_err("protocol version 13's contract grammar has no timeRange indexes");
1488 let message = match error {
1489 Error::Config(message) => message,
1490 other => panic!("expected Error::Config, got {other:?}"),
1491 };
1492 assert!(
1493 message.contains("protocol version 14"),
1494 "the refusal must name the real version floor, got: {message}"
1495 );
1496 }
1497
1498 #[test]
1499 fn a_time_range_query_encodes_the_typed_operand_for_protocol_version_14() {
1500 let platform_version = PlatformVersion::get(14).expect("protocol version 14 exists");
1501 let request = GetDocumentsRequest::try_from_platform_versioned(
1502 newest_time_range_query(),
1503 platform_version,
1504 )
1505 .expect("protocol version 14 hosts timeRange indexes");
1506 let Some(V1(v1)) = request.version else {
1507 panic!("protocol version 14 encodes on the v1 wire");
1508 };
1509 let [clause] = v1.where_clauses.as_slice() else {
1510 panic!("the pending selector must ride as exactly one IN_TIME_RANGE clause");
1511 };
1512 assert_eq!(clause.operator, ProtoWhereOperator::InTimeRange as i32);
1513 assert_eq!(
1514 clause.value, None,
1515 "the selection is not a field value; the generic operand stays unset"
1516 );
1517 let selection = clause
1518 .time_range
1519 .as_ref()
1520 .expect("the typed operand carries the selection");
1521 assert_eq!(selection.selector, ProtoTimeRangeSelector::Newest as i32);
1522 assert_eq!(selection.start_ms, None);
1523 assert_eq!(selection.grid, None, "a grid-less selection names no grid");
1524 }
1525
1526 #[test]
1527 fn a_by_start_selection_encodes_its_window_start_and_grid() {
1528 let platform_version = PlatformVersion::get(14).expect("protocol version 14 exists");
1529 let query = DocumentQuery::new(post_contract(), "post")
1530 .expect("the fixture has this document type")
1531 .with_time_range_grid(
1532 "$createdAt",
1533 TimeRangeSelector::ByStart {
1534 start_ms: 1_756_684_800_000,
1535 },
1536 TimeRangeGridSpec {
1537 range_seconds: 86_400,
1538 step_seconds: 86_400,
1539 phase_seconds: 0,
1540 },
1541 );
1542 let request = GetDocumentsRequest::try_from_platform_versioned(query, platform_version)
1543 .expect("protocol version 14 hosts timeRange indexes");
1544 let Some(V1(v1)) = request.version else {
1545 panic!("protocol version 14 encodes on the v1 wire");
1546 };
1547 let [clause] = v1.where_clauses.as_slice() else {
1548 panic!("the selection must ride as exactly one IN_TIME_RANGE clause");
1549 };
1550 let selection = clause
1551 .time_range
1552 .as_ref()
1553 .expect("the typed operand carries the selection");
1554 assert_eq!(selection.selector, ProtoTimeRangeSelector::ByStart as i32);
1555 assert_eq!(
1556 selection.start_ms,
1557 Some(1_756_684_800_000),
1558 "the absolute window start rides in the query itself"
1559 );
1560 assert_eq!(
1561 selection.grid,
1562 Some(ProtoTimeRangeGrid {
1563 range: 86_400,
1564 step: 86_400,
1565 phase: 0,
1566 }),
1567 "the grid repeats the contract's declared seconds verbatim"
1568 );
1569 }
1570
1571 #[test]
1572 fn a_query_without_time_range_clauses_still_encodes_for_protocol_version_13() {
1573 let platform_version = PlatformVersion::get(13).expect("protocol version 13 exists");
1574 let query = DocumentQuery::new(post_contract(), "post")
1575 .expect("the fixture has this document type");
1576 GetDocumentsRequest::try_from_platform_versioned(query, platform_version)
1577 .expect("the gate only refuses queries that carry a time-range selection");
1578 }
1579}