Skip to main content

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}