1mod kernel;
5
6use std::fmt::Display;
7use std::fmt::Formatter;
8
9pub use kernel::*;
10use prost::Message;
11use vortex_error::VortexResult;
12use vortex_error::vortex_bail;
13use vortex_session::VortexSession;
14use vortex_session::registry::CachedId;
15
16use crate::ArrayRef;
17use crate::Canonical;
18use crate::ExecutionCtx;
19use crate::IntoArray;
20use crate::arrays::ConstantArray;
21use crate::arrays::Decimal;
22use crate::arrays::Primitive;
23use crate::arrays::ScalarFnArray;
24use crate::builtins::ArrayBuiltins;
25use crate::dtype::DType;
26use crate::dtype::DType::Bool;
27use crate::expr::display::ExprDisplay;
28use crate::proto::expr as pb;
29use crate::scalar::Scalar;
30use crate::scalar_fn::Arity;
31use crate::scalar_fn::ChildName;
32use crate::scalar_fn::ExecutionArgs;
33use crate::scalar_fn::ScalarFnId;
34use crate::scalar_fn::ScalarFnVTable;
35use crate::scalar_fn::ScalarFnVTableExt;
36use crate::scalar_fn::fns::operators::Operator;
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
39pub struct BetweenOptions {
40 pub lower_strict: StrictComparison,
41 pub upper_strict: StrictComparison,
42}
43
44impl Display for BetweenOptions {
45 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
46 let lower_op = if self.lower_strict.is_strict() {
47 "<"
48 } else {
49 "<="
50 };
51 let upper_op = if self.upper_strict.is_strict() {
52 "<"
53 } else {
54 "<="
55 };
56 write!(f, "lower_strict: {}, upper_strict: {}", lower_op, upper_op)
57 }
58}
59
60#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
62pub enum StrictComparison {
63 Strict,
65 NonStrict,
67}
68
69impl StrictComparison {
70 pub const fn to_operator(&self) -> Operator {
71 match self {
72 StrictComparison::Strict => Operator::Lt,
73 StrictComparison::NonStrict => Operator::Lte,
74 }
75 }
76
77 pub const fn is_strict(&self) -> bool {
78 matches!(self, StrictComparison::Strict)
79 }
80}
81
82pub(super) fn short_circuit(
93 arr: &ArrayRef,
94 lower: &ArrayRef,
95 upper: &ArrayRef,
96 options: &BetweenOptions,
97) -> VortexResult<Option<ArrayRef>> {
98 let return_dtype =
99 Bool(arr.dtype().nullability() | lower.dtype().nullability() | upper.dtype().nullability());
100
101 if arr.is_empty() {
103 return Ok(Some(Canonical::empty(&return_dtype).into_array()));
104 }
105
106 let lower_is_null = lower.as_constant().is_some_and(|v| v.is_null());
107 let upper_is_null = upper.as_constant().is_some_and(|v| v.is_null());
108
109 if lower_is_null && upper_is_null {
112 return Ok(Some(
113 ConstantArray::new(Scalar::null(return_dtype), arr.len()).into_array(),
114 ));
115 }
116
117 if lower_is_null || upper_is_null {
120 return as_two_compares(arr, lower, upper, options).map(Some);
121 }
122
123 Ok(None)
124}
125
126fn as_two_compares(
130 arr: &ArrayRef,
131 lower: &ArrayRef,
132 upper: &ArrayRef,
133 options: &BetweenOptions,
134) -> VortexResult<ArrayRef> {
135 let lower_cmp = lower.binary(arr.clone(), options.lower_strict.to_operator())?;
136 let upper_cmp = arr.binary(upper.clone(), options.upper_strict.to_operator())?;
137 lower_cmp.binary(upper_cmp, Operator::And)
138}
139
140fn between_canonical(
144 arr: &ArrayRef,
145 lower: &ArrayRef,
146 upper: &ArrayRef,
147 options: &BetweenOptions,
148 ctx: &mut ExecutionCtx,
149) -> VortexResult<ArrayRef> {
150 if let Some(result) = short_circuit(arr, lower, upper, options)? {
151 return result.execute::<ArrayRef>(ctx);
154 }
155
156 if let Some(prim) = arr.as_opt::<Primitive>()
158 && let Some(result) =
159 <Primitive as BetweenKernel>::between(prim, lower, upper, options, ctx)?
160 {
161 return Ok(result);
162 }
163 if let Some(dec) = arr.as_opt::<Decimal>()
164 && let Some(result) = <Decimal as BetweenKernel>::between(dec, lower, upper, options, ctx)?
165 {
166 return Ok(result);
167 }
168
169 as_two_compares(arr, lower, upper, options)?.execute::<ArrayRef>(ctx)
172}
173
174#[derive(Clone)]
186pub struct Between;
187
188impl Between {
189 pub fn try_new(
195 array: ArrayRef,
196 lower: ArrayRef,
197 upper: ArrayRef,
198 options: BetweenOptions,
199 ) -> VortexResult<ScalarFnArray> {
200 ScalarFnArray::try_new(Between.bind(options), [array, lower, upper])
201 }
202}
203
204impl ScalarFnVTable for Between {
205 type Options = BetweenOptions;
206
207 fn id(&self) -> ScalarFnId {
208 static ID: CachedId = CachedId::new("vortex.between");
209 *ID
210 }
211
212 fn serialize(&self, instance: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
213 Ok(Some(
214 pb::BetweenOpts {
215 lower_strict: instance.lower_strict.is_strict(),
216 upper_strict: instance.upper_strict.is_strict(),
217 }
218 .encode_to_vec(),
219 ))
220 }
221
222 fn deserialize(
223 &self,
224 _metadata: &[u8],
225 _session: &VortexSession,
226 ) -> VortexResult<Self::Options> {
227 let opts = pb::BetweenOpts::decode(_metadata)?;
228 Ok(BetweenOptions {
229 lower_strict: if opts.lower_strict {
230 StrictComparison::Strict
231 } else {
232 StrictComparison::NonStrict
233 },
234 upper_strict: if opts.upper_strict {
235 StrictComparison::Strict
236 } else {
237 StrictComparison::NonStrict
238 },
239 })
240 }
241
242 fn arity(&self, _options: &Self::Options) -> Arity {
243 Arity::Exact(3)
244 }
245
246 fn child_name(&self, _instance: &Self::Options, child_idx: usize) -> ChildName {
247 match child_idx {
248 0 => ChildName::from("array"),
249 1 => ChildName::from("lower"),
250 2 => ChildName::from("upper"),
251 _ => unreachable!("Invalid child index {} for Between expression", child_idx),
252 }
253 }
254
255 fn fmt_sql(
256 &self,
257 options: &Self::Options,
258 expr: &dyn ExprDisplay,
259 f: &mut Formatter<'_>,
260 ) -> std::fmt::Result {
261 let lower_op = if options.lower_strict.is_strict() {
262 "<"
263 } else {
264 "<="
265 };
266 let upper_op = if options.upper_strict.is_strict() {
267 "<"
268 } else {
269 "<="
270 };
271 write!(
272 f,
273 "({} {} {} {} {})",
274 expr.display_child(1),
275 lower_op,
276 expr.display_child(0),
277 upper_op,
278 expr.display_child(2)
279 )
280 }
281
282 fn return_dtype(&self, _options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
283 let arr_dt = &arg_dtypes[0];
284 let lower_dt = &arg_dtypes[1];
285 let upper_dt = &arg_dtypes[2];
286
287 if !arr_dt.eq_ignore_nullability(lower_dt) {
288 vortex_bail!(
289 "Array dtype {} does not match lower dtype {}",
290 arr_dt,
291 lower_dt
292 );
293 }
294 if !arr_dt.eq_ignore_nullability(upper_dt) {
295 vortex_bail!(
296 "Array dtype {} does not match upper dtype {}",
297 arr_dt,
298 upper_dt
299 );
300 }
301
302 Ok(Bool(
303 arr_dt.nullability() | lower_dt.nullability() | upper_dt.nullability(),
304 ))
305 }
306
307 fn execute(
308 &self,
309 options: &Self::Options,
310 args: &dyn ExecutionArgs,
311 ctx: &mut ExecutionCtx,
312 ) -> VortexResult<ArrayRef> {
313 let arr = args.get(0)?;
314 let lower = args.get(1)?;
315 let upper = args.get(2)?;
316
317 if !arr.is_canonical() {
319 return arr.execute::<Canonical>(ctx)?.into_array().between(
320 lower,
321 upper,
322 options.clone(),
323 );
324 }
325
326 between_canonical(&arr, &lower, &upper, options, ctx)
327 }
328
329 fn is_strict(&self, _options: &Self::Options) -> bool {
330 false }
332
333 fn is_infallible(&self, _options: &Self::Options) -> bool {
334 true
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use std::sync::LazyLock;
341
342 use rstest::rstest;
343 use vortex_buffer::buffer;
344
345 use super::*;
346 use crate::IntoArray;
347 use crate::VortexSessionExecute;
348 use crate::arrays::BoolArray;
349 use crate::arrays::DecimalArray;
350 use crate::arrays::PrimitiveArray;
351 use crate::arrays::StructArray;
352 use crate::assert_arrays_eq;
353 use crate::dtype::DType;
354 use crate::dtype::DecimalDType;
355 use crate::dtype::Nullability;
356 use crate::dtype::PType;
357 use crate::expr::between;
358 use crate::expr::col;
359 use crate::expr::get_item;
360 use crate::expr::lit;
361 use crate::expr::root;
362 use crate::scalar::DecimalValue;
363 use crate::scalar::Scalar;
364 use crate::test_harness::to_int_indices;
365 use crate::validity::Validity;
366
367 static SESSION: LazyLock<VortexSession> = LazyLock::new(crate::array_session);
368
369 const NON_STRICT: BetweenOptions = BetweenOptions {
370 lower_strict: StrictComparison::NonStrict,
371 upper_strict: StrictComparison::NonStrict,
372 };
373
374 fn null_i32s(len: usize) -> ArrayRef {
376 let null = Scalar::null(DType::Primitive(PType::I32, Nullability::Nullable));
377 ConstantArray::new(null, len).into_array()
378 }
379
380 #[test]
381 fn lazy_validity() -> VortexResult<()> {
382 let ctx = &mut SESSION.create_execution_ctx();
383
384 let x = PrimitiveArray::from_option_iter([Some(10), Some(10), Some(1)]).into_array();
385 let lo = PrimitiveArray::from_option_iter([None, None, Some(0)]).into_array();
386 let hi = PrimitiveArray::from_option_iter([Some(5), Some(50), Some(5)]).into_array();
387
388 let data = StructArray::from_fields(&[("x", x), ("lo", lo), ("hi", hi)])?.into_array();
389 let lazy = data.apply(&between(col("x"), col("lo"), col("hi"), NON_STRICT))?;
390
391 let Validity::Array(validity) = lazy.validity()? else {
392 vortex_bail!("non-lazy validity");
393 };
394 assert_eq!(
395 validity.encoding_id().to_string(),
396 "vortex.scalar_fn_validity"
397 );
398 assert_arrays_eq!(validity, BoolArray::from_iter([true, false, true]), ctx);
399 Ok(())
400 }
401
402 #[test]
407 fn validity_agrees_with_execution() -> VortexResult<()> {
408 let ctx = &mut SESSION.create_execution_ctx();
409
410 let x = PrimitiveArray::from_option_iter([Some(10), Some(10), Some(1)]).into_array();
415 let lo = PrimitiveArray::from_option_iter([None, None, Some(0)]).into_array();
416 let hi = PrimitiveArray::from_option_iter([Some(5), Some(50), Some(5)]).into_array();
417 let data = StructArray::from_fields(&[("x", x), ("lo", lo), ("hi", hi)])?.into_array();
418
419 let expr = between(col("x"), col("lo"), col("hi"), NON_STRICT);
420
421 let executed = data
422 .clone()
423 .apply(&expr)?
424 .execute::<BoolArray>(ctx)?
425 .opt_bool_vec(ctx);
426
427 let validity_expr = expr.bind(data.dtype())?.validity()?;
428 let declared = data
429 .apply_bound(&validity_expr)?
430 .execute::<BoolArray>(ctx)?
431 .bool_vec(ctx);
432
433 assert_eq!(executed, [Some(false), None, Some(true)]);
434 assert_eq!(
435 executed.iter().map(Option::is_some).collect::<Vec<_>>(),
436 declared
437 );
438
439 Ok(())
440 }
441
442 #[test]
443 fn is_not_strict() {
444 let expr = between(
445 root(),
446 lit(0),
447 lit(100),
448 BetweenOptions {
449 lower_strict: StrictComparison::NonStrict,
450 upper_strict: StrictComparison::NonStrict,
451 },
452 );
453
454 assert!(!expr.as_scalar().is_some_and(|f| f.signature().is_strict()));
455 }
456
457 #[test]
458 fn test_display() {
459 let expr = between(
460 get_item("score", root()),
461 lit(10),
462 lit(50),
463 BetweenOptions {
464 lower_strict: StrictComparison::NonStrict,
465 upper_strict: StrictComparison::Strict,
466 },
467 );
468 assert_eq!(expr.to_string(), "(10i32 <= $.score < 50i32)");
469
470 let expr2 = between(
471 root(),
472 lit(0),
473 lit(100),
474 BetweenOptions {
475 lower_strict: StrictComparison::Strict,
476 upper_strict: StrictComparison::NonStrict,
477 },
478 );
479 assert_eq!(expr2.to_string(), "(0i32 < $ <= 100i32)");
480 }
481
482 #[rstest]
483 #[case(StrictComparison::NonStrict, StrictComparison::NonStrict, vec![0, 1, 2, 3])]
484 #[case(StrictComparison::NonStrict, StrictComparison::Strict, vec![0, 1])]
485 #[case(StrictComparison::Strict, StrictComparison::NonStrict, vec![0, 2])]
486 #[case(StrictComparison::Strict, StrictComparison::Strict, vec![0])]
487 fn test_bounds(
488 #[case] lower_strict: StrictComparison,
489 #[case] upper_strict: StrictComparison,
490 #[case] expected: Vec<u64>,
491 ) {
492 let lower = buffer![0, 0, 0, 0, 2].into_array();
493 let array = buffer![1, 0, 1, 0, 1].into_array();
494 let upper = buffer![2, 1, 1, 0, 0].into_array();
495 let ctx = &mut SESSION.create_execution_ctx();
496
497 let matches = between_canonical(
498 &array,
499 &lower,
500 &upper,
501 &BetweenOptions {
502 lower_strict,
503 upper_strict,
504 },
505 ctx,
506 )
507 .unwrap()
508 .execute::<BoolArray>(ctx)
509 .unwrap();
510
511 let indices = to_int_indices(matches, ctx).unwrap();
512 assert_eq!(indices, expected);
513 }
514
515 #[test]
516 fn test_constants() {
517 let lower = buffer![0, 0, 2, 0, 2].into_array();
518 let array = buffer![1, 0, 1, 0, 1].into_array();
519 let ctx = &mut SESSION.create_execution_ctx();
520
521 let upper = ConstantArray::new(
523 Scalar::null(DType::Primitive(PType::I32, Nullability::Nullable)),
524 5,
525 )
526 .into_array();
527
528 let matches = between_canonical(
529 &array,
530 &lower,
531 &upper,
532 &BetweenOptions {
533 lower_strict: StrictComparison::NonStrict,
534 upper_strict: StrictComparison::NonStrict,
535 },
536 ctx,
537 )
538 .unwrap()
539 .execute::<BoolArray>(ctx)
540 .unwrap();
541
542 assert_eq!(
544 matches.opt_bool_vec(ctx),
545 [None, None, Some(false), None, Some(false)]
546 );
547
548 let upper = ConstantArray::new(Scalar::from(2), 5).into_array();
550 let matches = between_canonical(
551 &array,
552 &lower,
553 &upper,
554 &BetweenOptions {
555 lower_strict: StrictComparison::NonStrict,
556 upper_strict: StrictComparison::NonStrict,
557 },
558 ctx,
559 )
560 .unwrap()
561 .execute::<BoolArray>(ctx)
562 .unwrap();
563 let indices = to_int_indices(matches, ctx).unwrap();
564 assert_eq!(indices, vec![0, 1, 3]);
565
566 let lower = ConstantArray::new(Scalar::from(0), 5).into_array();
568
569 let matches = between_canonical(
570 &array,
571 &lower,
572 &upper,
573 &BetweenOptions {
574 lower_strict: StrictComparison::NonStrict,
575 upper_strict: StrictComparison::NonStrict,
576 },
577 ctx,
578 )
579 .unwrap()
580 .execute::<BoolArray>(ctx)
581 .unwrap();
582 let indices = to_int_indices(matches, ctx).unwrap();
583 assert_eq!(indices, vec![0, 1, 2, 3, 4]);
584 }
585
586 #[rstest]
590 #[case::primitive_nulls(PrimitiveArray::from_option_iter([None::<i32>, None]).into_array())]
591 #[case::constant_null(null_i32s(2))]
592 fn null_lower_bound(#[case] lower: ArrayRef) -> VortexResult<()> {
593 let ctx = &mut SESSION.create_execution_ctx();
594 let array = buffer![10, 10].into_array();
595 let upper = buffer![5, 50].into_array();
596
597 let result = between_canonical(&array, &lower, &upper, &NON_STRICT, ctx)?
598 .execute::<BoolArray>(ctx)?;
599
600 assert_eq!(result.opt_bool_vec(ctx), [Some(false), None]);
602
603 Ok(())
604 }
605
606 #[test]
608 fn both_bounds_null() -> VortexResult<()> {
609 let ctx = &mut SESSION.create_execution_ctx();
610 let array = buffer![10, 10].into_array();
611 let bound = null_i32s(2);
612
613 let result = between_canonical(&array, &bound, &bound, &NON_STRICT, ctx)?
614 .execute::<BoolArray>(ctx)?;
615
616 assert_eq!(result.opt_bool_vec(ctx), [None, None]);
617
618 Ok(())
619 }
620
621 #[test]
622 fn test_between_decimal() {
623 let ctx = &mut SESSION.create_execution_ctx();
624 let values = buffer![100i128, 200i128, 300i128, 400i128];
625 let decimal_type = DecimalDType::new(3, 2);
626 let array = DecimalArray::new(values, decimal_type, Validity::NonNullable).into_array();
627
628 let lower = ConstantArray::new(
629 Scalar::decimal(
630 DecimalValue::I128(100i128),
631 decimal_type,
632 Nullability::NonNullable,
633 ),
634 array.len(),
635 )
636 .into_array();
637 let upper = ConstantArray::new(
638 Scalar::decimal(
639 DecimalValue::I128(400i128),
640 decimal_type,
641 Nullability::NonNullable,
642 ),
643 array.len(),
644 )
645 .into_array();
646
647 let between_strict = between_canonical(
649 &array,
650 &lower,
651 &upper,
652 &BetweenOptions {
653 lower_strict: StrictComparison::Strict,
654 upper_strict: StrictComparison::NonStrict,
655 },
656 ctx,
657 )
658 .unwrap();
659 assert_arrays_eq!(
660 between_strict,
661 BoolArray::from_iter([false, true, true, true]),
662 ctx
663 );
664
665 let between_strict = between_canonical(
667 &array,
668 &lower,
669 &upper,
670 &BetweenOptions {
671 lower_strict: StrictComparison::NonStrict,
672 upper_strict: StrictComparison::Strict,
673 },
674 ctx,
675 )
676 .unwrap();
677 assert_arrays_eq!(
678 between_strict,
679 BoolArray::from_iter([true, true, true, false]),
680 ctx
681 );
682 }
683
684 #[rstest]
690 #[case(DecimalValue::I16(1), DecimalValue::I32(82246), vec![0, 1, 2, 3])]
692 #[case(DecimalValue::I32(82246), DecimalValue::I16(4), vec![])]
694 #[case(DecimalValue::I16(1), DecimalValue::I32(-82246), vec![])]
696 #[case(DecimalValue::I32(-82246), DecimalValue::I16(2), vec![0, 1])]
698 fn test_between_decimal_mismatched_storage_types(
699 #[case] lower_val: DecimalValue,
700 #[case] upper_val: DecimalValue,
701 #[case] expected_indices: Vec<u64>,
702 ) {
703 let ctx = &mut SESSION.create_execution_ctx();
704 let decimal_type = DecimalDType::new(5, -67);
707 let array = DecimalArray::new(
708 buffer![1i16, 2i16, 3i16, 4i16],
709 decimal_type,
710 Validity::NonNullable,
711 )
712 .into_array();
713
714 let lower = ConstantArray::new(
715 Scalar::decimal(lower_val, decimal_type, Nullability::NonNullable),
716 array.len(),
717 )
718 .into_array();
719 let upper = ConstantArray::new(
720 Scalar::decimal(upper_val, decimal_type, Nullability::NonNullable),
721 array.len(),
722 )
723 .into_array();
724
725 let result = between_canonical(
726 &array,
727 &lower,
728 &upper,
729 &BetweenOptions {
730 lower_strict: StrictComparison::NonStrict,
731 upper_strict: StrictComparison::NonStrict,
732 },
733 ctx,
734 )
735 .unwrap()
736 .execute::<BoolArray>(ctx)
737 .unwrap();
738
739 assert_eq!(to_int_indices(result, ctx).unwrap(), expected_indices);
740 }
741}