Skip to main content

qail_core/transpiler/
conditions.rs

1use super::ToSql;
2use super::traits::SqlGenerator;
3use crate::ast::*;
4
5/// Context for parameterized query building.
6#[derive(Debug, Default)]
7pub struct ParamContext {
8    pub index: usize,
9    /// Collected parameter values in order
10    pub params: Vec<Value>,
11    /// Names of named parameters in order (for :name → $n mapping)
12    pub named_params: Vec<String>,
13}
14
15impl ParamContext {
16    pub fn new() -> Self {
17        Self {
18            index: 0,
19            params: Vec::new(),
20            named_params: Vec::new(),
21        }
22    }
23
24    /// Add a value and return the placeholder for it.
25    pub fn add_param(&mut self, value: Value, generator: &dyn SqlGenerator) -> String {
26        self.index += 1;
27        self.params.push(value);
28        generator.placeholder(self.index)
29    }
30
31    /// Add a named parameter and return the placeholder for it.
32    pub fn add_named_param(&mut self, name: String, generator: &dyn SqlGenerator) -> String {
33        self.index += 1;
34        self.named_params.push(name);
35        generator.placeholder(self.index)
36    }
37}
38
39/// Heuristic:
40/// 1. Split by '.'
41/// 2. If single part -> quote_identifier
42/// 3. If multiple parts:
43///    - If first part matches table name or any join alias -> Treat as "Table"."Col".
44///    - Else -> Treat as "Col"->"Field" (JSON).
45fn resolve_col_syntax(col: &str, cmd: &Qail, generator: &dyn SqlGenerator) -> String {
46    if col.starts_with('{') && col.ends_with('}') {
47        return col[1..col.len() - 1].to_string();
48    }
49
50    let parts: Vec<&str> = col.split('.').collect();
51    if parts.len() <= 1 {
52        return generator.quote_identifier(col);
53    }
54
55    let first = parts[0];
56
57    if first == cmd.table {
58        // table.col
59        return format!(
60            "{}.{}",
61            generator.quote_identifier(first),
62            generator.quote_identifier(parts[1])
63        );
64    }
65
66    for join in &cmd.joins {
67        if first == join.table {
68            // join_table.col
69            return format!(
70                "{}.{}",
71                generator.quote_identifier(first),
72                generator.quote_identifier(parts[1])
73            );
74        }
75    }
76
77    // Default: treated as JSON access on the first part
78    let col_name = parts[0];
79    let path = &parts[1..];
80    generator.json_access(col_name, path)
81}
82
83#[allow(clippy::borrowed_box)]
84pub trait ConditionToSql {
85    fn to_sql(&self, generator: &Box<dyn SqlGenerator>, context: Option<&Qail>) -> String;
86    fn to_value_sql(&self, generator: &Box<dyn SqlGenerator>) -> String;
87
88    /// Convert condition to SQL with parameterized values.
89    fn to_sql_parameterized(
90        &self,
91        generator: &Box<dyn SqlGenerator>,
92        context: Option<&Qail>,
93        params: &mut ParamContext,
94    ) -> String;
95}
96
97impl ConditionToSql for Condition {
98    /// Convert condition to SQL string.
99    fn to_sql(&self, generator: &Box<dyn SqlGenerator>, context: Option<&Qail>) -> String {
100        let col = match &self.left {
101            Expr::Named(name) => {
102                if name.starts_with('{') && name.ends_with('}') {
103                    name[1..name.len() - 1].to_string()
104                } else if let Some(cmd) = context {
105                    resolve_col_syntax(name, cmd, generator.as_ref())
106                } else {
107                    generator.quote_identifier(name)
108                }
109            }
110            Expr::JsonAccess {
111                column,
112                path_segments,
113                ..
114            } => {
115                let mut result = generator.quote_identifier(column);
116                for (path, as_text) in path_segments {
117                    let op = if *as_text { "->>" } else { "->" };
118                    if path.parse::<i64>().is_ok() {
119                        result.push_str(&format!("{}{}", op, path));
120                    } else {
121                        result.push_str(&format!("{}'{}'", op, path));
122                    }
123                }
124                result
125            }
126            expr => expr.to_string(),
127        };
128
129        if self.is_array_unnest {
130            let inner_condition = match self.op {
131                Operator::Eq => format!("_el = {}", self.to_value_sql(generator)),
132                Operator::Ne => format!("_el != {}", self.to_value_sql(generator)),
133                Operator::Gt => format!("_el > {}", self.to_value_sql(generator)),
134                Operator::Gte => format!("_el >= {}", self.to_value_sql(generator)),
135                Operator::Lt => format!("_el < {}", self.to_value_sql(generator)),
136                Operator::Lte => format!("_el <= {}", self.to_value_sql(generator)),
137                Operator::Fuzzy => {
138                    let val = match &self.value {
139                        Value::String(s) => format!("'%{}%'", s),
140                        Value::Param(n) => {
141                            let p = generator.placeholder(*n);
142                            generator.string_concat(&["'%'", &p, "'%'"])
143                        }
144                        v => format!("'%{}%'", v),
145                    };
146                    format!("_el {} {}", generator.fuzzy_operator(), val)
147                }
148                _ => format!("_el = {}", self.to_value_sql(generator)),
149            };
150            return format!(
151                "EXISTS (SELECT 1 FROM unnest({}) _el WHERE {})",
152                col, inner_condition
153            );
154        }
155
156        // Normal conditions
157        // Simple binary operators use sql_symbol() for unified handling
158        if self.op.is_simple_binary() {
159            return format!(
160                "{} {} {}",
161                col,
162                self.op.sql_symbol(),
163                self.to_value_sql(generator)
164            );
165        }
166
167        // Special operators that need custom handling
168        match self.op {
169            Operator::Fuzzy => {
170                let val = match &self.value {
171                    Value::String(s) => format!("'%{}%'", s),
172                    Value::Param(n) => {
173                        let p = generator.placeholder(*n);
174                        generator.string_concat(&["'%'", &p, "'%'"])
175                    }
176                    v => format!("'%{}%'", v),
177                };
178                format!("{} {} {}", col, generator.fuzzy_operator(), val)
179            }
180            Operator::In => generator.in_array(&col, &format!("{}", self.value)),
181            Operator::NotIn => generator.not_in_array(&col, &format!("{}", self.value)),
182            Operator::IsNull => format!("{} IS NULL", col),
183            Operator::IsNotNull => format!("{} IS NOT NULL", col),
184            Operator::Contains => generator.json_contains(&col, &self.to_value_sql(generator)),
185            Operator::KeyExists => generator.json_key_exists(&col, &self.to_value_sql(generator)),
186            // Postgres 17+ SQL/JSON standard functions
187            Operator::JsonExists => {
188                let path = self.to_value_sql(generator);
189                generator.json_exists(&col, path.trim_matches('\''))
190            }
191            Operator::JsonQuery => {
192                let path = self.to_value_sql(generator);
193                generator.json_query(&col, path.trim_matches('\''))
194            }
195            Operator::JsonValue => {
196                let path = self.to_value_sql(generator);
197                format!(
198                    "{} = {}",
199                    generator.json_value(&col, path.trim_matches('\'')),
200                    self.to_value_sql(generator)
201                )
202            }
203            Operator::Between => {
204                // Value is Array with 2 elements [min, max]
205                if let Value::Array(vals) = &self.value
206                    && vals.len() >= 2
207                {
208                    return format!("{} BETWEEN {} AND {}", col, vals[0], vals[1]);
209                }
210                format!("{} BETWEEN {}", col, self.value)
211            }
212            Operator::NotBetween => {
213                if let Value::Array(vals) = &self.value
214                    && vals.len() >= 2
215                {
216                    return format!("{} NOT BETWEEN {} AND {}", col, vals[0], vals[1]);
217                }
218                format!("{} NOT BETWEEN {}", col, self.value)
219            }
220            Operator::Exists => {
221                // EXISTS takes subquery, col is ignored
222                if let Value::Subquery(cmd) = &self.value {
223                    let subquery_sql = cmd.to_sql();
224                    format!("EXISTS ({})", subquery_sql)
225                } else {
226                    format!("EXISTS ({})", self.value)
227                }
228            }
229            Operator::NotExists => {
230                if let Value::Subquery(cmd) = &self.value {
231                    let subquery_sql = cmd.to_sql();
232                    format!("NOT EXISTS ({})", subquery_sql)
233                } else {
234                    format!("NOT EXISTS ({})", self.value)
235                }
236            }
237            // Simple binary operators are handled above by is_simple_binary()
238            _ => format!(
239                "{} {} {}",
240                col,
241                self.op.sql_symbol(),
242                self.to_value_sql(generator)
243            ),
244        }
245    }
246
247    fn to_value_sql(&self, generator: &Box<dyn SqlGenerator>) -> String {
248        match &self.value {
249            Value::Param(n) => generator.placeholder(*n),
250            Value::String(s) => format!("'{}'", s.replace('\'', "''")),
251            Value::Bool(b) => generator.bool_literal(*b),
252            Value::Subquery(cmd) => {
253                // Use ToSql trait to generate subquery SQL
254                use crate::transpiler::ToSql;
255                format!("({})", cmd.to_sql())
256            }
257            Value::Column(col) => {
258                // Determine if it's "table"."col" or just "col"
259                // Use resolve_col_syntax logic? Or simply quote?
260                // Usually Join ON RHS is just an identifier, but transpiler logic in resolve_col_syntax
261                // requires a Qail context which we don't have here efficiently (we have context: Option<&Qail> in other methods but strictly to_value_sql signature is fixed?).
262                // Wait, to_value_sql signature is: fn to_value_sql(&self, generator: &Box<dyn SqlGenerator>) -> String
263                // We don't have context here.
264                // However, we can use a basic split check or just quote full string.
265                // If col is "users.id", generator.quote_identifier("users.id") might quote the whole thing which is wrong for Postgres ("users.id" vs "users"."id").
266                // We should manually split if dot is present.
267                if col.contains('.') {
268                    let parts: Vec<&str> = col.split('.').collect();
269                    parts
270                        .iter()
271                        .map(|p| generator.quote_identifier(p))
272                        .collect::<Vec<String>>()
273                        .join(".")
274                } else {
275                    generator.quote_identifier(col)
276                }
277            }
278            v => v.to_string(),
279        }
280    }
281
282    fn to_sql_parameterized(
283        &self,
284        generator: &Box<dyn SqlGenerator>,
285        context: Option<&Qail>,
286        params: &mut ParamContext,
287    ) -> String {
288        let col = match &self.left {
289            Expr::Named(name) => {
290                if name.starts_with('{') && name.ends_with('}') {
291                    name[1..name.len() - 1].to_string()
292                } else if let Some(cmd) = context {
293                    resolve_col_syntax(name, cmd, generator.as_ref())
294                } else {
295                    generator.quote_identifier(name)
296                }
297            }
298            Expr::JsonAccess {
299                column,
300                path_segments,
301                ..
302            } => {
303                let mut result = generator.quote_identifier(column);
304                for (path, as_text) in path_segments {
305                    let op = if *as_text { "->>" } else { "->" };
306                    if path.parse::<i64>().is_ok() {
307                        result.push_str(&format!("{}{}", op, path));
308                    } else {
309                        result.push_str(&format!("{}'{}'", op, path));
310                    }
311                }
312                result
313            }
314            expr => expr.to_string(),
315        };
316
317        // Helper to convert value to placeholder
318        let value_placeholder = |v: &Value, p: &mut ParamContext| -> String {
319            match v {
320                Value::Param(n) => generator.placeholder(*n), // Already a placeholder
321                Value::NamedParam(name) => p.add_named_param(name.clone(), generator.as_ref()),
322                Value::Null => "NULL".to_string(),
323                other => p.add_param(other.clone(), generator.as_ref()),
324            }
325        };
326
327        match self.op {
328            Operator::Eq => {
329                // Raw conditions ({...}, op=Eq, value=Null) are now handled at col resolution
330                if matches!(self.value, Value::Null)
331                    && let Expr::Named(name) = &self.left
332                    && name.starts_with('{')
333                    && name.ends_with('}')
334                {
335                    return col; // col already contains raw SQL content
336                }
337                format!("{} = {}", col, value_placeholder(&self.value, params))
338            }
339            Operator::Fuzzy => {
340                // For LIKE, we need to wrap in wildcards
341                let placeholder = value_placeholder(&self.value, params);
342                format!("{} {} {}", col, generator.fuzzy_operator(), placeholder)
343            }
344            Operator::IsNull => format!("{} IS NULL", col),
345            Operator::IsNotNull => format!("{} IS NOT NULL", col),
346            Operator::In => generator.in_array(&col, &value_placeholder(&self.value, params)),
347            Operator::NotIn => {
348                generator.not_in_array(&col, &value_placeholder(&self.value, params))
349            }
350            Operator::Contains => {
351                generator.json_contains(&col, &value_placeholder(&self.value, params))
352            }
353            Operator::KeyExists => {
354                generator.json_key_exists(&col, &value_placeholder(&self.value, params))
355            }
356            Operator::JsonExists => {
357                let path = value_placeholder(&self.value, params);
358                generator.json_exists(&col, &path)
359            }
360            Operator::JsonQuery => {
361                let path = value_placeholder(&self.value, params);
362                generator.json_query(&col, &path)
363            }
364            Operator::JsonValue => {
365                let path = value_placeholder(&self.value, params);
366                format!(
367                    "{} = {}",
368                    generator.json_value(&col, &path),
369                    value_placeholder(&self.value, params)
370                )
371            }
372            Operator::Between => {
373                if let Value::Array(vals) = &self.value
374                    && vals.len() >= 2
375                {
376                    return format!("{} BETWEEN {} AND {}", col, vals[0], vals[1]);
377                }
378                format!("{} BETWEEN {}", col, self.value)
379            }
380            Operator::NotBetween => {
381                if let Value::Array(vals) = &self.value
382                    && vals.len() >= 2
383                {
384                    return format!("{} NOT BETWEEN {} AND {}", col, vals[0], vals[1]);
385                }
386                format!("{} NOT BETWEEN {}", col, self.value)
387            }
388            Operator::Exists => {
389                if let Value::Subquery(cmd) = &self.value {
390                    let subquery_sql = cmd.to_sql();
391                    format!("EXISTS ({})", subquery_sql)
392                } else {
393                    format!("EXISTS ({})", self.value)
394                }
395            }
396            Operator::NotExists => {
397                if let Value::Subquery(cmd) = &self.value {
398                    let subquery_sql = cmd.to_sql();
399                    format!("NOT EXISTS ({})", subquery_sql)
400                } else {
401                    format!("NOT EXISTS ({})", self.value)
402                }
403            }
404            // Simple operators (Ne, Gt, Gte, Lt, Lte, Like, NotLike, ILike, NotILike) use sql_symbol()
405            _ => format!(
406                "{} {} {}",
407                col,
408                self.op.sql_symbol(),
409                value_placeholder(&self.value, params)
410            ),
411        }
412    }
413}