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