1212use crate :: control:: state:: SharedState ;
1313use crate :: types:: TenantId ;
1414
15+ /// Signal from constant-expression evaluation that a `/` or `%` reached a
16+ /// zero divisor (nodedb issue #227).
17+ ///
18+ /// Kept local to this module — the procedural executor's constant folder is
19+ /// a separate, sqlparser-based tree-walk evaluator with no dependency on
20+ /// `nodedb_query` (whose own `EvalError::DivisionByZero`, from the
21+ /// DataFusion-backed row-expression evaluator fixed under issue #216,
22+ /// covers a different code path entirely). Every other historically
23+ /// `None`-folding case (unknown identifiers, unsupported operators,
24+ /// overflow, non-finite float results) is unaffected and keeps folding to
25+ /// `Ok(None)` — see `try_eval_constant`, `evaluate_to_value`.
26+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , thiserror:: Error ) ]
27+ enum ConstEvalError {
28+ #[ error( "division by zero" ) ]
29+ DivisionByZero ,
30+ }
31+
32+ impl From < ConstEvalError > for crate :: Error {
33+ fn from ( e : ConstEvalError ) -> Self {
34+ match e {
35+ ConstEvalError :: DivisionByZero => Self :: DivisionByZero ,
36+ }
37+ }
38+ }
39+
1540/// Evaluate a SQL boolean condition.
1641///
1742/// Fast path for constant TRUE/FALSE/1/0/NULL. Falls back to constant
@@ -25,7 +50,7 @@ pub async fn evaluate_condition(
2550 return Ok ( result) ;
2651 }
2752
28- match try_eval_constant ( condition_sql) {
53+ match try_eval_constant ( condition_sql) ? {
2954 Some ( nodedb_types:: Value :: Bool ( b) ) => Ok ( b) ,
3055 Some ( nodedb_types:: Value :: Integer ( i) ) => Ok ( i != 0 ) ,
3156 Some ( nodedb_types:: Value :: Float ( f) ) => Ok ( f != 0.0 ) ,
@@ -48,7 +73,7 @@ pub async fn evaluate_int(
4873 return Ok ( v) ;
4974 }
5075
51- match try_eval_constant ( sql) {
76+ match try_eval_constant ( sql) ? {
5277 Some ( nodedb_types:: Value :: Integer ( i) ) => Ok ( i) ,
5378 Some ( nodedb_types:: Value :: Float ( f) ) => Ok ( f as i64 ) ,
5479 _ => Err ( crate :: Error :: PlanError {
@@ -87,7 +112,7 @@ pub async fn evaluate_to_value(
87112 return Ok ( nodedb_types:: Value :: String ( inner. replace ( "''" , "'" ) ) ) ;
88113 }
89114
90- match try_eval_constant ( trimmed) {
115+ match try_eval_constant ( trimmed) ? {
91116 Some ( val) => Ok ( val) ,
92117 None => Ok ( nodedb_types:: Value :: Null ) ,
93118 }
@@ -97,7 +122,13 @@ pub async fn evaluate_to_value(
97122///
98123/// Handles basic arithmetic (+, -, *, /), comparisons, boolean logic,
99124/// and string concatenation on literal values.
100- fn try_eval_constant ( sql : & str ) -> Option < nodedb_types:: Value > {
125+ ///
126+ /// Returns `Ok(None)` for anything this mini-evaluator doesn't handle
127+ /// (unparseable input, unknown identifiers, unsupported operators,
128+ /// overflow) — callers fold that to `Value::Null`, matching historical
129+ /// behavior. Returns `Err(ConstEvalError::DivisionByZero)` only when
130+ /// evaluation reaches a `/` or `%` with a zero divisor (nodedb issue #227).
131+ fn try_eval_constant ( sql : & str ) -> Result < Option < nodedb_types:: Value > , ConstEvalError > {
101132 let trimmed = sql. trim ( ) ;
102133 let expr_str = if trimmed. to_uppercase ( ) . starts_with ( "SELECT " ) {
103134 trimmed[ 7 ..] . trim ( )
@@ -108,24 +139,28 @@ fn try_eval_constant(sql: &str) -> Option<nodedb_types::Value> {
108139 let dialect = sqlparser:: dialect:: PostgreSqlDialect { } ;
109140 let select_sql = format ! ( "SELECT {expr_str}" ) ;
110141 // reconstructed-sql: parser-only evaluates one constant-expression AST without dispatch
111- let stmts = sqlparser:: parser:: Parser :: parse_sql ( & dialect, & select_sql) . ok ( ) ?;
112- let stmt = stmts. first ( ) ?;
142+ let Ok ( stmts) = sqlparser:: parser:: Parser :: parse_sql ( & dialect, & select_sql) else {
143+ return Ok ( None ) ;
144+ } ;
145+ let Some ( stmt) = stmts. first ( ) else {
146+ return Ok ( None ) ;
147+ } ;
113148
114149 if let sqlparser:: ast:: Statement :: Query ( query) = stmt
115150 && let sqlparser:: ast:: SetExpr :: Select ( select) = & * query. body
116151 && let Some ( sqlparser:: ast:: SelectItem :: UnnamedExpr ( expr) ) = select. projection . first ( )
117152 {
118153 return eval_expr ( expr) ;
119154 }
120- None
155+ Ok ( None )
121156}
122157
123158/// Recursively evaluate a sqlparser expression tree.
124- fn eval_expr ( expr : & sqlparser:: ast:: Expr ) -> Option < nodedb_types:: Value > {
159+ fn eval_expr ( expr : & sqlparser:: ast:: Expr ) -> Result < Option < nodedb_types:: Value > , ConstEvalError > {
125160 use sqlparser:: ast:: { Expr , UnaryOperator , Value } ;
126161
127162 match expr {
128- Expr :: Value ( v) => match & v. value {
163+ Expr :: Value ( v) => Ok ( match & v. value {
129164 Value :: Number ( n, _) => {
130165 if let Ok ( i) = n. parse :: < i64 > ( ) {
131166 Some ( nodedb_types:: Value :: Integer ( i) )
@@ -139,94 +174,146 @@ fn eval_expr(expr: &sqlparser::ast::Expr) -> Option<nodedb_types::Value> {
139174 Value :: Boolean ( b) => Some ( nodedb_types:: Value :: Bool ( * b) ) ,
140175 Value :: Null => Some ( nodedb_types:: Value :: Null ) ,
141176 _ => None ,
142- } ,
177+ } ) ,
143178 Expr :: UnaryOp {
144179 op : UnaryOperator :: Minus ,
145180 expr : inner,
146- } => match eval_expr ( inner) ? {
147- nodedb_types:: Value :: Integer ( i) => Some ( nodedb_types:: Value :: Integer ( -i) ) ,
148- nodedb_types:: Value :: Float ( f) => Some ( nodedb_types:: Value :: Float ( -f) ) ,
181+ } => Ok ( match eval_expr ( inner) ? {
182+ Some ( nodedb_types:: Value :: Integer ( i) ) => Some ( nodedb_types:: Value :: Integer ( -i) ) ,
183+ Some ( nodedb_types:: Value :: Float ( f) ) => Some ( nodedb_types:: Value :: Float ( -f) ) ,
149184 _ => None ,
150- } ,
185+ } ) ,
151186 Expr :: UnaryOp {
152187 op : UnaryOperator :: Not ,
153188 expr : inner,
154- } => match eval_expr ( inner) ? {
155- nodedb_types:: Value :: Bool ( b) => Some ( nodedb_types:: Value :: Bool ( !b) ) ,
189+ } => Ok ( match eval_expr ( inner) ? {
190+ Some ( nodedb_types:: Value :: Bool ( b) ) => Some ( nodedb_types:: Value :: Bool ( !b) ) ,
156191 _ => None ,
192+ } ) ,
193+ Expr :: BinaryOp { left, op, right } => match ( eval_expr ( left) ?, eval_expr ( right) ?) {
194+ ( Some ( l) , Some ( r) ) => eval_binary_op ( & l, op, & r) ,
195+ _ => Ok ( None ) ,
157196 } ,
158- Expr :: BinaryOp { left, op, right } => {
159- let l = eval_expr ( left) ?;
160- let r = eval_expr ( right) ?;
161- eval_binary_op ( & l, op, & r)
162- }
163197 Expr :: Nested ( inner) => eval_expr ( inner) ,
164- _ => None ,
198+ _ => Ok ( None ) ,
165199 }
166200}
167201
168202fn eval_binary_op (
169203 l : & nodedb_types:: Value ,
170204 op : & sqlparser:: ast:: BinaryOperator ,
171205 r : & nodedb_types:: Value ,
172- ) -> Option < nodedb_types:: Value > {
206+ ) -> Result < Option < nodedb_types:: Value > , ConstEvalError > {
173207 use nodedb_types:: Value ;
174208 use sqlparser:: ast:: BinaryOperator :: * ;
175209
176210 match ( l, r) {
177211 ( Value :: Integer ( a) , Value :: Integer ( b) ) => match op {
178- Plus => Some ( Value :: Integer ( a. checked_add ( * b) ?) ) ,
179- Minus => Some ( Value :: Integer ( a. checked_sub ( * b) ?) ) ,
180- Multiply => Some ( Value :: Integer ( a. checked_mul ( * b) ?) ) ,
181- Divide if * b != 0 => Some ( Value :: Integer ( a. checked_div ( * b) ?) ) ,
182- Modulo if * b != 0 => Some ( Value :: Integer ( a. checked_rem ( * b) ?) ) ,
183- Gt => Some ( Value :: Bool ( a > b) ) ,
184- GtEq => Some ( Value :: Bool ( a >= b) ) ,
185- Lt => Some ( Value :: Bool ( a < b) ) ,
186- LtEq => Some ( Value :: Bool ( a <= b) ) ,
187- Eq => Some ( Value :: Bool ( a == b) ) ,
188- NotEq => Some ( Value :: Bool ( a != b) ) ,
189- _ => None ,
212+ Plus => Ok ( a. checked_add ( * b) . map ( Value :: Integer ) ) ,
213+ Minus => Ok ( a. checked_sub ( * b) . map ( Value :: Integer ) ) ,
214+ Multiply => Ok ( a. checked_mul ( * b) . map ( Value :: Integer ) ) ,
215+ Divide if * b != 0 => Ok ( a. checked_div ( * b) . map ( Value :: Integer ) ) ,
216+ Divide => Err ( ConstEvalError :: DivisionByZero ) ,
217+ Modulo if * b != 0 => Ok ( a. checked_rem ( * b) . map ( Value :: Integer ) ) ,
218+ Modulo => Err ( ConstEvalError :: DivisionByZero ) ,
219+ Gt => Ok ( Some ( Value :: Bool ( a > b) ) ) ,
220+ GtEq => Ok ( Some ( Value :: Bool ( a >= b) ) ) ,
221+ Lt => Ok ( Some ( Value :: Bool ( a < b) ) ) ,
222+ LtEq => Ok ( Some ( Value :: Bool ( a <= b) ) ) ,
223+ Eq => Ok ( Some ( Value :: Bool ( a == b) ) ) ,
224+ NotEq => Ok ( Some ( Value :: Bool ( a != b) ) ) ,
225+ _ => Ok ( None ) ,
190226 } ,
191227 ( Value :: Float ( a) , Value :: Float ( b) ) => match op {
192- Plus => Some ( Value :: Float ( a + b) ) ,
193- Minus => Some ( Value :: Float ( a - b) ) ,
194- Multiply => Some ( Value :: Float ( a * b) ) ,
195- Divide if * b != 0.0 && b. is_finite ( ) && * b != -0.0 => {
228+ Plus => Ok ( Some ( Value :: Float ( a + b) ) ) ,
229+ Minus => Ok ( Some ( Value :: Float ( a - b) ) ) ,
230+ Multiply => Ok ( Some ( Value :: Float ( a * b) ) ) ,
231+ // Non-finite divisor (NaN/Infinity) is unsupported input, not a
232+ // zero-divisor — keeps folding to `None` as before.
233+ Divide if !b. is_finite ( ) => Ok ( None ) ,
234+ Divide if * b == 0.0 => Err ( ConstEvalError :: DivisionByZero ) ,
235+ Divide => {
196236 let result = a / b;
197237 if result. is_finite ( ) {
198- Some ( Value :: Float ( result) )
238+ Ok ( Some ( Value :: Float ( result) ) )
199239 } else {
200- None
240+ Ok ( None )
201241 }
202242 }
203- Gt => Some ( Value :: Bool ( a > b) ) ,
204- GtEq => Some ( Value :: Bool ( a >= b) ) ,
205- Lt => Some ( Value :: Bool ( a < b) ) ,
206- LtEq => Some ( Value :: Bool ( a <= b) ) ,
207- Eq => Some ( Value :: Bool ( a == b) ) ,
208- NotEq => Some ( Value :: Bool ( a != b) ) ,
209- _ => None ,
243+ Gt => Ok ( Some ( Value :: Bool ( a > b) ) ) ,
244+ GtEq => Ok ( Some ( Value :: Bool ( a >= b) ) ) ,
245+ Lt => Ok ( Some ( Value :: Bool ( a < b) ) ) ,
246+ LtEq => Ok ( Some ( Value :: Bool ( a <= b) ) ) ,
247+ Eq => Ok ( Some ( Value :: Bool ( a == b) ) ) ,
248+ NotEq => Ok ( Some ( Value :: Bool ( a != b) ) ) ,
249+ _ => Ok ( None ) ,
210250 } ,
211251 ( Value :: Integer ( a) , Value :: Float ( b) ) => {
212252 eval_binary_op ( & Value :: Float ( * a as f64 ) , op, & Value :: Float ( * b) )
213253 }
214254 ( Value :: Float ( a) , Value :: Integer ( b) ) => {
215255 eval_binary_op ( & Value :: Float ( * a) , op, & Value :: Float ( * b as f64 ) )
216256 }
217- ( Value :: Bool ( a) , Value :: Bool ( b) ) => match op {
257+ ( Value :: Bool ( a) , Value :: Bool ( b) ) => Ok ( match op {
218258 And => Some ( Value :: Bool ( * a && * b) ) ,
219259 Or => Some ( Value :: Bool ( * a || * b) ) ,
220260 Eq => Some ( Value :: Bool ( a == b) ) ,
221261 NotEq => Some ( Value :: Bool ( a != b) ) ,
222262 _ => None ,
223- } ,
224- ( Value :: String ( a) , Value :: String ( b) ) => match op {
263+ } ) ,
264+ ( Value :: String ( a) , Value :: String ( b) ) => Ok ( match op {
225265 StringConcat => Some ( Value :: String ( format ! ( "{a}{b}" ) ) ) ,
226266 Eq => Some ( Value :: Bool ( a == b) ) ,
227267 NotEq => Some ( Value :: Bool ( a != b) ) ,
228268 _ => None ,
229- } ,
230- _ => None ,
269+ } ) ,
270+ _ => Ok ( None ) ,
271+ }
272+ }
273+
274+ #[ cfg( test) ]
275+ mod tests {
276+ use super :: * ;
277+
278+ // ── Division/modulo by zero (nodedb issue #227) ─────────────────────────
279+
280+ #[ test]
281+ fn integer_division_by_zero_signals_error ( ) {
282+ assert_eq ! (
283+ try_eval_constant( "10 / 0" ) ,
284+ Err ( ConstEvalError :: DivisionByZero )
285+ ) ;
286+ }
287+
288+ #[ test]
289+ fn integer_modulo_by_zero_signals_error ( ) {
290+ assert_eq ! (
291+ try_eval_constant( "10 % 0" ) ,
292+ Err ( ConstEvalError :: DivisionByZero )
293+ ) ;
294+ }
295+
296+ #[ test]
297+ fn float_division_by_zero_signals_error ( ) {
298+ assert_eq ! (
299+ try_eval_constant( "10.0 / 0.0" ) ,
300+ Err ( ConstEvalError :: DivisionByZero )
301+ ) ;
302+ }
303+
304+ /// Control: a non-zero divisor still computes normally.
305+ #[ test]
306+ fn integer_division_by_nonzero_still_computes ( ) {
307+ assert_eq ! (
308+ try_eval_constant( "10 / 2" ) ,
309+ Ok ( Some ( nodedb_types:: Value :: Integer ( 5 ) ) )
310+ ) ;
311+ }
312+
313+ /// Control: an unrelated `None` case (unknown identifier) is unaffected
314+ /// by the division-by-zero signal and still folds to `Ok(None)`.
315+ #[ test]
316+ fn unknown_identifier_still_folds_to_none ( ) {
317+ assert_eq ! ( try_eval_constant( "some_unbound_identifier" ) , Ok ( None ) ) ;
231318 }
232319}
0 commit comments