drive/query/drive_document_having_query/mod.rs
1//! Types and module structure for the **boolean-`HAVING` range** document
2//! query — `SELECT <agg> GROUP BY <prop> HAVING <agg> <op> <value>
3//! [ORDER BY <agg> ASC|DESC] LIMIT n`.
4//!
5//! A having-range query answers "which groups' aggregate falls inside a
6//! value bound?" ("hashtags with more than 100 posts") in `O(log n + k)`
7//! with a proof, by range-reading the same per-axis *secondary* Merk the
8//! ranked query walks (grovedb PR #657): the secondary is keyed by
9//! `(sort_key ‖ group_key)` with an order-preserving sort-key encoding,
10//! so an inclusive numeric bound on the aggregate is a contiguous byte
11//! range in the secondary's keyspace. The same contract opt-in applies —
12//! `rankedCountable` / `rankedSummable` / `rankedAverageable` (meta schema
13//! v3 / PV14) — and a `HAVING` on an axis the index does not declare is
14//! rejected, because serving it would mean walking every group.
15//!
16//! The implementation mirrors [`super::drive_document_ranked_query`]
17//! sibling-for-sibling and reuses its axis / entry / pagination types
18//! ([`RankedAxis`], [`RankedEntry`], [`RankedEntryValue`],
19//! [`super::RankedPaginationInputs`]) and its covering-index picker —
20//! both surfaces read the same tree, so sharing the resolution logic is
21//! what keeps them provably about the same subtree:
22//! - [`mode_detection`] — request-shape validation + the versioned
23//! `(select, group_by, having, order_by, limit)` →
24//! [`DocumentHavingMode`] resolution, including the operator →
25//! inclusive-bounds translation.
26//! - [`execute_range`] — the two executors on
27//! [`DriveDocumentHavingQuery`] (no-proof read, proof generation).
28//! - [`executors`] — the `impl Drive` wrappers the dispatcher calls.
29//! - [`drive_dispatcher`] — [`DocumentHavingRequest`] /
30//! [`DocumentHavingResponse`] and
31//! [`crate::drive::Drive::execute_document_having_request`].
32//! - [`tests`] (cfg `server` + `test`) — unit + integration tests.
33//!
34//! ## What makes this query shape different from ranked
35//!
36//! Ranked addresses groups by **rank position** (`k` best, starting at
37//! rank `offset`); having-range addresses them by **value bound**
38//! (`aggregate ∈ [lo, hi]`). Three consequences:
39//!
40//! 1. **The bound is part of the proof contract.** Prover and verifier
41//! build the same `Bounded` axis `PathQuery` from the request's
42//! inclusive bounds ([`AxisRangeBounds::inclusive_bounds_i128`]),
43//! and grovedb re-executes the proof against that traversal — so the
44//! two sides share one bounds-to-query translation, exactly as they
45//! share the grove path. Completeness comes from the Merk range
46//! proof: the boundary commitments show no in-range group was
47//! omitted.
48//! 2. **No `OFFSET`, no `start_at` — and no full pagination.** The
49//! range primitives take a limit but no skip, and a request carrying
50//! either knob is rejected loudly. A page cut at `limit` can only be
51//! continued past **distinct** aggregate values, by tightening the
52//! bound past the last value seen; a cut that lands **inside a tie**
53//! (several groups sharing the boundary aggregate) cannot be
54//! continued at all — moving the threshold past the tied value skips
55//! the uncollected tied groups, and keeping it returns the same
56//! page. Enumerating through a tie wider than [`MAX_HAVING_LIMIT`]
57//! needs a cursor on the `(sort_key ‖ group_key)` composite
58//! keyspace, a future capability; until then, size `limit` above the
59//! widest tie the data can produce, or accept the cut.
60//! 3. **Entry order is axis order in the walk direction.** Ascending by
61//! default (`ORDER BY` is optional here — the bound, not the
62//! ordering, is the point of the query); an explicit `ORDER BY` on
63//! the selected aggregate flips the walk. Ties break by group key in
64//! the direction of the walk, same as ranked.
65
66#[cfg(any(feature = "server", feature = "verify"))]
67use dpp::data_contract::document_type::{DocumentTypeRef, Index};
68#[cfg(any(feature = "server", feature = "verify"))]
69use dpp::version::PlatformVersion;
70#[cfg(any(feature = "server", feature = "verify"))]
71use std::collections::BTreeMap;
72
73#[cfg(any(feature = "server", feature = "verify"))]
74use super::drive_document_ranked_query::index_picker::{
75 encode_prefix_branches, find_ranked_index_for_axis, no_covering_index_message,
76};
77#[cfg(any(feature = "server", feature = "verify"))]
78use super::drive_document_ranked_query::{
79 path::indexed_property_name_tree_path_for_index, PrefixPin, RankedAxis,
80};
81#[cfg(any(feature = "server", feature = "verify"))]
82use crate::error::drive::DriveError;
83#[cfg(any(feature = "server", feature = "verify"))]
84use crate::error::query::QuerySyntaxError;
85#[cfg(any(feature = "server", feature = "verify"))]
86use crate::error::Error;
87#[cfg(any(feature = "server", feature = "verify"))]
88use crate::query::ResolvedTimeRange;
89#[cfg(any(feature = "server", feature = "verify"))]
90use grovedb::element::indexed::{encode_avg_sort_key, encode_count_sort_key, encode_sum_sort_key};
91#[cfg(any(feature = "server", feature = "verify"))]
92use grovedb::Query;
93
94#[cfg(any(feature = "server", feature = "verify"))]
95pub mod mode_detection;
96
97// Server-side execution paths.
98#[cfg(feature = "server")]
99pub mod drive_dispatcher;
100#[cfg(feature = "server")]
101pub mod execute_range;
102#[cfg(feature = "server")]
103pub mod executors;
104
105#[cfg(feature = "server")]
106pub use drive_dispatcher::{DocumentHavingRequest, DocumentHavingResponse};
107
108#[cfg(all(feature = "server", test))]
109mod tests;
110
111/// Hard ceiling on a having-range request's `LIMIT`. Same value and same
112/// rationale as [`super::drive_document_ranked_query::MAX_RANKED_LIMIT`]:
113/// the proof commits one secondary entry per returned group, so proof
114/// bytes grow linearly in the limit, and the ceiling is a hard rejection
115/// rather than a clamp because the limit is part of the traversal the
116/// verifier re-executes: a server-side clamp would truncate the walk
117/// and fail coverage under the client's own reconstruction.
118#[cfg(any(feature = "server", feature = "verify"))]
119pub const MAX_HAVING_LIMIT: u16 = 100;
120
121/// Inclusive numeric bounds on one axis of an indexed tree — the resolved
122/// form of a `HAVING <aggregate> <operator> <value>` clause.
123///
124/// One variant per axis because the three axes have three value types
125/// (`u64` count, `i64` sum, `i128` fixed-point average) and the bound
126/// arithmetic (operator translation, successor/predecessor at exclusive
127/// bounds) must be exact in the axis's own domain. Both bounds are
128/// **inclusive**; the operator translation in
129/// [`mode_detection`] normalizes every supported operator to this form,
130/// rejecting translations that would overflow (`> MAX`) or invert
131/// (`lo > hi`) instead of serving a silently-empty range.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133#[cfg(any(feature = "server", feature = "verify"))]
134pub enum AxisRangeBounds {
135 /// `COUNT(*) ∈ [lo, hi]`.
136 Count {
137 /// Inclusive lower bound.
138 lo: u64,
139 /// Inclusive upper bound.
140 hi: u64,
141 },
142 /// `SUM(field) ∈ [lo, hi]`.
143 Sum {
144 /// Inclusive lower bound.
145 lo: i64,
146 /// Inclusive upper bound.
147 hi: i64,
148 },
149 /// `AVG(field) ∈ [lo, hi]`, in the fixed-point domain described on
150 /// [`super::drive_document_ranked_query::RANKED_AVG_SCALE`].
151 Avg {
152 /// Inclusive lower bound (fixed point).
153 lo: i128,
154 /// Inclusive upper bound (fixed point).
155 hi: i128,
156 },
157}
158
159#[cfg(any(feature = "server", feature = "verify"))]
160impl AxisRangeBounds {
161 /// The axis these bounds constrain.
162 pub fn axis(&self) -> RankedAxis {
163 match self {
164 AxisRangeBounds::Count { .. } => RankedAxis::Count,
165 AxisRangeBounds::Sum { .. } => RankedAxis::Sum,
166 AxisRangeBounds::Avg { .. } => RankedAxis::Avg,
167 }
168 }
169
170 /// The bounds as inclusive `i128` values in the axis's own domain —
171 /// the form `AxisTraversal::Bounded` carries in the unified
172 /// `PathQuery`. Count and sum widen losslessly; avg is already
173 /// `i128` fixed point.
174 pub fn inclusive_bounds_i128(&self) -> (i128, i128) {
175 match *self {
176 AxisRangeBounds::Count { lo, hi } => (lo as i128, hi as i128),
177 AxisRangeBounds::Sum { lo, hi } => (lo as i128, hi as i128),
178 AxisRangeBounds::Avg { lo, hi } => (lo, hi),
179 }
180 }
181
182 /// The bounds as a byte range over the axis secondary's keyspace:
183 /// `(inclusive_lower, exclusive_upper)`, with `None` for an upper
184 /// bound at the axis's type maximum (no representable successor —
185 /// the range is unbounded above).
186 ///
187 /// Secondary keys are `(sort_key ‖ group_key)` with order-preserving
188 /// fixed-width sort keys, so the inclusive numeric range `[lo, hi]`
189 /// is exactly the byte range `[encode(lo), encode(hi + 1))`: the
190 /// exclusive upper at the *next* sort key admits every group-key
191 /// suffix under `hi` and nothing above it. This mirrors — and must
192 /// stay identical to — the bound construction inside grovedb's
193 /// `indexed_*_range` read primitives, so the no-proof read and the
194 /// proved read answer the same question.
195 ///
196 /// The `+ 1` cannot overflow: the `hi == MAX` case returns `None`
197 /// first.
198 pub fn secondary_key_bounds(&self) -> (Vec<u8>, Option<Vec<u8>>) {
199 match *self {
200 AxisRangeBounds::Count { lo, hi } => (
201 encode_count_sort_key(lo).to_vec(),
202 (hi != u64::MAX).then(|| encode_count_sort_key(hi + 1).to_vec()),
203 ),
204 AxisRangeBounds::Sum { lo, hi } => (
205 encode_sum_sort_key(lo).to_vec(),
206 (hi != i64::MAX).then(|| encode_sum_sort_key(hi + 1).to_vec()),
207 ),
208 AxisRangeBounds::Avg { lo, hi } => (
209 encode_avg_sort_key(lo).to_vec(),
210 (hi != i128::MAX).then(|| encode_avg_sort_key(hi + 1).to_vec()),
211 ),
212 }
213 }
214
215 /// The Merk query over the axis secondary that reads exactly these
216 /// bounds, walking in the requested direction.
217 ///
218 /// This is the **prover/verifier-agreement artifact** of the having
219 /// surface: grovedb's range-proof envelope is generated against this
220 /// query and verified against the verifier's own reconstruction of
221 /// it, so both sides must build it from the same bounds through this
222 /// one function — a divergence surfaces as a failed verification,
223 /// not a wrong answer.
224 pub fn merk_query(&self, descending: bool) -> Query {
225 let (lower, upper) = self.secondary_key_bounds();
226 let mut query = Query::new_with_direction(!descending);
227 match upper {
228 Some(upper) => query.insert_range(lower..upper),
229 None => query.insert_range_from(lower..),
230 }
231 query
232 }
233}
234
235/// The resolved shape of a having-range request: the bounds (which carry
236/// the axis), the walk direction, the limit, and the `(group property,
237/// aggregate field)` pair the index picker needs.
238///
239/// Produced by [`mode_detection::detect_having_mode`]. Parallels
240/// [`super::drive_document_ranked_query::DocumentRankedMode`].
241///
242/// Not `Eq`: the prefix pins carry [`Value`]s, whose float variant
243/// keeps the type at `PartialEq`.
244#[derive(Debug, Clone, PartialEq)]
245#[cfg(any(feature = "server", feature = "verify"))]
246pub struct DocumentHavingMode {
247 /// Inclusive bounds on the aggregate, in the axis's own domain.
248 pub bounds: AxisRangeBounds,
249 /// Walk direction: `true` reads matching groups from the largest
250 /// aggregate down. Defaults to `false` (ascending) when the request
251 /// carries no `ORDER BY`.
252 pub descending: bool,
253 /// Maximum number of matching groups to return —
254 /// `1 ..= MAX_HAVING_LIMIT`, required.
255 pub limit: u16,
256 /// The single `GROUP BY` property; must be the covering ranked
257 /// index's **last** property.
258 pub group_by_property: String,
259 /// The field the aggregate applies to. Empty for `COUNT(*)`; the
260 /// index's `summable` property for `SUM` / `AVG`.
261 pub aggregate_field: String,
262 /// The `where` prefix pins — one [`PrefixPin`] per clause, exactly
263 /// one per leading property of the covering compound index, in
264 /// request order (the resolver re-orders them into index order when
265 /// it encodes the path). A pin normally carries one value (an `==`
266 /// clause); at most one pin carries several (the single permitted
267 /// branching `IN`, whose elements fan the bound out across one
268 /// prefix branch each). Empty for the single-property form.
269 pub prefix_pins: Vec<PrefixPin>,
270}
271
272/// A resolved having-range query. Shared by the prover and the verifier —
273/// both build the grove path through
274/// [`DriveDocumentHavingQuery::indexed_property_name_tree_path`] and the
275/// secondary query through [`AxisRangeBounds::merk_query`], so the two
276/// cannot drift on which subtree or which range the proof is about.
277#[derive(Debug, Clone)]
278#[cfg(any(feature = "server", feature = "verify"))]
279pub struct DriveDocumentHavingQuery<'a> {
280 /// The document type being filtered.
281 pub document_type: DocumentTypeRef<'a>,
282 /// The contract id (32 bytes). Separate from `document_type` so the
283 /// verifier can build the query without the full contract.
284 pub contract_id: [u8; 32],
285 /// The document type name — a path segment.
286 pub document_type_name: String,
287 /// The covering ranked index. Its **last** property is the `GROUP
288 /// BY` property and the final path segment; any leading properties
289 /// are pinned by [`Self::prefix_branches`].
290 pub index: &'a Index,
291 /// The prefix **branches** — one inner `Vec<Vec<u8>>` of encoded
292 /// path segments per branch, in index-property order; always at
293 /// least one branch, several exactly when the request carried a
294 /// multi-element `IN` pin. Part of the prover/verifier agreement
295 /// exactly as on the ranked surface. Produced by
296 /// [`super::drive_document_ranked_query::index_picker::encode_prefix_branches`]
297 /// — crate-private so the resolver is the only public constructor
298 /// and the encoder's invariants hold on every externally obtainable
299 /// value.
300 pub(crate) prefix_branches: Vec<Vec<Vec<u8>>>,
301 /// Inclusive bounds on the aggregate. Carry the axis; the index must
302 /// declare the matching `ranked_*` flag.
303 pub bounds: AxisRangeBounds,
304 /// `true` walks the secondary from the largest matching aggregate
305 /// down. Tie ordering is by group key in the direction of the walk,
306 /// exactly as on the ranked surface.
307 pub descending: bool,
308 /// Maximum number of matching groups to return. Fewer entries come
309 /// back when fewer groups fall inside the bounds; that is not an
310 /// error. **More matching groups than `limit` are silently cut at
311 /// `limit`** — the walk stops, and nothing marks the cut. A caller
312 /// can continue past *distinct* aggregate values by tightening the
313 /// bound, but a cut inside a **tie** cannot be continued (see the
314 /// module docs): groups tied at the boundary aggregate that fell
315 /// past the limit stay unreachable until a composite-key cursor
316 /// exists, so size the limit above the widest expected tie.
317 pub limit: u16,
318}
319
320#[cfg(any(feature = "server", feature = "verify"))]
321impl DriveDocumentHavingQuery<'_> {
322 /// The resolved prefix branches, in canonical order — one per `IN`
323 /// element (a single branch without an `IN`). Read-only: the field is
324 /// crate-private so the resolver's encoder invariants cannot be
325 /// bypassed by construction or mutation.
326 pub fn prefix_branches(&self) -> &[Vec<Vec<u8>>] {
327 &self.prefix_branches
328 }
329}
330
331#[cfg(any(feature = "server", feature = "verify"))]
332impl DriveDocumentHavingQuery<'_> {
333 /// Path of one branch's terminal property-name tree — identical to
334 /// the ranked surface's path (including the pinned-prefix segments
335 /// of a compound index), because both read the same indexed
336 /// tree(s). See
337 /// [`DriveDocumentRankedQuery::indexed_property_name_tree_path`](super::drive_document_ranked_query::DriveDocumentRankedQuery::indexed_property_name_tree_path).
338 pub fn indexed_property_name_tree_path(&self, branch: usize) -> Result<Vec<Vec<u8>>, Error> {
339 let prefix_values =
340 self.prefix_branches
341 .get(branch)
342 .ok_or(Error::Drive(DriveError::NotSupported(
343 "ranked and having-range queries addressed a prefix branch outside the \
344 query's resolved branch set",
345 )))?;
346 indexed_property_name_tree_path_for_index(
347 &self.contract_id,
348 &self.document_type_name,
349 self.index,
350 prefix_values,
351 )
352 }
353}
354
355/// Resolve a validated [`DocumentHavingMode`] against a document type's
356/// indexes into the executable [`DriveDocumentHavingQuery`]: pick the
357/// covering index (shared with the ranked surface — both read the same
358/// indexed tree), encode the prefix pins into prefix **branches** (one
359/// branch for all-`==` pins, one branch per element of the single
360/// permitted `IN`), and assemble the query.
361///
362/// The **one** resolution path for the having surface, mirroring
363/// [`super::drive_document_ranked_query::index_picker::resolve_ranked_query_for_mode`]:
364/// the server's executors and the SDK's proof helpers both call it, so a
365/// proof and an unproven read (and the client's verification) are about
366/// the same subtree by construction.
367///
368/// `indexes` is threaded in separately for the same lifetime reason as
369/// the ranked resolver: the returned query's `&'a Index` must outlive
370/// this frame. Callers pass `document_type.indexes()`.
371#[cfg(any(feature = "server", feature = "verify"))]
372pub fn resolve_having_query_for_mode<'a>(
373 contract_id: [u8; 32],
374 document_type: DocumentTypeRef<'a>,
375 document_type_name: String,
376 indexes: &'a BTreeMap<String, Index>,
377 mode: &DocumentHavingMode,
378 resolved_time_ranges: &[ResolvedTimeRange],
379 platform_version: &PlatformVersion,
380) -> Result<DriveDocumentHavingQuery<'a>, Error> {
381 let axis = mode.bounds.axis();
382 let pin_fields: Vec<String> = mode
383 .prefix_pins
384 .iter()
385 .map(|pin| pin.field.clone())
386 .collect();
387 let index = find_ranked_index_for_axis(
388 indexes,
389 &mode.group_by_property,
390 &pin_fields,
391 axis,
392 &mode.aggregate_field,
393 resolved_time_ranges,
394 )
395 .ok_or_else(|| {
396 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
397 no_covering_index_message(
398 "having-range",
399 axis,
400 &mode.group_by_property,
401 &mode.prefix_pins,
402 &mode.aggregate_field,
403 ),
404 ))
405 })?;
406 let prefix_branches =
407 encode_prefix_branches(document_type, index, &mode.prefix_pins, platform_version)?;
408 Ok(DriveDocumentHavingQuery {
409 document_type,
410 contract_id,
411 document_type_name,
412 index,
413 prefix_branches,
414 bounds: mode.bounds,
415 descending: mode.descending,
416 limit: mode.limit,
417 })
418}