@@ -28,23 +28,29 @@ fn render_value(value: &LoroValue) -> String {
2828}
2929
3030impl Validator {
31+ ///
32+ /// Returns `Err(EvalError)` when a CHECK predicate cannot be *evaluated*
33+ /// (division/modulo by zero). This is distinct from an
34+ /// ordinary constraint failure (`Ok(Some(Violation))`): the caller lifts it
35+ /// to [`ValidationOutcome::EvalError`], a hard statement failure that
36+ /// declarative conflict policies must never "resolve".
3137 pub ( crate ) fn check_constraint (
3238 & self ,
3339 state : & impl RowLookup ,
3440 change : & ProposedChange ,
3541 constraint : & Constraint ,
36- ) -> Option < Violation > {
42+ ) -> Result < Option < Violation > , nodedb_query :: EvalError > {
3743 match & constraint. kind {
38- ConstraintKind :: Unique => self . check_unique ( state, change, constraint) ,
44+ ConstraintKind :: Unique => Ok ( self . check_unique ( state, change, constraint) ) ,
3945 ConstraintKind :: ForeignKey {
4046 ref_collection,
4147 ref_key,
4248 }
4349 | ConstraintKind :: BiTemporalFK {
4450 ref_collection,
4551 ref_key,
46- } => self . check_foreign_key ( state, change, constraint, ref_collection, ref_key) ,
47- ConstraintKind :: NotNull => self . check_not_null ( change, constraint) ,
52+ } => Ok ( self . check_foreign_key ( state, change, constraint, ref_collection, ref_key) ) ,
53+ ConstraintKind :: NotNull => Ok ( self . check_not_null ( change, constraint) ) ,
4854 ConstraintKind :: Check { expr, .. } => self . check_expr ( change, constraint, expr) ,
4955 }
5056 }
@@ -60,7 +66,7 @@ impl Validator {
6066 change : & ProposedChange ,
6167 constraint : & Constraint ,
6268 expr : & str ,
63- ) -> Option < Violation > {
69+ ) -> Result < Option < Violation > , nodedb_query :: EvalError > {
6470 // Build a row Value::Object from the proposed change's fields so the
6571 // expression evaluator can resolve column references by name.
6672 let mut row = std:: collections:: HashMap :: with_capacity ( change. fields . len ( ) ) ;
@@ -72,7 +78,10 @@ impl Validator {
7278 let parsed = match nodedb_query:: expr_parse:: parse_generated_expr ( expr) {
7379 Ok ( ( expr, _deps) ) => expr,
7480 Err ( e) => {
75- return Some ( Violation {
81+ // A malformed *stored* predicate is a catalog-integrity failure,
82+ // not an evaluation error — it stays an ordinary Violation
83+ // (fails closed) so a corrupt entry can't bypass its invariant.
84+ return Ok ( Some ( Violation {
7685 constraint_name : constraint. name . clone ( ) ,
7786 reason : format ! ( "invalid CHECK expression `{expr}`: {e}" ) ,
7887 hint : CompensationHint :: ManualIntervention {
@@ -81,20 +90,27 @@ impl Validator {
8190 constraint. name
8291 ) ,
8392 } ,
84- } ) ;
93+ } ) ) ;
8594 }
8695 } ;
8796
97+ // A division/modulo-by-zero inside the predicate
98+ // is neither a PASS nor an ordinary FAIL — it's an evaluation error,
99+ // propagated as `Err(EvalError)` and lifted by `validate` to
100+ // `ValidationOutcome::EvalError`. That keeps it out of the conflict-
101+ // policy machinery: an unevaluable predicate is a hard SQLSTATE-22012
102+ // failure, never something LastWriterWins et al. may "resolve".
88103 match parsed. eval ( & row_value) {
89- nodedb_types:: Value :: Bool ( true ) => None ,
90- nodedb_types:: Value :: Null => None ,
91- _ => Some ( Violation {
104+ Ok ( nodedb_types:: Value :: Bool ( true ) ) => Ok ( None ) ,
105+ Ok ( nodedb_types:: Value :: Null ) => Ok ( None ) ,
106+ Ok ( _ ) => Ok ( Some ( Violation {
92107 constraint_name : constraint. name . clone ( ) ,
93108 reason : format ! ( "CHECK `{}` failed: {expr}" , constraint. name) ,
94109 hint : CompensationHint :: ManualIntervention {
95110 reason : format ! ( "row violates CHECK predicate `{expr}`" ) ,
96111 } ,
97- } ) ,
112+ } ) ) ,
113+ Err ( e) => Err ( e) ,
98114 }
99115 }
100116
0 commit comments