Skip to main content

vortex_array/scalar_fn/fns/between/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4mod kernel;
5
6use std::fmt::Display;
7use std::fmt::Formatter;
8
9pub use kernel::*;
10use prost::Message;
11use vortex_array::expr::and;
12use vortex_error::VortexResult;
13use vortex_error::vortex_bail;
14use vortex_proto::expr as pb;
15use vortex_session::VortexSession;
16use vortex_session::registry::CachedId;
17
18use crate::ArrayRef;
19use crate::Canonical;
20use crate::ExecutionCtx;
21use crate::IntoArray;
22use crate::arrays::ConstantArray;
23use crate::arrays::Decimal;
24use crate::arrays::Primitive;
25use crate::builtins::ArrayBuiltins;
26use crate::dtype::DType;
27use crate::dtype::DType::Bool;
28use crate::expr::display::ExprDisplay;
29use crate::expr::expression::Expression;
30use crate::scalar::Scalar;
31use crate::scalar_fn::Arity;
32use crate::scalar_fn::ChildName;
33use crate::scalar_fn::ExecutionArgs;
34use crate::scalar_fn::ScalarFnId;
35use crate::scalar_fn::ScalarFnVTable;
36use crate::scalar_fn::fns::binary::execute_boolean;
37use crate::scalar_fn::fns::operators::CompareOperator;
38use crate::scalar_fn::fns::operators::Operator;
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41pub struct BetweenOptions {
42    pub lower_strict: StrictComparison,
43    pub upper_strict: StrictComparison,
44}
45
46impl Display for BetweenOptions {
47    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
48        let lower_op = if self.lower_strict.is_strict() {
49            "<"
50        } else {
51            "<="
52        };
53        let upper_op = if self.upper_strict.is_strict() {
54            "<"
55        } else {
56            "<="
57        };
58        write!(f, "lower_strict: {}, upper_strict: {}", lower_op, upper_op)
59    }
60}
61
62/// Strictness of the comparison.
63#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
64pub enum StrictComparison {
65    /// Strict bound (`<`)
66    Strict,
67    /// Non-strict bound (`<=`)
68    NonStrict,
69}
70
71impl StrictComparison {
72    pub const fn to_compare_operator(&self) -> CompareOperator {
73        match self {
74            StrictComparison::Strict => CompareOperator::Lt,
75            StrictComparison::NonStrict => CompareOperator::Lte,
76        }
77    }
78
79    pub const fn to_operator(&self) -> Operator {
80        match self {
81            StrictComparison::Strict => Operator::Lt,
82            StrictComparison::NonStrict => Operator::Lte,
83        }
84    }
85
86    pub const fn is_strict(&self) -> bool {
87        matches!(self, StrictComparison::Strict)
88    }
89}
90
91/// Common preconditions for between operations that apply to all arrays.
92///
93/// Returns `Some(result)` if the precondition short-circuits the between operation
94/// (empty array, null bounds), or `None` if between should proceed with the
95/// encoding-specific implementation.
96pub(super) fn precondition(
97    arr: &ArrayRef,
98    lower: &ArrayRef,
99    upper: &ArrayRef,
100) -> VortexResult<Option<ArrayRef>> {
101    let return_dtype =
102        Bool(arr.dtype().nullability() | lower.dtype().nullability() | upper.dtype().nullability());
103
104    // Bail early if the array is empty.
105    if arr.is_empty() {
106        return Ok(Some(Canonical::empty(&return_dtype).into_array()));
107    }
108
109    if lower.as_constant().is_some_and(|v| v.is_null())
110        || upper.as_constant().is_some_and(|v| v.is_null())
111    {
112        return Ok(Some(
113            ConstantArray::new(Scalar::null(return_dtype), arr.len()).into_array(),
114        ));
115    }
116
117    Ok(None)
118}
119
120/// Between on a canonical array by directly dispatching to the appropriate kernel.
121///
122/// Falls back to compare + boolean and if no kernel handles the input.
123fn between_canonical(
124    arr: &ArrayRef,
125    lower: &ArrayRef,
126    upper: &ArrayRef,
127    options: &BetweenOptions,
128    ctx: &mut ExecutionCtx,
129) -> VortexResult<ArrayRef> {
130    if let Some(result) = precondition(arr, lower, upper)? {
131        return Ok(result);
132    }
133
134    // Try type-specific kernels
135    if let Some(prim) = arr.as_opt::<Primitive>()
136        && let Some(result) =
137            <Primitive as BetweenKernel>::between(prim, lower, upper, options, ctx)?
138    {
139        return Ok(result);
140    }
141    if let Some(dec) = arr.as_opt::<Decimal>()
142        && let Some(result) = <Decimal as BetweenKernel>::between(dec, lower, upper, options, ctx)?
143    {
144        return Ok(result);
145    }
146
147    // TODO(joe): return lazy compare once the executor supports this
148    // Fall back to compare + boolean and
149    let lower_cmp = lower.clone().binary(
150        arr.clone(),
151        Operator::from(options.lower_strict.to_compare_operator()),
152    )?;
153    let upper_cmp = arr.clone().binary(
154        upper.clone(),
155        Operator::from(options.upper_strict.to_compare_operator()),
156    )?;
157    execute_boolean(lower_cmp, upper_cmp, Operator::And, ctx)
158}
159
160/// An optimized scalar expression to compute whether values fall between two bounds.
161///
162/// This expression takes three children:
163/// 1. The array of values to check.
164/// 2. The lower bound.
165/// 3. The upper bound.
166///
167/// The comparison strictness is controlled by the metadata.
168///
169/// NOTE: this expression will shortly be removed in favor of pipelined computation of two
170/// separate comparisons combined with a logical AND.
171#[derive(Clone)]
172pub struct Between;
173
174impl ScalarFnVTable for Between {
175    type Options = BetweenOptions;
176
177    fn id(&self) -> ScalarFnId {
178        static ID: CachedId = CachedId::new("vortex.between");
179        *ID
180    }
181
182    fn serialize(&self, instance: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
183        Ok(Some(
184            pb::BetweenOpts {
185                lower_strict: instance.lower_strict.is_strict(),
186                upper_strict: instance.upper_strict.is_strict(),
187            }
188            .encode_to_vec(),
189        ))
190    }
191
192    fn deserialize(
193        &self,
194        _metadata: &[u8],
195        _session: &VortexSession,
196    ) -> VortexResult<Self::Options> {
197        let opts = pb::BetweenOpts::decode(_metadata)?;
198        Ok(BetweenOptions {
199            lower_strict: if opts.lower_strict {
200                StrictComparison::Strict
201            } else {
202                StrictComparison::NonStrict
203            },
204            upper_strict: if opts.upper_strict {
205                StrictComparison::Strict
206            } else {
207                StrictComparison::NonStrict
208            },
209        })
210    }
211
212    fn arity(&self, _options: &Self::Options) -> Arity {
213        Arity::Exact(3)
214    }
215
216    fn child_name(&self, _instance: &Self::Options, child_idx: usize) -> ChildName {
217        match child_idx {
218            0 => ChildName::from("array"),
219            1 => ChildName::from("lower"),
220            2 => ChildName::from("upper"),
221            _ => unreachable!("Invalid child index {} for Between expression", child_idx),
222        }
223    }
224
225    fn fmt_sql(
226        &self,
227        options: &Self::Options,
228        expr: &dyn ExprDisplay,
229        f: &mut Formatter<'_>,
230    ) -> std::fmt::Result {
231        let lower_op = if options.lower_strict.is_strict() {
232            "<"
233        } else {
234            "<="
235        };
236        let upper_op = if options.upper_strict.is_strict() {
237            "<"
238        } else {
239            "<="
240        };
241        write!(
242            f,
243            "({} {} {} {} {})",
244            expr.display_child(1),
245            lower_op,
246            expr.display_child(0),
247            upper_op,
248            expr.display_child(2)
249        )
250    }
251
252    fn return_dtype(&self, _options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
253        let arr_dt = &arg_dtypes[0];
254        let lower_dt = &arg_dtypes[1];
255        let upper_dt = &arg_dtypes[2];
256
257        if !arr_dt.eq_ignore_nullability(lower_dt) {
258            vortex_bail!(
259                "Array dtype {} does not match lower dtype {}",
260                arr_dt,
261                lower_dt
262            );
263        }
264        if !arr_dt.eq_ignore_nullability(upper_dt) {
265            vortex_bail!(
266                "Array dtype {} does not match upper dtype {}",
267                arr_dt,
268                upper_dt
269            );
270        }
271
272        Ok(Bool(
273            arr_dt.nullability() | lower_dt.nullability() | upper_dt.nullability(),
274        ))
275    }
276
277    fn execute(
278        &self,
279        options: &Self::Options,
280        args: &dyn ExecutionArgs,
281        ctx: &mut ExecutionCtx,
282    ) -> VortexResult<ArrayRef> {
283        let arr = args.get(0)?;
284        let lower = args.get(1)?;
285        let upper = args.get(2)?;
286
287        // canonicalize the arr and we might be able to run a between kernels over that.
288        if !arr.is_canonical() {
289            return arr.execute::<Canonical>(ctx)?.into_array().between(
290                lower,
291                upper,
292                options.clone(),
293            );
294        }
295
296        between_canonical(&arr, &lower, &upper, options, ctx)
297    }
298
299    fn validity(
300        &self,
301        _options: &Self::Options,
302        expression: &Expression,
303    ) -> VortexResult<Option<Expression>> {
304        let arr = expression.child(0).validity()?;
305        let lower = expression.child(1).validity()?;
306        let upper = expression.child(2).validity()?;
307        Ok(Some(and(and(arr, lower), upper)))
308    }
309
310    fn is_strict(&self, _options: &Self::Options) -> bool {
311        false
312    }
313
314    fn is_fallible(&self, _options: &Self::Options) -> bool {
315        false
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use std::sync::LazyLock;
322
323    use rstest::rstest;
324    use vortex_buffer::buffer;
325
326    use super::*;
327    use crate::IntoArray;
328    use crate::VortexSessionExecute;
329    use crate::arrays::BoolArray;
330    use crate::arrays::DecimalArray;
331    use crate::assert_arrays_eq;
332    use crate::dtype::DType;
333    use crate::dtype::DecimalDType;
334    use crate::dtype::Nullability;
335    use crate::dtype::PType;
336    use crate::expr::between;
337    use crate::expr::get_item;
338    use crate::expr::lit;
339    use crate::expr::root;
340    use crate::scalar::DecimalValue;
341    use crate::scalar::Scalar;
342    use crate::test_harness::to_int_indices;
343    use crate::validity::Validity;
344
345    static SESSION: LazyLock<VortexSession> = LazyLock::new(crate::array_session);
346
347    #[test]
348    fn is_not_strict() {
349        let expr = between(
350            root(),
351            lit(0),
352            lit(100),
353            BetweenOptions {
354                lower_strict: StrictComparison::NonStrict,
355                upper_strict: StrictComparison::NonStrict,
356            },
357        );
358
359        assert!(!expr.signature().is_strict());
360    }
361
362    #[test]
363    fn test_display() {
364        let expr = between(
365            get_item("score", root()),
366            lit(10),
367            lit(50),
368            BetweenOptions {
369                lower_strict: StrictComparison::NonStrict,
370                upper_strict: StrictComparison::Strict,
371            },
372        );
373        assert_eq!(expr.to_string(), "(10i32 <= $.score < 50i32)");
374
375        let expr2 = between(
376            root(),
377            lit(0),
378            lit(100),
379            BetweenOptions {
380                lower_strict: StrictComparison::Strict,
381                upper_strict: StrictComparison::NonStrict,
382            },
383        );
384        assert_eq!(expr2.to_string(), "(0i32 < $ <= 100i32)");
385    }
386
387    #[rstest]
388    #[case(StrictComparison::NonStrict, StrictComparison::NonStrict, vec![0, 1, 2, 3])]
389    #[case(StrictComparison::NonStrict, StrictComparison::Strict, vec![0, 1])]
390    #[case(StrictComparison::Strict, StrictComparison::NonStrict, vec![0, 2])]
391    #[case(StrictComparison::Strict, StrictComparison::Strict, vec![0])]
392    fn test_bounds(
393        #[case] lower_strict: StrictComparison,
394        #[case] upper_strict: StrictComparison,
395        #[case] expected: Vec<u64>,
396    ) {
397        let lower = buffer![0, 0, 0, 0, 2].into_array();
398        let array = buffer![1, 0, 1, 0, 1].into_array();
399        let upper = buffer![2, 1, 1, 0, 0].into_array();
400        let ctx = &mut SESSION.create_execution_ctx();
401
402        let matches = between_canonical(
403            &array,
404            &lower,
405            &upper,
406            &BetweenOptions {
407                lower_strict,
408                upper_strict,
409            },
410            ctx,
411        )
412        .unwrap()
413        .execute::<BoolArray>(ctx)
414        .unwrap();
415
416        let indices = to_int_indices(matches, ctx).unwrap();
417        assert_eq!(indices, expected);
418    }
419
420    #[test]
421    fn test_constants() {
422        let lower = buffer![0, 0, 2, 0, 2].into_array();
423        let array = buffer![1, 0, 1, 0, 1].into_array();
424        let ctx = &mut SESSION.create_execution_ctx();
425
426        // upper is null
427        let upper = ConstantArray::new(
428            Scalar::null(DType::Primitive(PType::I32, Nullability::Nullable)),
429            5,
430        )
431        .into_array();
432
433        let matches = between_canonical(
434            &array,
435            &lower,
436            &upper,
437            &BetweenOptions {
438                lower_strict: StrictComparison::NonStrict,
439                upper_strict: StrictComparison::NonStrict,
440            },
441            ctx,
442        )
443        .unwrap()
444        .execute::<BoolArray>(ctx)
445        .unwrap();
446
447        let indices = to_int_indices(matches, ctx).unwrap();
448        assert!(indices.is_empty());
449
450        // upper is a fixed constant
451        let upper = ConstantArray::new(Scalar::from(2), 5).into_array();
452        let matches = between_canonical(
453            &array,
454            &lower,
455            &upper,
456            &BetweenOptions {
457                lower_strict: StrictComparison::NonStrict,
458                upper_strict: StrictComparison::NonStrict,
459            },
460            ctx,
461        )
462        .unwrap()
463        .execute::<BoolArray>(ctx)
464        .unwrap();
465        let indices = to_int_indices(matches, ctx).unwrap();
466        assert_eq!(indices, vec![0, 1, 3]);
467
468        // lower is also a constant
469        let lower = ConstantArray::new(Scalar::from(0), 5).into_array();
470
471        let matches = between_canonical(
472            &array,
473            &lower,
474            &upper,
475            &BetweenOptions {
476                lower_strict: StrictComparison::NonStrict,
477                upper_strict: StrictComparison::NonStrict,
478            },
479            ctx,
480        )
481        .unwrap()
482        .execute::<BoolArray>(ctx)
483        .unwrap();
484        let indices = to_int_indices(matches, ctx).unwrap();
485        assert_eq!(indices, vec![0, 1, 2, 3, 4]);
486    }
487
488    #[test]
489    fn test_between_decimal() {
490        let ctx = &mut SESSION.create_execution_ctx();
491        let values = buffer![100i128, 200i128, 300i128, 400i128];
492        let decimal_type = DecimalDType::new(3, 2);
493        let array = DecimalArray::new(values, decimal_type, Validity::NonNullable).into_array();
494
495        let lower = ConstantArray::new(
496            Scalar::decimal(
497                DecimalValue::I128(100i128),
498                decimal_type,
499                Nullability::NonNullable,
500            ),
501            array.len(),
502        )
503        .into_array();
504        let upper = ConstantArray::new(
505            Scalar::decimal(
506                DecimalValue::I128(400i128),
507                decimal_type,
508                Nullability::NonNullable,
509            ),
510            array.len(),
511        )
512        .into_array();
513
514        // Strict lower bound, non-strict upper bound
515        let between_strict = between_canonical(
516            &array,
517            &lower,
518            &upper,
519            &BetweenOptions {
520                lower_strict: StrictComparison::Strict,
521                upper_strict: StrictComparison::NonStrict,
522            },
523            ctx,
524        )
525        .unwrap();
526        assert_arrays_eq!(
527            between_strict,
528            BoolArray::from_iter([false, true, true, true]),
529            ctx
530        );
531
532        // Non-strict lower bound, strict upper bound
533        let between_strict = between_canonical(
534            &array,
535            &lower,
536            &upper,
537            &BetweenOptions {
538                lower_strict: StrictComparison::NonStrict,
539                upper_strict: StrictComparison::Strict,
540            },
541            ctx,
542        )
543        .unwrap();
544        assert_arrays_eq!(
545            between_strict,
546            BoolArray::from_iter([true, true, true, false]),
547            ctx
548        );
549    }
550
551    /// Regression test for a fuzzer crash where a bound scalar used a wider storage type (I32)
552    /// than the array's storage type (I16), causing the cast in `between_unpack` to fail.
553    ///
554    /// The fix casts the bound to the array's storage type and, when the cast fails, uses the
555    /// overflow direction to determine the result without falling back to Arrow.
556    #[rstest]
557    // Upper bound too large (I32 > i16::MAX): upper constraint always satisfied → result from lower only.
558    #[case(DecimalValue::I16(1), DecimalValue::I32(82246), vec![0, 1, 2, 3])]
559    // Lower bound too large (I32 > i16::MAX): lower constraint never satisfied → all false.
560    #[case(DecimalValue::I32(82246), DecimalValue::I16(4), vec![])]
561    // Upper bound too small (negative I32 < i16::MIN): upper constraint never satisfied → all false.
562    #[case(DecimalValue::I16(1), DecimalValue::I32(-82246), vec![])]
563    // Lower bound too small (negative I32 < i16::MIN): lower constraint always satisfied → result from upper only.
564    #[case(DecimalValue::I32(-82246), DecimalValue::I16(2), vec![0, 1])]
565    fn test_between_decimal_mismatched_storage_types(
566        #[case] lower_val: DecimalValue,
567        #[case] upper_val: DecimalValue,
568        #[case] expected_indices: Vec<u64>,
569    ) {
570        let ctx = &mut SESSION.create_execution_ctx();
571        // Array uses I16 storage with precision=5 (values fit in i16 even though precision=5
572        // nominally maps to I32 as the smallest storage type).
573        let decimal_type = DecimalDType::new(5, -67);
574        let array = DecimalArray::new(
575            buffer![1i16, 2i16, 3i16, 4i16],
576            decimal_type,
577            Validity::NonNullable,
578        )
579        .into_array();
580
581        let lower = ConstantArray::new(
582            Scalar::decimal(lower_val, decimal_type, Nullability::NonNullable),
583            array.len(),
584        )
585        .into_array();
586        let upper = ConstantArray::new(
587            Scalar::decimal(upper_val, decimal_type, Nullability::NonNullable),
588            array.len(),
589        )
590        .into_array();
591
592        let result = between_canonical(
593            &array,
594            &lower,
595            &upper,
596            &BetweenOptions {
597                lower_strict: StrictComparison::NonStrict,
598                upper_strict: StrictComparison::NonStrict,
599            },
600            ctx,
601        )
602        .unwrap()
603        .execute::<BoolArray>(ctx)
604        .unwrap();
605
606        assert_eq!(to_int_indices(result, ctx).unwrap(), expected_indices);
607    }
608}