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}