Expand description
FromProof + Fetch for [DocumentRankedEntries] — the
ranked (GROUP BY … ORDER BY <aggregate> LIMIT n [OFFSET m])
view of the unified getDocuments endpoint.
A ranked query answers “which n groups score highest (or lowest)
on an aggregate?” — top 5 restaurants by average grade — in
O(log n + k), with a proof. It reads a pre-sorted per-axis
secondary Merk maintained by the write path (grovedb PR #657)
rather than walking value trees, which is why it is cheap and why
its shape is so constrained.
Per-request resolution (which axis, which direction, how many
groups, how many ranks to skip, which index covers them) lives in
[super::ranked_proof_helpers]; this module is the thin
Fetch-side wrapper.
§Request shape
Exactly one aggregate select, exactly one group_by property,
exactly one ORDER BY clause naming that select’s aggregate, and a
LIMIT — plus an optional OFFSET. where clauses are pins on a
covering compound ranked index’s leading properties (one per leading
property, selecting which prefix’s own ranking the walk reads) —
absent for a single-property index. Each pin is an equality, except
that at most one may be an IN of 2..=10 distinct elements: one
walk per element, merged by (aggregate, encoded pin, group key),
with each merged entry carrying the encoded branch segment in
in_key (unset on single-branch responses; a single-element IN
normalizes to the equality pin). A non-zero OFFSET cannot combine
with the IN, nor can a null pin on another property. No
having, no start_at: each of those is rejected rather than
ignored, on both sides, because a ranked walk cannot honour them and
silently answering a different question is worse than an error.
DocumentQuery::order_by_selected_aggregate builds the ordering
clause, deriving the ordered field from the select through
rs-drive’s own key mapping (SUM(f) / AVG(f) are named by f,
COUNT(*) by the $count sentinel), so there is no way to name it
wrong by hand.
§Contract prerequisites
The index must opt in with rankedCountable / rankedSummable /
rankedAverageable (meta-schema v3, protocol version 14+). The
index may be single-property (group_by its property, no where)
or compound (group_by its trailing property, pin every leading one
— equality pins, at most one of them an IN, as above). Against a
protocol-version-13
node the request is refused — v13’s query table has no ranked path
and rejects the ordering as Unsupported. That is the intended
activation gate, not a bug: a v13 node and a v14 node must disagree
here and nowhere else, which is what lets a mixed-version network
run through the upgrade.
§Ranks, offsets, and the empty ranking
The fetch result carries
starting_rank
alongside the entries: entry i is the group at rank
starting_rank + i, which is what makes LIMIT 1 OFFSET 4
meaningful as “the 5th best” rather than “some entry”. On the proved
path that number is re-derived from the proof’s counted subtree
commitments, not taken from the node.
An offset past the end of the ranking is a legitimate, provable
answer: no entries, and starting_rank equal to the ranking’s whole
attested population. Empty rankings prove too — grovedb’s
paginated prover emits a guaranteed-empty range against an empty
axis secondary rather than refusing — so querying a freshly
registered contract with prove = true returns an empty page rather
than an error, and the proved and unproven paths agree.
§Reading the values
Entries come back in ranking order; do not re-sort. Averages
are fixed-point integers: divide by
RANKED_AVG_SCALE — a
re-export of grovedb’s own constant, which moved from 10^15 to
10^19 before release, so never hardcode the literal — or call
RankedEntryValue::as_f64,
which does that division for you.
How exact the average is depends on the path. Fetched with a
proof (the default, and what the examples below do), the fixed point
is the integer grovedb committed to and ranked on. Fetched without
one, the wire carries only an f64 of the average and the SDK
re-scales it back, so the digits past f64’s ~15–16 significant
decimals are reconstruction noise — fine to render, not something to
compare for equality. Ranking order is exact either way.
§Example: top 5 restaurants by average grade
SELECT AVG(grade) GROUP BY restaurantId ORDER BY avg(grade) DESC LIMIT 5
use dash_sdk::{Sdk, platform::{DataContract, DocumentQuery, Fetch, Identifier}};
use dash_sdk::drive::query::SelectProjection;
use dash_sdk::platform::documents::document_query::RankingDirection;
use drive_proof_verifier::{DocumentRankedEntries, RankedEntryValue, RANKED_AVG_SCALE};
use futures::executor::block_on;
let sdk = Sdk::new_mock();
let contract = block_on(DataContract::fetch(&sdk, Identifier::new(RESTAURANTS_CONTRACT_ID)))
.expect("fetch contract")
.expect("contract exists");
let query = DocumentQuery::new(contract, "review")
.expect("document type exists")
.with_select(SelectProjection::avg("grade"))
.with_group_by("restaurantId")
.order_by_selected_aggregate(RankingDirection::Descending)
.with_limit(5);
let ranked = block_on(DocumentRankedEntries::fetch(&sdk, query))
.expect("fetch succeeds")
.expect("a well-formed ranked query always answers");
// Entry order IS the ranking order — best first.
for (offset, entry) in ranked.entries.iter().enumerate() {
let rank = ranked.starting_rank + offset as u64;
let restaurant = String::from_utf8_lossy(&entry.key);
if let RankedEntryValue::AvgFixedPoint(fixed_point) = entry.value {
// `as_f64()` is this same division, for when you only want
// to display the number:
// let average = entry.value.as_f64();
// Keep the `fixed_point` itself when you need the exact
// integer the proof committed to — comparing two groups,
// reproducing the ranking, storing it. On a `prove = false`
// fetch that integer is a reconstruction from the wire's
// double, so it is only as precise as an `f64`.
let average = (fixed_point as f64) / (RANKED_AVG_SCALE as f64);
println!("#{}: {restaurant}: {average}", rank + 1);
}
}§Example: the 5th-best restaurant
SELECT AVG(grade) GROUP BY restaurantId ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4
let query = DocumentQuery::new(contract, "review")?
.with_select(SelectProjection::avg("grade"))
.with_group_by("restaurantId")
.order_by_selected_aggregate(RankingDirection::Descending)
.with_limit(1)
.with_offset(4);