Skip to main content

fraiseql_core/runtime/aggregation/
where_clause.rs

1//! Parameterized WHERE and HAVING clause SQL generation.
2
3use super::{
4    AggregationSqlGenerator, DatabaseType, FactTableMetadata, FraiseQLError, Result,
5    ValidatedHavingCondition, WhereClause, WhereOperator, to_snake_case,
6};
7
8impl AggregationSqlGenerator {
9    /// Convert a [`WhereClause`] AST to parameterized SQL, appending bind values to `params`.
10    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            // Reason: non_exhaustive requires catch-all for cross-crate matches
59            _ => Err(crate::FraiseQLError::Validation {
60                message: "Unknown WhereClause variant".to_string(),
61                path:    None,
62            }),
63        }
64    }
65
66    /// Parameterized WHERE for a denormalized (direct column) filter.
67    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    /// Parameterized WHERE for a JSONB dimension field.
118    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            // needs_escape is always true for semantic LIKE operators (Contains etc.)
163            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    /// Parameterized case-insensitive WHERE (ILIKE for PostgreSQL, `UPPER()` for others).
172    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    /// Build a parameterized `WHERE …` clause, or an empty string if the clause is empty.
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if WHERE clause generation fails.
203    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    /// Build a parameterized `HAVING …` clause.
217    ///
218    /// # Errors
219    ///
220    /// Returns an error if HAVING clause generation fails.
221    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}