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}