Skip to main content

nodedb_query/
expr.rs

1//! SqlExpr AST definition and core evaluation.
2
3use crate::json_ops::{
4    coerced_eq, compare_json, is_truthy, json_to_display_string, json_to_f64, to_json_number,
5};
6
7/// A serializable SQL expression that can be evaluated against a JSON document.
8#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
9pub enum SqlExpr {
10    /// Column reference: extract field value from the document.
11    Column(String),
12    /// Literal value.
13    Literal(serde_json::Value),
14    /// Binary operation: left op right.
15    BinaryOp {
16        left: Box<SqlExpr>,
17        op: BinaryOp,
18        right: Box<SqlExpr>,
19    },
20    /// Unary negation: -expr or NOT expr.
21    Negate(Box<SqlExpr>),
22    /// Scalar function call.
23    Function { name: String, args: Vec<SqlExpr> },
24    /// CAST(expr AS type).
25    Cast {
26        expr: Box<SqlExpr>,
27        to_type: CastType,
28    },
29    /// CASE WHEN cond1 THEN val1 ... ELSE default END.
30    Case {
31        operand: Option<Box<SqlExpr>>,
32        when_thens: Vec<(SqlExpr, SqlExpr)>,
33        else_expr: Option<Box<SqlExpr>>,
34    },
35    /// COALESCE(expr1, expr2, ...): first non-null value.
36    Coalesce(Vec<SqlExpr>),
37    /// NULLIF(expr1, expr2): returns NULL if expr1 = expr2, else expr1.
38    NullIf(Box<SqlExpr>, Box<SqlExpr>),
39    /// IS NULL / IS NOT NULL.
40    IsNull { expr: Box<SqlExpr>, negated: bool },
41}
42
43/// Binary operators.
44#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
45pub enum BinaryOp {
46    Add,
47    Sub,
48    Mul,
49    Div,
50    Mod,
51    Eq,
52    NotEq,
53    Gt,
54    GtEq,
55    Lt,
56    LtEq,
57    And,
58    Or,
59    Concat,
60}
61
62/// Target types for CAST.
63#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
64pub enum CastType {
65    Int,
66    Float,
67    String,
68    Bool,
69}
70
71/// A computed projection column: alias + expression.
72#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
73pub struct ComputedColumn {
74    pub alias: String,
75    pub expr: SqlExpr,
76}
77
78impl SqlExpr {
79    /// Evaluate this expression against a JSON document.
80    ///
81    /// Returns a JSON value. Column references look up fields in the document.
82    /// Missing fields return `null`. Arithmetic on non-numeric values returns `null`.
83    pub fn eval(&self, doc: &serde_json::Value) -> serde_json::Value {
84        match self {
85            SqlExpr::Column(name) => doc.get(name).cloned().unwrap_or(serde_json::Value::Null),
86
87            SqlExpr::Literal(v) => v.clone(),
88
89            SqlExpr::BinaryOp { left, op, right } => {
90                let l = left.eval(doc);
91                let r = right.eval(doc);
92                eval_binary_op(&l, *op, &r)
93            }
94
95            SqlExpr::Negate(inner) => {
96                let v = inner.eval(doc);
97                // Booleans: NOT (logical negation). Numbers: arithmetic negation.
98                if let Some(b) = v.as_bool() {
99                    serde_json::Value::Bool(!b)
100                } else {
101                    match json_to_f64(&v, false) {
102                        Some(n) => to_json_number(-n),
103                        None => serde_json::Value::Null,
104                    }
105                }
106            }
107
108            SqlExpr::Function { name, args } => {
109                let evaluated: Vec<serde_json::Value> = args.iter().map(|a| a.eval(doc)).collect();
110                crate::functions::eval_function(name, &evaluated)
111            }
112
113            SqlExpr::Cast { expr, to_type } => {
114                let v = expr.eval(doc);
115                crate::cast::eval_cast(&v, to_type)
116            }
117
118            SqlExpr::Case {
119                operand,
120                when_thens,
121                else_expr,
122            } => {
123                let op_val = operand.as_ref().map(|e| e.eval(doc));
124                for (when_expr, then_expr) in when_thens {
125                    let when_val = when_expr.eval(doc);
126                    let matches = match &op_val {
127                        Some(ov) => coerced_eq(ov, &when_val),
128                        None => is_truthy(&when_val),
129                    };
130                    if matches {
131                        return then_expr.eval(doc);
132                    }
133                }
134                match else_expr {
135                    Some(e) => e.eval(doc),
136                    None => serde_json::Value::Null,
137                }
138            }
139
140            SqlExpr::Coalesce(exprs) => {
141                for expr in exprs {
142                    let v = expr.eval(doc);
143                    if !v.is_null() {
144                        return v;
145                    }
146                }
147                serde_json::Value::Null
148            }
149
150            SqlExpr::NullIf(a, b) => {
151                let va = a.eval(doc);
152                let vb = b.eval(doc);
153                if coerced_eq(&va, &vb) {
154                    serde_json::Value::Null
155                } else {
156                    va
157                }
158            }
159
160            SqlExpr::IsNull { expr, negated } => {
161                let v = expr.eval(doc);
162                let is_null = v.is_null();
163                serde_json::Value::Bool(if *negated { !is_null } else { is_null })
164            }
165        }
166    }
167}
168
169fn eval_binary_op(
170    left: &serde_json::Value,
171    op: BinaryOp,
172    right: &serde_json::Value,
173) -> serde_json::Value {
174    match op {
175        BinaryOp::Add => match (json_to_f64(left, true), json_to_f64(right, true)) {
176            (Some(a), Some(b)) => to_json_number(a + b),
177            _ => serde_json::Value::Null,
178        },
179        BinaryOp::Sub => match (json_to_f64(left, true), json_to_f64(right, true)) {
180            (Some(a), Some(b)) => to_json_number(a - b),
181            _ => serde_json::Value::Null,
182        },
183        BinaryOp::Mul => match (json_to_f64(left, true), json_to_f64(right, true)) {
184            (Some(a), Some(b)) => to_json_number(a * b),
185            _ => serde_json::Value::Null,
186        },
187        BinaryOp::Div => match (json_to_f64(left, true), json_to_f64(right, true)) {
188            (Some(a), Some(b)) => {
189                if b == 0.0 {
190                    serde_json::Value::Null
191                } else {
192                    to_json_number(a / b)
193                }
194            }
195            _ => serde_json::Value::Null,
196        },
197        BinaryOp::Mod => match (json_to_f64(left, true), json_to_f64(right, true)) {
198            (Some(a), Some(b)) => {
199                if b == 0.0 {
200                    serde_json::Value::Null
201                } else {
202                    to_json_number(a % b)
203                }
204            }
205            _ => serde_json::Value::Null,
206        },
207        BinaryOp::Concat => {
208            let ls = json_to_display_string(left);
209            let rs = json_to_display_string(right);
210            serde_json::Value::String(format!("{ls}{rs}"))
211        }
212        BinaryOp::Eq => serde_json::Value::Bool(coerced_eq(left, right)),
213        BinaryOp::NotEq => serde_json::Value::Bool(!coerced_eq(left, right)),
214        BinaryOp::Gt => {
215            serde_json::Value::Bool(compare_json(left, right) == std::cmp::Ordering::Greater)
216        }
217        BinaryOp::GtEq => {
218            let c = compare_json(left, right);
219            serde_json::Value::Bool(
220                c == std::cmp::Ordering::Greater || c == std::cmp::Ordering::Equal,
221            )
222        }
223        BinaryOp::Lt => {
224            serde_json::Value::Bool(compare_json(left, right) == std::cmp::Ordering::Less)
225        }
226        BinaryOp::LtEq => {
227            let c = compare_json(left, right);
228            serde_json::Value::Bool(c == std::cmp::Ordering::Less || c == std::cmp::Ordering::Equal)
229        }
230        BinaryOp::And => serde_json::Value::Bool(is_truthy(left) && is_truthy(right)),
231        BinaryOp::Or => serde_json::Value::Bool(is_truthy(left) || is_truthy(right)),
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use serde_json::json;
239
240    fn doc() -> serde_json::Value {
241        json!({
242            "name": "Alice",
243            "age": 30,
244            "price": 10.5,
245            "qty": 4,
246            "active": true,
247            "email": null
248        })
249    }
250
251    #[test]
252    fn column_ref() {
253        let expr = SqlExpr::Column("name".into());
254        assert_eq!(expr.eval(&doc()), json!("Alice"));
255    }
256
257    #[test]
258    fn missing_column() {
259        let expr = SqlExpr::Column("missing".into());
260        assert_eq!(expr.eval(&doc()), json!(null));
261    }
262
263    #[test]
264    fn literal() {
265        let expr = SqlExpr::Literal(json!(42));
266        assert_eq!(expr.eval(&doc()), json!(42));
267    }
268
269    #[test]
270    fn add() {
271        let expr = SqlExpr::BinaryOp {
272            left: Box::new(SqlExpr::Column("price".into())),
273            op: BinaryOp::Add,
274            right: Box::new(SqlExpr::Literal(json!(1.5))),
275        };
276        assert_eq!(expr.eval(&doc()), json!(12));
277    }
278
279    #[test]
280    fn multiply() {
281        let expr = SqlExpr::BinaryOp {
282            left: Box::new(SqlExpr::Column("price".into())),
283            op: BinaryOp::Mul,
284            right: Box::new(SqlExpr::Column("qty".into())),
285        };
286        assert_eq!(expr.eval(&doc()), json!(42));
287    }
288
289    #[test]
290    fn case_when() {
291        let expr = SqlExpr::Case {
292            operand: None,
293            when_thens: vec![(
294                SqlExpr::BinaryOp {
295                    left: Box::new(SqlExpr::Column("age".into())),
296                    op: BinaryOp::GtEq,
297                    right: Box::new(SqlExpr::Literal(json!(18))),
298                },
299                SqlExpr::Literal(json!("adult")),
300            )],
301            else_expr: Some(Box::new(SqlExpr::Literal(json!("minor")))),
302        };
303        assert_eq!(expr.eval(&doc()), json!("adult"));
304    }
305
306    #[test]
307    fn coalesce() {
308        let expr = SqlExpr::Coalesce(vec![
309            SqlExpr::Column("email".into()),
310            SqlExpr::Literal(json!("default@example.com")),
311        ]);
312        assert_eq!(expr.eval(&doc()), json!("default@example.com"));
313    }
314
315    #[test]
316    fn is_null() {
317        let expr = SqlExpr::IsNull {
318            expr: Box::new(SqlExpr::Column("email".into())),
319            negated: false,
320        };
321        assert_eq!(expr.eval(&doc()), json!(true));
322    }
323}