1mod 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#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
64pub enum StrictComparison {
65 Strict,
67 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
91pub(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 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
120fn 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 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 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#[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 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 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 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 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 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 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 #[rstest]
557 #[case(DecimalValue::I16(1), DecimalValue::I32(82246), vec![0, 1, 2, 3])]
559 #[case(DecimalValue::I32(82246), DecimalValue::I16(4), vec![])]
561 #[case(DecimalValue::I16(1), DecimalValue::I32(-82246), vec![])]
563 #[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 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}