Skip to main content

Module document_ranked_entries

Module document_ranked_entries 

Source
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);