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    /// Return whether this expression kind is `COUNT`.
59    #[must_use]
60    pub(crate) const fn is_count_kind(kind: GroupAggregateKind) -> bool {
61        matches!(kind, GroupAggregateKind::Count)
62    }
63
64    /// Return whether this expression kind is `SUM`.
65    #[must_use]
66    pub(crate) const fn is_sum_kind(kind: GroupAggregateKind) -> bool {
67        matches!(kind, GroupAggregateKind::Sum)
68    }
69
70    /// Return whether this expression kind supports explicit field targets.
71    #[must_use]
72    pub(crate) const fn supports_field_targets_kind(kind: GroupAggregateKind) -> bool {
73        matches!(kind, GroupAggregateKind::Min | GroupAggregateKind::Max)
74    }
75
76    /// Return whether this expression kind belongs to the extrema family.
77    #[must_use]
78    pub(crate) const fn is_extrema_kind(kind: GroupAggregateKind) -> bool {
79        Self::supports_field_targets_kind(kind)
80    }
81
82    /// Return whether this expression kind supports first/last value projection.
83    #[must_use]
84    pub(crate) const fn supports_terminal_value_projection_kind(kind: GroupAggregateKind) -> bool {
85        matches!(kind, GroupAggregateKind::First | GroupAggregateKind::Last)
86    }
87
88    /// Return whether reducer updates for this kind require a decoded id payload.
89    #[must_use]
90    pub(crate) const fn requires_decoded_id_kind(kind: GroupAggregateKind) -> bool {
91        !matches!(
92            kind,
93            GroupAggregateKind::Count | GroupAggregateKind::Sum | GroupAggregateKind::Exists
94        )
95    }
96
97    /// Return whether grouped aggregate DISTINCT is supported for this kind.
98    #[must_use]
99    pub(crate) const fn supports_grouped_distinct_kind_v1(kind: GroupAggregateKind) -> bool {
100        matches!(
101            kind,
102            GroupAggregateKind::Count
103                | GroupAggregateKind::Min
104                | GroupAggregateKind::Max
105                | GroupAggregateKind::Sum
106        )
107    }
108
109    /// Return whether global DISTINCT without GROUP BY keys is supported for this kind.
110    #[must_use]
111    pub(crate) const fn supports_global_distinct_without_group_keys_kind(
112        kind: GroupAggregateKind,
113    ) -> bool {
114        matches!(kind, GroupAggregateKind::Count | GroupAggregateKind::Sum)
115    }
116
117    /// Return the canonical grouped aggregate fingerprint tag (v1).
118    #[must_use]
119    pub(crate) const fn fingerprint_tag_for_kind_v1(kind: GroupAggregateKind) -> u8 {
120        match kind {
121            GroupAggregateKind::Count => 0x01,
122            GroupAggregateKind::Sum => 0x02,
123            GroupAggregateKind::Exists => 0x03,
124            GroupAggregateKind::Min => 0x04,
125            GroupAggregateKind::Max => 0x05,
126            GroupAggregateKind::First => 0x06,
127            GroupAggregateKind::Last => 0x07,
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: GroupAggregateKind) -> Option<Direction> {
134        match kind {
135            GroupAggregateKind::Min => Some(Direction::Asc),
136            GroupAggregateKind::Max => Some(Direction::Desc),
137            GroupAggregateKind::Count
138            | GroupAggregateKind::Sum
139            | GroupAggregateKind::Exists
140            | GroupAggregateKind::First
141            | GroupAggregateKind::Last => None,
142        }
143    }
144
145    /// Return the canonical materialized fold direction for this kind.
146    #[must_use]
147    pub(crate) const fn materialized_fold_direction_for_kind(
148        kind: GroupAggregateKind,
149    ) -> Direction {
150        match kind {
151            GroupAggregateKind::Min => Direction::Desc,
152            GroupAggregateKind::Count
153            | GroupAggregateKind::Sum
154            | GroupAggregateKind::Exists
155            | GroupAggregateKind::Max
156            | GroupAggregateKind::First
157            | GroupAggregateKind::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: GroupAggregateKind) -> 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: GroupAggregateKind,
171        direction: Direction,
172        offset: usize,
173        page_limit: Option<usize>,
174    ) -> Option<usize> {
175        match kind {
176            GroupAggregateKind::Exists | GroupAggregateKind::First => {
177                Some(offset.saturating_add(1))
178            }
179            GroupAggregateKind::Min if direction == Direction::Asc => {
180                Some(offset.saturating_add(1))
181            }
182            GroupAggregateKind::Max if direction == Direction::Desc => {
183                Some(offset.saturating_add(1))
184            }
185            GroupAggregateKind::Last => page_limit.map(|limit| offset.saturating_add(limit)),
186            GroupAggregateKind::Count
187            | GroupAggregateKind::Sum
188            | GroupAggregateKind::Min
189            | GroupAggregateKind::Max => None,
190        }
191    }
192}
193
194/// Build `count(*)`.
195#[must_use]
196pub const fn count() -> AggregateExpr {
197    AggregateExpr::new(GroupAggregateKind::Count, None)
198}
199
200/// Build `count(field)`.
201#[must_use]
202pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
203    AggregateExpr::new(GroupAggregateKind::Count, Some(field.as_ref().to_string()))
204}
205
206/// Build `sum(field)`.
207#[must_use]
208pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
209    AggregateExpr::new(GroupAggregateKind::Sum, Some(field.as_ref().to_string()))
210}
211
212/// Build `exists`.
213#[must_use]
214pub const fn exists() -> AggregateExpr {
215    AggregateExpr::new(GroupAggregateKind::Exists, None)
216}
217
218/// Build `first`.
219#[must_use]
220pub const fn first() -> AggregateExpr {
221    AggregateExpr::new(GroupAggregateKind::First, None)
222}
223
224/// Build `last`.
225#[must_use]
226pub const fn last() -> AggregateExpr {
227    AggregateExpr::new(GroupAggregateKind::Last, None)
228}
229
230/// Build `min`.
231#[must_use]
232pub const fn min() -> AggregateExpr {
233    AggregateExpr::new(GroupAggregateKind::Min, None)
234}
235
236/// Build `min(field)`.
237#[must_use]
238pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
239    AggregateExpr::new(GroupAggregateKind::Min, Some(field.as_ref().to_string()))
240}
241
242/// Build `max`.
243#[must_use]
244pub const fn max() -> AggregateExpr {
245    AggregateExpr::new(GroupAggregateKind::Max, None)
246}
247
248/// Build `max(field)`.
249#[must_use]
250pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
251    AggregateExpr::new(GroupAggregateKind::Max, Some(field.as_ref().to_string()))
252}