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