use vortex_error::VortexResult;
use vortex_error::vortex_err;
use crate::ArrayRef;
use crate::ExecutionCtx;
use crate::array::ArrayView;
use crate::array::VTable;
use crate::arrays::Bool;
use crate::arrays::Constant;
use crate::arrays::scalar_fn::ExactScalarFn;
use crate::arrays::scalar_fn::ScalarFnArrayView;
use crate::kernel::ExecuteParentKernel;
use crate::optimizer::rules::ArrayParentReduceRule;
use crate::scalar_fn::fns::mask::Mask as MaskExpr;
pub trait MaskReduce: VTable {
const VALIDITY_IS_METADATA_ONLY: bool = false;
fn mask(array: ArrayView<'_, Self>, mask: &ArrayRef) -> VortexResult<Option<ArrayRef>>;
}
pub trait MaskKernel: VTable {
fn mask(
array: ArrayView<'_, Self>,
mask: &ArrayRef,
ctx: &mut ExecutionCtx,
) -> VortexResult<Option<ArrayRef>>;
}
#[derive(Default, Debug)]
pub struct MaskReduceAdaptor<V>(pub V);
impl<V> ArrayParentReduceRule<V> for MaskReduceAdaptor<V>
where
V: MaskReduce,
{
type Parent = ExactScalarFn<MaskExpr>;
fn reduce_parent(
&self,
array: ArrayView<'_, V>,
parent: ScalarFnArrayView<'_, MaskExpr>,
child_idx: usize,
) -> VortexResult<Option<ArrayRef>> {
if child_idx != 0 {
return Ok(None);
}
let parent_ref: ArrayRef = (*parent).clone();
let mask_child = parent_ref
.nth_child(1)
.ok_or_else(|| vortex_err!("Mask expression must have 2 children"))?;
if mask_child.as_opt::<Bool>().is_none() && mask_child.as_opt::<Constant>().is_none() {
let can_attach_mask =
V::VALIDITY_IS_METADATA_ONLY && array.validity()?.definitely_no_nulls();
if !can_attach_mask {
return Ok(None);
}
}
<V as MaskReduce>::mask(array, mask_child)
}
}
#[derive(Default, Debug)]
pub struct MaskExecuteAdaptor<V>(pub V);
impl<V> ExecuteParentKernel<V> for MaskExecuteAdaptor<V>
where
V: MaskKernel,
{
type Parent = ExactScalarFn<MaskExpr>;
fn execute_parent(
&self,
array: ArrayView<'_, V>,
parent: ScalarFnArrayView<'_, MaskExpr>,
child_idx: usize,
ctx: &mut ExecutionCtx,
) -> VortexResult<Option<ArrayRef>> {
if child_idx != 0 {
return Ok(None);
}
let mask_child = parent
.nth_child(1)
.ok_or_else(|| vortex_err!("Mask expression must have 2 children"))?;
<V as MaskKernel>::mask(array, mask_child, ctx)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use vortex_buffer::buffer;
use vortex_error::VortexResult;
use crate::ArrayEq;
use crate::ArrayRef;
use crate::EqMode;
use crate::IntoArray;
use crate::arrays::BoolArray;
use crate::arrays::ConstantArray;
use crate::arrays::Primitive;
use crate::arrays::PrimitiveArray;
use crate::arrays::ScalarFn;
use crate::arrays::scalar_fn::ExactScalarFn;
use crate::assert_arrays_eq;
use crate::builtins::ArrayBuiltins;
use crate::dtype::Nullability;
use crate::executor::VortexSessionExecute;
use crate::optimizer::ArrayOptimizer;
use crate::scalar::Scalar;
use crate::scalar_fn::fns::binary::Binary;
use crate::scalar_fn::fns::mask::Mask as MaskExpr;
use crate::scalar_fn::fns::operators::Operator;
use crate::validity::Validity;
fn lazy_mask() -> VortexResult<ArrayRef> {
Binary::try_new(
BoolArray::from_iter([true, true, false]).into_array(),
BoolArray::from_iter([true, false, true]).into_array(),
Operator::And,
)
.map(IntoArray::into_array)
}
#[rstest]
#[case::non_nullable(Validity::NonNullable)]
#[case::all_valid(Validity::AllValid)]
fn all_valid_input_attaches_lazy_mask(#[case] validity: Validity) -> VortexResult<()> {
let input = PrimitiveArray::new(buffer![1i32, 2, 3], validity);
let mask = lazy_mask()?;
let output = input.clone().into_array().mask(mask.clone())?;
let primitive = output.as_::<Primitive>();
assert!(
primitive
.buffer_handle()
.array_eq(input.buffer_handle(), EqMode::Ptr)
);
assert_eq!(output.dtype(), &input.dtype().as_nullable());
assert_eq!(output.len(), input.len());
let Validity::Array(validity) = output.validity()? else {
panic!("mask must remain array-backed");
};
assert!(ArrayRef::ptr_eq(&validity, &mask));
Ok(())
}
#[rstest]
#[case::partially_valid(false)]
#[case::all_true_bitmap(true)]
fn array_backed_validity_keeps_lazy_mask(#[case] all_true: bool) -> VortexResult<()> {
let validity = BoolArray::from_iter([true, all_true, true]).into_array();
let input =
PrimitiveArray::new(buffer![1i32, 2, 3], Validity::Array(validity)).into_array();
let output = input.mask(lazy_mask()?)?;
assert!(output.is::<ExactScalarFn<MaskExpr>>());
Ok(())
}
#[rstest]
#[case(true)]
#[case(false)]
fn constant_mask_reduces_into_input(#[case] mask_value: bool) -> VortexResult<()> {
let input = buffer![1i32, 2, 3, 4, 5].into_array();
let mask = ConstantArray::new(
Scalar::bool(mask_value, Nullability::NonNullable),
input.len(),
)
.into_array();
let masked = MaskExpr::try_new(input, mask)?.into_array();
assert!(
masked.is::<ScalarFn>(),
"expected an un-optimized ScalarFn wrapper before optimization"
);
let optimized = masked.optimize()?;
assert!(
!optimized.is::<ScalarFn>(),
"constant mask should not fall through to execution, got {}",
optimized.encoding_id()
);
assert!(
optimized.is::<Primitive>(),
"constant mask should reduce into the Primitive input, got {}",
optimized.encoding_id()
);
let mut ctx = crate::array_session().create_execution_ctx();
let expected = if mask_value {
PrimitiveArray::from_option_iter([Some(1i32), Some(2), Some(3), Some(4), Some(5)])
} else {
PrimitiveArray::from_option_iter([None::<i32>, None, None, None, None])
};
assert_arrays_eq!(optimized, expected, &mut ctx);
Ok(())
}
}