Expand description
FromProof + Fetch for [DocumentHavingEntries] — the
having-range (GROUP BY … HAVING <aggregate> <op> <value> LIMIT n) view of the unified getDocuments endpoint.
A having-range query answers “which groups’ aggregate falls inside a
value bound?” — hashtags with more than 100 posts — in
O(log n + k), with a proof whose Merk range boundaries also attest
completeness: a node cannot silently omit a matching group. It
reads the same pre-sorted per-axis secondary Merk the ranked
surface walks (grovedb PR #657), addressed by value bound instead of
by rank.
Per-request resolution (which axis, which bounds the operator
translates to, which index covers them) lives in
[super::having_proof_helpers]; this module is the thin
Fetch-side wrapper.
§Request shape
Exactly one aggregate select, exactly one group_by property,
exactly one having clause bounding the selected aggregate with
a contiguous-range operator (=, >, >=, <, <=, BETWEEN* —
!= and IN are rejected), and a LIMIT. ORDER BY is optional:
omitted means ascending by the aggregate; naming the selected
aggregate sets the direction. where clauses are pins on a covering
compound ranked index’s leading properties (one per leading
property, selecting which prefix’s groups the bound 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: the
bound fans out across one prefix branch per element and merges,
entries carrying the encoded branch segment in in_key (unset on
single-branch responses; a single-element IN normalizes to the
equality pin; a null pin on another property cannot combine with
the IN). No offset, no start_at.
§Contract prerequisites
Same as the ranked surface: 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).
Against a pre-v14 node the request is refused with “HAVING clause
is not yet implemented” — the intended activation gate.
§Reading the result
Entries come back in axis order in the walk direction; do not
re-sort. Fewer than n entries means fewer groups matched.
Exactly n may mean the match set was cut at the limit.
Tightening the bound past the last aggregate value seen continues
past distinct values only: a cut inside a tie (several groups
sharing the boundary aggregate) cannot be continued — the tied
groups past the limit stay unreachable until a composite-key cursor
exists — so size the limit above the widest expected tie. Averages
are fixed-point integers, exact on this (proved) path; see the
ranked module’s notes, which apply verbatim.
§Example: hashtags with more than 100 posts
SELECT COUNT(*) GROUP BY hashtag HAVING $count > 100 ORDER BY $count DESC LIMIT 100
use dash_sdk::{Sdk, platform::{DataContract, DocumentQuery, Fetch, Identifier}};
use dash_sdk::drive::query::{
HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator,
HavingRightOperand, SelectProjection,
};
use dash_sdk::platform::documents::document_query::RankingDirection;
use dpp::platform_value::Value;
use drive_proof_verifier::DocumentHavingEntries;
use futures::executor::block_on;
let sdk = Sdk::new_mock();
let contract = block_on(DataContract::fetch(&sdk, Identifier::new(POSTS_CONTRACT_ID)))
.expect("fetch contract")
.expect("contract exists");
let query = DocumentQuery::new(contract, "post")
.expect("document type exists")
.with_select(SelectProjection::count_star())
.with_group_by("hashtag")
.with_having(vec![HavingClause {
aggregate: HavingAggregate {
function: HavingAggregateFunction::Count,
field: String::new(),
},
operator: HavingOperator::GreaterThan,
right: HavingRightOperand::Value(Value::U64(100)),
}])
.order_by_selected_aggregate(RankingDirection::Descending)
.with_limit(100);
let matching = block_on(DocumentHavingEntries::fetch(&sdk, query))
.expect("fetch succeeds")
.expect("a well-formed having query always answers");
for entry in &matching.entries {
let hashtag = String::from_utf8_lossy(&entry.key);
println!("#{hashtag}: {} posts", entry.value.as_f64());
}