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