Skip to main content

icydb_core/db/query/builder/
aggregate.rs

1//! Module: query::builder::aggregate
2//! Responsibility: composable grouped/global aggregate expression builders.
3//! Does not own: aggregate validation policy or executor fold semantics.
4//! Boundary: fluent aggregate intent construction lowered into grouped specs.
5
6use crate::db::{direction::Direction, query::plan::AggregateKind};
7
8///
9/// AggregateExpr
10///
11/// Composable aggregate expression used by query/fluent aggregate entrypoints.
12/// This builder only carries declarative shape (`kind`, `target_field`,
13/// `distinct`) and does not perform semantic validation.
14///
15
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct AggregateExpr {
18    kind: AggregateKind,
19    target_field: Option<String>,
20    distinct: bool,
21}
22
23impl AggregateExpr {
24    /// Construct one aggregate expression from explicit shape components.
25    const fn new(kind: AggregateKind, target_field: Option<String>) -> Self {
26        Self {
27            kind,
28            target_field,
29            distinct: false,
30        }
31    }
32
33    /// Enable DISTINCT modifier for this aggregate expression.
34    #[must_use]
35    pub const fn distinct(mut self) -> Self {
36        self.distinct = true;
37        self
38    }
39
40    /// Borrow aggregate kind.
41    #[must_use]
42    pub(crate) const fn kind(&self) -> AggregateKind {
43        self.kind
44    }
45
46    /// Borrow optional target field.
47    #[must_use]
48    pub(crate) fn target_field(&self) -> Option<&str> {
49        self.target_field.as_deref()
50    }
51
52    /// Return true when DISTINCT is enabled.
53    #[must_use]
54    pub(crate) const fn is_distinct(&self) -> bool {
55        self.distinct
56    }
57
58    /// Build one aggregate expression directly from planner semantic parts.
59    pub(in crate::db::query) const fn from_semantic_parts(
60        kind: AggregateKind,
61        target_field: Option<String>,
62        distinct: bool,
63    ) -> Self {
64        Self {
65            kind,
66            target_field,
67            distinct,
68        }
69    }
70
71    /// Return whether this expression kind is `COUNT`.
72    #[must_use]
73    pub(crate) const fn is_count_kind(kind: AggregateKind) -> bool {
74        matches!(kind, AggregateKind::Count)
75    }
76
77    /// Return whether this expression kind is `SUM`.
78    #[must_use]
79    pub(crate) const fn is_sum_kind(kind: AggregateKind) -> bool {
80        matches!(kind, AggregateKind::Sum | AggregateKind::Avg)
81    }
82
83    /// Return whether this expression kind supports explicit field targets.
84    #[must_use]
85    pub(crate) const fn supports_field_targets_kind(kind: AggregateKind) -> bool {
86        matches!(
87            kind,
88            AggregateKind::Min | AggregateKind::Max | AggregateKind::Sum | AggregateKind::Avg
89        )
90    }
91
92    /// Return whether this expression kind belongs to the extrema family.
93    #[must_use]
94    pub(crate) const fn is_extrema_kind(kind: AggregateKind) -> bool {
95        Self::supports_field_targets_kind(kind)
96    }
97
98    /// Return whether reducer updates for this kind require a decoded id payload.
99    #[must_use]
100    pub(crate) const fn requires_decoded_id_kind(kind: AggregateKind) -> bool {
101        !matches!(
102            kind,
103            AggregateKind::Count | AggregateKind::Sum | AggregateKind::Avg | AggregateKind::Exists
104        )
105    }
106
107    /// Return whether grouped aggregate DISTINCT is supported for this kind.
108    #[must_use]
109    pub(crate) const fn supports_grouped_distinct_kind_v1(kind: AggregateKind) -> bool {
110        matches!(
111            kind,
112            AggregateKind::Count
113                | AggregateKind::Min
114                | AggregateKind::Max
115                | AggregateKind::Sum
116                | AggregateKind::Avg
117        )
118    }
119
120    /// Return whether global DISTINCT without GROUP BY keys is supported for this kind.
121    #[must_use]
122    pub(crate) const fn supports_global_distinct_without_group_keys_kind(
123        kind: AggregateKind,
124    ) -> bool {
125        matches!(
126            kind,
127            AggregateKind::Count | AggregateKind::Sum | AggregateKind::Avg
128        )
129    }
130
131    /// Return the canonical extrema traversal direction for this kind.
132    #[must_use]
133    pub(crate) const fn extrema_direction_for_kind(kind: AggregateKind) -> Option<Direction> {
134        match kind {
135            AggregateKind::Min => Some(Direction::Asc),
136            AggregateKind::Max => Some(Direction::Desc),
137            AggregateKind::Count
138            | AggregateKind::Sum
139            | AggregateKind::Avg
140            | AggregateKind::Exists
141            | AggregateKind::First
142            | AggregateKind::Last => None,
143        }
144    }
145
146    /// Return the canonical materialized fold direction for this kind.
147    #[must_use]
148    pub(crate) const fn materialized_fold_direction_for_kind(kind: AggregateKind) -> Direction {
149        match kind {
150            AggregateKind::Min => Direction::Desc,
151            AggregateKind::Count
152            | AggregateKind::Sum
153            | AggregateKind::Avg
154            | AggregateKind::Exists
155            | AggregateKind::Max
156            | AggregateKind::First
157            | AggregateKind::Last => Direction::Asc,
158        }
159    }
160
161    /// Return true when this kind can use bounded aggregate probe hints.
162    #[must_use]
163    pub(crate) const fn supports_bounded_probe_hint_for_kind(kind: AggregateKind) -> bool {
164        !Self::is_count_kind(kind) && !Self::is_sum_kind(kind)
165    }
166
167    /// Derive a bounded aggregate probe fetch hint for this kind.
168    #[must_use]
169    pub(crate) fn bounded_probe_fetch_hint_for_kind(
170        kind: AggregateKind,
171        direction: Direction,
172        offset: usize,
173        page_limit: Option<usize>,
174    ) -> Option<usize> {
175        match kind {
176            AggregateKind::Exists | AggregateKind::First => Some(offset.saturating_add(1)),
177            AggregateKind::Min if direction == Direction::Asc => Some(offset.saturating_add(1)),
178            AggregateKind::Max if direction == Direction::Desc => Some(offset.saturating_add(1)),
179            AggregateKind::Last => page_limit.map(|limit| offset.saturating_add(limit)),
180            AggregateKind::Count
181            | AggregateKind::Sum
182            | AggregateKind::Avg
183            | AggregateKind::Min
184            | AggregateKind::Max => None,
185        }
186    }
187}
188
189/// Build `count(*)`.
190#[must_use]
191pub const fn count() -> AggregateExpr {
192    AggregateExpr::new(AggregateKind::Count, None)
193}
194
195/// Build `count(field)`.
196#[must_use]
197pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
198    AggregateExpr::new(AggregateKind::Count, Some(field.as_ref().to_string()))
199}
200
201/// Build `sum(field)`.
202#[must_use]
203pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
204    AggregateExpr::new(AggregateKind::Sum, Some(field.as_ref().to_string()))
205}
206
207/// Build `avg(field)`.
208#[must_use]
209pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
210    AggregateExpr::new(AggregateKind::Avg, Some(field.as_ref().to_string()))
211}
212
213/// Build `exists`.
214#[must_use]
215pub const fn exists() -> AggregateExpr {
216    AggregateExpr::new(AggregateKind::Exists, None)
217}
218
219/// Build `first`.
220#[must_use]
221pub const fn first() -> AggregateExpr {
222    AggregateExpr::new(AggregateKind::First, None)
223}
224
225/// Build `last`.
226#[must_use]
227pub const fn last() -> AggregateExpr {
228    AggregateExpr::new(AggregateKind::Last, None)
229}
230
231/// Build `min`.
232#[must_use]
233pub const fn min() -> AggregateExpr {
234    AggregateExpr::new(AggregateKind::Min, None)
235}
236
237/// Build `min(field)`.
238#[must_use]
239pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
240    AggregateExpr::new(AggregateKind::Min, Some(field.as_ref().to_string()))
241}
242
243/// Build `max`.
244#[must_use]
245pub const fn max() -> AggregateExpr {
246    AggregateExpr::new(AggregateKind::Max, None)
247}
248
249/// Build `max(field)`.
250#[must_use]
251pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
252    AggregateExpr::new(AggregateKind::Max, Some(field.as_ref().to_string()))
253}
254
255/// Build one non-field-target terminal aggregate expression from one kind.
256#[must_use]
257pub(crate) fn terminal_expr_for_kind(kind: AggregateKind) -> AggregateExpr {
258    match kind {
259        AggregateKind::Count => count(),
260        AggregateKind::Exists => exists(),
261        AggregateKind::Min => min(),
262        AggregateKind::Max => max(),
263        AggregateKind::First => first(),
264        AggregateKind::Last => last(),
265        AggregateKind::Sum | AggregateKind::Avg => {
266            unreachable!("terminal_expr_for_kind does not support SUM/AVG field-target kinds")
267        }
268    }
269}
270
271/// Build one field-target extrema aggregate expression from one kind.
272#[must_use]
273pub(crate) fn field_target_extrema_expr_for_kind(
274    kind: AggregateKind,
275    field: impl AsRef<str>,
276) -> AggregateExpr {
277    match kind {
278        AggregateKind::Min => min_by(field),
279        AggregateKind::Max => max_by(field),
280        _ => {
281            unreachable!(
282                "field_target_extrema_expr_for_kind requires MIN/MAX kind for field-target extrema"
283            )
284        }
285    }
286}