fraiseql_core/runtime/aggregation/
where_clause.rs1use super::{
4 AggregationSqlGenerator, DatabaseType, FactTableMetadata, FraiseQLError, Result,
5 ValidatedHavingCondition, WhereClause, WhereOperator, to_snake_case,
6};
7
8impl AggregationSqlGenerator {
9 pub(super) fn where_clause_to_sql_parameterized(
11 &self,
12 clause: &WhereClause,
13 metadata: &FactTableMetadata,
14 params: &mut Vec<serde_json::Value>,
15 ) -> Result<String> {
16 match clause {
17 WhereClause::Field {
18 path,
19 operator,
20 value,
21 } => {
22 let field_name = &path[0];
23 let is_denormalized =
24 metadata.denormalized_filters.iter().any(|f| f.name == *field_name);
25 if is_denormalized {
26 self.generate_direct_column_where_parameterized(
27 field_name, operator, value, params,
28 )
29 } else {
30 let jsonb_column = &metadata.dimensions.name;
31 self.generate_jsonb_where_parameterized(
32 jsonb_column,
33 path,
34 operator,
35 value,
36 params,
37 )
38 }
39 },
40 WhereClause::And(clauses) => {
41 let conditions: Vec<String> = clauses
42 .iter()
43 .map(|c| self.where_clause_to_sql_parameterized(c, metadata, params))
44 .collect::<Result<Vec<_>>>()?;
45 Ok(format!("({})", conditions.join(" AND ")))
46 },
47 WhereClause::Or(clauses) => {
48 let conditions: Vec<String> = clauses
49 .iter()
50 .map(|c| self.where_clause_to_sql_parameterized(c, metadata, params))
51 .collect::<Result<Vec<_>>>()?;
52 Ok(format!("({})", conditions.join(" OR ")))
53 },
54 WhereClause::Not(inner) => {
55 let s = self.where_clause_to_sql_parameterized(inner, metadata, params)?;
56 Ok(format!("NOT ({s})"))
57 },
58 WhereClause::NativeField {
59 column,
60 pg_cast,
61 operator,
62 value,
63 } => {
64 let col_ref = self.quote_identifier(column);
68 let pre_len = params.len();
69 let sql = self.generate_direct_column_where_parameterized(
70 &col_ref, operator, value, params,
71 )?;
72
73 if self.database_type == DatabaseType::PostgreSQL
77 && !pg_cast.is_empty()
78 && params.len() == pre_len + 1
79 {
80 let ph = self.placeholder(pre_len);
81 Ok(sql.replace(&ph, &format!("{ph}::{pg_cast}")))
82 } else {
83 Ok(sql)
84 }
85 },
86 _ => Err(crate::FraiseQLError::Validation {
88 message: "Unknown WhereClause variant".to_string(),
89 path: None,
90 }),
91 }
92 }
93
94 pub(super) fn generate_direct_column_where_parameterized(
96 &self,
97 field: &str,
98 operator: &WhereOperator,
99 value: &serde_json::Value,
100 params: &mut Vec<serde_json::Value>,
101 ) -> Result<String> {
102 if matches!(operator, WhereOperator::IsNull) {
103 return Ok(format!("{field} IS NULL"));
104 }
105
106 let op_sql = self.operator_to_sql(operator);
107
108 if matches!(operator, WhereOperator::In | WhereOperator::Nin) {
109 let arr = value.as_array().ok_or_else(|| {
110 FraiseQLError::validation("IN/NOT IN operators require array values")
111 })?;
112 let phs: Vec<String> = arr.iter().map(|v| self.emit_value_param(v, params)).collect();
113 return Ok(format!("{field} {op_sql} ({})", phs.join(", ")));
114 }
115
116 if matches!(
117 operator,
118 WhereOperator::Contains
119 | WhereOperator::Startswith
120 | WhereOperator::Endswith
121 | WhereOperator::Like
122 ) {
123 let s = value
124 .as_str()
125 .ok_or_else(|| FraiseQLError::validation("LIKE operators require string values"))?;
126 let (ph, needs_escape) = self.emit_like_pattern_param(operator, s, params);
127 return if needs_escape {
128 Ok(format!("{field} {op_sql} {ph} ESCAPE '!'"))
129 } else {
130 Ok(format!("{field} {op_sql} {ph}"))
131 };
132 }
133
134 if operator.is_case_insensitive() {
135 let s = value.as_str().ok_or_else(|| {
136 FraiseQLError::validation("Case-insensitive operators require string values")
137 })?;
138 return self.generate_case_insensitive_where_parameterized(field, operator, s, params);
139 }
140
141 let ph = self.emit_value_param(value, params);
142 Ok(format!("{field} {op_sql} {ph}"))
143 }
144
145 pub(super) fn generate_jsonb_where_parameterized(
147 &self,
148 jsonb_column: &str,
149 path: &[String],
150 operator: &WhereOperator,
151 value: &serde_json::Value,
152 params: &mut Vec<serde_json::Value>,
153 ) -> Result<String> {
154 let field_path = &path[0];
155 let db_field_path = to_snake_case(field_path);
156 let jsonb_extract = self.jsonb_extract_sql(jsonb_column, &db_field_path);
157 let op_sql = self.operator_to_sql(operator);
158
159 if matches!(operator, WhereOperator::IsNull) {
160 return Ok(format!("{jsonb_extract} IS NULL"));
161 }
162
163 if operator.is_case_insensitive() {
164 let s = value.as_str().ok_or_else(|| {
165 FraiseQLError::validation("Case-insensitive operators require string values")
166 })?;
167 return self.generate_case_insensitive_where_parameterized(
168 &jsonb_extract,
169 operator,
170 s,
171 params,
172 );
173 }
174
175 if matches!(operator, WhereOperator::In | WhereOperator::Nin) {
176 let arr = value.as_array().ok_or_else(|| {
177 FraiseQLError::validation("IN/NOT IN operators require array values")
178 })?;
179 let phs: Vec<String> = arr.iter().map(|v| self.emit_value_param(v, params)).collect();
180 return Ok(format!("{jsonb_extract} {op_sql} ({})", phs.join(", ")));
181 }
182
183 if matches!(
184 operator,
185 WhereOperator::Contains | WhereOperator::Startswith | WhereOperator::Endswith
186 ) {
187 let s = value
188 .as_str()
189 .ok_or_else(|| FraiseQLError::validation("LIKE operators require string values"))?;
190 let (ph, _) = self.emit_like_pattern_param(operator, s, params);
192 return Ok(format!("{jsonb_extract} {op_sql} {ph} ESCAPE '!'"));
193 }
194
195 let ph = self.emit_value_param(value, params);
196 Ok(format!("{jsonb_extract} {op_sql} {ph}"))
197 }
198
199 pub(super) fn generate_case_insensitive_where_parameterized(
201 &self,
202 column: &str,
203 operator: &WhereOperator,
204 value_str: &str,
205 params: &mut Vec<serde_json::Value>,
206 ) -> Result<String> {
207 let op = self.operator_to_sql(operator);
208 if self.database_type == DatabaseType::PostgreSQL {
209 let (ph, needs_escape) = self.emit_like_pattern_param(operator, value_str, params);
210 Ok(if needs_escape {
211 format!("{column} {op} {ph} ESCAPE '!'")
212 } else {
213 format!("{column} {op} {ph}")
214 })
215 } else {
216 let upper = value_str.to_uppercase();
217 let (ph, needs_escape) = self.emit_like_pattern_param(operator, &upper, params);
218 Ok(if needs_escape {
219 format!("UPPER({column}) LIKE {ph} ESCAPE '!'")
220 } else {
221 format!("UPPER({column}) LIKE {ph}")
222 })
223 }
224 }
225
226 pub fn build_where_clause_parameterized(
232 &self,
233 where_clause: &WhereClause,
234 metadata: &FactTableMetadata,
235 params: &mut Vec<serde_json::Value>,
236 ) -> Result<String> {
237 if where_clause.is_empty() {
238 return Ok(String::new());
239 }
240 let cond = self.where_clause_to_sql_parameterized(where_clause, metadata, params)?;
241 Ok(format!("WHERE {cond}"))
242 }
243
244 pub(super) fn build_having_clause_parameterized(
250 &self,
251 having_conditions: &[ValidatedHavingCondition],
252 params: &mut Vec<serde_json::Value>,
253 ) -> Result<String> {
254 if having_conditions.is_empty() {
255 return Ok(String::new());
256 }
257 let mut conditions = Vec::new();
258 for condition in having_conditions {
259 let aggregate_sql = self.aggregate_expression_to_sql(&condition.aggregate)?;
260 let operator_sql = condition.operator.sql_operator();
261 let value_sql = self.emit_value_param(&condition.value, params);
262 conditions.push(format!("{aggregate_sql} {operator_sql} {value_sql}"));
263 }
264 Ok(format!("HAVING {}", conditions.join(" AND ")))
265 }
266}