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 _ => Err(crate::FraiseQLError::Validation {
60 message: "Unknown WhereClause variant".to_string(),
61 path: None,
62 }),
63 }
64 }
65
66 pub(super) fn generate_direct_column_where_parameterized(
68 &self,
69 field: &str,
70 operator: &WhereOperator,
71 value: &serde_json::Value,
72 params: &mut Vec<serde_json::Value>,
73 ) -> Result<String> {
74 if matches!(operator, WhereOperator::IsNull) {
75 return Ok(format!("{field} IS NULL"));
76 }
77
78 let op_sql = self.operator_to_sql(operator);
79
80 if matches!(operator, WhereOperator::In | WhereOperator::Nin) {
81 let arr = value.as_array().ok_or_else(|| {
82 FraiseQLError::validation("IN/NOT IN operators require array values")
83 })?;
84 let phs: Vec<String> = arr.iter().map(|v| self.emit_value_param(v, params)).collect();
85 return Ok(format!("{field} {op_sql} ({})", phs.join(", ")));
86 }
87
88 if matches!(
89 operator,
90 WhereOperator::Contains
91 | WhereOperator::Startswith
92 | WhereOperator::Endswith
93 | WhereOperator::Like
94 ) {
95 let s = value
96 .as_str()
97 .ok_or_else(|| FraiseQLError::validation("LIKE operators require string values"))?;
98 let (ph, needs_escape) = self.emit_like_pattern_param(operator, s, params);
99 return if needs_escape {
100 Ok(format!("{field} {op_sql} {ph} ESCAPE '!'"))
101 } else {
102 Ok(format!("{field} {op_sql} {ph}"))
103 };
104 }
105
106 if operator.is_case_insensitive() {
107 let s = value.as_str().ok_or_else(|| {
108 FraiseQLError::validation("Case-insensitive operators require string values")
109 })?;
110 return self.generate_case_insensitive_where_parameterized(field, operator, s, params);
111 }
112
113 let ph = self.emit_value_param(value, params);
114 Ok(format!("{field} {op_sql} {ph}"))
115 }
116
117 pub(super) fn generate_jsonb_where_parameterized(
119 &self,
120 jsonb_column: &str,
121 path: &[String],
122 operator: &WhereOperator,
123 value: &serde_json::Value,
124 params: &mut Vec<serde_json::Value>,
125 ) -> Result<String> {
126 let field_path = &path[0];
127 let db_field_path = to_snake_case(field_path);
128 let jsonb_extract = self.jsonb_extract_sql(jsonb_column, &db_field_path);
129 let op_sql = self.operator_to_sql(operator);
130
131 if matches!(operator, WhereOperator::IsNull) {
132 return Ok(format!("{jsonb_extract} IS NULL"));
133 }
134
135 if operator.is_case_insensitive() {
136 let s = value.as_str().ok_or_else(|| {
137 FraiseQLError::validation("Case-insensitive operators require string values")
138 })?;
139 return self.generate_case_insensitive_where_parameterized(
140 &jsonb_extract,
141 operator,
142 s,
143 params,
144 );
145 }
146
147 if matches!(operator, WhereOperator::In | WhereOperator::Nin) {
148 let arr = value.as_array().ok_or_else(|| {
149 FraiseQLError::validation("IN/NOT IN operators require array values")
150 })?;
151 let phs: Vec<String> = arr.iter().map(|v| self.emit_value_param(v, params)).collect();
152 return Ok(format!("{jsonb_extract} {op_sql} ({})", phs.join(", ")));
153 }
154
155 if matches!(
156 operator,
157 WhereOperator::Contains | WhereOperator::Startswith | WhereOperator::Endswith
158 ) {
159 let s = value
160 .as_str()
161 .ok_or_else(|| FraiseQLError::validation("LIKE operators require string values"))?;
162 let (ph, _) = self.emit_like_pattern_param(operator, s, params);
164 return Ok(format!("{jsonb_extract} {op_sql} {ph} ESCAPE '!'"));
165 }
166
167 let ph = self.emit_value_param(value, params);
168 Ok(format!("{jsonb_extract} {op_sql} {ph}"))
169 }
170
171 pub(super) fn generate_case_insensitive_where_parameterized(
173 &self,
174 column: &str,
175 operator: &WhereOperator,
176 value_str: &str,
177 params: &mut Vec<serde_json::Value>,
178 ) -> Result<String> {
179 let op = self.operator_to_sql(operator);
180 if self.database_type == DatabaseType::PostgreSQL {
181 let (ph, needs_escape) = self.emit_like_pattern_param(operator, value_str, params);
182 Ok(if needs_escape {
183 format!("{column} {op} {ph} ESCAPE '!'")
184 } else {
185 format!("{column} {op} {ph}")
186 })
187 } else {
188 let upper = value_str.to_uppercase();
189 let (ph, needs_escape) = self.emit_like_pattern_param(operator, &upper, params);
190 Ok(if needs_escape {
191 format!("UPPER({column}) LIKE {ph} ESCAPE '!'")
192 } else {
193 format!("UPPER({column}) LIKE {ph}")
194 })
195 }
196 }
197
198 pub fn build_where_clause_parameterized(
204 &self,
205 where_clause: &WhereClause,
206 metadata: &FactTableMetadata,
207 params: &mut Vec<serde_json::Value>,
208 ) -> Result<String> {
209 if where_clause.is_empty() {
210 return Ok(String::new());
211 }
212 let cond = self.where_clause_to_sql_parameterized(where_clause, metadata, params)?;
213 Ok(format!("WHERE {cond}"))
214 }
215
216 pub(super) fn build_having_clause_parameterized(
222 &self,
223 having_conditions: &[ValidatedHavingCondition],
224 params: &mut Vec<serde_json::Value>,
225 ) -> Result<String> {
226 if having_conditions.is_empty() {
227 return Ok(String::new());
228 }
229 let mut conditions = Vec::new();
230 for condition in having_conditions {
231 let aggregate_sql = self.aggregate_expression_to_sql(&condition.aggregate)?;
232 let operator_sql = condition.operator.sql_operator();
233 let value_sql = self.emit_value_param(&condition.value, params);
234 conditions.push(format!("{aggregate_sql} {operator_sql} {value_sql}"));
235 }
236 Ok(format!("HAVING {}", conditions.join(" AND ")))
237 }
238}