Skip to main content

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}