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 grovedb::element::indexed::{encode_avg_sort_key, encode_count_sort_key, encode_sum_sort_key};
89#[cfg(any(feature = "server", feature = "verify"))]
90use grovedb::Query;
91
92#[cfg(any(feature = "server", feature = "verify"))]
93pub mod mode_detection;
94
95// Server-side execution paths.
96#[cfg(feature = "server")]
97pub mod drive_dispatcher;
98#[cfg(feature = "server")]
99pub mod execute_range;
100#[cfg(feature = "server")]
101pub mod executors;
102
103#[cfg(feature = "server")]
104pub use drive_dispatcher::{DocumentHavingRequest, DocumentHavingResponse};
105
106#[cfg(all(feature = "server", test))]
107mod tests;
108
109/// Hard ceiling on a having-range request's `LIMIT`. Same value and same
110/// rationale as [`super::drive_document_ranked_query::MAX_RANKED_LIMIT`]:
111/// the proof commits one secondary entry per returned group, so proof
112/// bytes grow linearly in the limit, and the ceiling is a hard rejection
113/// rather than a clamp because the limit is part of the traversal the
114/// verifier re-executes: a server-side clamp would truncate the walk
115/// and fail coverage under the client's own reconstruction.
116#[cfg(any(feature = "server", feature = "verify"))]
117pub const MAX_HAVING_LIMIT: u16 = 100;
118
119/// Inclusive numeric bounds on one axis of an indexed tree — the resolved
120/// form of a `HAVING <aggregate> <operator> <value>` clause.
121///
122/// One variant per axis because the three axes have three value types
123/// (`u64` count, `i64` sum, `i128` fixed-point average) and the bound
124/// arithmetic (operator translation, successor/predecessor at exclusive
125/// bounds) must be exact in the axis's own domain. Both bounds are
126/// **inclusive**; the operator translation in
127/// [`mode_detection`] normalizes every supported operator to this form,
128/// rejecting translations that would overflow (`> MAX`) or invert
129/// (`lo > hi`) instead of serving a silently-empty range.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131#[cfg(any(feature = "server", feature = "verify"))]
132pub enum AxisRangeBounds {
133 /// `COUNT(*) ∈ [lo, hi]`.
134 Count {
135 /// Inclusive lower bound.
136 lo: u64,
137 /// Inclusive upper bound.
138 hi: u64,
139 },
140 /// `SUM(field) ∈ [lo, hi]`.
141 Sum {
142 /// Inclusive lower bound.
143 lo: i64,
144 /// Inclusive upper bound.
145 hi: i64,
146 },
147 /// `AVG(field) ∈ [lo, hi]`, in the fixed-point domain described on
148 /// [`super::drive_document_ranked_query::RANKED_AVG_SCALE`].
149 Avg {
150 /// Inclusive lower bound (fixed point).
151 lo: i128,
152 /// Inclusive upper bound (fixed point).
153 hi: i128,
154 },
155}
156
157#[cfg(any(feature = "server", feature = "verify"))]
158impl AxisRangeBounds {
159 /// The axis these bounds constrain.
160 pub fn axis(&self) -> RankedAxis {
161 match self {
162 AxisRangeBounds::Count { .. } => RankedAxis::Count,
163 AxisRangeBounds::Sum { .. } => RankedAxis::Sum,
164 AxisRangeBounds::Avg { .. } => RankedAxis::Avg,
165 }
166 }
167
168 /// The bounds as inclusive `i128` values in the axis's own domain —
169 /// the form `AxisTraversal::Bounded` carries in the unified
170 /// `PathQuery`. Count and sum widen losslessly; avg is already
171 /// `i128` fixed point.
172 pub fn inclusive_bounds_i128(&self) -> (i128, i128) {
173 match *self {
174 AxisRangeBounds::Count { lo, hi } => (lo as i128, hi as i128),
175 AxisRangeBounds::Sum { lo, hi } => (lo as i128, hi as i128),
176 AxisRangeBounds::Avg { lo, hi } => (lo, hi),
177 }
178 }
179
180 /// The bounds as a byte range over the axis secondary's keyspace:
181 /// `(inclusive_lower, exclusive_upper)`, with `None` for an upper
182 /// bound at the axis's type maximum (no representable successor —
183 /// the range is unbounded above).
184 ///
185 /// Secondary keys are `(sort_key ‖ group_key)` with order-preserving
186 /// fixed-width sort keys, so the inclusive numeric range `[lo, hi]`
187 /// is exactly the byte range `[encode(lo), encode(hi + 1))`: the
188 /// exclusive upper at the *next* sort key admits every group-key
189 /// suffix under `hi` and nothing above it. This mirrors — and must
190 /// stay identical to — the bound construction inside grovedb's
191 /// `indexed_*_range` read primitives, so the no-proof read and the
192 /// proved read answer the same question.
193 ///
194 /// The `+ 1` cannot overflow: the `hi == MAX` case returns `None`
195 /// first.
196 pub fn secondary_key_bounds(&self) -> (Vec<u8>, Option<Vec<u8>>) {
197 match *self {
198 AxisRangeBounds::Count { lo, hi } => (
199 encode_count_sort_key(lo).to_vec(),
200 (hi != u64::MAX).then(|| encode_count_sort_key(hi + 1).to_vec()),
201 ),
202 AxisRangeBounds::Sum { lo, hi } => (
203 encode_sum_sort_key(lo).to_vec(),
204 (hi != i64::MAX).then(|| encode_sum_sort_key(hi + 1).to_vec()),
205 ),
206 AxisRangeBounds::Avg { lo, hi } => (
207 encode_avg_sort_key(lo).to_vec(),
208 (hi != i128::MAX).then(|| encode_avg_sort_key(hi + 1).to_vec()),
209 ),
210 }
211 }
212
213 /// The Merk query over the axis secondary that reads exactly these
214 /// bounds, walking in the requested direction.
215 ///
216 /// This is the **prover/verifier-agreement artifact** of the having
217 /// surface: grovedb's range-proof envelope is generated against this
218 /// query and verified against the verifier's own reconstruction of
219 /// it, so both sides must build it from the same bounds through this
220 /// one function — a divergence surfaces as a failed verification,
221 /// not a wrong answer.
222 pub fn merk_query(&self, descending: bool) -> Query {
223 let (lower, upper) = self.secondary_key_bounds();
224 let mut query = Query::new_with_direction(!descending);
225 match upper {
226 Some(upper) => query.insert_range(lower..upper),
227 None => query.insert_range_from(lower..),
228 }
229 query
230 }
231}
232
233/// The resolved shape of a having-range request: the bounds (which carry
234/// the axis), the walk direction, the limit, and the `(group property,
235/// aggregate field)` pair the index picker needs.
236///
237/// Produced by [`mode_detection::detect_having_mode`]. Parallels
238/// [`super::drive_document_ranked_query::DocumentRankedMode`].
239///
240/// Not `Eq`: the prefix pins carry [`Value`]s, whose float variant
241/// keeps the type at `PartialEq`.
242#[derive(Debug, Clone, PartialEq)]
243#[cfg(any(feature = "server", feature = "verify"))]
244pub struct DocumentHavingMode {
245 /// Inclusive bounds on the aggregate, in the axis's own domain.
246 pub bounds: AxisRangeBounds,
247 /// Walk direction: `true` reads matching groups from the largest
248 /// aggregate down. Defaults to `false` (ascending) when the request
249 /// carries no `ORDER BY`.
250 pub descending: bool,
251 /// Maximum number of matching groups to return —
252 /// `1 ..= MAX_HAVING_LIMIT`, required.
253 pub limit: u16,
254 /// The single `GROUP BY` property; must be the covering ranked
255 /// index's **last** property.
256 pub group_by_property: String,
257 /// The field the aggregate applies to. Empty for `COUNT(*)`; the
258 /// index's `summable` property for `SUM` / `AVG`.
259 pub aggregate_field: String,
260 /// The `where` prefix pins — one [`PrefixPin`] per clause, exactly
261 /// one per leading property of the covering compound index, in
262 /// request order (the resolver re-orders them into index order when
263 /// it encodes the path). A pin normally carries one value (an `==`
264 /// clause); at most one pin carries several (the single permitted
265 /// branching `IN`, whose elements fan the bound out across one
266 /// prefix branch each). Empty for the single-property form.
267 pub prefix_pins: Vec<PrefixPin>,
268}
269
270/// A resolved having-range query. Shared by the prover and the verifier —
271/// both build the grove path through
272/// [`DriveDocumentHavingQuery::indexed_property_name_tree_path`] and the
273/// secondary query through [`AxisRangeBounds::merk_query`], so the two
274/// cannot drift on which subtree or which range the proof is about.
275#[derive(Debug, Clone)]
276#[cfg(any(feature = "server", feature = "verify"))]
277pub struct DriveDocumentHavingQuery<'a> {
278 /// The document type being filtered.
279 pub document_type: DocumentTypeRef<'a>,
280 /// The contract id (32 bytes). Separate from `document_type` so the
281 /// verifier can build the query without the full contract.
282 pub contract_id: [u8; 32],
283 /// The document type name — a path segment.
284 pub document_type_name: String,
285 /// The covering ranked index. Its **last** property is the `GROUP
286 /// BY` property and the final path segment; any leading properties
287 /// are pinned by [`Self::prefix_branches`].
288 pub index: &'a Index,
289 /// The prefix **branches** — one inner `Vec<Vec<u8>>` of encoded
290 /// path segments per branch, in index-property order; always at
291 /// least one branch, several exactly when the request carried a
292 /// multi-element `IN` pin. Part of the prover/verifier agreement
293 /// exactly as on the ranked surface. Produced by
294 /// [`super::drive_document_ranked_query::index_picker::encode_prefix_branches`]
295 /// — crate-private so the resolver is the only public constructor
296 /// and the encoder's invariants hold on every externally obtainable
297 /// value.
298 pub(crate) prefix_branches: Vec<Vec<Vec<u8>>>,
299 /// Inclusive bounds on the aggregate. Carry the axis; the index must
300 /// declare the matching `ranked_*` flag.
301 pub bounds: AxisRangeBounds,
302 /// `true` walks the secondary from the largest matching aggregate
303 /// down. Tie ordering is by group key in the direction of the walk,
304 /// exactly as on the ranked surface.
305 pub descending: bool,
306 /// Maximum number of matching groups to return. Fewer entries come
307 /// back when fewer groups fall inside the bounds; that is not an
308 /// error. **More matching groups than `limit` are silently cut at
309 /// `limit`** — the walk stops, and nothing marks the cut. A caller
310 /// can continue past *distinct* aggregate values by tightening the
311 /// bound, but a cut inside a **tie** cannot be continued (see the
312 /// module docs): groups tied at the boundary aggregate that fell
313 /// past the limit stay unreachable until a composite-key cursor
314 /// exists, so size the limit above the widest expected tie.
315 pub limit: u16,
316}
317
318#[cfg(any(feature = "server", feature = "verify"))]
319impl DriveDocumentHavingQuery<'_> {
320 /// The resolved prefix branches, in canonical order — one per `IN`
321 /// element (a single branch without an `IN`). Read-only: the field is
322 /// crate-private so the resolver's encoder invariants cannot be
323 /// bypassed by construction or mutation.
324 pub fn prefix_branches(&self) -> &[Vec<Vec<u8>>] {
325 &self.prefix_branches
326 }
327}
328
329#[cfg(any(feature = "server", feature = "verify"))]
330impl DriveDocumentHavingQuery<'_> {
331 /// Path of one branch's terminal property-name tree — identical to
332 /// the ranked surface's path (including the pinned-prefix segments
333 /// of a compound index), because both read the same indexed
334 /// tree(s). See
335 /// [`DriveDocumentRankedQuery::indexed_property_name_tree_path`](super::drive_document_ranked_query::DriveDocumentRankedQuery::indexed_property_name_tree_path).
336 pub fn indexed_property_name_tree_path(&self, branch: usize) -> Result<Vec<Vec<u8>>, Error> {
337 let prefix_values =
338 self.prefix_branches
339 .get(branch)
340 .ok_or(Error::Drive(DriveError::NotSupported(
341 "ranked and having-range queries addressed a prefix branch outside the \
342 query's resolved branch set",
343 )))?;
344 indexed_property_name_tree_path_for_index(
345 &self.contract_id,
346 &self.document_type_name,
347 self.index,
348 prefix_values,
349 )
350 }
351}
352
353/// Resolve a validated [`DocumentHavingMode`] against a document type's
354/// indexes into the executable [`DriveDocumentHavingQuery`]: pick the
355/// covering index (shared with the ranked surface — both read the same
356/// indexed tree), encode the prefix pins into prefix **branches** (one
357/// branch for all-`==` pins, one branch per element of the single
358/// permitted `IN`), and assemble the query.
359///
360/// The **one** resolution path for the having surface, mirroring
361/// [`super::drive_document_ranked_query::index_picker::resolve_ranked_query_for_mode`]:
362/// the server's executors and the SDK's proof helpers both call it, so a
363/// proof and an unproven read (and the client's verification) are about
364/// the same subtree by construction.
365///
366/// `indexes` is threaded in separately for the same lifetime reason as
367/// the ranked resolver: the returned query's `&'a Index` must outlive
368/// this frame. Callers pass `document_type.indexes()`.
369#[cfg(any(feature = "server", feature = "verify"))]
370pub fn resolve_having_query_for_mode<'a>(
371 contract_id: [u8; 32],
372 document_type: DocumentTypeRef<'a>,
373 document_type_name: String,
374 indexes: &'a BTreeMap<String, Index>,
375 mode: &DocumentHavingMode,
376 platform_version: &PlatformVersion,
377) -> Result<DriveDocumentHavingQuery<'a>, Error> {
378 let axis = mode.bounds.axis();
379 let pin_fields: Vec<String> = mode
380 .prefix_pins
381 .iter()
382 .map(|pin| pin.field.clone())
383 .collect();
384 let index = find_ranked_index_for_axis(
385 indexes,
386 &mode.group_by_property,
387 &pin_fields,
388 axis,
389 &mode.aggregate_field,
390 )
391 .ok_or_else(|| {
392 Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
393 no_covering_index_message(
394 "having-range",
395 axis,
396 &mode.group_by_property,
397 &mode.prefix_pins,
398 &mode.aggregate_field,
399 ),
400 ))
401 })?;
402 let prefix_branches =
403 encode_prefix_branches(document_type, index, &mode.prefix_pins, platform_version)?;
404 Ok(DriveDocumentHavingQuery {
405 document_type,
406 contract_id,
407 document_type_name,
408 index,
409 prefix_branches,
410 bounds: mode.bounds,
411 descending: mode.descending,
412 limit: mode.limit,
413 })
414}