Skip to content

Commit b58f1d5

Browse files
committed
Add MaskNullAsFalse execution target and make Mask strict
Mask execution now requires a non-nullable boolean array and errors on nullable input. The new MaskNullAsFalse executable preserves the previous null-as-false coercion for filter and pruning predicates over nullable data, where SQL semantics treat NULL as not matching. Predicate-evaluation call sites (filter, prune, dict filter, is_constant) use MaskNullAsFalse; validity-array call sites keep the stricter Mask. Signed-off-by: Claude <noreply@anthropic.com>
1 parent d7134a9 commit b58f1d5

11 files changed

Lines changed: 131 additions & 37 deletions

File tree

vortex-array/public-api.lock

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14734,6 +14734,20 @@ pub fn vortex_array::scalar_fn::fns::zip::ZipExecuteAdaptor<V>::execute_parent(&
1473414734

1473514735
pub mod vortex_array::mask
1473614736

14737+
pub struct vortex_array::mask::MaskNullAsFalse(_)
14738+
14739+
impl vortex_array::mask::MaskNullAsFalse
14740+
14741+
pub fn vortex_array::mask::MaskNullAsFalse::into_mask(self) -> vortex_mask::Mask
14742+
14743+
impl core::convert::From<vortex_array::mask::MaskNullAsFalse> for vortex_mask::Mask
14744+
14745+
pub fn vortex_mask::Mask::from(vortex_array::mask::MaskNullAsFalse) -> Self
14746+
14747+
impl vortex_array::Executable for vortex_array::mask::MaskNullAsFalse
14748+
14749+
pub fn vortex_array::mask::MaskNullAsFalse::execute(vortex_array::ArrayRef, &mut vortex_array::ExecutionCtx) -> vortex_error::VortexResult<Self>
14750+
1473714751
pub mod vortex_array::matcher
1473814752

1473914753
pub struct vortex_array::matcher::AnyArray
@@ -26326,6 +26340,10 @@ impl vortex_array::Executable for vortex_array::arrays::null::NullArray
2632626340

2632726341
pub fn vortex_array::arrays::null::NullArray::execute(vortex_array::ArrayRef, &mut vortex_array::ExecutionCtx) -> vortex_error::VortexResult<Self>
2632826342

26343+
impl vortex_array::Executable for vortex_array::mask::MaskNullAsFalse
26344+
26345+
pub fn vortex_array::mask::MaskNullAsFalse::execute(vortex_array::ArrayRef, &mut vortex_array::ExecutionCtx) -> vortex_error::VortexResult<Self>
26346+
2632926347
impl vortex_array::Executable for vortex_buffer::bit::buf::BitBuffer
2633026348

2633126349
pub fn vortex_buffer::bit::buf::BitBuffer::execute(vortex_array::ArrayRef, &mut vortex_array::ExecutionCtx) -> vortex_error::VortexResult<Self>

vortex-array/src/aggregate_fn/fns/is_constant/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ mod varbin;
1313
use vortex_error::VortexExpect;
1414
use vortex_error::VortexResult;
1515
use vortex_error::vortex_bail;
16-
use vortex_mask::Mask;
1716

1817
use self::bool::check_bool_constant;
1918
use self::decimal::check_decimal_constant;
@@ -44,6 +43,7 @@ use crate::expr::stats::Precision;
4443
use crate::expr::stats::Stat;
4544
use crate::expr::stats::StatsProvider;
4645
use crate::expr::stats::StatsProviderExt;
46+
use crate::mask::MaskNullAsFalse;
4747
use crate::scalar::Scalar;
4848
use crate::scalar_fn::fns::operators::Operator;
4949

@@ -74,7 +74,7 @@ fn arrays_value_equal(a: &ArrayRef, b: &ArrayRef, ctx: &mut ExecutionCtx) -> Vor
7474
// Compare values element-wise. Result is null where both inputs are null,
7575
// true/false where both are valid.
7676
let eq_result = a.binary(b.clone(), Operator::Eq)?;
77-
let eq_result = eq_result.execute::<Mask>(ctx)?;
77+
let eq_result = eq_result.execute::<MaskNullAsFalse>(ctx)?.into_mask();
7878

7979
Ok(eq_result.true_count() == valid_count)
8080
}

vortex-array/src/mask.rs

Lines changed: 82 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,90 @@ use crate::columnar::Columnar;
1717
use crate::dtype::DType;
1818

1919
impl Executable for Mask {
20+
/// Executes a boolean array into a [`Mask`].
21+
///
22+
/// The array must have a non-nullable boolean dtype. Use [`MaskNullAsFalse`] to execute a
23+
/// nullable boolean array, coercing null elements to `false`.
2024
fn execute(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self> {
21-
if !matches!(array.dtype(), DType::Bool(_)) {
22-
vortex_bail!("Mask array must have boolean dtype, not {}", array.dtype());
23-
}
25+
execute_mask(array, ctx, NullHandling::Reject)
26+
}
27+
}
2428

25-
if let Some(constant) = array.as_opt::<Constant>() {
26-
let mask_value = constant.scalar().as_bool().value().unwrap_or(false);
27-
return Ok(Mask::new(array.len(), mask_value));
28-
}
29+
/// An [`Executable`] target that executes a boolean array into a [`Mask`], coercing null
30+
/// elements to `false`.
31+
///
32+
/// [`Mask`] itself requires a non-nullable boolean array and errors on nullable input. Use this
33+
/// wrapper for filter and pruning predicates over nullable data, where SQL semantics treat
34+
/// `NULL` as not matching.
35+
pub struct MaskNullAsFalse(Mask);
2936

30-
let array_len = array.len();
31-
Ok(match array.execute(ctx)? {
32-
Columnar::Constant(s) => {
33-
Mask::new(array_len, s.scalar().as_bool().value().unwrap_or(false))
34-
}
35-
Columnar::Canonical(a) => {
36-
let bool = a.into_array().execute::<BoolArray>(ctx)?;
37-
let mask = bool
38-
.as_ref()
39-
.validity()?
40-
.execute_mask(bool.as_ref().len(), ctx)?;
41-
let bits = bool.into_bit_buffer();
42-
// To handle nullable boolean arrays, we treat nulls as false in the mask.
43-
// TODO(ngates): is this correct? Feels like we should just force the caller to
44-
// pass non-nullable boolean arrays.
45-
mask.bitand(&Mask::from(bits))
46-
}
47-
})
37+
impl MaskNullAsFalse {
38+
/// Consumes the wrapper and returns the underlying [`Mask`].
39+
pub fn into_mask(self) -> Mask {
40+
self.0
41+
}
42+
}
43+
44+
impl From<MaskNullAsFalse> for Mask {
45+
fn from(value: MaskNullAsFalse) -> Self {
46+
value.0
47+
}
48+
}
49+
50+
impl Executable for MaskNullAsFalse {
51+
fn execute(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self> {
52+
execute_mask(array, ctx, NullHandling::AsFalse).map(Self)
53+
}
54+
}
55+
56+
/// How [`execute_mask`] treats null elements of a nullable boolean array.
57+
enum NullHandling {
58+
/// Error if the boolean array is nullable.
59+
Reject,
60+
/// Treat null elements as `false`.
61+
AsFalse,
62+
}
63+
64+
fn execute_mask(
65+
array: ArrayRef,
66+
ctx: &mut ExecutionCtx,
67+
null_handling: NullHandling,
68+
) -> VortexResult<Mask> {
69+
if !matches!(array.dtype(), DType::Bool(_)) {
70+
vortex_bail!("Mask array must have boolean dtype, not {}", array.dtype());
4871
}
72+
73+
if let Some(constant) = array.as_opt::<Constant>() {
74+
let mask_value = constant.scalar().as_bool().value().unwrap_or(false);
75+
return Ok(Mask::new(array.len(), mask_value));
76+
}
77+
78+
let array_len = array.len();
79+
Ok(match array.execute(ctx)? {
80+
Columnar::Constant(s) => {
81+
Mask::new(array_len, s.scalar().as_bool().value().unwrap_or(false))
82+
}
83+
Columnar::Canonical(a) => {
84+
let bool = a.into_array().execute::<BoolArray>(ctx)?;
85+
match null_handling {
86+
NullHandling::Reject => {
87+
if bool.as_ref().dtype().is_nullable() {
88+
vortex_bail!(
89+
"Mask requires a non-nullable boolean array, not {}; \
90+
use MaskNullAsFalse to coerce nulls to false",
91+
bool.as_ref().dtype()
92+
);
93+
}
94+
Mask::from(bool.into_bit_buffer())
95+
}
96+
NullHandling::AsFalse => {
97+
let validity = bool
98+
.as_ref()
99+
.validity()?
100+
.execute_mask(bool.as_ref().len(), ctx)?;
101+
validity.bitand(&Mask::from(bool.into_bit_buffer()))
102+
}
103+
}
104+
}
105+
})
49106
}

vortex-cuda/src/layout.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ use vortex::layout::sequence::SendableSequentialStream;
5959
use vortex::layout::sequence::SequencePointer;
6060
use vortex::layout::vtable;
6161
use vortex::mask::Mask;
62+
use vortex::mask::MaskNullAsFalse;
6263
use vortex::scalar::Scalar;
6364
use vortex::scalar::ScalarTruncation;
6465
use vortex::scalar::lower_bound;
@@ -329,12 +330,12 @@ impl LayoutReader for CudaFlatReader {
329330
let array = array.apply(&expr)?;
330331
let array = array.filter(mask.clone())?;
331332
let mut ctx = session.create_execution_ctx();
332-
let array_mask = array.execute::<Mask>(&mut ctx)?;
333+
let array_mask = array.execute::<MaskNullAsFalse>(&mut ctx)?.into_mask();
333334
mask.intersect_by_rank(&array_mask)
334335
} else {
335336
let array = array.apply(&expr)?;
336337
let mut ctx = session.create_execution_ctx();
337-
let array_mask = array.execute::<Mask>(&mut ctx)?;
338+
let array_mask = array.execute::<MaskNullAsFalse>(&mut ctx)?.into_mask();
338339
mask.bitand(&array_mask)
339340
};
340341

vortex-layout/src/layouts/dict/reader.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use vortex_array::dtype::DType;
2121
use vortex_array::dtype::FieldMask;
2222
use vortex_array::expr::Expression;
2323
use vortex_array::expr::root;
24+
use vortex_array::mask::MaskNullAsFalse;
2425
use vortex_array::optimizer::ArrayOptimizer;
2526
use vortex_error::VortexError;
2627
use vortex_error::VortexExpect;
@@ -212,7 +213,10 @@ impl LayoutReader for DictReader {
212213
let mask = mask.await?;
213214

214215
let mut ctx = session.create_execution_ctx();
215-
let dict_mask = values.take(codes)?.execute::<Mask>(&mut ctx)?;
216+
let dict_mask = values
217+
.take(codes)?
218+
.execute::<MaskNullAsFalse>(&mut ctx)?
219+
.into_mask();
216220

217221
Ok(mask.bitand(&dict_mask))
218222
}))

vortex-layout/src/layouts/flat/reader.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use vortex_array::VortexSessionExecute;
1515
use vortex_array::dtype::DType;
1616
use vortex_array::dtype::FieldMask;
1717
use vortex_array::expr::Expression;
18+
use vortex_array::mask::MaskNullAsFalse;
1819
use vortex_array::serde::SerializedArray;
1920
use vortex_error::VortexExpect;
2021
use vortex_error::VortexResult;
@@ -155,14 +156,14 @@ impl LayoutReader for FlatReader {
155156
let array = array.apply(&expr)?;
156157
let array = array.filter(mask.clone())?;
157158
let mut ctx = session.create_execution_ctx();
158-
let array_mask = array.execute::<Mask>(&mut ctx)?;
159+
let array_mask = array.execute::<MaskNullAsFalse>(&mut ctx)?.into_mask();
159160

160161
mask.intersect_by_rank(&array_mask)
161162
} else {
162163
// Run over the full array, with a simpler bitand at the end.
163164
let array = array.apply(&expr)?;
164165
let mut ctx = session.create_execution_ctx();
165-
let array_mask = array.execute::<Mask>(&mut ctx)?;
166+
let array_mask = array.execute::<MaskNullAsFalse>(&mut ctx)?.into_mask();
166167

167168
mask.bitand(&array_mask)
168169
};

vortex-layout/src/layouts/partitioned.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ use vortex_array::dtype::DType;
1515
use vortex_array::dtype::Nullability;
1616
use vortex_array::expr::Expression;
1717
use vortex_array::expr::transform::PartitionedExpr;
18+
use vortex_array::mask::MaskNullAsFalse;
1819
use vortex_array::validity::Validity;
1920
use vortex_error::VortexError;
2021
use vortex_error::VortexResult;
21-
use vortex_mask::Mask;
2222
use vortex_session::VortexSession;
2323

2424
use crate::ArrayFuture;
@@ -90,7 +90,10 @@ impl<P: Send + Sync + 'static> PartitionedExprEval<P> for PartitionedExpr<P> {
9090
.into_array();
9191

9292
let mut ctx = session.create_execution_ctx();
93-
let root_mask = root_scope.apply(&self.root)?.execute::<Mask>(&mut ctx)?;
93+
let root_mask = root_scope
94+
.apply(&self.root)?
95+
.execute::<MaskNullAsFalse>(&mut ctx)?
96+
.into_mask();
9497

9598
let mask = mask.bitand(&root_mask);
9699

vortex-layout/src/layouts/row_idx/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use vortex_array::expr::root;
3232
use vortex_array::expr::transform::PartitionedExpr;
3333
use vortex_array::expr::transform::partition;
3434
use vortex_array::expr::transform::replace;
35+
use vortex_array::mask::MaskNullAsFalse;
3536
use vortex_array::scalar::PValue;
3637
use vortex_error::VortexExpect;
3738
use vortex_error::VortexResult;
@@ -295,7 +296,10 @@ fn row_idx_mask_future(
295296
let array = idx_array(row_offset, &row_range).into_array();
296297

297298
let mut ctx = session.create_execution_ctx();
298-
let result_mask = array.apply(&expr)?.execute::<Mask>(&mut ctx)?;
299+
let result_mask = array
300+
.apply(&expr)?
301+
.execute::<MaskNullAsFalse>(&mut ctx)?
302+
.into_mask();
299303

300304
Ok(result_mask.bitand(&mask.await?))
301305
})

vortex-layout/src/layouts/zoned/zone_map.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use vortex_array::arrays::StructArray;
1414
use vortex_array::dtype::DType;
1515
use vortex_array::expr::Expression;
1616
use vortex_array::expr::stats::Stat;
17+
use vortex_array::mask::MaskNullAsFalse;
1718
use vortex_array::scalar_fn::internal::row_count::contains_row_count;
1819
use vortex_array::scalar_fn::internal::row_count::substitute_row_count;
1920
use vortex_array::validity::Validity;
@@ -99,12 +100,14 @@ impl ZoneMap {
99100
let applied = self.array.clone().into_array().apply(predicate)?;
100101

101102
if num_zones == 0 || !contains_row_count(&applied) {
102-
return applied.execute::<Mask>(&mut ctx);
103+
return Ok(applied.execute::<MaskNullAsFalse>(&mut ctx)?.into_mask());
103104
}
104105

105106
let row_count_array = row_count_array(self.zone_len, self.row_count, num_zones)?;
106107
let substituted = substitute_row_count(applied, &row_count_array)?;
107-
substituted.execute::<Mask>(&mut ctx)
108+
Ok(substituted
109+
.execute::<MaskNullAsFalse>(&mut ctx)?
110+
.into_mask())
108111
}
109112
}
110113

vortex/public-api.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ pub mod vortex::mask
112112

113113
pub use vortex::mask::<<vortex_mask::*>>
114114

115+
pub use vortex::mask::MaskNullAsFalse
116+
115117
pub mod vortex::metrics
116118

117119
pub use vortex::metrics::<<vortex_metrics::*>>

0 commit comments

Comments
 (0)