qail_core/transpiler/
conditions.rs

1use crate::ast::*;
2use super::traits::SqlGenerator;
3
4/// Context for parameterized query building.
5#[derive(Debug, Default)]
6pub struct ParamContext {
7    /// Current parameter index (1-based for Postgres $1, $2, etc.)
8    pub index: usize,
9    /// Collected parameter values in order
10    pub params: Vec<Value>,
11}
12
13impl ParamContext {
14    pub fn new() -> Self {
15        Self { index: 0, params: Vec::new() }
16    }
17
18    /// Add a value and return the placeholder for it.
19    pub fn add_param(&mut self, value: Value, generator: &dyn SqlGenerator) -> String {
20        self.index += 1;
21        self.params.push(value);
22        generator.placeholder(self.index)
23    }
24}
25
26/// Helper to resolve a column identifier that might be a JSON path or a JOIN reference.
27/// 
28/// Heuristic:
29/// 1. Split by '.'
30/// 2. If single part -> quote_identifier
31/// 3. If multiple parts:
32///    - If first part matches table name or any join alias -> Treat as "Table"."Col".
33///    - Else -> Treat as "Col"->"Field" (JSON).
34fn resolve_col_syntax(col: &str, cmd: &QailCmd, generator: &dyn SqlGenerator) -> String {
35    let parts: Vec<&str> = col.split('.').collect();
36    if parts.len() <= 1 {
37        return generator.quote_identifier(col);
38    }
39    
40    let first = parts[0];
41    
42    // Check main table matches
43    if first == cmd.table {
44        // table.col
45        return format!("{}.{}", generator.quote_identifier(first), generator.quote_identifier(parts[1]));
46    }
47    
48    // Check joins matches
49    for join in &cmd.joins {
50        if first == join.table {
51             // join_table.col
52             return format!("{}.{}", generator.quote_identifier(first), generator.quote_identifier(parts[1]));
53        }
54    }
55    
56    // Default: treated as JSON access on the first part
57    let col_name = parts[0];
58    let path = &parts[1..];
59    generator.json_access(col_name, path)
60}
61
62pub trait ConditionToSql {
63    fn to_sql(&self, generator: &Box<dyn SqlGenerator>, context: Option<&QailCmd>) -> String;
64    fn to_value_sql(&self, generator: &Box<dyn SqlGenerator>) -> String;
65    
66    /// Convert condition to SQL with parameterized values.
67    /// Returns the SQL fragment and updates the ParamContext with extracted values.
68    fn to_sql_parameterized(
69        &self, 
70        generator: &Box<dyn SqlGenerator>, 
71        context: Option<&QailCmd>,
72        params: &mut ParamContext
73    ) -> String;
74}
75
76impl ConditionToSql for Condition {
77    /// Convert condition to SQL string.
78    fn to_sql(&self, generator: &Box<dyn SqlGenerator>, context: Option<&QailCmd>) -> String {
79        let col = if let Some(cmd) = context {
80            resolve_col_syntax(&self.column, cmd, generator.as_ref())
81        } else {
82            generator.quote_identifier(&self.column)
83        };
84
85        // Handle array unnest conditions
86        if self.is_array_unnest {
87             let inner_condition = match self.op {
88                Operator::Eq => format!("_el = {}", self.to_value_sql(generator)),
89                Operator::Ne => format!("_el != {}", self.to_value_sql(generator)),
90                Operator::Gt => format!("_el > {}", self.to_value_sql(generator)),
91                Operator::Gte => format!("_el >= {}", self.to_value_sql(generator)),
92                Operator::Lt => format!("_el < {}", self.to_value_sql(generator)),
93                Operator::Lte => format!("_el <= {}", self.to_value_sql(generator)),
94                Operator::Fuzzy => {
95                    let val = match &self.value {
96                        Value::String(s) => format!("'%{}%'", s),
97                        Value::Param(n) => {
98                             let p = generator.placeholder(*n);
99                             generator.string_concat(&["'%'", &p, "'%'"])
100                        },
101                         v => format!("'%{}%'", v),
102                    };
103                    format!("_el {} {}", generator.fuzzy_operator(), val)
104                }
105                _ => format!("_el = {}", self.to_value_sql(generator)),
106            };
107            return format!(
108                "EXISTS (SELECT 1 FROM unnest({}) _el WHERE {})",
109                col, inner_condition
110            );
111        }
112        
113        // Normal conditions
114        match self.op {
115            Operator::Eq => format!("{} = {}", col, self.to_value_sql(generator)),
116            Operator::Ne => format!("{} != {}", col, self.to_value_sql(generator)),
117            Operator::Gt => format!("{} > {}", col, self.to_value_sql(generator)),
118            Operator::Gte => format!("{} >= {}", col, self.to_value_sql(generator)),
119            Operator::Lt => format!("{} < {}", col, self.to_value_sql(generator)),
120            Operator::Lte => format!("{} <= {}", col, self.to_value_sql(generator)),
121            Operator::Fuzzy => {
122                let val = match &self.value {
123                    Value::String(s) => format!("'%{}%'", s),
124                    Value::Param(n) => {
125                        let p = generator.placeholder(*n);
126                        generator.string_concat(&["'%'", &p, "'%'"])
127                    },
128                    v => format!("'%{}%'", v),
129                };
130                format!("{} {} {}", col, generator.fuzzy_operator(), val)
131            }
132            Operator::In => format!("{} = ANY({})", col, self.value), // TODO: ANY() is Postgres specific, move to generator?
133            Operator::NotIn => format!("{} != ALL({})", col, self.value),
134            Operator::IsNull => format!("{} IS NULL", col),
135            Operator::IsNotNull => format!("{} IS NOT NULL", col),
136            Operator::Contains => generator.json_contains(&col, &self.to_value_sql(generator)),
137            Operator::KeyExists => generator.json_key_exists(&col, &self.to_value_sql(generator)),
138            // Postgres 17+ SQL/JSON standard functions
139            Operator::JsonExists => {
140                let path = self.to_value_sql(generator);
141                generator.json_exists(&col, &path.trim_matches('\''))
142            }
143            Operator::JsonQuery => {
144                let path = self.to_value_sql(generator);
145                generator.json_query(&col, &path.trim_matches('\''))
146            }
147            Operator::JsonValue => {
148                let path = self.to_value_sql(generator);
149                format!("{} = {}", generator.json_value(&col, &path.trim_matches('\'')), self.to_value_sql(generator))
150            }
151        }
152    }
153
154    fn to_value_sql(&self, generator: &Box<dyn SqlGenerator>) -> String {
155        match &self.value {
156            Value::Param(n) => generator.placeholder(*n),
157            Value::String(s) => format!("'{}'", s.replace('\'', "''")),
158            Value::Bool(b) => generator.bool_literal(*b),
159            Value::Subquery(cmd) => {
160                // Use ToSql trait to generate subquery SQL
161                use crate::transpiler::ToSql;
162                format!("({})", cmd.to_sql())
163            }
164            v => v.to_string(), 
165        }
166    }
167
168    fn to_sql_parameterized(
169        &self, 
170        generator: &Box<dyn SqlGenerator>, 
171        context: Option<&QailCmd>,
172        params: &mut ParamContext
173    ) -> String {
174        let col = if let Some(cmd) = context {
175            resolve_col_syntax(&self.column, cmd, generator.as_ref())
176        } else {
177            generator.quote_identifier(&self.column)
178        };
179
180        // Helper to convert value to placeholder
181        let value_placeholder = |v: &Value, p: &mut ParamContext| -> String {
182            match v {
183                Value::Param(n) => generator.placeholder(*n), // Already a placeholder
184                Value::Null => "NULL".to_string(),
185                other => p.add_param(other.clone(), generator.as_ref()),
186            }
187        };
188
189        match self.op {
190            Operator::Eq => format!("{} = {}", col, value_placeholder(&self.value, params)),
191            Operator::Ne => format!("{} != {}", col, value_placeholder(&self.value, params)),
192            Operator::Gt => format!("{} > {}", col, value_placeholder(&self.value, params)),
193            Operator::Gte => format!("{} >= {}", col, value_placeholder(&self.value, params)),
194            Operator::Lt => format!("{} < {}", col, value_placeholder(&self.value, params)),
195            Operator::Lte => format!("{} <= {}", col, value_placeholder(&self.value, params)),
196            Operator::Fuzzy => {
197                // For LIKE, we need to wrap in wildcards
198                let placeholder = value_placeholder(&self.value, params);
199                format!("{} {} {}", col, generator.fuzzy_operator(), placeholder)
200            }
201            Operator::IsNull => format!("{} IS NULL", col),
202            Operator::IsNotNull => format!("{} IS NOT NULL", col),
203            Operator::In => format!("{} = ANY({})", col, value_placeholder(&self.value, params)),
204            Operator::NotIn => format!("{} != ALL({})", col, value_placeholder(&self.value, params)),
205            Operator::Contains => generator.json_contains(&col, &value_placeholder(&self.value, params)),
206            Operator::KeyExists => generator.json_key_exists(&col, &value_placeholder(&self.value, params)),
207            Operator::JsonExists => {
208                let path = value_placeholder(&self.value, params);
209                generator.json_exists(&col, &path)
210            }
211            Operator::JsonQuery => {
212                let path = value_placeholder(&self.value, params);
213                generator.json_query(&col, &path)
214            }
215            Operator::JsonValue => {
216                let path = value_placeholder(&self.value, params);
217                format!("{} = {}", generator.json_value(&col, &path), value_placeholder(&self.value, params))
218            }
219        }
220    }
221}