From 6f2fcfef22da088e984f88a6497270b1a07555b6 Mon Sep 17 00:00:00 2001 From: gitRasheed <99349713+gitRasheed@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:10:09 +0100 Subject: [PATCH] fix(array): between over a constant null array is null, not false The Constant between reduce rule compared the constant scalar with the bounds directly. Scalar orders null below every value, so an all-null chunk (which compression stores as a ConstantArray) gave a non-null false, and not(between(x, a, b)) over it selected every row instead of none. Return a null constant when the array constant is null. Signed-off-by: gitRasheed <99349713+gitRasheed@users.noreply.github.com> --- .../src/arrays/constant/compute/between.rs | 21 +++++++++---- vortex-array/src/scalar_fn/fns/between/mod.rs | 30 +++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/vortex-array/src/arrays/constant/compute/between.rs b/vortex-array/src/arrays/constant/compute/between.rs index d79dff2fc17..25101b3a9b6 100644 --- a/vortex-array/src/arrays/constant/compute/between.rs +++ b/vortex-array/src/arrays/constant/compute/between.rs @@ -8,6 +8,7 @@ use crate::IntoArray; use crate::array::ArrayView; use crate::arrays::Constant; use crate::arrays::ConstantArray; +use crate::dtype::DType; use crate::scalar::Scalar; use crate::scalar_fn::fns::between::BetweenOptions; use crate::scalar_fn::fns::between::BetweenReduce; @@ -24,6 +25,19 @@ impl BetweenReduce for Constant { .zip(lower.as_constant()) .zip(upper.as_constant()) { + let nullability = array.dtype().nullability() + | lower.dtype().nullability() + | upper.dtype().nullability(); + + // `Scalar` orders null below every value, so comparing a null constant would give a + // non-null false. The result is null instead. + if constant.is_null() { + return Ok(Some( + ConstantArray::new(Scalar::null(DType::Bool(nullability)), array.len()) + .into_array(), + )); + } + let BetweenOptions { lower_strict, upper_strict, @@ -43,12 +57,7 @@ impl BetweenReduce for Constant { let result = lower_result && upper_result; - let scalar = Scalar::bool( - result, - array.dtype().nullability() - | lower.dtype().nullability() - | upper.dtype().nullability(), - ); + let scalar = Scalar::bool(result, nullability); return Ok(Some(ConstantArray::new(scalar, array.len()).into_array())); } diff --git a/vortex-array/src/scalar_fn/fns/between/mod.rs b/vortex-array/src/scalar_fn/fns/between/mod.rs index adc269b40fc..b7727edb6c4 100644 --- a/vortex-array/src/scalar_fn/fns/between/mod.rs +++ b/vortex-array/src/scalar_fn/fns/between/mod.rs @@ -618,6 +618,36 @@ mod tests { Ok(()) } + /// Compression stores an all-null chunk as a [`ConstantArray`]. Its rows must come out null + /// rather than false, since `NOT` turns a false row into a match. + #[rstest] + #[case::f64(Scalar::null_native::(), Scalar::from(-0.0f64), Scalar::from(0.0f64))] + #[case::i32(Scalar::null_native::(), Scalar::from(0i32), Scalar::from(0i32))] + fn constant_null_array( + #[case] null: Scalar, + #[case] lower: Scalar, + #[case] upper: Scalar, + ) -> VortexResult<()> { + let ctx = &mut SESSION.create_execution_ctx(); + let array = ConstantArray::new(null, 3).into_array(); + let lower = ConstantArray::new(lower, 3).into_array(); + let upper = ConstantArray::new(upper, 3).into_array(); + + let between = array.between(lower, upper, NON_STRICT)?; + let not_between = between.not()?; + + assert_eq!( + between.execute::(ctx)?.opt_bool_vec(ctx), + [None, None, None] + ); + assert_eq!( + not_between.execute::(ctx)?.opt_bool_vec(ctx), + [None, None, None] + ); + + Ok(()) + } + #[test] fn test_between_decimal() { let ctx = &mut SESSION.create_execution_ctx();