Skip to main content

alopex_sql/planner/
aggregate_expr.rs

1use crate::planner::typed_expr::{SortExpr, TypedExpr};
2use crate::planner::types::ResolvedType;
3
4/// Supported aggregate function types.
5#[derive(Debug, Clone, PartialEq)]
6pub enum AggregateFunction {
7    Count,
8    Sum,
9    Total,
10    Avg,
11    Min,
12    Max,
13    GroupConcat {
14        separator: Option<String>,
15    },
16    StringAgg {
17        separator: Option<String>,
18    },
19    /// Ordered-set aggregate `PERCENTILE_DISC(fraction) WITHIN GROUP
20    /// (ORDER BY ...)` (issue #148). `fraction` is validated by the planner to
21    /// be a literal in `[0, 1]`. Issue #154 (PERCENTILE_CONT / MODE) extends
22    /// this enum with sibling variants reusing the same ordered-input path.
23    PercentileDisc {
24        fraction: f64,
25    },
26}
27
28/// Aggregate expression definition.
29#[derive(Debug, Clone)]
30pub struct AggregateExpr {
31    pub function: AggregateFunction,
32    pub arg: Option<TypedExpr>,
33    pub distinct: bool,
34    pub result_type: ResolvedType,
35    /// `FILTER (WHERE predicate)`: rows where the predicate is not TRUE are
36    /// skipped before the accumulator (and any DISTINCT set) sees them.
37    pub filter: Option<TypedExpr>,
38    /// Aggregate-local ordering. Non-empty only for order-sensitive
39    /// aggregates (GROUP_CONCAT / STRING_AGG / ordered-set aggregates); the
40    /// planner discards validated ORDER BY on order-insensitive aggregates
41    /// (D3 in docs/sql-aggregate-filter-within-group.md).
42    pub order_by: Vec<SortExpr>,
43}
44
45impl AggregateExpr {
46    pub fn count_star() -> Self {
47        Self {
48            function: AggregateFunction::Count,
49            arg: None,
50            distinct: false,
51            result_type: ResolvedType::BigInt,
52            filter: None,
53            order_by: Vec::new(),
54        }
55    }
56
57    pub fn count(arg: TypedExpr, distinct: bool) -> Self {
58        Self {
59            function: AggregateFunction::Count,
60            arg: Some(arg),
61            distinct,
62            result_type: ResolvedType::BigInt,
63            filter: None,
64            order_by: Vec::new(),
65        }
66    }
67
68    pub fn sum(arg: TypedExpr) -> Self {
69        let result_type = sum_result_type(&arg.resolved_type);
70        Self {
71            function: AggregateFunction::Sum,
72            arg: Some(arg),
73            distinct: false,
74            result_type,
75            filter: None,
76            order_by: Vec::new(),
77        }
78    }
79
80    pub fn total(arg: TypedExpr) -> Self {
81        Self {
82            function: AggregateFunction::Total,
83            arg: Some(arg),
84            distinct: false,
85            result_type: ResolvedType::Double,
86            filter: None,
87            order_by: Vec::new(),
88        }
89    }
90
91    pub fn avg(arg: TypedExpr) -> Self {
92        Self {
93            function: AggregateFunction::Avg,
94            arg: Some(arg),
95            distinct: false,
96            result_type: ResolvedType::Double,
97            filter: None,
98            order_by: Vec::new(),
99        }
100    }
101
102    pub fn min(arg: TypedExpr) -> Self {
103        let result_type = arg.resolved_type.clone();
104        Self {
105            function: AggregateFunction::Min,
106            arg: Some(arg),
107            distinct: false,
108            result_type,
109            filter: None,
110            order_by: Vec::new(),
111        }
112    }
113
114    pub fn max(arg: TypedExpr) -> Self {
115        let result_type = arg.resolved_type.clone();
116        Self {
117            function: AggregateFunction::Max,
118            arg: Some(arg),
119            distinct: false,
120            result_type,
121            filter: None,
122            order_by: Vec::new(),
123        }
124    }
125}
126
127/// Return the SQL result type for `SUM` over a value of `input_type`.
128///
129/// Fixed-width integral inputs retain their integral type. All other numeric
130/// inputs accumulate and return DOUBLE, matching the historical floating-point
131/// behaviour and keeping `TOTAL`/`AVG` semantics distinct.
132/// `SUM` keeps integer inputs exact, but accumulates them in a wider type: a
133/// 32-bit accumulator overflows on ordinary data, so summing INTEGER yields
134/// BIGINT. PostgreSQL sums int4 into int8 for the same reason, and DuckDB
135/// widens further to hugeint.
136pub fn sum_result_type(input_type: &ResolvedType) -> ResolvedType {
137    match input_type {
138        ResolvedType::Integer | ResolvedType::BigInt => ResolvedType::BigInt,
139        _ => ResolvedType::Double,
140    }
141}