1use nodedb_types::Value;
11
12use crate::value_ops::{coerced_eq, is_truthy, to_value_number, value_to_f64};
13
14use super::binary::eval_binary_op;
15use super::types::{SqlExpr, WHOLE_ROW_COLUMN};
16
17#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
31pub enum EvalError {
32 #[error("division by zero")]
33 DivisionByZero,
34 #[error("function {name}() has no evaluator")]
35 UnknownFunction {
36 name: String,
38 },
39 #[error("{function}(): vector dimension mismatch: expected {expected}, got {got}")]
42 VectorDimensionMismatch {
43 function: &'static str,
44 expected: usize,
46 got: usize,
48 },
49 #[error("{function}(): argument {position} must be {expected}, got {got}")]
51 ArgumentType {
52 function: &'static str,
53 position: usize,
55 expected: &'static str,
56 got: &'static str,
58 },
59 #[error("{function}(): invalid JSONPath {path:?}: {reason}")]
61 InvalidJsonPath {
62 function: &'static str,
63 path: String,
64 reason: String,
65 },
66 #[error("{function}(): integer total out of range")]
68 NumericOverflow { function: &'static str },
69}
70
71struct RowScope<'a> {
76 new_doc: &'a Value,
77 old_doc: Option<&'a Value>,
80 excluded_doc: Option<&'a Value>,
84}
85
86impl<'a> RowScope<'a> {
87 fn column(&self, name: &str) -> Value {
88 row_field(self.new_doc, name)
89 }
90
91 fn old_column(&self, name: &str) -> Value {
92 match self.old_doc {
93 Some(old) => row_field(old, name),
94 None => Value::Null,
95 }
96 }
97
98 fn excluded_column(&self, name: &str) -> Value {
99 match self.excluded_doc {
100 Some(excluded) => row_field(excluded, name),
101 None => Value::Null,
102 }
103 }
104}
105
106fn row_field(doc: &Value, name: &str) -> Value {
109 if name == WHOLE_ROW_COLUMN {
110 return doc.clone();
111 }
112 doc.get(name).cloned().unwrap_or(Value::Null)
113}
114
115impl SqlExpr {
116 pub fn eval(&self, doc: &Value) -> Result<Value, EvalError> {
129 self.eval_scope(&RowScope {
130 new_doc: doc,
131 old_doc: None,
132 excluded_doc: None,
133 })
134 }
135
136 pub fn eval_with_old(&self, new_doc: &Value, old_doc: &Value) -> Result<Value, EvalError> {
140 self.eval_scope(&RowScope {
141 new_doc,
142 old_doc: Some(old_doc),
143 excluded_doc: None,
144 })
145 }
146
147 pub fn eval_with_excluded(&self, doc: &Value, excluded: &Value) -> Result<Value, EvalError> {
152 self.eval_scope(&RowScope {
153 new_doc: doc,
154 old_doc: None,
155 excluded_doc: Some(excluded),
156 })
157 }
158
159 fn eval_scope(&self, scope: &RowScope<'_>) -> Result<Value, EvalError> {
167 match self {
168 SqlExpr::Column(name) => Ok(scope.column(name)),
169 SqlExpr::OldColumn(name) => Ok(scope.old_column(name)),
170 SqlExpr::ExcludedColumn(name) => Ok(scope.excluded_column(name)),
171
172 SqlExpr::Literal(v) => Ok(v.clone()),
173
174 SqlExpr::BinaryOp { left, op, right } => {
175 let l = left.eval_scope(scope)?;
176 let r = right.eval_scope(scope)?;
177 eval_binary_op(&l, *op, &r)
178 }
179
180 SqlExpr::Negate(inner) => {
181 let v = inner.eval_scope(scope)?;
182 if let Some(b) = v.as_bool() {
183 Ok(Value::Bool(!b))
184 } else {
185 match value_to_f64(&v, false) {
186 Some(n) => Ok(to_value_number(-n)),
187 None => Ok(Value::Null),
188 }
189 }
190 }
191
192 SqlExpr::Function { name, args } => {
193 let evaluated: Vec<Value> = args
194 .iter()
195 .map(|a| a.eval_scope(scope))
196 .collect::<Result<_, EvalError>>()?;
197 crate::functions::eval_function(name, &evaluated)
198 }
199
200 SqlExpr::Cast { expr, to_type } => {
201 let v = expr.eval_scope(scope)?;
202 Ok(crate::cast::eval_cast(&v, to_type))
203 }
204
205 SqlExpr::Case {
206 operand,
207 when_thens,
208 else_expr,
209 } => {
210 let op_val = match operand {
211 Some(e) => Some(e.eval_scope(scope)?),
212 None => None,
213 };
214 for (when_expr, then_expr) in when_thens {
215 let when_val = when_expr.eval_scope(scope)?;
216 let matches = match &op_val {
217 Some(ov) => coerced_eq(ov, &when_val),
218 None => is_truthy(&when_val),
219 };
220 if matches {
221 return then_expr.eval_scope(scope);
222 }
223 }
224 match else_expr {
225 Some(e) => e.eval_scope(scope),
226 None => Ok(Value::Null),
227 }
228 }
229
230 SqlExpr::Coalesce(exprs) => {
231 for expr in exprs {
232 let v = expr.eval_scope(scope)?;
233 if !v.is_null() {
234 return Ok(v);
235 }
236 }
237 Ok(Value::Null)
238 }
239
240 SqlExpr::NullIf(a, b) => {
241 let va = a.eval_scope(scope)?;
242 let vb = b.eval_scope(scope)?;
243 if coerced_eq(&va, &vb) {
244 Ok(Value::Null)
245 } else {
246 Ok(va)
247 }
248 }
249
250 SqlExpr::IsNull { expr, negated } => {
251 let v = expr.eval_scope(scope)?;
252 let is_null = v.is_null();
253 Ok(Value::Bool(if *negated { !is_null } else { is_null }))
254 }
255 }
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::super::types::BinaryOp;
262 use super::*;
263
264 fn doc() -> Value {
265 Value::Object(
266 [
267 ("name".to_string(), Value::String("Alice".into())),
268 ("age".to_string(), Value::Integer(30)),
269 ("price".to_string(), Value::Float(10.5)),
270 ("qty".to_string(), Value::Integer(4)),
271 ("active".to_string(), Value::Bool(true)),
272 ("email".to_string(), Value::Null),
273 ]
274 .into_iter()
275 .collect(),
276 )
277 }
278
279 #[test]
280 fn column_ref() {
281 let expr = SqlExpr::Column("name".into());
282 assert_eq!(expr.eval(&doc()).unwrap(), Value::String("Alice".into()));
283 }
284
285 #[test]
286 fn missing_column() {
287 let expr = SqlExpr::Column("missing".into());
288 assert_eq!(expr.eval(&doc()).unwrap(), Value::Null);
289 }
290
291 #[test]
292 fn whole_row_column_is_the_document() {
293 let expr = SqlExpr::Column(WHOLE_ROW_COLUMN.into());
294 assert_eq!(expr.eval(&doc()).unwrap(), doc());
295 let old = SqlExpr::OldColumn(WHOLE_ROW_COLUMN.into());
296 let before = Value::Object(Default::default());
297 assert_eq!(old.eval_with_old(&doc(), &before).unwrap(), before);
298 }
299
300 #[test]
301 fn to_jsonb_of_the_whole_row_keeps_every_field_type() {
302 let expr = SqlExpr::Function {
303 name: "to_jsonb".into(),
304 args: vec![SqlExpr::Column(WHOLE_ROW_COLUMN.into())],
305 };
306 assert_eq!(expr.eval(&doc()).unwrap(), doc());
307 }
308
309 #[test]
310 fn literal() {
311 let expr = SqlExpr::Literal(Value::Integer(42));
312 assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(42));
313 }
314
315 #[test]
316 fn add() {
317 let expr = SqlExpr::BinaryOp {
318 left: Box::new(SqlExpr::Column("price".into())),
319 op: BinaryOp::Add,
320 right: Box::new(SqlExpr::Literal(Value::Float(1.5))),
321 };
322 assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(12));
323 }
324
325 #[test]
326 fn multiply() {
327 let expr = SqlExpr::BinaryOp {
328 left: Box::new(SqlExpr::Column("price".into())),
329 op: BinaryOp::Mul,
330 right: Box::new(SqlExpr::Column("qty".into())),
331 };
332 assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(42));
333 }
334
335 #[test]
336 fn case_when() {
337 let expr = SqlExpr::Case {
338 operand: None,
339 when_thens: vec![(
340 SqlExpr::BinaryOp {
341 left: Box::new(SqlExpr::Column("age".into())),
342 op: BinaryOp::GtEq,
343 right: Box::new(SqlExpr::Literal(Value::Integer(18))),
344 },
345 SqlExpr::Literal(Value::String("adult".into())),
346 )],
347 else_expr: Some(Box::new(SqlExpr::Literal(Value::String("minor".into())))),
348 };
349 assert_eq!(expr.eval(&doc()).unwrap(), Value::String("adult".into()));
350 }
351
352 #[test]
353 fn coalesce() {
354 let expr = SqlExpr::Coalesce(vec![
355 SqlExpr::Column("email".into()),
356 SqlExpr::Literal(Value::String("default@example.com".into())),
357 ]);
358 assert_eq!(
359 expr.eval(&doc()).unwrap(),
360 Value::String("default@example.com".into())
361 );
362 }
363
364 #[test]
365 fn is_null() {
366 let expr = SqlExpr::IsNull {
367 expr: Box::new(SqlExpr::Column("email".into())),
368 negated: false,
369 };
370 assert_eq!(expr.eval(&doc()).unwrap(), Value::Bool(true));
371 }
372
373 #[test]
376 fn integer_division_by_zero_errors() {
377 let expr = SqlExpr::BinaryOp {
378 left: Box::new(SqlExpr::Literal(Value::Integer(1))),
379 op: BinaryOp::Div,
380 right: Box::new(SqlExpr::Literal(Value::Integer(0))),
381 };
382 assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero));
383 }
384
385 #[test]
386 fn float_division_by_zero_errors() {
387 let expr = SqlExpr::BinaryOp {
388 left: Box::new(SqlExpr::Literal(Value::Float(1.5))),
389 op: BinaryOp::Div,
390 right: Box::new(SqlExpr::Literal(Value::Float(0.0))),
391 };
392 assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero));
393 }
394
395 #[test]
396 fn decimal_division_by_decimal_zero_errors() {
397 let expr = SqlExpr::BinaryOp {
398 left: Box::new(SqlExpr::Literal(Value::Decimal(
399 rust_decimal::Decimal::new(15, 1),
400 ))),
401 op: BinaryOp::Div,
402 right: Box::new(SqlExpr::Literal(Value::Decimal(
403 rust_decimal::Decimal::ZERO,
404 ))),
405 };
406 assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero));
407 }
408
409 #[test]
410 fn modulo_by_zero_errors() {
411 let expr = SqlExpr::BinaryOp {
412 left: Box::new(SqlExpr::Literal(Value::Integer(5))),
413 op: BinaryOp::Mod,
414 right: Box::new(SqlExpr::Literal(Value::Integer(0))),
415 };
416 assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero));
417 }
418
419 #[test]
420 fn scalar_mod_function_by_zero_errors() {
421 let expr = SqlExpr::Function {
422 name: "mod".into(),
423 args: vec![
424 SqlExpr::Literal(Value::Integer(5)),
425 SqlExpr::Literal(Value::Integer(0)),
426 ],
427 };
428 assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero));
429 }
430
431 #[test]
434 fn case_laziness_untaken_branch_not_evaluated() {
435 let expr = SqlExpr::Case {
436 operand: None,
437 when_thens: vec![(
438 SqlExpr::Literal(Value::Bool(false)),
439 SqlExpr::BinaryOp {
440 left: Box::new(SqlExpr::Literal(Value::Integer(1))),
441 op: BinaryOp::Div,
442 right: Box::new(SqlExpr::Literal(Value::Integer(0))),
443 },
444 )],
445 else_expr: Some(Box::new(SqlExpr::Literal(Value::Integer(0)))),
446 };
447 assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(0));
448 }
449
450 #[test]
453 fn coalesce_laziness_short_circuits_before_division() {
454 let expr = SqlExpr::Coalesce(vec![
455 SqlExpr::Literal(Value::Integer(1)),
456 SqlExpr::BinaryOp {
457 left: Box::new(SqlExpr::Literal(Value::Integer(1))),
458 op: BinaryOp::Div,
459 right: Box::new(SqlExpr::Literal(Value::Integer(0))),
460 },
461 ]);
462 assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(1));
463 }
464
465 #[test]
468 fn null_arithmetic_still_folds_to_null() {
469 let expr = SqlExpr::BinaryOp {
470 left: Box::new(SqlExpr::Literal(Value::Null)),
471 op: BinaryOp::Add,
472 right: Box::new(SqlExpr::Literal(Value::Integer(1))),
473 };
474 assert_eq!(expr.eval(&doc()).unwrap(), Value::Null);
475 }
476
477 #[test]
479 fn valid_division_still_succeeds() {
480 let expr = SqlExpr::BinaryOp {
481 left: Box::new(SqlExpr::Literal(Value::Integer(10))),
482 op: BinaryOp::Div,
483 right: Box::new(SqlExpr::Literal(Value::Integer(2))),
484 };
485 assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(5));
486 }
487}