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 this expression kind supports first/last value projection.
99    #[must_use]
100    pub(crate) const fn supports_terminal_value_projection_kind(kind: AggregateKind) -> bool {
101        matches!(kind, AggregateKind::First | AggregateKind::Last)
102    }
103
104    /// Return whether reducer updates for this kind require a decoded id payload.
105    #[must_use]
106    pub(crate) const fn requires_decoded_id_kind(kind: AggregateKind) -> bool {
107        !matches!(
108            kind,
109            AggregateKind::Count | AggregateKind::Sum | AggregateKind::Avg | AggregateKind::Exists
110        )
111    }
112
113    /// Return whether grouped aggregate DISTINCT is supported for this kind.
114    #[must_use]
115    pub(crate) const fn supports_grouped_distinct_kind_v1(kind: AggregateKind) -> bool {
116        matches!(
117            kind,
118            AggregateKind::Count
119                | AggregateKind::Min
120                | AggregateKind::Max
121                | AggregateKind::Sum
122                | AggregateKind::Avg
123        )
124    }
125
126    /// Return whether global DISTINCT without GROUP BY keys is supported for this kind.
127    #[must_use]
128    pub(crate) const fn supports_global_distinct_without_group_keys_kind(
129        kind: AggregateKind,
130    ) -> bool {
131        matches!(
132            kind,
133            AggregateKind::Count | AggregateKind::Sum | AggregateKind::Avg
134        )
135    }
136
137    /// Return the canonical extrema traversal direction for this kind.
138    #[must_use]
139    pub(crate) const fn extrema_direction_for_kind(kind: AggregateKind) -> Option<Direction> {
140        match kind {
141            AggregateKind::Min => Some(Direction::Asc),
142            AggregateKind::Max => Some(Direction::Desc),
143            AggregateKind::Count
144            | AggregateKind::Sum
145            | AggregateKind::Avg
146            | AggregateKind::Exists
147            | AggregateKind::First
148            | AggregateKind::Last => None,
149        }
150    }
151
152    /// Return the canonical materialized fold direction for this kind.
153    #[must_use]
154    pub(crate) const fn materialized_fold_direction_for_kind(kind: AggregateKind) -> Direction {
155        match kind {
156            AggregateKind::Min => Direction::Desc,
157            AggregateKind::Count
158            | AggregateKind::Sum
159            | AggregateKind::Avg
160            | AggregateKind::Exists
161            | AggregateKind::Max
162            | AggregateKind::First
163            | AggregateKind::Last => Direction::Asc,
164        }
165    }
166
167    /// Return true when this kind can use bounded aggregate probe hints.
168    #[must_use]
169    pub(crate) const fn supports_bounded_probe_hint_for_kind(kind: AggregateKind) -> bool {
170        !Self::is_count_kind(kind) && !Self::is_sum_kind(kind)
171    }
172
173    /// Derive a bounded aggregate probe fetch hint for this kind.
174    #[must_use]
175    pub(crate) fn bounded_probe_fetch_hint_for_kind(
176        kind: AggregateKind,
177        direction: Direction,
178        offset: usize,
179        page_limit: Option<usize>,
180    ) -> Option<usize> {
181        match kind {
182            AggregateKind::Exists | AggregateKind::First => Some(offset.saturating_add(1)),
183            AggregateKind::Min if direction == Direction::Asc => Some(offset.saturating_add(1)),
184            AggregateKind::Max if direction == Direction::Desc => Some(offset.saturating_add(1)),
185            AggregateKind::Last => page_limit.map(|limit| offset.saturating_add(limit)),
186            AggregateKind::Count
187            | AggregateKind::Sum
188            | AggregateKind::Avg
189            | AggregateKind::Min
190            | AggregateKind::Max => None,
191        }
192    }
193}
194
195/// Build `count(*)`.
196#[must_use]
197pub const fn count() -> AggregateExpr {
198    AggregateExpr::new(AggregateKind::Count, None)
199}
200
201/// Build `count(field)`.
202#[must_use]
203pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
204    AggregateExpr::new(AggregateKind::Count, Some(field.as_ref().to_string()))
205}
206
207/// Build `sum(field)`.
208#[must_use]
209pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
210    AggregateExpr::new(AggregateKind::Sum, Some(field.as_ref().to_string()))
211}
212
213/// Build `avg(field)`.
214#[must_use]
215pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
216    AggregateExpr::new(AggregateKind::Avg, Some(field.as_ref().to_string()))
217}
218
219/// Build `exists`.
220#[must_use]
221pub const fn exists() -> AggregateExpr {
222    AggregateExpr::new(AggregateKind::Exists, None)
223}
224
225/// Build `first`.
226#[must_use]
227pub const fn first() -> AggregateExpr {
228    AggregateExpr::new(AggregateKind::First, None)
229}
230
231/// Build `last`.
232#[must_use]
233pub const fn last() -> AggregateExpr {
234    AggregateExpr::new(AggregateKind::Last, None)
235}
236
237/// Build `min`.
238#[must_use]
239pub const fn min() -> AggregateExpr {
240    AggregateExpr::new(AggregateKind::Min, None)
241}
242
243/// Build `min(field)`.
244#[must_use]
245pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
246    AggregateExpr::new(AggregateKind::Min, Some(field.as_ref().to_string()))
247}
248
249/// Build `max`.
250#[must_use]
251pub const fn max() -> AggregateExpr {
252    AggregateExpr::new(AggregateKind::Max, None)
253}
254
255/// Build `max(field)`.
256#[must_use]
257pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
258    AggregateExpr::new(AggregateKind::Max, Some(field.as_ref().to_string()))
259}