nodedb_query/expr/
eval.rs1use nodedb_types::Value;
9
10use crate::value_ops::{coerced_eq, is_truthy, to_value_number, value_to_f64};
11
12use super::binary::eval_binary_op;
13use super::types::SqlExpr;
14
15struct RowScope<'a> {
20 new_doc: &'a Value,
21 old_doc: Option<&'a Value>,
24}
25
26impl<'a> RowScope<'a> {
27 fn column(&self, name: &str) -> Value {
28 self.new_doc.get(name).cloned().unwrap_or(Value::Null)
29 }
30
31 fn old_column(&self, name: &str) -> Value {
32 match self.old_doc {
33 Some(old) => old.get(name).cloned().unwrap_or(Value::Null),
34 None => Value::Null,
35 }
36 }
37}
38
39impl SqlExpr {
40 pub fn eval(&self, doc: &Value) -> Value {
47 self.eval_scope(&RowScope {
48 new_doc: doc,
49 old_doc: None,
50 })
51 }
52
53 pub fn eval_with_old(&self, new_doc: &Value, old_doc: &Value) -> Value {
57 self.eval_scope(&RowScope {
58 new_doc,
59 old_doc: Some(old_doc),
60 })
61 }
62
63 fn eval_scope(&self, scope: &RowScope<'_>) -> Value {
66 match self {
67 SqlExpr::Column(name) => scope.column(name),
68 SqlExpr::OldColumn(name) => scope.old_column(name),
69
70 SqlExpr::Literal(v) => v.clone(),
71
72 SqlExpr::BinaryOp { left, op, right } => {
73 let l = left.eval_scope(scope);
74 let r = right.eval_scope(scope);
75 eval_binary_op(&l, *op, &r)
76 }
77
78 SqlExpr::Negate(inner) => {
79 let v = inner.eval_scope(scope);
80 if let Some(b) = v.as_bool() {
81 Value::Bool(!b)
82 } else {
83 match value_to_f64(&v, false) {
84 Some(n) => to_value_number(-n),
85 None => Value::Null,
86 }
87 }
88 }
89
90 SqlExpr::Function { name, args } => {
91 let evaluated: Vec<Value> = args.iter().map(|a| a.eval_scope(scope)).collect();
92 crate::functions::eval_function(name, &evaluated)
93 }
94
95 SqlExpr::Cast { expr, to_type } => {
96 let v = expr.eval_scope(scope);
97 crate::cast::eval_cast(&v, to_type)
98 }
99
100 SqlExpr::Case {
101 operand,
102 when_thens,
103 else_expr,
104 } => {
105 let op_val = operand.as_ref().map(|e| e.eval_scope(scope));
106 for (when_expr, then_expr) in when_thens {
107 let when_val = when_expr.eval_scope(scope);
108 let matches = match &op_val {
109 Some(ov) => coerced_eq(ov, &when_val),
110 None => is_truthy(&when_val),
111 };
112 if matches {
113 return then_expr.eval_scope(scope);
114 }
115 }
116 match else_expr {
117 Some(e) => e.eval_scope(scope),
118 None => Value::Null,
119 }
120 }
121
122 SqlExpr::Coalesce(exprs) => {
123 for expr in exprs {
124 let v = expr.eval_scope(scope);
125 if !v.is_null() {
126 return v;
127 }
128 }
129 Value::Null
130 }
131
132 SqlExpr::NullIf(a, b) => {
133 let va = a.eval_scope(scope);
134 let vb = b.eval_scope(scope);
135 if coerced_eq(&va, &vb) {
136 Value::Null
137 } else {
138 va
139 }
140 }
141
142 SqlExpr::IsNull { expr, negated } => {
143 let v = expr.eval_scope(scope);
144 let is_null = v.is_null();
145 Value::Bool(if *negated { !is_null } else { is_null })
146 }
147 }
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::super::types::BinaryOp;
154 use super::*;
155
156 fn doc() -> Value {
157 Value::Object(
158 [
159 ("name".to_string(), Value::String("Alice".into())),
160 ("age".to_string(), Value::Integer(30)),
161 ("price".to_string(), Value::Float(10.5)),
162 ("qty".to_string(), Value::Integer(4)),
163 ("active".to_string(), Value::Bool(true)),
164 ("email".to_string(), Value::Null),
165 ]
166 .into_iter()
167 .collect(),
168 )
169 }
170
171 #[test]
172 fn column_ref() {
173 let expr = SqlExpr::Column("name".into());
174 assert_eq!(expr.eval(&doc()), Value::String("Alice".into()));
175 }
176
177 #[test]
178 fn missing_column() {
179 let expr = SqlExpr::Column("missing".into());
180 assert_eq!(expr.eval(&doc()), Value::Null);
181 }
182
183 #[test]
184 fn literal() {
185 let expr = SqlExpr::Literal(Value::Integer(42));
186 assert_eq!(expr.eval(&doc()), Value::Integer(42));
187 }
188
189 #[test]
190 fn add() {
191 let expr = SqlExpr::BinaryOp {
192 left: Box::new(SqlExpr::Column("price".into())),
193 op: BinaryOp::Add,
194 right: Box::new(SqlExpr::Literal(Value::Float(1.5))),
195 };
196 assert_eq!(expr.eval(&doc()), Value::Integer(12));
197 }
198
199 #[test]
200 fn multiply() {
201 let expr = SqlExpr::BinaryOp {
202 left: Box::new(SqlExpr::Column("price".into())),
203 op: BinaryOp::Mul,
204 right: Box::new(SqlExpr::Column("qty".into())),
205 };
206 assert_eq!(expr.eval(&doc()), Value::Integer(42));
207 }
208
209 #[test]
210 fn case_when() {
211 let expr = SqlExpr::Case {
212 operand: None,
213 when_thens: vec![(
214 SqlExpr::BinaryOp {
215 left: Box::new(SqlExpr::Column("age".into())),
216 op: BinaryOp::GtEq,
217 right: Box::new(SqlExpr::Literal(Value::Integer(18))),
218 },
219 SqlExpr::Literal(Value::String("adult".into())),
220 )],
221 else_expr: Some(Box::new(SqlExpr::Literal(Value::String("minor".into())))),
222 };
223 assert_eq!(expr.eval(&doc()), Value::String("adult".into()));
224 }
225
226 #[test]
227 fn coalesce() {
228 let expr = SqlExpr::Coalesce(vec![
229 SqlExpr::Column("email".into()),
230 SqlExpr::Literal(Value::String("default@example.com".into())),
231 ]);
232 assert_eq!(
233 expr.eval(&doc()),
234 Value::String("default@example.com".into())
235 );
236 }
237
238 #[test]
239 fn is_null() {
240 let expr = SqlExpr::IsNull {
241 expr: Box::new(SqlExpr::Column("email".into())),
242 negated: false,
243 };
244 assert_eq!(expr.eval(&doc()), Value::Bool(true));
245 }
246}