Skip to content

Commit b74f5ce

Browse files
committed
feat: implement IsNotNull expression in vortex expression library
Add a first-class IsNotNull scalar function instead of composing Not(IsNull(...)). This simplifies the expression tree, enables direct stat_falsification for zone map pruning, and updates all integration points (DataFusion, DuckDB, Python/Substrait). The stat_falsification uses is_constant && null_count > 0 as an approximation since there is no RowCount stat yet. Closes: #6040
1 parent d08c89b commit b74f5ce

13 files changed

Lines changed: 368 additions & 22 deletions

File tree

vortex-array/src/builtins.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::scalar_fn::fns::binary::Binary;
2828
use crate::scalar_fn::fns::cast::Cast;
2929
use crate::scalar_fn::fns::fill_null::FillNull;
3030
use crate::scalar_fn::fns::get_item::GetItem;
31+
use crate::scalar_fn::fns::is_not_null::IsNotNull;
3132
use crate::scalar_fn::fns::is_null::IsNull;
3233
use crate::scalar_fn::fns::list_contains::ListContains;
3334
use crate::scalar_fn::fns::mask::Mask;
@@ -49,6 +50,9 @@ pub trait ExprBuiltins: Sized {
4950
/// Is null check.
5051
fn is_null(&self) -> VortexResult<Expression>;
5152

53+
/// Is not null check.
54+
fn is_not_null(&self) -> VortexResult<Expression>;
55+
5256
/// Mask the expression using the given boolean mask.
5357
/// The resulting expression's validity is the intersection of the original expression's
5458
/// validity.
@@ -84,6 +88,10 @@ impl ExprBuiltins for Expression {
8488
IsNull.try_new_expr(EmptyOptions, [self.clone()])
8589
}
8690

91+
fn is_not_null(&self) -> VortexResult<Expression> {
92+
IsNotNull.try_new_expr(EmptyOptions, [self.clone()])
93+
}
94+
8795
fn mask(&self, mask: Expression) -> VortexResult<Expression> {
8896
Mask.try_new_expr(EmptyOptions, [self.clone(), mask])
8997
}
@@ -118,6 +126,9 @@ pub trait ArrayBuiltins: Sized {
118126
/// Is null check.
119127
fn is_null(&self) -> VortexResult<ArrayRef>;
120128

129+
/// Is not null check.
130+
fn is_not_null(&self) -> VortexResult<ArrayRef>;
131+
121132
/// Mask the array using the given boolean mask.
122133
/// The resulting array's validity is the intersection of the original array's validity
123134
/// and the mask's validity.
@@ -182,6 +193,12 @@ impl ArrayBuiltins for ArrayRef {
182193
.optimize()
183194
}
184195

196+
fn is_not_null(&self) -> VortexResult<ArrayRef> {
197+
IsNotNull
198+
.try_new_array(self.len(), EmptyOptions, [self.clone()])?
199+
.optimize()
200+
}
201+
185202
fn mask(self, mask: ArrayRef) -> VortexResult<ArrayRef> {
186203
Mask.try_new_array(self.len(), EmptyOptions, [self, mask])?
187204
.optimize()

vortex-array/src/expr/exprs.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use crate::scalar_fn::fns::dynamic::DynamicComparisonExpr;
2929
use crate::scalar_fn::fns::dynamic::Rhs;
3030
use crate::scalar_fn::fns::fill_null::FillNull;
3131
use crate::scalar_fn::fns::get_item::GetItem;
32+
use crate::scalar_fn::fns::is_not_null::IsNotNull;
3233
use crate::scalar_fn::fns::is_null::IsNull;
3334
use crate::scalar_fn::fns::like::Like;
3435
use crate::scalar_fn::fns::like::LikeOptions;
@@ -547,6 +548,20 @@ pub fn is_null(child: Expression) -> Expression {
547548
IsNull.new_expr(EmptyOptions, vec![child])
548549
}
549550

551+
// ---- IsNotNull ----
552+
553+
/// Creates an expression that checks for non-null values.
554+
///
555+
/// Returns a boolean array indicating which positions contain non-null values.
556+
///
557+
/// ```rust
558+
/// # use vortex_array::expr::{is_not_null, root};
559+
/// let expr = is_not_null(root());
560+
/// ```
561+
pub fn is_not_null(child: Expression) -> Expression {
562+
IsNotNull.new_expr(EmptyOptions, vec![child])
563+
}
564+
550565
// ---- Like ----
551566

552567
/// Creates a SQL LIKE expression.

vortex-array/src/scalar_fn/erased.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ use crate::scalar_fn::ScalarFnId;
3131
use crate::scalar_fn::ScalarFnVTable;
3232
use crate::scalar_fn::ScalarFnVTableExt;
3333
use crate::scalar_fn::SimplifyCtx;
34-
use crate::scalar_fn::fns::is_null::IsNull;
35-
use crate::scalar_fn::fns::not::Not;
34+
use crate::scalar_fn::fns::is_not_null::IsNotNull;
3635
use crate::scalar_fn::options::ScalarFnOptions;
3736
use crate::scalar_fn::signature::ScalarFnSignature;
3837
use crate::scalar_fn::typed::DynScalarFn;
@@ -135,11 +134,7 @@ impl ScalarFnRef {
135134
pub fn validity(&self, expr: &Expression) -> VortexResult<Expression> {
136135
Ok(self.0.validity(expr)?.unwrap_or_else(|| {
137136
// TODO(ngates): make validity a mandatory method on VTable to avoid this fallback.
138-
// TODO(ngates): add an IsNotNull expression.
139-
Not.new_expr(
140-
EmptyOptions,
141-
[IsNull.new_expr(EmptyOptions, [expr.clone()])],
142-
)
137+
IsNotNull.new_expr(EmptyOptions, [expr.clone()])
143138
}))
144139
}
145140

0 commit comments

Comments
 (0)