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