Skip to main content

rust_ef/query/
having_expr.rs

1//! HAVING expression AST: `HavingExpr`, `AggKind`, `CompareOp`.
2
3use crate::provider::DbValue;
4
5/// Aggregate function kind for `HavingExpr`.
6///
7/// Used by `linq!(having ...)` (Form B) when the having clause contains
8/// nested boolean expressions (`AND`/`OR`/`NOT`) or aggregate-versus-aggregate
9/// comparisons (`COUNT(b.id) > SUM(b.views)`).
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum AggKind {
12    Count,
13    Sum,
14    Avg,
15    Min,
16    Max,
17}
18
19impl AggKind {
20    /// Returns the SQL keyword for this aggregate.
21    pub fn sql_name(&self) -> &'static str {
22        match self {
23            AggKind::Count => "COUNT",
24            AggKind::Sum => "SUM",
25            AggKind::Avg => "AVG",
26            AggKind::Min => "MIN",
27            AggKind::Max => "MAX",
28        }
29    }
30
31    /// Parses an aggregate name (case-insensitive).
32    pub fn from_name(name: &str) -> Option<Self> {
33        match name.to_uppercase().as_str() {
34            "COUNT" => Some(AggKind::Count),
35            "SUM" => Some(AggKind::Sum),
36            "AVG" => Some(AggKind::Avg),
37            "MIN" => Some(AggKind::Min),
38            "MAX" => Some(AggKind::Max),
39            _ => None,
40        }
41    }
42}
43
44/// Comparison operator for `HavingExpr`.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum CompareOp {
47    Eq,
48    Ne,
49    Gt,
50    Ge,
51    Lt,
52    Le,
53}
54
55impl CompareOp {
56    /// Returns the SQL operator string.
57    pub fn sql_name(&self) -> &'static str {
58        match self {
59            CompareOp::Eq => "=",
60            CompareOp::Ne => "!=",
61            CompareOp::Gt => ">",
62            CompareOp::Ge => ">=",
63            CompareOp::Lt => "<",
64            CompareOp::Le => "<=",
65        }
66    }
67
68    /// Parses a comparison operator from its SQL symbol.
69    pub fn from_symbol(symbol: &str) -> Option<Self> {
70        match symbol {
71            "=" => Some(CompareOp::Eq),
72            "!=" => Some(CompareOp::Ne),
73            ">" => Some(CompareOp::Gt),
74            ">=" => Some(CompareOp::Ge),
75            "<" => Some(CompareOp::Lt),
76            "<=" => Some(CompareOp::Le),
77            _ => None,
78        }
79    }
80}
81
82/// AST node for `HAVING` expressions.
83///
84/// Supports boolean combinations (`AND`/`OR`/`NOT`) and aggregate-versus-aggregate
85/// comparisons in addition to the basic `agg(col) op value` form. Generated by
86/// `linq!(having ...)` (Form B) expansion and compiled to SQL by `to_sql`.
87#[derive(Debug, Clone)]
88pub enum HavingExpr {
89    /// `agg(col) op value` — basic comparison against a literal.
90    Compare {
91        agg: AggKind,
92        col: String,
93        op: CompareOp,
94        value: DbValue,
95    },
96    /// `expr AND expr`.
97    And(Box<HavingExpr>, Box<HavingExpr>),
98    /// `expr OR expr`.
99    Or(Box<HavingExpr>, Box<HavingExpr>),
100    /// `NOT expr`.
101    Not(Box<HavingExpr>),
102    /// `agg(col1) op agg(col2)` — aggregate-vs-aggregate comparison (no bound parameter).
103    CompareAgg {
104        left_agg: AggKind,
105        left_col: String,
106        op: CompareOp,
107        right_agg: AggKind,
108        right_col: String,
109    },
110}
111
112impl HavingExpr {
113    /// Recursively compiles the expression into a SQL fragment using the
114    /// provider-specific placeholder syntax (`?` for SQLite/MySQL, `$N` for
115    /// PostgreSQL).
116    ///
117    /// `param_idx` is advanced past each bound parameter in left-to-right
118    /// order, matching the order produced by [`HavingExpr::collect_params`].
119    /// This ensures PostgreSQL's 1-indexed `$N` placeholders stay contiguous
120    /// with the WHERE clause's placeholders.
121    pub fn to_sql(
122        &self,
123        gen: &dyn crate::provider::ISqlGenerator,
124        param_idx: &mut usize,
125    ) -> String {
126        match self {
127            Self::Compare {
128                agg,
129                col,
130                op,
131                value: _,
132            } => {
133                let placeholder = gen.parameter_placeholder(*param_idx);
134                *param_idx += 1;
135                format!(
136                    "{}({}) {} {}",
137                    agg.sql_name(),
138                    col,
139                    op.sql_name(),
140                    placeholder
141                )
142            }
143            Self::And(left, right) => format!(
144                "({} AND {})",
145                left.to_sql(gen, param_idx),
146                right.to_sql(gen, param_idx)
147            ),
148            Self::Or(left, right) => format!(
149                "({} OR {})",
150                left.to_sql(gen, param_idx),
151                right.to_sql(gen, param_idx)
152            ),
153            Self::Not(inner) => format!("NOT ({})", inner.to_sql(gen, param_idx)),
154            Self::CompareAgg {
155                left_agg,
156                left_col,
157                op,
158                right_agg,
159                right_col,
160            } => format!(
161                "{}({}) {} {}({})",
162                left_agg.sql_name(),
163                left_col,
164                op.sql_name(),
165                right_agg.sql_name(),
166                right_col
167            ),
168        }
169    }
170
171    /// Collects bound parameter values in the same left-to-right order that
172    /// [`HavingExpr::to_sql`] emits placeholders. Used to populate the query
173    /// parameter vector at registration time so that `compile_sql` returns
174    /// params matching the placeholder order.
175    pub fn collect_params(&self) -> Vec<DbValue> {
176        match self {
177            Self::Compare { value, .. } => vec![value.clone()],
178            Self::And(left, right) | Self::Or(left, right) => {
179                let mut v = left.collect_params();
180                v.extend(right.collect_params());
181                v
182            }
183            Self::Not(inner) => inner.collect_params(),
184            Self::CompareAgg { .. } => Vec::new(),
185        }
186    }
187}