@@ -131,7 +131,7 @@ pub(crate) fn execute_compare(
131131 // Constant-constant fast path
132132 if let ( Some ( lhs_const) , Some ( rhs_const) ) = ( lhs. as_opt :: < Constant > ( ) , rhs. as_opt :: < Constant > ( ) )
133133 {
134- let result = scalar_cmp ( lhs_const. scalar ( ) , rhs_const. scalar ( ) , op) ;
134+ let result = scalar_cmp ( lhs_const. scalar ( ) , rhs_const. scalar ( ) , op) ? ;
135135 return Ok ( ConstantArray :: new ( result, lhs. len ( ) ) . into_array ( ) ) ;
136136 }
137137
@@ -150,7 +150,7 @@ fn arrow_compare_arrays(
150150
151151 // Arrow's vectorized comparison kernels don't support nested types.
152152 // For nested types, fall back to `make_comparator` which does element-wise comparison.
153- let array : BooleanArray = if left. dtype ( ) . is_nested ( ) || right. dtype ( ) . is_nested ( ) {
153+ let arrow_array : BooleanArray = if left. dtype ( ) . is_nested ( ) || right. dtype ( ) . is_nested ( ) {
154154 let rhs = right. to_array ( ) . into_arrow_preferred ( ) ?;
155155 let lhs = left. to_array ( ) . into_arrow ( rhs. data_type ( ) ) ?;
156156
@@ -176,24 +176,43 @@ fn arrow_compare_arrays(
176176 CompareOperator :: Lte => cmp:: lt_eq ( & lhs, & rhs) ?,
177177 }
178178 } ;
179- from_arrow_array_with_len ( & array, left. len ( ) , nullable)
179+
180+ from_arrow_array_with_len ( & arrow_array, left. len ( ) , nullable)
180181}
181182
182- pub fn scalar_cmp ( lhs : & Scalar , rhs : & Scalar , operator : CompareOperator ) -> Scalar {
183+ pub fn scalar_cmp ( lhs : & Scalar , rhs : & Scalar , operator : CompareOperator ) -> VortexResult < Scalar > {
183184 if lhs. is_null ( ) | rhs. is_null ( ) {
184- Scalar :: null ( DType :: Bool ( Nullability :: Nullable ) )
185- } else {
186- let b = match operator {
187- CompareOperator :: Eq => lhs == rhs,
188- CompareOperator :: NotEq => lhs != rhs,
189- CompareOperator :: Gt => lhs > rhs,
190- CompareOperator :: Gte => lhs >= rhs,
191- CompareOperator :: Lt => lhs < rhs,
192- CompareOperator :: Lte => lhs <= rhs,
193- } ;
185+ return Ok ( Scalar :: null ( DType :: Bool ( Nullability :: Nullable ) ) ) ;
186+ }
187+
188+ let nullability = lhs. dtype ( ) . nullability ( ) | rhs. dtype ( ) . nullability ( ) ;
194189
195- Scalar :: bool ( b, lhs. dtype ( ) . nullability ( ) | rhs. dtype ( ) . nullability ( ) )
190+ // Equality and inequality can be determined without `partial_cmp`.
191+ match operator {
192+ CompareOperator :: Eq => return Ok ( Scalar :: bool ( lhs == rhs, nullability) ) ,
193+ CompareOperator :: NotEq => return Ok ( Scalar :: bool ( lhs != rhs, nullability) ) ,
194+ _ => { }
196195 }
196+
197+ // We do this instead of `<` and `>` to ensure we do not lose a type mismatch error.
198+ let ordering = lhs. partial_cmp ( rhs) . ok_or_else ( || {
199+ vortex_error:: vortex_err!(
200+ "Cannot compare scalars with incompatible types: {} and {}" ,
201+ lhs. dtype( ) ,
202+ rhs. dtype( )
203+ )
204+ } ) ?;
205+
206+ let b = match operator {
207+ CompareOperator :: Gt => ordering. is_gt ( ) ,
208+ CompareOperator :: Gte => ordering. is_ge ( ) ,
209+ CompareOperator :: Lt => ordering. is_lt ( ) ,
210+ CompareOperator :: Lte => ordering. is_le ( ) ,
211+ // Already handled above.
212+ CompareOperator :: Eq | CompareOperator :: NotEq => unreachable ! ( ) ,
213+ } ;
214+
215+ Ok ( Scalar :: bool ( b, nullability) )
197216}
198217
199218/// Compare two Arrow arrays element-wise using [`make_comparator`].
@@ -251,8 +270,12 @@ mod tests {
251270 use crate :: dtype:: FieldNames ;
252271 use crate :: dtype:: Nullability ;
253272 use crate :: dtype:: PType ;
273+ use crate :: extension:: datetime:: TimeUnit ;
274+ use crate :: extension:: datetime:: Timestamp ;
275+ use crate :: extension:: datetime:: TimestampOptions ;
254276 use crate :: scalar:: Scalar ;
255277 use crate :: scalar_fn:: fns:: binary:: compare:: ConstantArray ;
278+ use crate :: scalar_fn:: fns:: operators:: CompareOperator ;
256279 use crate :: scalar_fn:: fns:: operators:: Operator ;
257280 use crate :: test_harness:: to_int_indices;
258281 use crate :: validity:: Validity ;
@@ -479,6 +502,49 @@ mod tests {
479502 assert_arrays_eq ! ( result, expected) ;
480503 }
481504
505+ /// Regression test: `scalar_cmp` must error when comparing scalars with incompatible
506+ /// extension types (e.g., timestamps with different time units) rather than silently
507+ /// returning a wrong result.
508+ #[ test]
509+ fn scalar_cmp_incompatible_extension_types_errors ( ) {
510+ let ms_scalar = Scalar :: extension :: < Timestamp > (
511+ TimestampOptions {
512+ unit : TimeUnit :: Milliseconds ,
513+ tz : None ,
514+ } ,
515+ Scalar :: from ( 1704067200000i64 ) ,
516+ ) ;
517+ let s_scalar = Scalar :: extension :: < Timestamp > (
518+ TimestampOptions {
519+ unit : TimeUnit :: Seconds ,
520+ tz : None ,
521+ } ,
522+ Scalar :: from ( 1704067200i64 ) ,
523+ ) ;
524+
525+ // Ordering comparisons must error on incompatible types.
526+ assert ! ( super :: scalar_cmp( & ms_scalar, & s_scalar, CompareOperator :: Gt ) . is_err( ) ) ;
527+ assert ! ( super :: scalar_cmp( & ms_scalar, & s_scalar, CompareOperator :: Lt ) . is_err( ) ) ;
528+ assert ! ( super :: scalar_cmp( & ms_scalar, & s_scalar, CompareOperator :: Gte ) . is_err( ) ) ;
529+ assert ! ( super :: scalar_cmp( & ms_scalar, & s_scalar, CompareOperator :: Lte ) . is_err( ) ) ;
530+
531+ // Equality comparisons should succeed (and return false since the types differ).
532+ assert_eq ! (
533+ super :: scalar_cmp( & ms_scalar, & s_scalar, CompareOperator :: Eq )
534+ . unwrap( )
535+ . as_bool( )
536+ . value( ) ,
537+ Some ( false ) ,
538+ ) ;
539+ assert_eq ! (
540+ super :: scalar_cmp( & ms_scalar, & s_scalar, CompareOperator :: NotEq )
541+ . unwrap( )
542+ . as_bool( )
543+ . value( ) ,
544+ Some ( true ) ,
545+ ) ;
546+ }
547+
482548 #[ test]
483549 fn test_empty_list ( ) {
484550 let list = ListViewArray :: new (
0 commit comments