1use crate::json_ops::{
4 coerced_eq, compare_json, is_truthy, json_to_display_string, json_to_f64, to_json_number,
5};
6
7#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
9pub enum SqlExpr {
10 Column(String),
12 Literal(serde_json::Value),
14 BinaryOp {
16 left: Box<SqlExpr>,
17 op: BinaryOp,
18 right: Box<SqlExpr>,
19 },
20 Negate(Box<SqlExpr>),
22 Function { name: String, args: Vec<SqlExpr> },
24 Cast {
26 expr: Box<SqlExpr>,
27 to_type: CastType,
28 },
29 Case {
31 operand: Option<Box<SqlExpr>>,
32 when_thens: Vec<(SqlExpr, SqlExpr)>,
33 else_expr: Option<Box<SqlExpr>>,
34 },
35 Coalesce(Vec<SqlExpr>),
37 NullIf(Box<SqlExpr>, Box<SqlExpr>),
39 IsNull { expr: Box<SqlExpr>, negated: bool },
41}
42
43#[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#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
64pub enum CastType {
65 Int,
66 Float,
67 String,
68 Bool,
69}
70
71#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
73pub struct ComputedColumn {
74 pub alias: String,
75 pub expr: SqlExpr,
76}
77
78impl SqlExpr {
79 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 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}