dash_platform_queries/documents/document_ranked_entries.rs
1//! `FromProof` + `Fetch` for [`DocumentRankedEntries`] — the
2//! **ranked** (`GROUP BY … ORDER BY <aggregate> LIMIT n [OFFSET m]`)
3//! view of the unified `getDocuments` endpoint.
4//!
5//! A ranked query answers "which `n` groups score highest (or lowest)
6//! on an aggregate?" — *top 5 restaurants by average grade* — in
7//! `O(log n + k)`, with a proof. It reads a pre-sorted per-axis
8//! *secondary* Merk maintained by the write path (grovedb PR #657)
9//! rather than walking value trees, which is why it is cheap and why
10//! its shape is so constrained.
11//!
12//! Per-request resolution (which axis, which direction, how many
13//! groups, how many ranks to skip, which index covers them) lives in
14//! [`super::ranked_proof_helpers`]; this module is the thin
15//! `Fetch`-side wrapper.
16//!
17//! ## Request shape
18//!
19//! Exactly one aggregate `select`, exactly one `group_by` property,
20//! exactly one `ORDER BY` clause naming that select's aggregate, and a
21//! `LIMIT` — plus an optional `OFFSET`. `where` clauses are pins on a
22//! covering compound ranked index's leading properties (one per leading
23//! property, selecting which prefix's own ranking the walk reads) —
24//! absent for a single-property index. Each pin is an equality, except
25//! that **at most one** may be an `IN` of 2..=10 distinct elements: one
26//! walk per element, merged by `(aggregate, encoded pin, group key)`,
27//! with each merged entry carrying the encoded branch segment in
28//! `in_key` (unset on single-branch responses; a single-element `IN`
29//! normalizes to the equality pin). A non-zero `OFFSET` cannot combine
30//! with the `IN`, nor can a `null` pin on another property. No
31//! `having`, no `start_at`: each of those is rejected rather than
32//! ignored, on both sides, because a ranked walk cannot honour them and
33//! silently answering a different question is worse than an error.
34//!
35//! [`DocumentQuery::order_by_selected_aggregate`] builds the ordering
36//! clause, deriving the ordered field from the `select` through
37//! rs-drive's own key mapping (`SUM(f)` / `AVG(f)` are named by `f`,
38//! `COUNT(*)` by the `$count` sentinel), so there is no way to name it
39//! wrong by hand.
40//!
41//! ## Contract prerequisites
42//!
43//! The index must opt in with `rankedCountable` / `rankedSummable` /
44//! `rankedAverageable` (meta-schema v3, **protocol version 14+**). The
45//! index may be single-property (`group_by` its property, no `where`)
46//! or compound (`group_by` its trailing property, pin every leading one
47//! — equality pins, at most one of them an `IN`, as above). Against a
48//! protocol-version-13
49//! node the request is refused — v13's query table has no ranked path
50//! and rejects the ordering as `Unsupported`. That is the intended
51//! activation gate, not a bug: a v13 node and a v14 node must disagree
52//! here and nowhere else, which is what lets a mixed-version network
53//! run through the upgrade.
54//!
55//! ## Ranks, offsets, and the empty ranking
56//!
57//! The fetch result carries
58//! [`starting_rank`](drive_proof_verifier::DocumentRankedEntries::starting_rank)
59//! alongside the entries: entry `i` is the group at rank
60//! `starting_rank + i`, which is what makes `LIMIT 1 OFFSET 4`
61//! meaningful as "the 5th best" rather than "some entry". On the proved
62//! path that number is re-derived from the proof's counted subtree
63//! commitments, not taken from the node.
64//!
65//! An offset past the end of the ranking is a legitimate, provable
66//! answer: no entries, and `starting_rank` equal to the ranking's whole
67//! attested population. **Empty rankings prove too** — grovedb's
68//! paginated prover emits a guaranteed-empty range against an empty
69//! axis secondary rather than refusing — so querying a freshly
70//! registered contract with `prove = true` returns an empty page rather
71//! than an error, and the proved and unproven paths agree.
72//!
73//! ## Reading the values
74//!
75//! Entries come back in ranking order; **do not re-sort**. Averages
76//! are fixed-point integers: divide by
77//! [`RANKED_AVG_SCALE`](drive_proof_verifier::RANKED_AVG_SCALE) — a
78//! re-export of grovedb's own constant, which moved from `10^15` to
79//! `10^19` before release, so never hardcode the literal — or call
80//! [`RankedEntryValue::as_f64`](drive_proof_verifier::RankedEntryValue::as_f64),
81//! which does that division for you.
82//!
83//! **How exact the average is depends on the path.** Fetched with a
84//! proof (the default, and what the examples below do), the fixed point
85//! is the integer grovedb committed to and ranked on. Fetched without
86//! one, the wire carries only an `f64` of the average and the SDK
87//! re-scales it back, so the digits past `f64`'s ~15–16 significant
88//! decimals are reconstruction noise — fine to render, not something to
89//! compare for equality. Ranking *order* is exact either way.
90//!
91//! ## Example: top 5 restaurants by average grade
92//!
93//! `SELECT AVG(grade) GROUP BY restaurantId ORDER BY avg(grade) DESC LIMIT 5`
94//!
95//! ```rust,ignore
96//! use dash_sdk::{Sdk, platform::{DataContract, DocumentQuery, Fetch, Identifier}};
97//! use dash_sdk::drive::query::SelectProjection;
98//! use dash_sdk::platform::documents::document_query::RankingDirection;
99//! use drive_proof_verifier::{DocumentRankedEntries, RankedEntryValue, RANKED_AVG_SCALE};
100//! use futures::executor::block_on;
101//!
102//! # const RESTAURANTS_CONTRACT_ID: [u8; 32] = [0; 32];
103//! let sdk = Sdk::new_mock();
104//! let contract = block_on(DataContract::fetch(&sdk, Identifier::new(RESTAURANTS_CONTRACT_ID)))
105//! .expect("fetch contract")
106//! .expect("contract exists");
107//!
108//! let query = DocumentQuery::new(contract, "review")
109//! .expect("document type exists")
110//! .with_select(SelectProjection::avg("grade"))
111//! .with_group_by("restaurantId")
112//! .order_by_selected_aggregate(RankingDirection::Descending)
113//! .with_limit(5);
114//!
115//! let ranked = block_on(DocumentRankedEntries::fetch(&sdk, query))
116//! .expect("fetch succeeds")
117//! .expect("a well-formed ranked query always answers");
118//!
119//! // Entry order IS the ranking order — best first.
120//! for (offset, entry) in ranked.entries.iter().enumerate() {
121//! let rank = ranked.starting_rank + offset as u64;
122//! let restaurant = String::from_utf8_lossy(&entry.key);
123//! if let RankedEntryValue::AvgFixedPoint(fixed_point) = entry.value {
124//! // `as_f64()` is this same division, for when you only want
125//! // to display the number:
126//! // let average = entry.value.as_f64();
127//! // Keep the `fixed_point` itself when you need the exact
128//! // integer the proof committed to — comparing two groups,
129//! // reproducing the ranking, storing it. On a `prove = false`
130//! // fetch that integer is a reconstruction from the wire's
131//! // double, so it is only as precise as an `f64`.
132//! let average = (fixed_point as f64) / (RANKED_AVG_SCALE as f64);
133//! println!("#{}: {restaurant}: {average}", rank + 1);
134//! }
135//! }
136//! ```
137//!
138//! ## Example: the 5th-best restaurant
139//!
140//! `SELECT AVG(grade) GROUP BY restaurantId ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4`
141//!
142//! ```rust,ignore
143//! # use dash_sdk::platform::{DataContract, DocumentQuery};
144//! # use dash_sdk::platform::documents::document_query::RankingDirection;
145//! # use dash_sdk::drive::query::SelectProjection;
146//! # fn example(contract: DataContract) -> Result<(), dash_sdk::Error> {
147//! let query = DocumentQuery::new(contract, "review")?
148//! .with_select(SelectProjection::avg("grade"))
149//! .with_group_by("restaurantId")
150//! .order_by_selected_aggregate(RankingDirection::Descending)
151//! .with_limit(1)
152//! .with_offset(4);
153//! # Ok(())
154//! # }
155//! ```
156
157use crate::documents::document_query::DocumentQuery;
158use crate::documents::ranked_proof_helpers::{assert_ranked_shape, verify_ranked_query};
159use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata};
160use dash_context_provider::ContextProvider;
161use dpp::dashcore::Network;
162use dpp::version::PlatformVersion;
163use drive_proof_verifier::{DocumentRankedEntries, FromProof};
164
165impl FromProof<DocumentQuery> for DocumentRankedEntries {
166 type Request = DocumentQuery;
167 type Response = GetDocumentsResponse;
168
169 /// A request outside the ranked grammar is not refused by every node: a node routing
170 /// with an older query table serves it as a plain grouped aggregate, whose proof the
171 /// ranked verifier then refuses. Its pending time-range selections only ever add
172 /// equality pins, which the grammar cannot refuse a request for lacking, so checking
173 /// before they are resolved refuses nothing the verifier accepts.
174 fn check_request(
175 request: &Self::Request,
176 platform_version: &PlatformVersion,
177 ) -> Result<(), drive_proof_verifier::Error> {
178 request.ensure_no_sub_queries()?;
179 assert_ranked_shape(request, platform_version).map(|_| ())
180 }
181
182 fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
183 request: I,
184 response: O,
185 _network: Network,
186 platform_version: &PlatformVersion,
187 provider: &'a dyn ContextProvider,
188 ) -> Result<(Option<Self>, ResponseMetadata, Proof), drive_proof_verifier::Error>
189 where
190 Self: 'a,
191 {
192 let request: Self::Request = request.into();
193 let response: Self::Response = response.into();
194 // Unlike the count / sum / average impls there is no separate
195 // `assert_select_is_*` pre-check here: the ranked grammar
196 // check is the first step of resolution and returns the
197 // resolved mode, so it runs once, inside the helper. See
198 // `ranked_proof_helpers::assert_ranked_shape`.
199 let (page, mtd, proof) =
200 verify_ranked_query(request, response, platform_version, provider)?;
201 Ok((page.map(DocumentRankedEntries::from_verified), mtd, proof))
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 //! Offline tests for the ranked client surface: the ordering
208 //! builder, the request→wire encoding, and the client-side grammar
209 //! mirror.
210 //!
211 //! Proof verification needs a populated Drive and a real grovedb
212 //! proof, so it is exercised where those exist: rs-drive's
213 //! `drive_document_ranked_query::tests` runs prover and verifier
214 //! against a live Drive (including a bit-flip sweep proving no
215 //! tamper survives with the honest root hash), and rs-drive-abci's
216 //! `ranked_tests` pins the wire encoding of the same values. The
217 //! SDK's own network-backed suites live in `tests/fetch/` and run
218 //! against recorded vectors; there is no ranked vector yet because
219 //! recording one needs a protocol-version-14 network, which does
220 //! not exist offline.
221
222 use super::*;
223 use crate::documents::document_query::RankingDirection;
224 use crate::documents::ranked_proof_helpers::assert_ranked_shape;
225 use dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::select as proto_select;
226 use dapi_grpc::platform::v0::get_documents_request::{
227 order_clause, GetDocumentsRequestV1, OrderClause as ProtoOrderClause,
228 Version as RequestVersion,
229 };
230 use dapi_grpc::platform::v0::GetDocumentsRequest;
231 use dpp::data_contract::DataContract;
232 use dpp::tests::fixtures::get_data_contract_fixture;
233 use dpp::version::TryFromPlatformVersioned;
234 use drive::query::{
235 HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, HavingRightOperand,
236 SelectProjection, WhereClause, WhereOperator, RANKED_COUNT_ORDER_KEY,
237 };
238 use std::sync::Arc;
239
240 /// The protocol version the ranked surface activates at. Pinned
241 /// as a literal so a future bump of `PlatformVersion::latest()`
242 /// past 14 doesn't silently change what these tests encode.
243 fn platform_version() -> &'static PlatformVersion {
244 PlatformVersion::latest()
245 }
246
247 /// Any contract with a known document type — the wire encoder is
248 /// schema-agnostic (it never resolves `select` / `group_by` /
249 /// `order_by` field names against the contract; the *server* does
250 /// that), so the fixture's document type carries the encoding
251 /// test fine while the field names stay the headline example's,
252 /// keeping this comparable to rs-drive-abci's `ranked_tests`.
253 fn contract() -> Arc<DataContract> {
254 Arc::new(
255 get_data_contract_fixture(None, 0, platform_version().protocol_version)
256 .data_contract_owned(),
257 )
258 }
259
260 /// `SELECT AVG(grade) GROUP BY restaurantId ORDER BY grade DESC LIMIT 5`.
261 fn top_five_by_avg_grade() -> DocumentQuery {
262 DocumentQuery::new(contract(), "niceDocument")
263 .expect("the fixture has this document type")
264 .with_select(SelectProjection::avg("grade"))
265 .with_group_by("restaurantId")
266 .order_by_selected_aggregate(RankingDirection::Descending)
267 .with_limit(5)
268 }
269
270 fn encode(query: DocumentQuery) -> GetDocumentsRequest {
271 GetDocumentsRequest::try_from_platform_versioned(query, platform_version())
272 .expect("a ranked query encodes onto the V1 wire")
273 }
274
275 fn v1_of(request: GetDocumentsRequest) -> GetDocumentsRequestV1 {
276 match request.version.expect("the encoder always sets a version") {
277 RequestVersion::V1(v1) => v1,
278 RequestVersion::V0(_) => {
279 panic!("a ranked query must encode onto the V1 wire; V0 has no `order_by` targets")
280 }
281 }
282 }
283
284 /// The ordered field name, asserted through the wire's `target`
285 /// oneof so a future change to the aggregate-target spelling can't
286 /// pass this test by accident.
287 fn ordered_field(clause: &ProtoOrderClause) -> &str {
288 match clause.target.as_ref().expect("the target is always set") {
289 order_clause::Target::Field(field) => field.as_str(),
290 other => panic!("a ranked ORDER BY rides the field target, got {other:?}"),
291 }
292 }
293
294 /// The headline round-trip: the SDK must put a ranked query on the
295 /// wire in **exactly** the shape rs-drive-abci's
296 /// `avg_axis_top_k_returns_fixed_point_entries` proved the server
297 /// accepts. Asserted field by field rather than with a single
298 /// struct comparison so a failure names the field that drifted.
299 #[test]
300 fn ranked_query_encodes_the_proven_wire_shape() {
301 let v1 = v1_of(encode(top_five_by_avg_grade()));
302
303 // One aggregate select, naming the averaged property.
304 assert_eq!(v1.selects.len(), 1, "a ranked query has exactly one select");
305 assert_eq!(v1.selects[0].function, proto_select::Function::Avg as i32);
306 assert_eq!(v1.selects[0].field, "grade");
307
308 // One GROUP BY property — the ranked index's only property.
309 assert_eq!(v1.group_by, vec!["restaurantId".to_string()]);
310
311 // One ORDER BY clause naming the select's aggregate, descending.
312 assert_eq!(
313 v1.order_by.len(),
314 1,
315 "a ranked query has exactly one ordering clause — it is the ranking"
316 );
317 assert_eq!(ordered_field(&v1.order_by[0]), "grade");
318 assert!(
319 !v1.order_by[0].ascending,
320 "Descending is the `top n` reading and must not invert on the wire"
321 );
322
323 // The ranking's n rides `limit`.
324 assert_eq!(v1.limit, Some(5));
325
326 // Everything else must be at its "unset" wire value: a ranked
327 // request that carried any of these would be rejected.
328 assert!(v1.where_clauses.is_empty());
329 assert!(v1.having.is_empty());
330 assert_eq!(v1.offset, None);
331 assert!(v1.start.is_none());
332 assert!(v1.prove, "the Fetch path always requests a proof");
333 }
334
335 /// **The 5th-best group.** `LIMIT 1 OFFSET 4` is the whole point of
336 /// the offset surface, and the offset has to survive onto the wire
337 /// — a dropped one silently answers "the best" instead.
338 #[test]
339 fn the_fifth_best_encodes_limit_one_offset_four() {
340 let query = DocumentQuery::new(contract(), "niceDocument")
341 .expect("the fixture has this document type")
342 .with_select(SelectProjection::avg("grade"))
343 .with_group_by("restaurantId")
344 .order_by_selected_aggregate(RankingDirection::Descending)
345 .with_limit(1)
346 .with_offset(4);
347
348 let v1 = v1_of(encode(query.clone()));
349 assert_eq!(v1.limit, Some(1));
350 assert_eq!(v1.offset, Some(4));
351 assert!(!v1.order_by[0].ascending);
352
353 // And the client-side grammar resolves it to the same page the
354 // prover will produce.
355 let mode = assert_ranked_shape(&query, platform_version())
356 .expect("LIMIT 1 OFFSET 4 is well-formed");
357 assert_eq!(mode.k, 1);
358 assert_eq!(mode.offset, 4);
359 assert!(mode.descending);
360 }
361
362 /// An offset far past any plausible population is **not** capped:
363 /// grovedb counts the skipped region from the subtree aggregates
364 /// rather than walking it, on both `prove` settings, so the cost of
365 /// a deep page does not grow with the offset — `O(log n)` in the
366 /// size of the ranking, not in how far you page — and there is
367 /// nothing for a cap to protect.
368 #[test]
369 fn a_very_deep_offset_is_not_capped() {
370 let query = top_five_by_avg_grade().with_limit(1).with_offset(u32::MAX);
371 assert_eq!(v1_of(encode(query.clone())).offset, Some(u32::MAX));
372 let mode = assert_ranked_shape(&query, platform_version())
373 .expect("an offset past the end is a provable answer, not an error");
374 assert_eq!(mode.offset, u32::MAX);
375 }
376
377 /// `COUNT(*)` rankings are ordered by the **`$count` sentinel**,
378 /// not by a property name — the axis counts documents per group, so
379 /// there is no property to name. The builder derives it from the
380 /// select through rs-drive's own mapping, which is the only way the
381 /// client and the server can be guaranteed to agree.
382 #[test]
383 fn count_star_ranking_orders_by_the_count_sentinel() {
384 let query = DocumentQuery::new(contract(), "niceDocument")
385 .expect("the fixture has this document type")
386 .with_select(SelectProjection::count_star())
387 .with_group_by("restaurantId")
388 .order_by_selected_aggregate(RankingDirection::Descending)
389 .with_limit(2);
390
391 let v1 = v1_of(encode(query));
392 assert_eq!(v1.selects[0].function, proto_select::Function::Count as i32);
393 assert_eq!(v1.selects[0].field, "");
394 assert_eq!(ordered_field(&v1.order_by[0]), RANKED_COUNT_ORDER_KEY);
395 assert_eq!(
396 RANKED_COUNT_ORDER_KEY, "$count",
397 "the sentinel is wire-visible; changing it is a protocol change"
398 );
399 }
400
401 /// `Ascending` is the "bottom n" reading and must reach the wire as
402 /// `ascending: true`. Pinned separately from the descending case
403 /// because a builder that ignored its argument would still pass the
404 /// headline test.
405 #[test]
406 fn ascending_encodes_the_bottom_n_reading() {
407 let query = top_five_by_avg_grade()
408 .order_by_selected_aggregate(RankingDirection::Ascending)
409 .with_limit(1);
410 let v1 = v1_of(encode(query.clone()));
411 assert!(v1.order_by[0].ascending);
412
413 let mode = assert_ranked_shape(&query, platform_version()).expect("ASC LIMIT 1 is valid");
414 assert!(!mode.descending, "ASC ranks lowest-first");
415 assert_eq!(mode.k, 1, "ASC LIMIT 1 is the single worst-ranked group");
416 }
417
418 /// `order_by_selected_aggregate` **replaces** rather than appends.
419 /// A ranked query takes exactly one ordering clause, so a builder
420 /// that pushed would turn a second call — or a call after an
421 /// unrelated `with_order_by` — into a request the server rejects.
422 #[test]
423 fn order_by_selected_aggregate_replaces_any_prior_ordering() {
424 let query = top_five_by_avg_grade()
425 .with_order_by(drive::query::OrderClause {
426 field: "restaurantId".to_string(),
427 ascending: true,
428 })
429 .order_by_selected_aggregate(RankingDirection::Descending);
430
431 assert_eq!(query.order_by_clauses.len(), 1);
432 assert_eq!(query.order_by_clauses[0].field, "grade");
433 assert!(assert_ranked_shape(&query, platform_version()).is_ok());
434 }
435
436 /// The client-side grammar must resolve the same
437 /// `(axis, descending, k, offset)` tuple the server does — that
438 /// tuple goes into the proof envelope and is re-checked by the
439 /// verifier, so a client that resolved it differently could not
440 /// verify an honest proof.
441 #[test]
442 fn assert_ranked_shape_resolves_the_ranking() {
443 let mode = assert_ranked_shape(&top_five_by_avg_grade(), platform_version())
444 .expect("the headline query is well-formed");
445 assert!(mode.descending, "DESC ranks highest-first");
446 assert_eq!(mode.k, 5);
447 assert_eq!(mode.offset, 0, "an unset OFFSET is rank 0");
448 assert_eq!(mode.group_by_property, "restaurantId");
449 assert_eq!(mode.aggregate_field, "grade");
450 }
451
452 /// Every knob a ranked walk cannot honour is rejected **client
453 /// side**, before a round trip. Each of these is also rejected by
454 /// the server; mirroring them here turns a network error into an
455 /// immediate, specific one.
456 #[test]
457 fn assert_ranked_shape_rejects_what_a_ranking_cannot_honour() {
458 let base = top_five_by_avg_grade();
459
460 // An ordering on something other than the selected aggregate
461 // asks for an order the secondary cannot produce.
462 let wrong_order = {
463 let mut q = base.clone();
464 q.order_by_clauses = vec![drive::query::OrderClause {
465 field: "restaurantId".to_string(),
466 ascending: true,
467 }];
468 q
469 };
470 assert!(
471 assert_ranked_shape(&wrong_order, platform_version()).is_err(),
472 "ordering by the GROUP BY property is not a ranking by the aggregate"
473 );
474
475 // No ordering at all: a plain grouped aggregate, and the caller
476 // wanted `DocumentSplitAverages`.
477 let no_order = {
478 let mut q = base.clone();
479 q.order_by_clauses = Vec::new();
480 q
481 };
482 assert!(
483 assert_ranked_shape(&no_order, platform_version()).is_err(),
484 "a query with no ordering is not a ranked query"
485 );
486
487 let with_where = {
488 let mut q = base.clone();
489 q.where_clauses = vec![WhereClause {
490 field: "restaurantId".to_string(),
491 operator: WhereOperator::GreaterThan,
492 value: dpp::platform_value::Value::Text("a".to_string()),
493 }];
494 q
495 };
496 let err = assert_ranked_shape(&with_where, platform_version())
497 .expect_err("the axis secondary cannot rank a filtered subset");
498 assert!(format!("{err}").contains("where"));
499
500 // HAVING is a boolean per-group predicate and the ranked
501 // executor cannot drop groups from the middle of its walk.
502 let with_having = base.clone().with_having(vec![HavingClause {
503 aggregate: HavingAggregate {
504 function: HavingAggregateFunction::Avg,
505 field: "grade".to_string(),
506 },
507 operator: HavingOperator::GreaterThan,
508 right: HavingRightOperand::Value(dpp::platform_value::Value::U64(4)),
509 }]);
510 assert!(
511 assert_ranked_shape(&with_having, platform_version()).is_err(),
512 "a ranking cannot also filter its groups"
513 );
514 }
515
516 /// The ranking's `n` rides `limit`, and an out-of-range one is
517 /// rejected rather than clamped: `k` is part of the traversal the
518 /// client rebuilds to verify, so a silent clamp would produce a
519 /// page the client's reconstruction did not ask for.
520 #[test]
521 fn assert_ranked_shape_rejects_an_out_of_range_limit() {
522 // `0` is `DocumentQuery`'s "unset" sentinel, and a ranking with
523 // no `n` has no size; `101` is past `MAX_RANKED_LIMIT`.
524 for limit in [0u32, 101] {
525 let query = top_five_by_avg_grade().with_limit(limit);
526 assert!(
527 assert_ranked_shape(&query, platform_version()).is_err(),
528 "LIMIT {limit} is outside 1..=100 and must be rejected, not clamped"
529 );
530 }
531 }
532
533 /// The V0 wire has no `offset` field at all, so encoding one there
534 /// has to fail loudly. Dropping it would page from rank 0 while the
535 /// caller believed they had skipped ahead.
536 #[test]
537 fn a_v0_encode_refuses_to_silently_drop_an_offset() {
538 let mut v0_version = PlatformVersion::latest().clone();
539 v0_version
540 .drive_abci
541 .query
542 .document_query
543 .default_current_version = 0;
544
545 let query = DocumentQuery::new(contract(), "niceDocument")
546 .expect("doctype exists")
547 .with_offset(4);
548
549 let err = GetDocumentsRequest::try_from_platform_versioned(query, &v0_version)
550 .expect_err("V0 cannot carry an offset");
551 assert!(
552 format!("{err}").contains("offset"),
553 "the refusal must name the field that cannot be carried, got: {err}"
554 );
555 }
556}