From a3494a5f669180efabf5c106be4d090973dfb6ce Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Thu, 23 Jul 2026 12:26:54 -0400 Subject: [PATCH] Rename scalar function strictness contract Signed-off-by: Connor Tsui --- docs/concepts/expressions.md | 2 +- .../src/json_to_variant_tests.rs | 2 +- vortex-array/src/arrays/dict/compute/rules.rs | 18 +++--- .../src/arrays/scalar_fn/vtable/mod.rs | 4 ++ vortex-array/src/expr/analysis/labeling.rs | 3 + vortex-array/src/expr/analysis/mod.rs | 5 +- .../src/expr/analysis/null_sensitive.rs | 62 ------------------- vortex-array/src/expr/analysis/strict.rs | 56 +++++++++++++++++ vortex-array/src/expr/mod.rs | 2 +- vortex-array/src/scalar_fn/fns/between/mod.rs | 17 ++++- .../src/scalar_fn/fns/binary/boolean.rs | 3 +- vortex-array/src/scalar_fn/fns/binary/mod.rs | 9 ++- vortex-array/src/scalar_fn/fns/byte_length.rs | 4 +- vortex-array/src/scalar_fn/fns/case_when.rs | 5 +- vortex-array/src/scalar_fn/fns/cast/mod.rs | 6 +- vortex-array/src/scalar_fn/fns/dynamic.rs | 17 ++++- vortex-array/src/scalar_fn/fns/ext_storage.rs | 4 +- .../src/scalar_fn/fns/fill_null/mod.rs | 5 +- vortex-array/src/scalar_fn/fns/get_item.rs | 4 +- vortex-array/src/scalar_fn/fns/is_not_null.rs | 9 +-- vortex-array/src/scalar_fn/fns/is_null.rs | 10 +-- vortex-array/src/scalar_fn/fns/like/mod.rs | 6 +- .../src/scalar_fn/fns/list_contains/mod.rs | 6 +- vortex-array/src/scalar_fn/fns/list_length.rs | 4 +- vortex-array/src/scalar_fn/fns/list_sum.rs | 4 +- vortex-array/src/scalar_fn/fns/literal.rs | 4 +- vortex-array/src/scalar_fn/fns/mask/mod.rs | 4 ++ vortex-array/src/scalar_fn/fns/merge.rs | 2 +- vortex-array/src/scalar_fn/fns/not/mod.rs | 27 +++++++- vortex-array/src/scalar_fn/fns/pack.rs | 6 +- vortex-array/src/scalar_fn/fns/root.rs | 4 +- vortex-array/src/scalar_fn/fns/select.rs | 2 +- vortex-array/src/scalar_fn/fns/stat.rs | 4 ++ .../src/scalar_fn/fns/variant_get/mod.rs | 4 ++ vortex-array/src/scalar_fn/fns/zip/mod.rs | 5 +- vortex-array/src/scalar_fn/foreign.rs | 4 ++ .../src/scalar_fn/internal/row_count.rs | 4 +- vortex-array/src/scalar_fn/signature.rs | 8 +-- vortex-array/src/scalar_fn/typed.rs | 6 +- vortex-array/src/scalar_fn/vtable.rs | 47 ++++++++++---- vortex-geo/src/scalar_fn/contains.rs | 4 +- vortex-geo/src/scalar_fn/distance.rs | 4 +- vortex-geo/src/scalar_fn/envelope.rs | 4 +- vortex-geo/src/scalar_fn/intersects.rs | 4 +- vortex-json/src/json_to_variant.rs | 9 ++- vortex-layout/src/layouts/dict/reader.rs | 10 +-- vortex-layout/src/layouts/row_idx/expr.rs | 4 ++ vortex-row/src/encode.rs | 4 +- vortex-row/src/size.rs | 4 +- .../src/scalar_fns/cosine_similarity.rs | 4 +- vortex-tensor/src/scalar_fns/inner_product.rs | 4 +- vortex-tensor/src/scalar_fns/l2_denorm.rs | 4 +- vortex-tensor/src/scalar_fns/l2_norm.rs | 4 +- 53 files changed, 280 insertions(+), 181 deletions(-) delete mode 100644 vortex-array/src/expr/analysis/null_sensitive.rs create mode 100644 vortex-array/src/expr/analysis/strict.rs diff --git a/docs/concepts/expressions.md b/docs/concepts/expressions.md index 3c4704ca71a..2a8c5a4b78b 100644 --- a/docs/concepts/expressions.md +++ b/docs/concepts/expressions.md @@ -7,7 +7,7 @@ to the returned data. ## Scalar Functions Expressions are defined as abstract scalar functions. These vtables define the signature of the function, properties -such as whether the function is null-sensitive, and the actual logic for executing the function over input arrays. +such as whether the function is strict, and the actual logic for executing the function over input arrays. The built-in scalar functions can be found in the `vortex-array::expr` module, with additional use-case specific functions provided by integration and plugin crates. diff --git a/encodings/parquet-variant/src/json_to_variant_tests.rs b/encodings/parquet-variant/src/json_to_variant_tests.rs index f8406ab5d2d..5bc09a426f7 100644 --- a/encodings/parquet-variant/src/json_to_variant_tests.rs +++ b/encodings/parquet-variant/src/json_to_variant_tests.rs @@ -145,7 +145,7 @@ fn converts_json_extension_input() -> VortexResult<()> { #[test] fn dict_encoded_input_converts_each_row() -> VortexResult<()> { // A dictionary-encoded JSON column exercises the dict-pushdown / canonicalization path: - // `json_to_variant` is not null-sensitive, so it pushes into the dict values (canonical + // `json_to_variant` is strict, so it pushes into the dict values (canonical // JSON extension values) where the kernel fires; either way every row must convert correctly. let values = json_input(VarBinViewArray::from_iter_str(["1", "2"]).into_array())?; let codes = PrimitiveArray::from_iter([0u8, 1, 0, 1, 0]).into_array(); diff --git a/vortex-array/src/arrays/dict/compute/rules.rs b/vortex-array/src/arrays/dict/compute/rules.rs index 33babbdc265..8863775002e 100644 --- a/vortex-array/src/arrays/dict/compute/rules.rs +++ b/vortex-array/src/arrays/dict/compute/rules.rs @@ -149,17 +149,18 @@ impl ArrayParentReduceRule for DictionaryScalarFnValuesPushDownRule { return Ok(None); } - // If the scalar function is null-sensitive, then we cannot push it down to values if - // we have any nulls in the codes. + // Before this rewrite, a null code supplies null for this argument while the constant + // arguments retain their values. After the rewrite, the null code masks the function's + // result. Those are equivalent only for a strict function. if array.codes().dtype().is_nullable() && !matches!( array.codes().validity()?, Validity::NonNullable | Validity::AllValid ) - && sig.is_null_sensitive() + && !sig.is_strict() { tracing::trace!( - "Not pushing down null-sensitive scalar function {} over dictionary with null codes {}", + "Not pushing down non-strict scalar function {} over dictionary with null codes {}", parent.scalar_fn(), Dict.id(), ); @@ -182,11 +183,10 @@ impl ArrayParentReduceRule for DictionaryScalarFnValuesPushDownRule { .into_array() .optimize()?; - // We can only push down null-sensitive functions when we have all-valid codes. - // In these cases, we cannot have the codes influence the nullability of the output DType. - // Therefore, we cast the codes to be non-nullable and then cast the dictionary output - // back to nullable if needed. - if sig.is_null_sensitive() && array.codes().dtype().is_nullable() { + // A non-strict function reaches this point only when the codes are all valid, but their + // dtype may still be nullable. Remove that declared nullability while rebuilding the + // dictionary, then cast its output to the function's declared dtype. + if !sig.is_strict() && array.codes().dtype().is_nullable() { let new_codes = array.codes().cast(array.codes().dtype().as_nonnullable())?; let new_dict = unsafe { DictArray::new_unchecked(new_codes, new_values) diff --git a/vortex-array/src/arrays/scalar_fn/vtable/mod.rs b/vortex-array/src/arrays/scalar_fn/vtable/mod.rs index d98e00975ea..96dedb15713 100644 --- a/vortex-array/src/arrays/scalar_fn/vtable/mod.rs +++ b/vortex-array/src/arrays/scalar_fn/vtable/mod.rs @@ -337,4 +337,8 @@ impl scalar_fn::ScalarFnVTable for ArrayExpr { let validity_array = options.0.validity()?.to_array(options.0.len()); Ok(Some(ArrayExpr.new_expr(FakeEq(validity_array), []))) } + + fn is_strict(&self, _options: &Self::Options) -> bool { + true + } } diff --git a/vortex-array/src/expr/analysis/labeling.rs b/vortex-array/src/expr/analysis/labeling.rs index 721241a14c9..85bb7e5e5f9 100644 --- a/vortex-array/src/expr/analysis/labeling.rs +++ b/vortex-array/src/expr/analysis/labeling.rs @@ -10,6 +10,9 @@ use crate::expr::traversal::NodeExt; use crate::expr::traversal::NodeVisitor; use crate::expr::traversal::TraversalOrder; +/// Boolean labels keyed by each expression node in a tree. +pub type BooleanLabels<'a> = HashMap<&'a Expression, bool>; + /// Label each node in an expression tree using a bottom-up traversal. /// /// This function separates tree labeling into two distinct steps: diff --git a/vortex-array/src/expr/analysis/mod.rs b/vortex-array/src/expr/analysis/mod.rs index f5208a31be8..00982a12255 100644 --- a/vortex-array/src/expr/analysis/mod.rs +++ b/vortex-array/src/expr/analysis/mod.rs @@ -5,13 +5,12 @@ pub mod annotation; mod fallible; pub mod immediate_access; mod labeling; -mod null_sensitive; mod referenced_field_paths; +mod strict; pub use annotation::*; pub use fallible::label_is_fallible; pub use immediate_access::*; pub use labeling::*; -pub use null_sensitive::BooleanLabels; -pub use null_sensitive::label_null_sensitive; pub use referenced_field_paths::referenced_field_paths; +pub use strict::label_strict; diff --git a/vortex-array/src/expr/analysis/null_sensitive.rs b/vortex-array/src/expr/analysis/null_sensitive.rs deleted file mode 100644 index 45944dc1bcb..00000000000 --- a/vortex-array/src/expr/analysis/null_sensitive.rs +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -use vortex_utils::aliases::hash_map::HashMap; - -use super::labeling::label_tree; -use crate::expr::Expression; - -pub type BooleanLabels<'a> = HashMap<&'a Expression, bool>; - -/// Label each expression in the tree with whether it is null-sensitive. -/// -/// See [`crate::scalar_fn::ScalarFnVTable::is_null_sensitive`] for a definition of null sensitivity. -/// This function operates on a tree of expressions, not just a single expression. -pub fn label_null_sensitive(expr: &Expression) -> BooleanLabels<'_> { - label_tree( - expr, - |expr| expr.signature().is_null_sensitive(), - |acc, &child| acc | child, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::expr::col; - use crate::expr::eq; - use crate::expr::is_null; - use crate::expr::lit; - - #[test] - fn test_null_sensitive_with_is_null() { - let expr = is_null(col("col1")); - let labels = label_null_sensitive(&expr); - - // The root expression should be null-sensitive - assert_eq!(labels.get(&expr), Some(&true)); - } - - #[test] - fn test_null_sensitive_without_is_null() { - let expr = eq(col("col1"), lit(5)); - let labels = label_null_sensitive(&expr); - - // Since the default is conservative (true), all expressions are sensitive - assert_eq!(labels.get(&expr), Some(&true)); - } - - #[test] - fn test_null_sensitive_nested() { - let left = eq(col("col1"), lit(5)); - let right = is_null(col("col2")); - let expr = eq(left.clone(), right.clone()); - - let labels = label_null_sensitive(&expr); - - // With conservative defaults, all are sensitive - assert_eq!(labels.get(&left), Some(&true)); - assert_eq!(labels.get(&right), Some(&true)); - assert_eq!(labels.get(&expr), Some(&true)); - } -} diff --git a/vortex-array/src/expr/analysis/strict.rs b/vortex-array/src/expr/analysis/strict.rs new file mode 100644 index 00000000000..df2147df60a --- /dev/null +++ b/vortex-array/src/expr/analysis/strict.rs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +use super::BooleanLabels; +use super::labeling::label_tree; +use crate::expr::Expression; + +/// Label each expression with whether its entire subtree is strict. +/// +/// A subtree is strict only when the node's scalar function and every child subtree are strict. +/// See [`crate::scalar_fn::ScalarFnVTable::is_strict`] for the scalar-function contract. +pub fn label_strict(expr: &Expression) -> BooleanLabels<'_> { + label_tree( + expr, + |expr| expr.signature().is_strict(), + |acc, &child| acc & child, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::expr::col; + use crate::expr::eq; + use crate::expr::is_null; + use crate::expr::lit; + + #[test] + fn test_non_strict_with_is_null() { + let expr = is_null(col("col1")); + let labels = label_strict(&expr); + + assert_eq!(labels.get(&expr), Some(&false)); + } + + #[test] + fn test_strict_expression() { + let expr = eq(lit(4), lit(5)); + let labels = label_strict(&expr); + + assert_eq!(labels.get(&expr), Some(&true)); + } + + #[test] + fn test_non_strict_child_makes_parent_subtree_non_strict() { + let left = eq(lit(4), lit(5)); + let right = is_null(col("col2")); + let expr = eq(left.clone(), right.clone()); + + let labels = label_strict(&expr); + + assert_eq!(labels.get(&left), Some(&true)); + assert_eq!(labels.get(&right), Some(&false)); + assert_eq!(labels.get(&expr), Some(&false)); + } +} diff --git a/vortex-array/src/expr/mod.rs b/vortex-array/src/expr/mod.rs index 15fc88a4c23..e71e641a204 100644 --- a/vortex-array/src/expr/mod.rs +++ b/vortex-array/src/expr/mod.rs @@ -12,7 +12,7 @@ //! //! Each node references a scalar function defined by a //! [`ScalarFnVTable`](crate::scalar_fn::ScalarFnVTable). The vtable declares the function signature, -//! properties such as null-sensitivity, and the logic that executes it over input arrays. Built-in +//! properties such as strictness, and the logic that executes it over input arrays. Built-in //! functions live in [`crate::scalar_fn`]; integration and plugin crates supply additional, //! use-case-specific functions. //! diff --git a/vortex-array/src/scalar_fn/fns/between/mod.rs b/vortex-array/src/scalar_fn/fns/between/mod.rs index 01c4fe06d31..1a2e038b2fb 100644 --- a/vortex-array/src/scalar_fn/fns/between/mod.rs +++ b/vortex-array/src/scalar_fn/fns/between/mod.rs @@ -306,7 +306,7 @@ impl ScalarFnVTable for Between { Ok(Some(and(and(arr, lower), upper))) } - fn is_null_sensitive(&self, _instance: &Self::Options) -> bool { + fn is_strict(&self, _options: &Self::Options) -> bool { false } @@ -343,6 +343,21 @@ mod tests { static SESSION: LazyLock = LazyLock::new(crate::array_session); + #[test] + fn is_not_strict() { + let expr = between( + root(), + lit(0), + lit(100), + BetweenOptions { + lower_strict: StrictComparison::NonStrict, + upper_strict: StrictComparison::NonStrict, + }, + ); + + assert!(!expr.signature().is_strict()); + } + #[test] fn test_display() { let expr = between( diff --git a/vortex-array/src/scalar_fn/fns/binary/boolean.rs b/vortex-array/src/scalar_fn/fns/binary/boolean.rs index 550b6c2c701..78f683eddf2 100644 --- a/vortex-array/src/scalar_fn/fns/binary/boolean.rs +++ b/vortex-array/src/scalar_fn/fns/binary/boolean.rs @@ -43,7 +43,8 @@ use crate::validity::Validity; /// operand without falling back to ordinary execution. /// /// Vortex's boolean [`Operator::And`] and [`Operator::Or`] variants use Kleene semantics; there is -/// no separate two-valued boolean operator path to dispatch here. +/// no separate two-valued boolean operator path to dispatch here. Consequently, they are not +/// strict: `false AND null` is `false`, and `true OR null` is `true`. pub trait BooleanKernel: VTable { /// Execute `lhs rhs` using Kleene boolean semantics. fn boolean( diff --git a/vortex-array/src/scalar_fn/fns/binary/mod.rs b/vortex-array/src/scalar_fn/fns/binary/mod.rs index fd2d35771c7..8a986c079a0 100644 --- a/vortex-array/src/scalar_fn/fns/binary/mod.rs +++ b/vortex-array/src/scalar_fn/fns/binary/mod.rs @@ -262,11 +262,10 @@ impl ScalarFnVTable for Binary { }) } - fn is_null_sensitive(&self, operator: &Operator) -> bool { - // Kleene AND/OR is not strict (`false AND null = false`, `true OR null = true`), so - // these operators cannot be pushed through dictionary null codes. This is consistent - // with `validity` returning `None` for AND/OR above. - matches!(operator, Operator::And | Operator::Or) + fn is_strict(&self, operator: &Operator) -> bool { + // Kleene AND/OR is not strict (`false AND null = false`, `true OR null = true`), which is + // consistent with `validity` returning `None` for these operators above. + !matches!(operator, Operator::And | Operator::Or) } fn is_fallible(&self, operator: &Operator) -> bool { diff --git a/vortex-array/src/scalar_fn/fns/byte_length.rs b/vortex-array/src/scalar_fn/fns/byte_length.rs index cfab5290a8f..3a9072c29c1 100644 --- a/vortex-array/src/scalar_fn/fns/byte_length.rs +++ b/vortex-array/src/scalar_fn/fns/byte_length.rs @@ -131,8 +131,8 @@ impl ScalarFnVTable for ByteLength { Ok(Some(expression.child(0).validity()?)) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - false + fn is_strict(&self, _options: &Self::Options) -> bool { + true } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-array/src/scalar_fn/fns/case_when.rs b/vortex-array/src/scalar_fn/fns/case_when.rs index d57026e00f5..189556c1fdb 100644 --- a/vortex-array/src/scalar_fn/fns/case_when.rs +++ b/vortex-array/src/scalar_fn/fns/case_when.rs @@ -300,8 +300,9 @@ impl ScalarFnVTable for CaseWhen { Ok(Some(crate::expr::fill_null(x.clone(), fill.clone()))) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - true + fn is_strict(&self, _options: &Self::Options) -> bool { + // A null in an unselected branch does not force a null output. + false } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-array/src/scalar_fn/fns/cast/mod.rs b/vortex-array/src/scalar_fn/fns/cast/mod.rs index 859e76b4932..311b199cf76 100644 --- a/vortex-array/src/scalar_fn/fns/cast/mod.rs +++ b/vortex-array/src/scalar_fn/fns/cast/mod.rs @@ -171,9 +171,9 @@ impl ScalarFnVTable for Cast { })) } - // This might apply a nullability - fn is_null_sensitive(&self, _instance: &DType) -> bool { - true + fn is_strict(&self, _instance: &DType) -> bool { + // Cast options can pin a non-nullable output dtype instead of propagating nullability. + false } } diff --git a/vortex-array/src/scalar_fn/fns/dynamic.rs b/vortex-array/src/scalar_fn/fns/dynamic.rs index 74058aa19f2..a741fcd9217 100644 --- a/vortex-array/src/scalar_fn/fns/dynamic.rs +++ b/vortex-array/src/scalar_fn/fns/dynamic.rs @@ -119,8 +119,7 @@ impl ScalarFnVTable for DynamicComparison { .into_array()) } - // Defer to the child - fn is_null_sensitive(&self, _instance: &Self::Options) -> bool { + fn is_strict(&self, _options: &Self::Options) -> bool { false } } @@ -286,6 +285,20 @@ mod tests { use crate::dtype::PType; use crate::expr::dynamic; use crate::expr::root; + + #[test] + fn is_not_strict() { + let expr = dynamic( + CompareOperator::Lt, + || None, + DType::Primitive(PType::I32, Nullability::NonNullable), + true, + root(), + ); + + assert!(!expr.signature().is_strict()); + } + #[test] fn return_dtype_bool() -> VortexResult<()> { let expr = dynamic( diff --git a/vortex-array/src/scalar_fn/fns/ext_storage.rs b/vortex-array/src/scalar_fn/fns/ext_storage.rs index 4a014a3325b..bad86f553d9 100644 --- a/vortex-array/src/scalar_fn/fns/ext_storage.rs +++ b/vortex-array/src/scalar_fn/fns/ext_storage.rs @@ -93,8 +93,8 @@ impl ScalarFnVTable for ExtStorage { Ok(Some(expression.child(0).validity()?)) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - false + fn is_strict(&self, _options: &Self::Options) -> bool { + true } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-array/src/scalar_fn/fns/fill_null/mod.rs b/vortex-array/src/scalar_fn/fns/fill_null/mod.rs index c9eae72ad08..34504e7a519 100644 --- a/vortex-array/src/scalar_fn/fns/fill_null/mod.rs +++ b/vortex-array/src/scalar_fn/fns/fill_null/mod.rs @@ -132,8 +132,9 @@ impl ScalarFnVTable for FillNull { Ok(Some(expression.child(1).validity()?)) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - true + fn is_strict(&self, _options: &Self::Options) -> bool { + // This function replaces null input values instead of propagating them. + false } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-array/src/scalar_fn/fns/get_item.rs b/vortex-array/src/scalar_fn/fns/get_item.rs index b9cb9202f38..dd51a7269c6 100644 --- a/vortex-array/src/scalar_fn/fns/get_item.rs +++ b/vortex-array/src/scalar_fn/fns/get_item.rs @@ -185,8 +185,7 @@ impl ScalarFnVTable for GetItem { Ok(None) } - // This will apply struct nullability field. We could add a dtype?? - fn is_null_sensitive(&self, _field_name: &FieldName) -> bool { + fn is_strict(&self, _field_name: &FieldName) -> bool { true } @@ -227,6 +226,7 @@ mod tests { fn get_item_by_name() { let st = test_array(); let get_item = get_item("a", root()); + assert!(get_item.signature().is_strict()); let item = st.into_array().apply(&get_item).unwrap(); assert_eq!(item.dtype(), &DType::from(PType::I32)) } diff --git a/vortex-array/src/scalar_fn/fns/is_not_null.rs b/vortex-array/src/scalar_fn/fns/is_not_null.rs index 75d86db5ee8..acb3b9e0f35 100644 --- a/vortex-array/src/scalar_fn/fns/is_not_null.rs +++ b/vortex-array/src/scalar_fn/fns/is_not_null.rs @@ -88,8 +88,9 @@ impl ScalarFnVTable for IsNotNull { } } - fn is_null_sensitive(&self, _instance: &Self::Options) -> bool { - true + fn is_strict(&self, _instance: &Self::Options) -> bool { + // Null input produces the non-null boolean value `false`. + false } fn is_fallible(&self, _instance: &Self::Options) -> bool { @@ -244,8 +245,8 @@ mod tests { } #[test] - fn test_is_not_null_sensitive() { - assert!(is_not_null(col("a")).signature().is_null_sensitive()); + fn test_is_not_null_is_not_strict() { + assert!(!is_not_null(col("a")).signature().is_strict()); } #[test] diff --git a/vortex-array/src/scalar_fn/fns/is_null.rs b/vortex-array/src/scalar_fn/fns/is_null.rs index 5e605ca6ee0..118c00ab987 100644 --- a/vortex-array/src/scalar_fn/fns/is_null.rs +++ b/vortex-array/src/scalar_fn/fns/is_null.rs @@ -79,8 +79,9 @@ impl ScalarFnVTable for IsNull { } } - fn is_null_sensitive(&self, _instance: &Self::Options) -> bool { - true + fn is_strict(&self, _instance: &Self::Options) -> bool { + // Null input produces the non-null boolean value `true`. + false } fn is_fallible(&self, _instance: &Self::Options) -> bool { @@ -249,8 +250,7 @@ mod tests { } #[test] - fn test_is_null_sensitive() { - // is_null itself is null-sensitive - assert!(is_null(col("a")).signature().is_null_sensitive()); + fn test_is_null_is_not_strict() { + assert!(!is_null(col("a")).signature().is_strict()); } } diff --git a/vortex-array/src/scalar_fn/fns/like/mod.rs b/vortex-array/src/scalar_fn/fns/like/mod.rs index bdedce7112b..3637e0384a3 100644 --- a/vortex-array/src/scalar_fn/fns/like/mod.rs +++ b/vortex-array/src/scalar_fn/fns/like/mod.rs @@ -164,8 +164,8 @@ impl ScalarFnVTable for Like { Ok(Some(and(child_validity, pattern_validity))) } - fn is_null_sensitive(&self, _instance: &Self::Options) -> bool { - false + fn is_strict(&self, _instance: &Self::Options) -> bool { + true } fn is_fallible(&self, _options: &Self::Options) -> bool { @@ -723,7 +723,7 @@ mod tests { #[test] fn signature() { let like_expr = like(root(), lit("%test%")); - assert!(!like_expr.signature().is_null_sensitive()); + assert!(like_expr.signature().is_strict()); assert!(!like_expr.signature().is_fallible()); } diff --git a/vortex-array/src/scalar_fn/fns/list_contains/mod.rs b/vortex-array/src/scalar_fn/fns/list_contains/mod.rs index 41918a9dffc..18fce32d701 100644 --- a/vortex-array/src/scalar_fn/fns/list_contains/mod.rs +++ b/vortex-array/src/scalar_fn/fns/list_contains/mod.rs @@ -121,9 +121,9 @@ impl ScalarFnVTable for ListContains { compute_list_contains(&list_array, &value_array, ctx) } - // Nullability matters for contains([], x) where x is false. - fn is_null_sensitive(&self, _instance: &Self::Options) -> bool { - true + // An empty list can produce false even when the needle is null. + fn is_strict(&self, _options: &Self::Options) -> bool { + false } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-array/src/scalar_fn/fns/list_length.rs b/vortex-array/src/scalar_fn/fns/list_length.rs index 41180b1ae20..e1ce38353cc 100644 --- a/vortex-array/src/scalar_fn/fns/list_length.rs +++ b/vortex-array/src/scalar_fn/fns/list_length.rs @@ -108,8 +108,8 @@ impl ScalarFnVTable for ListLength { Ok(Some(expression.child(0).validity()?)) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - false + fn is_strict(&self, _options: &Self::Options) -> bool { + true } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-array/src/scalar_fn/fns/list_sum.rs b/vortex-array/src/scalar_fn/fns/list_sum.rs index a5f8d219db6..78ea4dfd1eb 100644 --- a/vortex-array/src/scalar_fn/fns/list_sum.rs +++ b/vortex-array/src/scalar_fn/fns/list_sum.rs @@ -120,8 +120,8 @@ impl ScalarFnVTable for ListSum { } } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - false + fn is_strict(&self, _options: &Self::Options) -> bool { + true } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-array/src/scalar_fn/fns/literal.rs b/vortex-array/src/scalar_fn/fns/literal.rs index 5181a5250dd..0449ae27e29 100644 --- a/vortex-array/src/scalar_fn/fns/literal.rs +++ b/vortex-array/src/scalar_fn/fns/literal.rs @@ -101,8 +101,8 @@ impl ScalarFnVTable for Literal { Ok(Some(lit(scalar.is_valid()))) } - fn is_null_sensitive(&self, _instance: &Self::Options) -> bool { - false + fn is_strict(&self, _instance: &Self::Options) -> bool { + true } fn is_fallible(&self, _instance: &Self::Options) -> bool { diff --git a/vortex-array/src/scalar_fn/fns/mask/mod.rs b/vortex-array/src/scalar_fn/fns/mask/mod.rs index 4dd55948ec8..4c081e99c30 100644 --- a/vortex-array/src/scalar_fn/fns/mask/mod.rs +++ b/vortex-array/src/scalar_fn/fns/mask/mod.rs @@ -133,6 +133,10 @@ impl ScalarFnVTable for Mask { expression.child(1).clone(), ))) } + + fn is_strict(&self, _options: &Self::Options) -> bool { + true + } } /// Try to handle masking when at least one of the input or mask is a constant array. diff --git a/vortex-array/src/scalar_fn/fns/merge.rs b/vortex-array/src/scalar_fn/fns/merge.rs index 7b9d57fcdca..5f00c64e879 100644 --- a/vortex-array/src/scalar_fn/fns/merge.rs +++ b/vortex-array/src/scalar_fn/fns/merge.rs @@ -241,7 +241,7 @@ impl ScalarFnVTable for Merge { Ok(Some(lit(true))) } - fn is_null_sensitive(&self, _instance: &Self::Options) -> bool { + fn is_strict(&self, _options: &Self::Options) -> bool { true } diff --git a/vortex-array/src/scalar_fn/fns/not/mod.rs b/vortex-array/src/scalar_fn/fns/not/mod.rs index d0f9e400bde..21b1c53112d 100644 --- a/vortex-array/src/scalar_fn/fns/not/mod.rs +++ b/vortex-array/src/scalar_fn/fns/not/mod.rs @@ -98,8 +98,8 @@ impl ScalarFnVTable for Not { child.execute::(ctx)?.not() } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - false + fn is_strict(&self, _options: &Self::Options) -> bool { + true } fn is_fallible(&self, _options: &Self::Options) -> bool { @@ -109,10 +109,13 @@ impl ScalarFnVTable for Not { #[cfg(test)] mod tests { + use vortex_error::VortexResult; + use crate::IntoArray; use crate::VortexSessionExecute; use crate::array_session; use crate::arrays::bool::BoolArrayExt; + use crate::assert_arrays_eq; use crate::dtype::DType; use crate::dtype::Nullability; use crate::expr::col; @@ -122,6 +125,26 @@ mod tests { use crate::expr::test_harness; use crate::scalar_fn::fns::not::BoolArray; + #[test] + fn is_strict() { + assert!(not(root()).signature().is_strict()); + } + + #[test] + fn preserves_nulls() -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + let input = BoolArray::from_iter([Some(false), None, Some(true)]).into_array(); + + let result = input.apply(¬(root()))?; + + assert_arrays_eq!( + result, + BoolArray::from_iter([Some(true), None, Some(false)]), + &mut ctx + ); + Ok(()) + } + #[test] fn invert_booleans() { let mut ctx = array_session().create_execution_ctx(); diff --git a/vortex-array/src/scalar_fn/fns/pack.rs b/vortex-array/src/scalar_fn/fns/pack.rs index 45475623d32..84983d9d55e 100644 --- a/vortex-array/src/scalar_fn/fns/pack.rs +++ b/vortex-array/src/scalar_fn/fns/pack.rs @@ -150,9 +150,9 @@ impl ScalarFnVTable for Pack { .execute(ctx) } - // This applies a nullability - fn is_null_sensitive(&self, _instance: &Self::Options) -> bool { - true + fn is_strict(&self, _instance: &Self::Options) -> bool { + // A null field value does not force the packed struct row to be null. + false } fn is_fallible(&self, _instance: &Self::Options) -> bool { diff --git a/vortex-array/src/scalar_fn/fns/root.rs b/vortex-array/src/scalar_fn/fns/root.rs index 7bd5b758796..646bed19a1b 100644 --- a/vortex-array/src/scalar_fn/fns/root.rs +++ b/vortex-array/src/scalar_fn/fns/root.rs @@ -77,8 +77,8 @@ impl ScalarFnVTable for Root { vortex_bail!("Root expression is not executable") } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - false + fn is_strict(&self, _options: &Self::Options) -> bool { + true } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-array/src/scalar_fn/fns/select.rs b/vortex-array/src/scalar_fn/fns/select.rs index 2fbe4593503..2e0bbd02975 100644 --- a/vortex-array/src/scalar_fn/fns/select.rs +++ b/vortex-array/src/scalar_fn/fns/select.rs @@ -232,7 +232,7 @@ impl ScalarFnVTable for Select { Ok(None) } - fn is_null_sensitive(&self, _instance: &FieldSelection) -> bool { + fn is_strict(&self, _options: &FieldSelection) -> bool { true } diff --git a/vortex-array/src/scalar_fn/fns/stat.rs b/vortex-array/src/scalar_fn/fns/stat.rs index 84fc5760495..612298424e4 100644 --- a/vortex-array/src/scalar_fn/fns/stat.rs +++ b/vortex-array/src/scalar_fn/fns/stat.rs @@ -123,6 +123,10 @@ impl ScalarFnVTable for StatFn { let dtype = stat_dtype(options.aggregate_fn(), input.dtype())?; stat_array(&input, options.aggregate_fn(), dtype, args.row_count()) } + + fn is_strict(&self, _options: &Self::Options) -> bool { + false + } } fn stat_dtype(aggregate_fn: &AggregateFnRef, input_dtype: &DType) -> VortexResult { diff --git a/vortex-array/src/scalar_fn/fns/variant_get/mod.rs b/vortex-array/src/scalar_fn/fns/variant_get/mod.rs index fb9308c024e..d3edb9de485 100644 --- a/vortex-array/src/scalar_fn/fns/variant_get/mod.rs +++ b/vortex-array/src/scalar_fn/fns/variant_get/mod.rs @@ -154,6 +154,10 @@ impl ScalarFnVTable for VariantGet { let array = ChunkedArray::try_new(chunks, dtype)?.into_array(); VariantArray::try_new(array, None).map(|array| array.into_array()) } + + fn is_strict(&self, _options: &Self::Options) -> bool { + true + } } fn variant_get_scalar( diff --git a/vortex-array/src/scalar_fn/fns/zip/mod.rs b/vortex-array/src/scalar_fn/fns/zip/mod.rs index 40fedbe85eb..5cb84bc737e 100644 --- a/vortex-array/src/scalar_fn/fns/zip/mod.rs +++ b/vortex-array/src/scalar_fn/fns/zip/mod.rs @@ -155,8 +155,9 @@ impl ScalarFnVTable for Zip { Ok(None) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - true + fn is_strict(&self, _options: &Self::Options) -> bool { + // A null in an unselected branch does not force a null output. + false } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-array/src/scalar_fn/foreign.rs b/vortex-array/src/scalar_fn/foreign.rs index d2abd3b9a95..94fd75b08f1 100644 --- a/vortex-array/src/scalar_fn/foreign.rs +++ b/vortex-array/src/scalar_fn/foreign.rs @@ -118,4 +118,8 @@ impl ScalarFnVTable for ForeignScalarFnVTable { ) -> VortexResult { vortex_bail!("Cannot execute unknown scalar function '{}'", self.id); } + + fn is_strict(&self, _options: &Self::Options) -> bool { + false + } } diff --git a/vortex-array/src/scalar_fn/internal/row_count.rs b/vortex-array/src/scalar_fn/internal/row_count.rs index eee838f803d..7f78acbef64 100644 --- a/vortex-array/src/scalar_fn/internal/row_count.rs +++ b/vortex-array/src/scalar_fn/internal/row_count.rs @@ -83,8 +83,8 @@ impl ScalarFnVTable for RowCount { vortex_bail!("RowCount must be substituted before evaluation") } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - false + fn is_strict(&self, _options: &Self::Options) -> bool { + true } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-array/src/scalar_fn/signature.rs b/vortex-array/src/scalar_fn/signature.rs index 45b2fe2ef51..0ae925b41d2 100644 --- a/vortex-array/src/scalar_fn/signature.rs +++ b/vortex-array/src/scalar_fn/signature.rs @@ -21,10 +21,10 @@ impl ScalarFnSignature<'_> { self.inner.child_name(index) } - /// Returns whether this expression itself is null-sensitive. - /// See [`crate::scalar_fn::ScalarFnVTable::is_null_sensitive`]. - pub fn is_null_sensitive(&self) -> bool { - self.inner.is_null_sensitive() + /// Returns whether this expression itself is strict. + /// See [`crate::scalar_fn::ScalarFnVTable::is_strict`]. + pub fn is_strict(&self) -> bool { + self.inner.is_strict() } /// Returns whether this expression itself is fallible. diff --git a/vortex-array/src/scalar_fn/typed.rs b/vortex-array/src/scalar_fn/typed.rs index 1b14a9d613d..a6620735ff2 100644 --- a/vortex-array/src/scalar_fn/typed.rs +++ b/vortex-array/src/scalar_fn/typed.rs @@ -89,7 +89,7 @@ pub(super) trait DynScalarFn: 'static + Send + Sync + super::sealed::Sealed { ) -> VortexResult>; fn arity(&self) -> Arity; fn child_name(&self, child_idx: usize) -> ChildName; - fn is_null_sensitive(&self) -> bool; + fn is_strict(&self) -> bool; fn is_fallible(&self) -> bool; // Expression methods — take &Expression for tree traversal @@ -184,8 +184,8 @@ impl DynScalarFn for TypedScalarFnInstance { V::child_name(&self.vtable, &self.options, child_idx) } - fn is_null_sensitive(&self) -> bool { - V::is_null_sensitive(&self.vtable, &self.options) + fn is_strict(&self) -> bool { + V::is_strict(&self.vtable, &self.options) } fn is_fallible(&self) -> bool { diff --git a/vortex-array/src/scalar_fn/vtable.rs b/vortex-array/src/scalar_fn/vtable.rs index c66afc34932..41f1ea25fc7 100644 --- a/vortex-array/src/scalar_fn/vtable.rs +++ b/vortex-array/src/scalar_fn/vtable.rs @@ -192,24 +192,49 @@ pub trait ScalarFnVTable: 'static + Sized + Clone + Send + Sync { Ok(None) } - /// Returns whether this expression itself is null-sensitive. Conservatively default to *true*. + /// Returns whether this expression itself is strict. /// - /// An expression is null-sensitive if it directly operates on null values, - /// such as `is_null`. Most expressions are not null-sensitive. + /// Strict has the same value-level meaning as PostgreSQL `STRICT`: if any input value at row + /// `i` is null, the output value at row `i` is null. Formally, `f` is strict iff at each row, + /// for some total function `g` over non-null values, /// - /// The property we are interested in is if the expression (e) distributes over `mask`. - /// Define a `mask(a, m)` expression that applies the boolean array `m` to the validity of the - /// array `a`. + /// ```text + /// f(v1, .., vk) = NULL if vj = NULL for some j + /// f(v1, .., vk) = g(v1, .., vk) otherwise + /// ``` /// - /// A unary expression `e` is not null-sensitive iff forall arrays `a` and masks `m`, - /// `e(mask(a, m)) == mask(e(a), m)`. + /// That is, any null input yields a null output, and non-null outputs depend only on the (all + /// non-null) inputs at that row. Lifted columnwise, this is equivalent to the mask-hoisting + /// law: for any argument `aj = mask(aj', m)`, /// - /// This can be extended to an n-ary expression. + /// ```text + /// f(a1, .., mask(aj', m), .., ak) == mask(f(a1, .., aj', .., ak), m) + /// ``` + /// + /// Optimizations rely on this per-argument form when pushing a function through dictionary + /// codes while leaving sibling constants unmasked. It is stronger than commuting with masking + /// all arguments at once: Kleene `AND` and `OR` satisfy the all-arguments law but are not + /// strict because, for example, `false AND null = false`. + /// + /// Two consequences that optimizations rely on: + /// 1. Output validity is precomputable as the `AND` of the input validities, so + /// `valid(f(a1, .., ak)) = valid(a1) ∧ .. ∧ valid(ak)`. + /// 2. Values behind null slots are irrelevant, so kernels may compute `g` densely over all + /// lanes (including garbage) and apply validity afterwards. + /// + /// Returning `true` also requires [`ScalarFnVTable::return_dtype`] to propagate nullability: + /// if any input dtype is nullable, the output dtype must be nullable. For example, `cast` is + /// value-strict but can pin a non-nullable output dtype through its options, so it must return + /// `false`. + /// + /// Nullary functions are vacuously strict because they have no input values. + /// + /// Conservatively defaults to `false` (non-strict). /// /// This method only checks the expression itself, not its children. - fn is_null_sensitive(&self, options: &Self::Options) -> bool { + fn is_strict(&self, options: &Self::Options) -> bool { _ = options; - true + false } /// Returns whether this expression is semantically fallible. Conservatively defaults to diff --git a/vortex-geo/src/scalar_fn/contains.rs b/vortex-geo/src/scalar_fn/contains.rs index c8f54951420..4c3aff332a4 100644 --- a/vortex-geo/src/scalar_fn/contains.rs +++ b/vortex-geo/src/scalar_fn/contains.rs @@ -96,8 +96,8 @@ impl ScalarFnVTable for GeoContains { union_child_validities(expression) } - fn is_null_sensitive(&self, _: &Self::Options) -> bool { - false + fn is_strict(&self, _: &Self::Options) -> bool { + true } } diff --git a/vortex-geo/src/scalar_fn/distance.rs b/vortex-geo/src/scalar_fn/distance.rs index ed783e384a2..e9c3a8a0942 100644 --- a/vortex-geo/src/scalar_fn/distance.rs +++ b/vortex-geo/src/scalar_fn/distance.rs @@ -96,8 +96,8 @@ impl ScalarFnVTable for GeoDistance { union_child_validities(expression) } - fn is_null_sensitive(&self, _: &Self::Options) -> bool { - false + fn is_strict(&self, _: &Self::Options) -> bool { + true } } diff --git a/vortex-geo/src/scalar_fn/envelope.rs b/vortex-geo/src/scalar_fn/envelope.rs index d04a125147f..3ef59bb59af 100644 --- a/vortex-geo/src/scalar_fn/envelope.rs +++ b/vortex-geo/src/scalar_fn/envelope.rs @@ -213,8 +213,8 @@ impl ScalarFnVTable for GeoEnvelope { Ok(None) } - fn is_null_sensitive(&self, _: &Self::Options) -> bool { - false + fn is_strict(&self, _: &Self::Options) -> bool { + true } } diff --git a/vortex-geo/src/scalar_fn/intersects.rs b/vortex-geo/src/scalar_fn/intersects.rs index 7f5c9b1dead..758ef7f8129 100644 --- a/vortex-geo/src/scalar_fn/intersects.rs +++ b/vortex-geo/src/scalar_fn/intersects.rs @@ -94,8 +94,8 @@ impl ScalarFnVTable for GeoIntersects { union_child_validities(expression) } - fn is_null_sensitive(&self, _: &Self::Options) -> bool { - false + fn is_strict(&self, _: &Self::Options) -> bool { + true } } diff --git a/vortex-json/src/json_to_variant.rs b/vortex-json/src/json_to_variant.rs index d0febf4e646..ffb5101a1da 100644 --- a/vortex-json/src/json_to_variant.rs +++ b/vortex-json/src/json_to_variant.rs @@ -183,12 +183,11 @@ impl ScalarFnVTable for JsonToVariant { .execute::(ctx) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { + fn is_strict(&self, _options: &Self::Options) -> bool { // `json_to_variant` maps null rows to null rows and the JSON literal `null` to a - // variant-null value independently of which other rows are null, so it commutes with - // validity masking. Marking it not null-sensitive also lets it push through dictionaries - // into their JSON extension values, where the kernel fires directly. - false + // variant-null value. Its output dtype propagates the input nullability, so it is strict + // and can push through dictionaries into their JSON extension values. + true } } diff --git a/vortex-layout/src/layouts/dict/reader.rs b/vortex-layout/src/layouts/dict/reader.rs index 84c484dd5fa..2c88fad4cf0 100644 --- a/vortex-layout/src/layouts/dict/reader.rs +++ b/vortex-layout/src/layouts/dict/reader.rs @@ -171,8 +171,8 @@ const PUSHDOWN_ANNOTATION: &str = ""; /// right is the optional inner part that we want to apply to array before /// canonicalizing. /// -/// We want to push to array only if expression has a negative cost, is -/// infallible and null-insensitive. +/// We want to push to the array only if the expression has a negative cost, is infallible, and is +/// strict. Strictness ensures dictionary null codes still force a null result after pushdown. fn split_expression_for_pushdown( expr: Expression, dtype: &DType, @@ -181,7 +181,7 @@ fn split_expression_for_pushdown( let annotations = direct_annotations(&expr, |expr| { let signature = expr.signature(); if !signature.is_fallible() - && !signature.is_null_sensitive() + && signature.is_strict() && is_negative_cost(expr.id()) && references_root.get(&expr).copied().unwrap_or(true) { @@ -240,7 +240,7 @@ impl LayoutReader for DictReader { expr: &Expression, mask: MaskFuture, ) -> VortexResult { - // TODO(joe): fix up expr partitioning with fallible & null sensitive annotations + // TODO(joe): fix up expr partitioning with fallibility and strictness annotations let values_eval = self.values_eval(expr.clone()); // We register interest on the entire codes row_range for now, there @@ -272,7 +272,7 @@ impl LayoutReader for DictReader { expr: &Expression, mask: MaskFuture, ) -> VortexResult>> { - // TODO: fix up expr partitioning with fallible & null sensitive annotations + // TODO: fix up expr partitioning with fallibility and strictness annotations let codes_eval = self .codes .projection_evaluation(row_range, &root(), mask) diff --git a/vortex-layout/src/layouts/row_idx/expr.rs b/vortex-layout/src/layouts/row_idx/expr.rs index 95ddce3d762..c0043542f93 100644 --- a/vortex-layout/src/layouts/row_idx/expr.rs +++ b/vortex-layout/src/layouts/row_idx/expr.rs @@ -61,6 +61,10 @@ impl ScalarFnVTable for RowIdx { "RowIdxExpr should not be executed directly, use it in the context of a Vortex scan and it will be substituted for a row index array" ); } + + fn is_strict(&self, _options: &Self::Options) -> bool { + true + } } pub fn row_idx() -> Expression { diff --git a/vortex-row/src/encode.rs b/vortex-row/src/encode.rs index 04feec89415..840e48a4331 100644 --- a/vortex-row/src/encode.rs +++ b/vortex-row/src/encode.rs @@ -90,8 +90,8 @@ impl ScalarFnVTable for RowEncode { execute_row_encode(options, args, ctx) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - true + fn is_strict(&self, _options: &Self::Options) -> bool { + false } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-row/src/size.rs b/vortex-row/src/size.rs index 6636c4e9f34..7e48a3a0ea7 100644 --- a/vortex-row/src/size.rs +++ b/vortex-row/src/size.rs @@ -242,8 +242,8 @@ impl ScalarFnVTable for RowSize { .into_array()) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - true + fn is_strict(&self, _options: &Self::Options) -> bool { + false } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-tensor/src/scalar_fns/cosine_similarity.rs b/vortex-tensor/src/scalar_fns/cosine_similarity.rs index ffe198eb0fa..de6f0614471 100644 --- a/vortex-tensor/src/scalar_fns/cosine_similarity.rs +++ b/vortex-tensor/src/scalar_fns/cosine_similarity.rs @@ -183,8 +183,8 @@ impl ScalarFnVTable for CosineSimilarity { union_child_validities(expression) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - false + fn is_strict(&self, _options: &Self::Options) -> bool { + true } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-tensor/src/scalar_fns/inner_product.rs b/vortex-tensor/src/scalar_fns/inner_product.rs index cb17d179161..f46b03ac94e 100644 --- a/vortex-tensor/src/scalar_fns/inner_product.rs +++ b/vortex-tensor/src/scalar_fns/inner_product.rs @@ -167,8 +167,8 @@ impl ScalarFnVTable for InnerProduct { union_child_validities(expression) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - false + fn is_strict(&self, _options: &Self::Options) -> bool { + true } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-tensor/src/scalar_fns/l2_denorm.rs b/vortex-tensor/src/scalar_fns/l2_denorm.rs index b879ab3731c..7195265790f 100644 --- a/vortex-tensor/src/scalar_fns/l2_denorm.rs +++ b/vortex-tensor/src/scalar_fns/l2_denorm.rs @@ -260,8 +260,8 @@ impl ScalarFnVTable for L2Denorm { union_child_validities(expression) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - false + fn is_strict(&self, _options: &Self::Options) -> bool { + true } fn is_fallible(&self, _options: &Self::Options) -> bool { diff --git a/vortex-tensor/src/scalar_fns/l2_norm.rs b/vortex-tensor/src/scalar_fns/l2_norm.rs index 0eeb6777ab3..73c1538abc9 100644 --- a/vortex-tensor/src/scalar_fns/l2_norm.rs +++ b/vortex-tensor/src/scalar_fns/l2_norm.rs @@ -188,8 +188,8 @@ impl ScalarFnVTable for L2Norm { union_child_validities(expression) } - fn is_null_sensitive(&self, _options: &Self::Options) -> bool { - false + fn is_strict(&self, _options: &Self::Options) -> bool { + true } fn is_fallible(&self, _options: &Self::Options) -> bool {