@@ -26,6 +26,7 @@ use crate::scalar_fn::ChildName;
2626use crate :: scalar_fn:: ExecutionArgs ;
2727use crate :: scalar_fn:: ScalarFnId ;
2828use crate :: scalar_fn:: ScalarFnVTable ;
29+ use crate :: scalar_fn:: SimplifyCtx ;
2930use crate :: scalar_fn:: fns:: literal:: Literal ;
3031use crate :: scalar_fn:: fns:: operators:: CompareOperator ;
3132use crate :: scalar_fn:: fns:: operators:: Operator ;
@@ -43,40 +44,6 @@ use crate::scalar::Scalar;
4344#[ derive( Clone ) ]
4445pub struct Binary ;
4546
46- fn simplify_and ( lhs : & Expression , rhs : & Expression ) -> Option < Expression > {
47- match ( bool_literal ( lhs) , bool_literal ( rhs) ) {
48- ( Some ( Some ( false ) ) , _) | ( _, Some ( Some ( false ) ) ) => Some ( lit ( false ) ) ,
49- ( Some ( Some ( true ) ) , _) => Some ( rhs. clone ( ) ) ,
50- ( _, Some ( Some ( true ) ) ) => Some ( lhs. clone ( ) ) ,
51- ( Some ( None ) , Some ( None ) ) => Some ( lhs. clone ( ) ) ,
52- _ => None ,
53- }
54- }
55-
56- fn simplify_or ( lhs : & Expression , rhs : & Expression ) -> Option < Expression > {
57- match ( bool_literal ( lhs) , bool_literal ( rhs) ) {
58- ( Some ( Some ( true ) ) , _) | ( _, Some ( Some ( true ) ) ) => Some ( lit ( true ) ) ,
59- ( Some ( Some ( false ) ) , _) => Some ( rhs. clone ( ) ) ,
60- ( _, Some ( Some ( false ) ) ) => Some ( lhs. clone ( ) ) ,
61- ( Some ( None ) , Some ( None ) ) => Some ( lhs. clone ( ) ) ,
62- _ => None ,
63- }
64- }
65-
66- fn bool_literal ( expr : & Expression ) -> Option < Option < bool > > {
67- expr. as_opt :: < Literal > ( ) ?
68- . as_bool_opt ( )
69- . map ( |value| value. value ( ) )
70- }
71-
72- fn is_null_literal ( expr : & Expression ) -> bool {
73- expr. as_opt :: < Literal > ( ) . is_some_and ( Scalar :: is_null)
74- }
75-
76- fn null_bool ( ) -> Expression {
77- lit ( Scalar :: null ( DType :: Bool ( Nullability :: Nullable ) ) )
78- }
79-
8047impl ScalarFnVTable for Binary {
8148 type Options = Operator ;
8249
@@ -201,17 +168,67 @@ impl ScalarFnVTable for Binary {
201168 let lhs = expr. child ( 0 ) ;
202169 let rhs = expr. child ( 1 ) ;
203170
204- if operator. is_comparison ( ) && ( is_null_literal ( lhs) || is_null_literal ( rhs) ) {
205- return Ok ( Some ( null_bool ( ) ) ) ;
206- }
207-
171+ let bool_literal = |expr : & Expression | {
172+ expr. as_opt :: < Literal > ( ) ?
173+ . as_bool_opt ( )
174+ . map ( |value| value. value ( ) )
175+ } ;
176+
177+ // AND/OR use Kleene three-valued logic. `None` below is a boolean null.
178+ //
179+ // AND:
180+ // - false AND x => false
181+ // - true AND x => x
182+ // - null AND null => null
183+ //
184+ // OR:
185+ // - true OR x => true
186+ // - false OR x => x
187+ // - null OR null => null
188+ //
189+ // Other null cases either fall out of the identity/annihilator rules
190+ // above (`null AND true`, `null OR false`) or cannot be simplified under
191+ // Kleene semantics (`null AND x`, `null OR x` for non-literal `x`).
208192 Ok ( match operator {
209- Operator :: And => simplify_and ( lhs, rhs) ,
210- Operator :: Or => simplify_or ( lhs, rhs) ,
193+ Operator :: And => match ( bool_literal ( lhs) , bool_literal ( rhs) ) {
194+ ( Some ( Some ( false ) ) , _) | ( _, Some ( Some ( false ) ) ) => Some ( lit ( false ) ) ,
195+ ( Some ( Some ( true ) ) , _) => Some ( rhs. clone ( ) ) ,
196+ ( _, Some ( Some ( true ) ) ) => Some ( lhs. clone ( ) ) ,
197+ ( Some ( None ) , Some ( None ) ) => Some ( lhs. clone ( ) ) ,
198+ _ => None ,
199+ } ,
200+ Operator :: Or => match ( bool_literal ( lhs) , bool_literal ( rhs) ) {
201+ ( Some ( Some ( true ) ) , _) | ( _, Some ( Some ( true ) ) ) => Some ( lit ( true ) ) ,
202+ ( Some ( Some ( false ) ) , _) => Some ( rhs. clone ( ) ) ,
203+ ( _, Some ( Some ( false ) ) ) => Some ( lhs. clone ( ) ) ,
204+ ( Some ( None ) , Some ( None ) ) => Some ( lhs. clone ( ) ) ,
205+ _ => None ,
206+ } ,
211207 _ => None ,
212208 } )
213209 }
214210
211+ fn simplify (
212+ & self ,
213+ operator : & Operator ,
214+ expr : & Expression ,
215+ ctx : & dyn SimplifyCtx ,
216+ ) -> VortexResult < Option < Expression > > {
217+ let is_literal_null =
218+ |expr : & Expression | expr. as_opt :: < Literal > ( ) . is_some_and ( Scalar :: is_null) ;
219+
220+ if operator. is_comparison ( )
221+ && ( is_literal_null ( expr. child ( 0 ) ) || is_literal_null ( expr. child ( 1 ) ) )
222+ {
223+ // Validate the comparison before reducing it. This preserves type
224+ // errors for expressions like `int_col = null_utf8`.
225+ ctx. return_dtype ( expr) ?;
226+ return Ok ( Some ( lit ( Scalar :: null ( DType :: Bool ( Nullability :: Nullable ) ) ) ) ) ;
227+ }
228+
229+ Ok ( None )
230+ }
231+
215232 fn validity (
216233 & self ,
217234 operator : & Operator ,
@@ -257,6 +274,7 @@ impl ScalarFnVTable for Binary {
257274#[ cfg( test) ]
258275mod tests {
259276 use vortex_error:: VortexExpect ;
277+ use vortex_error:: VortexResult ;
260278
261279 use super :: * ;
262280 use crate :: LEGACY_SESSION ;
@@ -265,6 +283,7 @@ mod tests {
265283 use crate :: builtins:: ArrayBuiltins ;
266284 use crate :: dtype:: DType ;
267285 use crate :: dtype:: Nullability ;
286+ use crate :: dtype:: PType ;
268287 use crate :: expr:: Expression ;
269288 use crate :: expr:: and_collect;
270289 use crate :: expr:: col;
@@ -389,6 +408,36 @@ mod tests {
389408 ) ;
390409 }
391410
411+ #[ test]
412+ fn comparison_with_typed_null_simplifies_after_type_check ( ) -> VortexResult < ( ) > {
413+ let dtype = test_harness:: struct_dtype ( ) ;
414+
415+ let expr = eq (
416+ col ( "col1" ) ,
417+ lit ( Scalar :: null ( DType :: Primitive (
418+ PType :: U16 ,
419+ Nullability :: Nullable ,
420+ ) ) ) ,
421+ ) ;
422+
423+ assert_eq ! (
424+ expr. optimize_recursive( & dtype) ?,
425+ lit( Scalar :: null( DType :: Bool ( Nullability :: Nullable ) ) )
426+ ) ;
427+ Ok ( ( ) )
428+ }
429+
430+ #[ test]
431+ fn comparison_with_incompatible_null_still_type_checks ( ) {
432+ let dtype = test_harness:: struct_dtype ( ) ;
433+ let expr = eq (
434+ col ( "col1" ) ,
435+ lit ( Scalar :: null ( DType :: Utf8 ( Nullability :: Nullable ) ) ) ,
436+ ) ;
437+
438+ assert ! ( expr. optimize_recursive( & dtype) . is_err( ) ) ;
439+ }
440+
392441 #[ test]
393442 fn test_display_print ( ) {
394443 let expr = gt ( lit ( 1 ) , lit ( 2 ) ) ;
0 commit comments