Skip to main content

drive/query/
having.rs

1//! `HAVING` clause types for the v1 `getDocuments` count surface.
2//!
3//! HAVING differs from WHERE in two structural ways the type
4//! system needs to reflect:
5//! - The **left** operand is a per-group aggregate (`COUNT(*)`,
6//!   `SUM(field)`, `AVG(field)`) rather than a raw row field.
7//! - The **right** operand is either a concrete value (`> 5`,
8//!   `BETWEEN 5 AND 10`, `IN (5, 10, 15)`) **or** a cross-group
9//!   ranking (`EQ MAX`, `IN TOP(5)`, `> MIN`). The ranking
10//!   right-operands (`MIN` / `MAX` / `TOP(N)` / `BOTTOM(N)`) are
11//!   meta-aggregates computed over the set of group results, so
12//!   `HAVING COUNT(*) IN TOP(5)` reads as "this group's count is
13//!   among the five largest group counts" — a concise way to
14//!   express top-N/bottom-N selection without window functions or
15//!   `ORDER BY` + `LIMIT`.
16//!
17//! The operator set matches [`crate::query::WhereOperator`] minus
18//! `STARTS_WITH` (prefix matching has no meaning on a scalar
19//! aggregate result, even one that's a string): scalar comparison,
20//! `IN`, and all four `BETWEEN*` variants all carry through.
21//!
22//! Multi-clause HAVING (`HAVING COUNT(*) > 5 AND SUM(amount) > 100`)
23//! is expressed by repeating [`HavingClause`] at the request
24//! level — implicit AND, same shape as multiple `where_clauses`
25//! entries.
26//!
27//! These types are shared between the wire-decoding layer
28//! (`rs-drive-abci/src/query/document_query/v1/conversions.rs`)
29//! and the SDK's request builder
30//! (`rs-sdk/src/platform/documents/document_query.rs`) so the
31//! drive-side struct is the single source of truth for the shape.
32//! The server currently rejects any non-empty `having` with
33//! `QuerySyntaxError::Unsupported("HAVING clause is not yet
34//! implemented")` — the types exist so the wire surface is stable
35//! when execution lands.
36
37use dpp::platform_value::Value;
38#[cfg(feature = "serde")]
39use serde::{Deserialize, Serialize};
40
41/// Aggregate function applied to a group on the left side of a
42/// [`HavingClause`]. These are the per-group aggregates whose
43/// result is the scalar / numeric value the right-side operand
44/// compares against.
45///
46/// `MIN` / `MAX` / `TOP` / `BOTTOM` deliberately don't appear
47/// here — they're cross-group ranking primitives that live on
48/// the right side via [`HavingRanking`] (e.g.
49/// `HAVING COUNT(*) EQ MAX`).
50#[derive(Copy, Clone, Debug, PartialEq, Eq)]
51#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
52pub enum HavingAggregateFunction {
53    /// `COUNT(*)` when [`HavingAggregate::field`] is empty,
54    /// otherwise `COUNT(field)`.
55    Count,
56    /// `SUM(field)`. Numeric field required.
57    Sum,
58    /// `AVG(field)`. Numeric field required; result is `f64`.
59    Avg,
60}
61
62/// Aggregate operand for the left side of a [`HavingClause`]. See
63/// [`HavingAggregateFunction`] for the per-function `field`
64/// requirements (empty only for `COUNT(*)`).
65#[derive(Clone, Debug, PartialEq, Eq)]
66#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
67pub struct HavingAggregate {
68    /// The aggregate function applied to the group.
69    pub function: HavingAggregateFunction,
70    /// The field the aggregate is applied to. Empty only when
71    /// `function == Count` (to express `COUNT(*)`).
72    pub field: String,
73}
74
75/// Cross-group ranking primitive on the right side of a
76/// [`HavingClause`]. The ranking is computed over the **set of
77/// group results** (one per row produced by `GROUP BY`), not over
78/// the raw rows — so `HAVING COUNT(*) EQ MAX` selects groups
79/// whose count equals the maximum count across all groups.
80#[derive(Copy, Clone, Debug, PartialEq, Eq)]
81#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
82pub enum HavingRankingKind {
83    /// Smallest group-aggregate value across the result set
84    /// (single scalar).
85    Min,
86    /// Largest group-aggregate value across the result set
87    /// (single scalar).
88    Max,
89    /// Set of the `N` largest group-aggregate values. Pair with
90    /// `IN` for membership (`COUNT(*) IN TOP(5)`); single-value
91    /// operators (`EQ`, `>`, `<`, …) treat `TOP(1)` as the
92    /// maximum.
93    Top,
94    /// Set of the `N` smallest group-aggregate values. Symmetric
95    /// counterpart to [`Self::Top`].
96    Bottom,
97}
98
99/// Cross-group ranking operand: `kind` plus an optional `n` (only
100/// meaningful for [`HavingRankingKind::Top`] /
101/// [`HavingRankingKind::Bottom`]).
102#[derive(Copy, Clone, Debug, PartialEq, Eq)]
103#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
104pub struct HavingRanking {
105    /// Which ranking primitive.
106    pub kind: HavingRankingKind,
107    /// Required for `Top` / `Bottom` (1-indexed: `n=1` is the
108    /// single largest / smallest); must be `None` for `Min` /
109    /// `Max`. The wire allows it on `Min` / `Max` for forward
110    /// compatibility, but evaluation rejects it as a malformed
111    /// ranking.
112    pub n: Option<u64>,
113}
114
115/// Right-side operand of a [`HavingClause`]. Either a concrete
116/// value (literal scalar or list-shaped operand for
117/// `BETWEEN*`/`IN`) or a cross-group ranking reference
118/// ([`HavingRanking`]).
119///
120/// The split lives at the type level so the wire decoder rejects
121/// half-built clauses ("operator says `IN`, right side is `MIN`
122/// ranking with `n` set") at conversion time rather than letting
123/// them reach the evaluator as ambiguous state.
124#[derive(Clone, Debug, PartialEq)]
125#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
126pub enum HavingRightOperand {
127    /// Concrete value: scalar for `=` / `!=` / `<` / `<=` / `>` /
128    /// `>=`; 2-element list `[lower, upper]` for `Between*`;
129    /// list of candidates for `In`.
130    Value(Value),
131    /// Cross-group ranking reference. Operator compatibility:
132    /// scalar comparison operators work with `Min` / `Max` /
133    /// `Top(1)` / `Bottom(1)`; `In` works with `Top(N)` /
134    /// `Bottom(N)` (membership in the top-N / bottom-N set).
135    Ranking(HavingRanking),
136}
137
138/// Comparison operator for a [`HavingClause`]. Mirrors
139/// [`crate::query::WhereOperator`] minus `STARTS_WITH` (prefix
140/// matching has no natural meaning against a scalar aggregate
141/// result, even a string-typed one). `BETWEEN*` operand semantics
142/// match `WhereOperator`: a 2-element list `[lower, upper]`; `IN`
143/// expects a list of candidate values (or a cross-group ranking
144/// set via [`HavingRightOperand::Ranking`]).
145#[derive(Copy, Clone, Debug, PartialEq, Eq)]
146#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
147pub enum HavingOperator {
148    /// `aggregate = value`.
149    Equal,
150    /// `aggregate != value`.
151    NotEqual,
152    /// `aggregate > value`.
153    GreaterThan,
154    /// `aggregate >= value`.
155    GreaterThanOrEquals,
156    /// `aggregate < value`.
157    LessThan,
158    /// `aggregate <= value`.
159    LessThanOrEquals,
160    /// `aggregate BETWEEN lower AND upper` (inclusive on both
161    /// ends). `value` must be a 2-element list `[lower, upper]`.
162    Between,
163    /// `aggregate > lower AND aggregate < upper` (exclusive on
164    /// both ends). `value` shape same as `Between`.
165    BetweenExcludeBounds,
166    /// `aggregate > lower AND aggregate <= upper` (exclusive on
167    /// the left bound only). `value` shape same as `Between`.
168    BetweenExcludeLeft,
169    /// `aggregate >= lower AND aggregate < upper` (exclusive on
170    /// the right bound only). `value` shape same as `Between`.
171    BetweenExcludeRight,
172    /// `aggregate IN (v1, v2, …)`. `value` must be a list of
173    /// candidate values matching the aggregate's result type.
174    In,
175}
176
177/// Single `HAVING <aggregate> <op> <right>` clause.
178///
179/// Multiple [`HavingClause`] entries in the request-level
180/// `repeated HavingClause having` field are combined with implicit
181/// `AND` — same semantics as multiple `where_clauses` entries.
182/// `HAVING COUNT(*) > 5 AND SUM(amount) > 100` is two clauses, not
183/// a tree; the wire has no dedicated `AND` node because the
184/// repeated field already expresses it. Future `OR` capability
185/// would land as an additional wire shape (e.g. a `HavingGroup`
186/// message with a logical-op tag) rather than overloading this
187/// type.
188#[derive(Clone, Debug, PartialEq)]
189#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
190pub struct HavingClause {
191    /// Left-side per-group aggregate operand.
192    pub aggregate: HavingAggregate,
193    /// Comparison operator.
194    pub operator: HavingOperator,
195    /// Right-side operand — either a concrete value or a
196    /// cross-group ranking. See [`HavingRightOperand`] for the
197    /// shape contract.
198    pub right: HavingRightOperand,
199}